diff --git a/froide_govplan/admin.py b/froide_govplan/admin.py index 58df0c1..adbfcf8 100644 --- a/froide_govplan/admin.py +++ b/froide_govplan/admin.py @@ -3,11 +3,17 @@ from django.contrib import admin, auth from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ +from adminsortable2.admin import SortableAdminMixin from tinymce.widgets import TinyMCE from froide.helper.widgets import TagAutocompleteWidget -from .models import Government, GovernmentPlan, GovernmentPlanUpdate +from .models import ( + Government, + GovernmentPlan, + GovernmentPlanSection, + GovernmentPlanUpdate, +) User = auth.get_user_model() @@ -208,9 +214,26 @@ class GovernmentPlanUpdateAdmin(admin.ModelAdmin): return super().has_change_permission(request, obj=obj) +class GovernmentPlanSectionAdmin(SortableAdminMixin, admin.ModelAdmin): + save_on_top = True + prepopulated_fields = {"slug": ("title",)} + search_fields = ("title",) + raw_id_fields = ("categories",) + list_display = ( + "title", + "featured", + ) + list_filter = ( + "featured", + "categories", + "government", + ) + + admin.site.register(Government, GovernmentAdmin) admin.site.register(GovernmentPlan, GovernmentPlanAdmin) admin.site.register(GovernmentPlanUpdate, GovernmentPlanUpdateAdmin) +admin.site.register(GovernmentPlanSection, GovernmentPlanSectionAdmin) govplan_admin_site = GovPlanAdminSite(name="govplan") govplan_admin_site.register(GovernmentPlan, GovernmentPlanAdmin) diff --git a/froide_govplan/migrations/0005_governmentplansection.py b/froide_govplan/migrations/0005_governmentplansection.py new file mode 100644 index 0000000..3cf2947 --- /dev/null +++ b/froide_govplan/migrations/0005_governmentplansection.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.12 on 2022-03-14 10:14 + +import cms.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import filer.fields.image + + +class Migration(migrations.Migration): + + dependencies = [ + ('publicbody', '0039_publicbody_alternative_emails'), + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ('cms', '0022_auto_20180620_1551'), + ('froide_govplan', '0004_auto_20220311_2330'), + ] + + operations = [ + migrations.CreateModel( + name='GovernmentPlanSection', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255, verbose_name='title')), + ('slug', models.SlugField(max_length=255, unique=True, verbose_name='slug')), + ('description', models.TextField(blank=True, verbose_name='description')), + ('icon', models.CharField(blank=True, help_text='Enter an icon name from the FontAwesome 4 icon set', max_length=50, verbose_name='Icon')), + ('order', models.PositiveIntegerField(default=0)), + ('featured', models.DateTimeField(blank=True, null=True)), + ('categories', models.ManyToManyField(blank=True, to='publicbody.Category')), + ('content_placeholder', cms.models.fields.PlaceholderField(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, slotname='content', to='cms.placeholder')), + ('government', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='froide_govplan.government', verbose_name='government')), + ('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')), + ], + options={ + 'verbose_name': 'Government plan section', + 'verbose_name_plural': 'Government plan sections', + 'ordering': ('order', 'title'), + }, + ), + ] diff --git a/froide_govplan/models.py b/froide_govplan/models.py index bd100df..67b45da 100644 --- a/froide_govplan/models.py +++ b/froide_govplan/models.py @@ -15,9 +15,11 @@ from froide.organization.models import Organization from froide.publicbody.models import Category, Jurisdiction, PublicBody try: + from cms.models.fields import PlaceholderField from cms.models.pluginmodel import CMSPlugin except ImportError: CMSPlugin = None + PlaceholderField = None class PlanStatus(models.TextChoices): @@ -251,6 +253,60 @@ class GovernmentPlanFollower(Follower): verbose_name_plural = _("Government plan followers") +class GovernmentPlanSection(models.Model): + government = models.ForeignKey( + Government, on_delete=models.CASCADE, verbose_name=_("government") + ) + + title = models.CharField(max_length=255, verbose_name=_("title")) + slug = models.SlugField(max_length=255, unique=True, verbose_name=_("slug")) + + categories = models.ManyToManyField(Category, blank=True) + + description = models.TextField(blank=True, verbose_name=_("description")) + image = FilerImageField( + null=True, + blank=True, + default=None, + verbose_name=_("image"), + on_delete=models.SET_NULL, + ) + + icon = models.CharField( + _("Icon"), + max_length=50, + blank=True, + help_text=_( + """Enter an icon name from the FontAwesome 4 icon set""" + ), + ) + order = models.PositiveIntegerField(default=0) + featured = models.DateTimeField(null=True, blank=True) + + if PlaceholderField: + content_placeholder = PlaceholderField("content") + + class Meta: + verbose_name = _("Government plan section") + verbose_name_plural = _("Government plan sections") + ordering = ( + "order", + "title", + ) + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse( + "govplan:section", + kwargs={"gov": self.government.slug, "section": self.slug}, + ) + + def get_absolute_domain_url(self): + return settings.SITE_URL + self.get_absolute_url() + + if CMSPlugin: PLUGIN_TEMPLATES = [ diff --git a/froide_govplan/templates/froide_govplan/section.html b/froide_govplan/templates/froide_govplan/section.html new file mode 100644 index 0000000..f58575c --- /dev/null +++ b/froide_govplan/templates/froide_govplan/section.html @@ -0,0 +1,29 @@ +{% extends CMS_TEMPLATE %} + +{% load i18n %} +{% load markup %} +{% load cms_tags %} +{% load follow_tags %} + +{% block title %}{{ object.title }}{% endblock %} + + +{% block app_body %} +
+ +
+ {{ 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 %} + + {% render_placeholder object.content_placeholder %} + +
+{% endblock %} diff --git a/froide_govplan/urls.py b/froide_govplan/urls.py index f8455a4..9174f96 100644 --- a/froide_govplan/urls.py +++ b/froide_govplan/urls.py @@ -2,7 +2,7 @@ from django.urls import path from django.utils.translation import pgettext_lazy from .admin import govplan_admin_site -from .views import GovPlanDetailView +from .views import GovPlanDetailView, GovPlanSectionDetailView app_name = "govplan" @@ -13,4 +13,9 @@ urlpatterns = [ GovPlanDetailView.as_view(), name="plan", ), + path( + pgettext_lazy("url part", "//"), + GovPlanSectionDetailView.as_view(), + name="section", + ), ] diff --git a/froide_govplan/utils.py b/froide_govplan/utils.py index 3ca9b39..54b93e7 100644 --- a/froide_govplan/utils.py +++ b/froide_govplan/utils.py @@ -3,9 +3,9 @@ import re from django.template.defaultfilters import slugify -from froide.publicbody.models import PublicBody +from froide.publicbody.models import Category, PublicBody -from .models import GovernmentPlan +from .models import GovernmentPlan, GovernmentPlanSection class PlanImporter(object): @@ -47,13 +47,28 @@ class PlanImporter(object): plan.title = title plan.slug = slugify(title) - def handle_categories(self, plan, categories): + def handle_categories(self, plan, category_name): categories = [ - x.strip() for x in re.split(r" & | und ", categories) if x.strip() + x.strip() for x in re.split(r" & | und ", category_name) if x.strip() ] + self.make_section(category_name, "-".join(categories), categories) if categories: self.post_save_list.append(lambda p: p.categories.set(*categories)) + def make_section(self, section_name, section_slug, categories): + slug = slugify(section_slug) + section, _created = GovernmentPlanSection.objects.get_or_create( + slug=slug, + defaults={ + "government": self.government, + "title": section_name, + }, + ) + section.categories.set([self.get_category(c) for c in categories]) + + def get_category(self, cat_name): + return Category.objects.get(name=cat_name) + def handle_reference(self, plan, reference): plan.reference = ", ".join(re.split(r"\s*[,/]\s*", reference)) diff --git a/froide_govplan/views.py b/froide_govplan/views.py index f48eb56..bdb2650 100644 --- a/froide_govplan/views.py +++ b/froide_govplan/views.py @@ -1,16 +1,51 @@ +from django.shortcuts import get_object_or_404 from django.views.generic import DetailView -from .models import GovernmentPlan +from .models import Government, GovernmentPlan, GovernmentPlanSection -class GovPlanDetailView(DetailView): +class GovernmentMixin: + def get(self, *args, **kwargs): + self.get_government() + return super().get(*args, **kwargs) + + def get_government(self): + filter_kwarg = {} + if not self.request.user.is_authenticated or not self.request.user.is_staff: + filter_kwarg["public"] = True + self.government = get_object_or_404( + Government, slug=self.kwargs["gov"], **filter_kwarg + ) + + +class GovPlanSectionDetailView(GovernmentMixin, DetailView): + slug_url_kwarg = "section" + template_name = "froide_govplan/section.html" + + def get_queryset(self): + return GovernmentPlanSection.objects.filter(government=self.government) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["plans"] = ( + GovernmentPlan.objects.filter( + categories__in=self.object.categories.all(), government=self.government + ) + .distinct() + .order_by("order", "title") + ) + return context + + +class GovPlanDetailView(GovernmentMixin, DetailView): slug_url_kwarg = "plan" template_name = "froide_govplan/detail.html" def get_queryset(self): + qs = GovernmentPlan.objects.filter(government=self.government) if self.request.user.is_authenticated and self.request.user.is_staff: - return GovernmentPlan.objects.all() - return GovernmentPlan.objects.filter(public=True) + return qs + return qs.filter(public=True) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)