commit 19170142c3adf063cf771a029c7dc6163cd2df5e Author: Stefan Wehrmeyer Date: Tue Feb 15 21:09:09 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..802daca --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.mo +*.egg-info/ +__pycache__ +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dacae98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2022 FragDenStaat.de / Stefan Wehrmeyer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..da93c20 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md LICENSE +recursive-include froide_govplan/templates * +recursive-include froide_govplan/static * diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a559e5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Froide govplan + +A Django app that allows tracking government plans. diff --git a/froide_govplan/__init__.py b/froide_govplan/__init__.py new file mode 100644 index 0000000..aa76fbe --- /dev/null +++ b/froide_govplan/__init__.py @@ -0,0 +1,3 @@ +__version__ = "0.0.1" + +default_app_config = "froide_govplan.apps.FroideGovPlanConfig" diff --git a/froide_govplan/admin.py b/froide_govplan/admin.py new file mode 100644 index 0000000..bf7e0d4 --- /dev/null +++ b/froide_govplan/admin.py @@ -0,0 +1,82 @@ +from django import forms +from django.contrib import admin +from django.urls import reverse_lazy + +from tinymce.widgets import TinyMCE + +from froide.helper.widgets import TagAutocompleteWidget + +from .models import Government, GovernmentPlan, GovernmentPlanUpdate + + +class GovernmentPlanAdminForm(forms.ModelForm): + class Meta: + model = GovernmentPlan + fields = "__all__" + widgets = { + "categories": TagAutocompleteWidget( + autocomplete_url=reverse_lazy("api:category-autocomplete") + ), + } + + +class GovernmentPlanUpdateAdminForm(forms.ModelForm): + class Meta: + model = GovernmentPlanUpdate + fields = "__all__" + widgets = { + "content": TinyMCE(attrs={'cols': 80, 'rows': 30}) + } + + +class GovernmentAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("name",)} + list_display = ("name", "public", "start_date", "end_date") + list_filter = ("public",) + + +class GovernmentPlanAdmin(admin.ModelAdmin): + form = GovernmentPlanAdminForm + + save_on_top = True + prepopulated_fields = {"slug": ("title",)} + raw_id_fields = ("responsible_publicbody",) + list_display = ( + "title", + "public", + "status", + "rating", + ) + list_filter = ("status", "rating", "public", "government") + + +class GovernmentPlanUpdateAdmin(admin.ModelAdmin): + form = GovernmentPlanUpdateAdminForm + raw_id_fields = ("user", "foirequest") + list_display = ( + "plan", + "user", + "timestamp", + "status", + "rating", + "public", + ) + list_filter = ( + "status", + "public", + ) + search_fields = ("title", "plan__title",) + date_hierarchy = "timestamp" + + def get_queryset(self, request): + qs = super().get_queryset(request) + qs = qs.prefetch_related( + "plan", + "user", + ) + return qs + + +admin.site.register(Government, GovernmentAdmin) +admin.site.register(GovernmentPlan, GovernmentPlanAdmin) +admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin) diff --git a/froide_govplan/apps.py b/froide_govplan/apps.py new file mode 100644 index 0000000..c9b9184 --- /dev/null +++ b/froide_govplan/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class FroideGovPlanConfig(AppConfig): + name = "froide_govplan" + verbose_name = _("GovPlan App") diff --git a/froide_govplan/cms_apps.py b/froide_govplan/cms_apps.py new file mode 100644 index 0000000..c6a21ee --- /dev/null +++ b/froide_govplan/cms_apps.py @@ -0,0 +1,13 @@ +from django.utils.translation import gettext_lazy as _ + +from cms.app_base import CMSApp +from cms.apphook_pool import apphook_pool + + +@apphook_pool.register +class GovPlanCMSApp(CMSApp): + name = _("GovPlan CMS App") + app_name = "govplan" + + def get_urls(self, page=None, language=None, **kwargs): + return ["froide_govplan.urls"] diff --git a/froide_govplan/cms_plugins.py b/froide_govplan/cms_plugins.py new file mode 100644 index 0000000..83661b2 --- /dev/null +++ b/froide_govplan/cms_plugins.py @@ -0,0 +1,24 @@ +from django.utils.translation import gettext_lazy as _ + +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool + +from .models import PLUGIN_TEMPLATES, GovernmentPlansCMSPlugin + + +@plugin_pool.register_plugin +class GovernmentPlansPlugin(CMSPluginBase): + name = _("Government plans") + model = GovernmentPlansCMSPlugin + filter_horizontal = ("categories",) + cache = True + + def get_render_template(self, context, instance, placeholder): + return instance.template or PLUGIN_TEMPLATES[0][0] + + def render(self, context, instance, placeholder): + context = super().render(context, instance, placeholder) + context["object_list"] = instance.get_plans( + context["request"], published_only=False + ) + return context diff --git a/froide_govplan/locale/de/LC_MESSAGES/django.po b/froide_govplan/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..775da6e --- /dev/null +++ b/froide_govplan/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,149 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-02-15 22:13+0100\n" +"PO-Revision-Date: 2022-02-15 22:18+0100\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Last-Translator: Stefan Wehrmeyer \n" +"Language-Team: \n" +"X-Generator: Poedit 2.3\n" + +#: apps.py:7 +msgid "GovPlan App" +msgstr "" + +#: cms_apps.py:9 +msgid "GovPlan CMS App" +msgstr "" + +#: cms_plugins.py:11 models.py:109 +msgid "Government plans" +msgstr "Regierungsvorhaben" + +#: models.py:23 +msgid "not started" +msgstr "nicht begonnen" + +#: models.py:24 +msgid "started" +msgstr "begonnen" + +#: models.py:25 +msgid "partially implemented" +msgstr "teilweise umgesetzt" + +#: models.py:26 +msgid "implemented" +msgstr "umgesetzt" + +#: models.py:27 +msgid "deferred" +msgstr "verschoben" + +#: models.py:31 +msgid "terrible" +msgstr "sehr schlecht" + +#: models.py:32 +msgid "bad" +msgstr "schlecht" + +#: models.py:33 +msgid "OK" +msgstr "mittelmäßig" + +#: models.py:34 +msgid "good" +msgstr "gut" + +#: models.py:35 +msgid "excellent" +msgstr "sehr gut" + +#: models.py:52 +msgid "Government" +msgstr "Regierung" + +#: models.py:53 +msgid "Governments" +msgstr "Regierungen" + +#: models.py:66 +msgid "Categorized Government Plan" +msgstr "Kategorisiertes Regierungsvorhaben" + +#: models.py:67 +msgid "Categorized Government Plans" +msgstr "Kategorisierte Regierungsvorhaben" + +#: models.py:79 +msgid "image" +msgstr "Bild" + +#: models.py:94 models.py:180 +msgid "categories" +msgstr "Kategorien" + +#: models.py:108 +msgid "Government plan" +msgstr "Regierungsvorhaben" + +#: models.py:160 +msgid "Plan update" +msgstr "Entwicklung beim Vorhaben" + +#: models.py:161 +msgid "Plan updates" +msgstr "Entwicklungen beim Vorhaben" + +#: models.py:170 +msgid "Normal" +msgstr "Standard" + +#: models.py:184 +msgid "number of plans" +msgstr "Anzahl der Vorhaben" + +#: models.py:184 +msgid "0 means all the plans" +msgstr "0 heißt all Vorhaben" + +#: models.py:187 +msgid "offset" +msgstr "Start" + +#: models.py:189 +msgid "number of plans to skip from top of list" +msgstr "Anzahl der Vorhaben, die ausgelassen werden" + +#: models.py:192 +msgid "template" +msgstr "Template" + +#: models.py:196 +msgid "template used to display the plugin" +msgstr "Template, mit dem das Plugin angezeigt wird" + +#: models.py:215 +msgid "All matching plans" +msgstr "Alle zutreffenden Vorhaben" + +#: models.py:216 +#, python-format +msgid "%s matching plans" +msgstr "%s zutreffenden Vorhaben" + +#: urls.py:10 +msgctxt "url part" +msgid "/plan//" +msgstr "/vorhaben//" diff --git a/froide_govplan/migrations/0001_initial.py b/froide_govplan/migrations/0001_initial.py new file mode 100644 index 0000000..04d0152 --- /dev/null +++ b/froide_govplan/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# Generated by Django 3.2.8 on 2022-02-15 19:55 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import filer.fields.image +import taggit.managers + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('publicbody', '0039_publicbody_alternative_emails'), + ('auth', '0012_alter_user_first_name_max_length'), + ('foirequest', '0054_alter_foirequest_options'), + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ('organization', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CategorizedGovernmentPlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'Categorized Government Plan', + 'verbose_name_plural': 'Categorized Government Plans', + }, + ), + migrations.CreateModel( + name='Government', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('slug', models.SlugField(max_length=255, unique=True)), + ('public', models.BooleanField(default=False)), + ('description', models.TextField(blank=True)), + ('start_date', models.DateField(blank=True, null=True)), + ('end_date', models.DateField(blank=True, null=True)), + ('planning_document', models.URLField(blank=True)), + ('jurisdiction', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='publicbody.jurisdiction')), + ], + options={ + 'verbose_name': 'Government', + 'verbose_name_plural': 'Governments', + }, + ), + migrations.CreateModel( + name='GovernmentPlan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('slug', models.SlugField(max_length=255, unique=True)), + ('description', models.TextField(blank=True)), + ('public', models.BooleanField(default=False)), + ('status', models.CharField(choices=[('not_started', 'not started'), ('started', 'started'), ('partially_implemented', 'partially implemented'), ('implemented', 'implemented'), ('deferred', 'deferred')], default='needs_approval', max_length=25)), + ('rating', models.IntegerField(blank=True, choices=[(1, 'terrible'), (2, 'bad'), (3, 'OK'), (4, 'good'), (5, 'excellent')], null=True)), + ('reference', models.CharField(blank=True, max_length=255)), + ('categories', taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='froide_govplan.CategorizedGovernmentPlan', to='publicbody.Category', verbose_name='categories')), + ('government', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='froide_govplan.government')), + ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='auth.group')), + ('image', filer.fields.image.FilerImageField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.FILER_IMAGE_MODEL, verbose_name='image')), + ('responsible_publicbody', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='publicbody.publicbody')), + ], + options={ + 'verbose_name': 'Government plan', + 'verbose_name_plural': 'Government plans', + 'ordering': ('reference', 'title'), + }, + ), + migrations.CreateModel( + name='GovernmentPlanUpdate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('title', models.CharField(blank=True, max_length=1024)), + ('content', models.TextField(blank=True)), + ('status', models.CharField(blank=True, choices=[('not_started', 'not started'), ('started', 'started'), ('partially_implemented', 'partially implemented'), ('implemented', 'implemented'), ('deferred', 'deferred')], default='', max_length=25)), + ('rating', models.IntegerField(blank=True, choices=[(1, 'terrible'), (2, 'bad'), (3, 'OK'), (4, 'good'), (5, 'excellent')], null=True)), + ('public', models.BooleanField(default=False)), + ('foirequest', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='foirequest.foirequest')), + ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organization.organization')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='updates', to='froide_govplan.governmentplan')), + ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Plan update', + 'verbose_name_plural': 'Plan updates', + 'ordering': ('-timestamp',), + 'get_latest_by': 'timestamp', + }, + ), + migrations.AddField( + model_name='categorizedgovernmentplan', + name='content_object', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='froide_govplan.governmentplan'), + ), + migrations.AddField( + model_name='categorizedgovernmentplan', + name='tag', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='categorized_governmentplan', to='publicbody.category'), + ), + ] diff --git a/froide_govplan/migrations/0002_auto_20220215_2113.py b/froide_govplan/migrations/0002_auto_20220215_2113.py new file mode 100644 index 0000000..c4494f5 --- /dev/null +++ b/froide_govplan/migrations/0002_auto_20220215_2113.py @@ -0,0 +1,42 @@ +# Generated by Django 3.2.8 on 2022-02-15 20:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('publicbody', '0039_publicbody_alternative_emails'), + ('organization', '0001_initial'), + ('cms', '0022_auto_20180620_1551'), + ('froide_govplan', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='governmentplan', + name='organization', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='organization.organization'), + ), + migrations.AddField( + model_name='governmentplanupdate', + name='url', + field=models.URLField(blank=True), + ), + migrations.CreateModel( + name='GovernmentPlansCMSPlugin', + fields=[ + ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='froide_govplan_governmentplanscmsplugin', serialize=False, to='cms.cmsplugin')), + ('count', models.PositiveIntegerField(default=1, help_text='0 means all the plans', verbose_name='number of plans')), + ('offset', models.PositiveIntegerField(default=0, help_text='number of plans to skip from top of list', verbose_name='offset')), + ('template', models.CharField(blank=True, choices=[('froide_govplan/plugins/default.html', 'Normal')], help_text='template used to display the plugin', max_length=250, verbose_name='template')), + ('categories', models.ManyToManyField(blank=True, to='publicbody.Category', verbose_name='categories')), + ('government', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='froide_govplan.government')), + ], + options={ + 'abstract': False, + }, + bases=('cms.cmsplugin',), + ), + ] diff --git a/froide_govplan/migrations/__init__.py b/froide_govplan/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/froide_govplan/models.py b/froide_govplan/models.py new file mode 100644 index 0000000..466b4ee --- /dev/null +++ b/froide_govplan/models.py @@ -0,0 +1,243 @@ +from django.conf import settings +from django.contrib.auth.models import Group +from django.db import models +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from filer.fields.image import FilerImageField +from taggit.managers import TaggableManager +from taggit.models import TaggedItemBase + +from froide.foirequest.models import FoiRequest +from froide.organization.models import Organization +from froide.publicbody.models import Category, Jurisdiction, PublicBody + +try: + from cms.models.pluginmodel import CMSPlugin +except ImportError: + CMSPlugin = None + + +class PlanStatus(models.TextChoices): + NOT_STARTED = ("not_started", _("not started")) + STARTED = ("started", _("started")) + PARTIALLY_IMPLEMENTED = ("partially_implemented", _("partially implemented")) + IMPLEMENTED = ("implemented", _("implemented")) + DEFERRED = ("deferred", _("deferred")) + + +class PlanRating(models.IntegerChoices): + TERRIBLE = 1, _("terrible") + BAD = 2, _("bad") + OK = 3, _("OK") + GOOD = 4, _("good") + EXCELLENT = 5, _("excellent") + + +class Government(models.Model): + name = models.CharField(max_length=255) + slug = models.SlugField(max_length=255, unique=True) + + public = models.BooleanField(default=False) + jurisdiction = models.ForeignKey(Jurisdiction, null=True, on_delete=models.SET_NULL) + description = models.TextField(blank=True) + + start_date = models.DateField(null=True, blank=True) + end_date = models.DateField(null=True, blank=True) + + planning_document = models.URLField(blank=True) + + class Meta: + verbose_name = _("Government") + verbose_name_plural = _("Governments") + + def __str__(self): + return self.name + + +class CategorizedGovernmentPlan(TaggedItemBase): + tag = models.ForeignKey( + Category, on_delete=models.CASCADE, related_name="categorized_governmentplan" + ) + content_object = models.ForeignKey("GovernmentPlan", on_delete=models.CASCADE) + + class Meta: + verbose_name = _("Categorized Government Plan") + verbose_name_plural = _("Categorized Government Plans") + + +class GovernmentPlan(models.Model): + government = models.ForeignKey(Government, on_delete=models.CASCADE) + title = models.CharField(max_length=255) + slug = models.SlugField(max_length=255, unique=True) + + image = FilerImageField( + null=True, + blank=True, + default=None, + verbose_name=_("image"), + on_delete=models.SET_NULL, + ) + + description = models.TextField(blank=True) + public = models.BooleanField(default=False) + + status = models.CharField( + max_length=25, choices=PlanStatus.choices, default="needs_approval" + ) + rating = models.IntegerField(choices=PlanRating.choices, null=True, blank=True) + + reference = models.CharField(max_length=255, blank=True) + + categories = TaggableManager( + through=CategorizedGovernmentPlan, verbose_name=_("categories"), blank=True + ) + responsible_publicbody = models.ForeignKey( + PublicBody, null=True, blank=True, on_delete=models.SET_NULL + ) + + organization = models.ForeignKey( + Organization, null=True, blank=True, on_delete=models.SET_NULL + ) + + group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) + + class Meta: + ordering = ("reference", "title") + verbose_name = _("Government plan") + verbose_name_plural = _("Government plans") + + def __str__(self): + return 'GovernmentPlan "%s" (#%s)' % (self.title, self.pk) + + def get_absolute_url(self): + return reverse("govplan:plan", kwargs={ + "gov": self.government.slug, + "plan": self.slug + }) + + def get_absolute_domain_url(self): + return settings.SITE_URL + self.get_absolute_url() + + def get_reference_link(self): + if self.reference.startswith("https://"): + return self.reference + return "{}#{}".format( + self.government.planning_document, + self.reference + ) + + +class GovernmentPlanUpdate(models.Model): + plan = models.ForeignKey( + GovernmentPlan, on_delete=models.CASCADE, related_name="updates" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL + ) + organization = models.ForeignKey( + Organization, null=True, blank=True, on_delete=models.SET_NULL + ) + timestamp = models.DateTimeField(default=timezone.now) + title = models.CharField(max_length=1024, blank=True) + content = models.TextField(blank=True) + url = models.URLField(blank=True) + + status = models.CharField( + max_length=25, choices=PlanStatus.choices, default="", blank=True + ) + rating = models.IntegerField(choices=PlanRating.choices, null=True, blank=True) + public = models.BooleanField(default=False) + + foirequest = models.ForeignKey( + FoiRequest, null=True, blank=True, on_delete=models.SET_NULL + ) + + class Meta: + ordering = ("-timestamp",) + get_latest_by = "timestamp" + verbose_name = _("Plan update") + verbose_name_plural = _("Plan updates") + + def __str__(self): + return "Plan Update (%s)" % (self.pk,) + + +if CMSPlugin: + + PLUGIN_TEMPLATES = [ + ("froide_govplan/plugins/default.html", _("Normal")), + ] + + class GovernmentPlansCMSPlugin(CMSPlugin): + """ + CMS Plugin for displaying latest articles + """ + + government = models.ForeignKey(Government, null=True, blank=True, on_delete=models.SET_NULL) + categories = models.ManyToManyField( + Category, verbose_name=_("categories"), blank=True + ) + + count = models.PositiveIntegerField( + _("number of plans"), default=1, help_text=_("0 means all the plans") + ) + offset = models.PositiveIntegerField( + _("offset"), + default=0, + help_text=_("number of plans to skip from top of list"), + ) + template = models.CharField( + _("template"), + blank=True, + max_length=250, + choices=PLUGIN_TEMPLATES, + help_text=_("template used to display the plugin"), + ) + + @property + def render_template(self): + """ + Override render_template to use + the template_to_render attribute + """ + return self.template_to_render + + def copy_relations(self, old_instance): + """ + Duplicate ManyToMany relations on plugin copy + """ + self.categories.set(old_instance.categories.all()) + + def __str__(self): + if self.count == 0: + return str(_("All matching plans")) + return _("%s matching plans") % self.count + + def get_plans(self, request, published_only=True): + if ( + published_only + or not request + or not getattr(request, "toolbar", False) + or not request.toolbar.edit_mode_active + ): + plans = GovernmentPlan.objects.filter(public=True) + else: + plans = GovernmentPlan.objects.all() + + filters = {} + if self.government: + filters["government"] = self.government + + cat_list = self.categories.all().values_list("id", flat=True) + if cat_list: + filters["categories__in"] = cat_list + + plans = plans.filter(**filters).distinct() + plans = plans.prefetch_related( + "categories", "government", "organization" + ) + if self.count == 0: + return plans[self.offset:] + return plans[self.offset : self.offset + self.count] diff --git a/froide_govplan/templates/froide_govplan/detail.html b/froide_govplan/templates/froide_govplan/detail.html new file mode 100644 index 0000000..8641afc --- /dev/null +++ b/froide_govplan/templates/froide_govplan/detail.html @@ -0,0 +1,81 @@ +{% extends CMS_TEMPLATE %} + +{% load i18n %} +{% load markup %} +{% load cms_tags %} + +{% block title %}{{ object.title }}{% endblock %} + + +{% block app_body %} +
+ +
+ Vorhaben der {{ object.government.name }} +

+ {{ object.title }} +

+
+ + {% if object.image %} + {# this should not be a dependency! #} + {% include "fds_blog/includes/_picture.html" with picture=object.image %} + {% endif %} + +

Status: {{ object.get_status_display }}

+ {% if object.rating %} +

Rating: {{ object.get_rating_display }}

+ {% endif %} + + {{ object.description | safe }} + +

+ Quelle +

+ + {% if object.responsible_publicbody %} +

+ Federführung: + + {{ object.responsible_publicbody.name }} + +

+ {% endif %} + + {% if object.organization %} +

+ Dieses Vorhaben wird beobachtet von + + {{ object.organization.name }} + +

+ {% endif %} + +

Neueste Entwicklungen

+ {% for update in updates %} +

{{ update.title }}

+ + + + von {% if update.user %}{{ update.user.get_full_name }}{% endif %}{% if update.organization %}({{ update.organization.name }}){% endif %} + + + {{ update.content|markdown }} + + {% if update.url %} + Mehr lesen + {% endif %} + + {% if update.foirequest %} +

+ Relevante Anfrage: + + {{ update.foirequest.title }} + +

+ {% endif %} + {% empty %} + Keine Entwicklungen + {% endfor %} +
+{% endblock %} diff --git a/froide_govplan/templates/froide_govplan/plugins/default.html b/froide_govplan/templates/froide_govplan/plugins/default.html new file mode 100644 index 0000000..f50a109 --- /dev/null +++ b/froide_govplan/templates/froide_govplan/plugins/default.html @@ -0,0 +1,9 @@ + diff --git a/froide_govplan/urls.py b/froide_govplan/urls.py new file mode 100644 index 0000000..df609f5 --- /dev/null +++ b/froide_govplan/urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from django.utils.translation import pgettext_lazy + +from .views import GovPlanDetailView + +app_name = "govplan" + +urlpatterns = [ + path( + pgettext_lazy("url part", "/plan//"), + GovPlanDetailView.as_view(), + name="plan", + ), +] diff --git a/froide_govplan/views.py b/froide_govplan/views.py new file mode 100644 index 0000000..d7c1e89 --- /dev/null +++ b/froide_govplan/views.py @@ -0,0 +1,18 @@ +from django.views.generic import DetailView + +from .models import GovernmentPlan + + +class GovPlanDetailView(DetailView): + slug_url_kwarg = "plan" + template_name = "froide_govplan/detail.html" + + def get_queryset(self): + return GovernmentPlan.objects.filter(public=True) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["updates"] = self.object.updates.filter(public=True).order_by( + "-timestamp" + ) + return context diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..0bd3287 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,17 @@ +[wheel] +universal = 1 + +[flake8] +extend-ignore = E203,E501,C901 +max-line-length = 88 +select = C,E,F,W,B,B950 +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist +max-complexity = 10 + +[isort] +profile = black +src_paths = froide +default_section = THIRDPARTY +known_first_party = froide +known_django = django +sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3e9d782 --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import os +import re +import codecs + +from setuptools import setup, find_packages + + +def read(*parts): + filename = os.path.join(os.path.dirname(__file__), *parts) + with codecs.open(filename, encoding='utf-8') as fp: + return fp.read() + + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + + +setup( + name="froide_govplan", + version=find_version("froide_govplan", "__init__.py"), + url='https://github.com/okfde/froide-govplan', + license='MIT', + description="Froide govplan app", + long_description=read('README.md'), + author='Stefan Wehrmeyer', + author_email='mail@stefanwehrmeyer.com', + packages=find_packages(), + install_requires=[ + 'froide', + ], + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Utilities', + ], + zip_safe=False, +)