diff --git a/peachjam/migrations/0126_courtclass_slug.py b/peachjam/migrations/0126_courtclass_slug.py new file mode 100644 index 000000000..7da4de2b0 --- /dev/null +++ b/peachjam/migrations/0126_courtclass_slug.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.24 on 2024-04-23 08:26 + +from django.db import migrations, models +from django.utils.text import slugify + + +def create_slug(apps, schema_editor): + CourtClass = apps.get_model("peachjam", "CourtClass") + for court_class in CourtClass.objects.all(): + court_class.slug = slugify(court_class.name) + court_class.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("peachjam", "0125_judgment_auto_assign_title"), + ] + + operations = [ + migrations.AddField( + model_name="courtclass", + name="slug", + field=models.SlugField(default="", max_length=255, verbose_name="slug"), + preserve_default=False, + ), + migrations.RunPython(create_slug, migrations.RunPython.noop), + ] diff --git a/peachjam/migrations/0127_alter_courtclass_slug.py b/peachjam/migrations/0127_alter_courtclass_slug.py new file mode 100644 index 000000000..8e81dfbcc --- /dev/null +++ b/peachjam/migrations/0127_alter_courtclass_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.24 on 2024-04-23 08:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("peachjam", "0126_courtclass_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="courtclass", + name="slug", + field=models.SlugField(max_length=255, unique=True, verbose_name="slug"), + ), + ] diff --git a/peachjam/migrations/0128_courtclass_show_listing_page.py b/peachjam/migrations/0128_courtclass_show_listing_page.py new file mode 100644 index 000000000..583815408 --- /dev/null +++ b/peachjam/migrations/0128_courtclass_show_listing_page.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.24 on 2024-04-23 08:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("peachjam", "0127_alter_courtclass_slug"), + ] + + operations = [ + migrations.AddField( + model_name="courtclass", + name="show_listing_page", + field=models.BooleanField(default=False), + ), + ] diff --git a/peachjam/models/judgment.py b/peachjam/models/judgment.py index f0948ce74..b46c9d178 100644 --- a/peachjam/models/judgment.py +++ b/peachjam/models/judgment.py @@ -74,7 +74,9 @@ def __str__(self): class CourtClass(models.Model): name = models.CharField(_("name"), max_length=100, null=False, unique=True) description = models.TextField(_("description"), null=True, blank=True) + slug = models.SlugField(_("slug"), max_length=255, null=False, unique=True) order = models.IntegerField(_("order"), null=True, blank=True) + show_listing_page = models.BooleanField(null=False, default=False) class Meta: ordering = ( @@ -87,6 +89,13 @@ class Meta: def __str__(self): return self.name + def get_absolute_url(self): + return reverse("court_class", args=[self.slug]) + + def save(self, *args, **kwargs): + self.slug = slugify(self.name) + return super().save(*args, **kwargs) + class Court(models.Model): name = models.CharField(_("name"), max_length=255, null=False, unique=True) @@ -128,6 +137,9 @@ class Meta: def __str__(self): return self.name + def get_absolute_url(self): + return reverse("court", args=[self.code]) + class CourtRegistryManager(models.Manager): def get_queryset(self): diff --git a/peachjam/static/stylesheets/_global.scss b/peachjam/static/stylesheets/_global.scss index 5333f20c4..138394e6d 100644 --- a/peachjam/static/stylesheets/_global.scss +++ b/peachjam/static/stylesheets/_global.scss @@ -18,7 +18,8 @@ main { flex-grow: 1; } -.courts__column-wrapper { +// newspaper-like dynamic columns +.flow-columns { columns: 3 auto; @include media-breakpoint-down(lg) { @@ -28,10 +29,10 @@ main { @include media-breakpoint-down(sm) { columns: 1; } -} -.courts__item { - break-inside: avoid; + .flow-columns-group { + break-inside: avoid; + } } mark { diff --git a/peachjam/templates/peachjam/_court_class_month_list.html b/peachjam/templates/peachjam/_court_class_month_list.html new file mode 100644 index 000000000..7d45f7598 --- /dev/null +++ b/peachjam/templates/peachjam/_court_class_month_list.html @@ -0,0 +1,11 @@ +{% extends "peachjam/_court_months_list.html" %} +{% load i18n %} +{% block month_link %} + {{ m|date:"F" }} +{% endblock %} +{% block month_option %} + +{% endblock %} diff --git a/peachjam/templates/peachjam/_court_class_years_list.html b/peachjam/templates/peachjam/_court_class_years_list.html new file mode 100644 index 000000000..e6ef9c082 --- /dev/null +++ b/peachjam/templates/peachjam/_court_class_years_list.html @@ -0,0 +1,11 @@ +{% extends "peachjam/_court_years_list.html" %} +{% load i18n %} +{% block year_link %} + {{ y.year }} +{% endblock %} +{% block year_option %} + +{% endblock %} diff --git a/peachjam/templates/peachjam/_court_list.html b/peachjam/templates/peachjam/_court_list.html index ae64bb421..f32ec9b42 100644 --- a/peachjam/templates/peachjam/_court_list.html +++ b/peachjam/templates/peachjam/_court_list.html @@ -1,9 +1,14 @@ {% load peachjam i18n %} {% if court_classes %} -
+
{% for court_class in court_classes %} -
+

{{ court_class }}

+ {% if court_class.show_listing_page %} + + {% endif %} {% if court_class.courts.all %}
    {% for court in court_class.courts.all %} diff --git a/peachjam/templates/peachjam/_court_months_list.html b/peachjam/templates/peachjam/_court_months_list.html new file mode 100644 index 000000000..0d4472fc9 --- /dev/null +++ b/peachjam/templates/peachjam/_court_months_list.html @@ -0,0 +1,38 @@ +{% load i18n %} +{% if months and years %} +
    + + +
      + {% for m in months %} +
    • + {% if m|date:"F" == month %} + {{ m|date:"F" }} + {% else %} + {% block month_link %} + {{ m|date:"F" }} + {% endblock %} + {% endif %} +
    • + {% endfor %} +
    +
    + +{% endif %} diff --git a/peachjam/templates/peachjam/_court_registries_months_list.html b/peachjam/templates/peachjam/_court_registries_months_list.html new file mode 100644 index 000000000..9e5726e97 --- /dev/null +++ b/peachjam/templates/peachjam/_court_registries_months_list.html @@ -0,0 +1,11 @@ +{% extends "peachjam/_court_months_list.html" %} +{% load i18n %} +{% block month_link %} + {{ m|date:"F" }} +{% endblock %} +{% block month_option %} + +{% endblock %} diff --git a/peachjam/templates/peachjam/_court_registries_years_list.html b/peachjam/templates/peachjam/_court_registries_years_list.html new file mode 100644 index 000000000..5c07f69b9 --- /dev/null +++ b/peachjam/templates/peachjam/_court_registries_years_list.html @@ -0,0 +1,11 @@ +{% extends "peachjam/_court_years_list.html" %} +{% load i18n %} +{% block year_link %} + {{ y.year }} +{% endblock %} +{% block year_option %} + +{% endblock %} diff --git a/peachjam/templates/peachjam/_court_years_list.html b/peachjam/templates/peachjam/_court_years_list.html new file mode 100644 index 000000000..1341923a2 --- /dev/null +++ b/peachjam/templates/peachjam/_court_years_list.html @@ -0,0 +1,37 @@ +{% load i18n %} +{% if years %} +
    + + +
      + {% for y in years %} +
    • + {% if y.year == year %} + {{ y.year }} + {% else %} + {% block year_link %} + {{ y.year }} + {% endblock %} + {% endif %} +
    • + {% endfor %} +
    +
    + +{% endif %} diff --git a/peachjam/templates/peachjam/_registries.html b/peachjam/templates/peachjam/_registries.html index 1277913a0..0aa50f1cd 100644 --- a/peachjam/templates/peachjam/_registries.html +++ b/peachjam/templates/peachjam/_registries.html @@ -1,15 +1,15 @@ {% load i18n %} {% if registries %}

    {{ registry_label_plural }}

    -
    +
    {% for group in registry_groups %} -
    +
      {% for r in group %} {% if r.code == registry.code %}
    • {{ r.name }}
    • {% else %} -
    • +
    • {{ r.name }}
    • {% endif %} diff --git a/peachjam/templates/peachjam/court_class_detail.html b/peachjam/templates/peachjam/court_class_detail.html new file mode 100644 index 000000000..70d5578ef --- /dev/null +++ b/peachjam/templates/peachjam/court_class_detail.html @@ -0,0 +1,8 @@ +{% extends 'peachjam/court_detail.html' %} +{% load i18n %} +{% block year_list %} + {% include "peachjam/_court_class_years_list.html" %} +{% endblock %} +{% block month_list %} + {% include "peachjam/_court_class_month_list.html" %} +{% endblock %} diff --git a/peachjam/templates/peachjam/court_detail.html b/peachjam/templates/peachjam/court_detail.html index 4cbc2e3f6..5eed9cbdb 100644 --- a/peachjam/templates/peachjam/court_detail.html +++ b/peachjam/templates/peachjam/court_detail.html @@ -1,6 +1,6 @@ {% extends 'peachjam/layouts/document_list.html' %} {% load i18n %} -{% block title %}{{ formatted_court_name }}{% endblock %} +{% block title %}{{ page_title }}{% endblock %} {% block breadcrumbs %}
      @@ -23,13 +39,17 @@ {% endwith %} {% endblock %} {% block page-header %} -

      {{ formatted_court_name }}

      +

      {{ page_title }}

      {% include 'peachjam/_document_count.html' %} - {% include 'peachjam/_registries.html' %} - {% include 'peachjam/_years_list.html' %} - {% if months %} - {% include 'peachjam/_months_list.html' %} - {% endif %} + {% block court_list %} + {% include 'peachjam/_registries.html' %} + {% endblock %} + {% block year_list %} + {% include 'peachjam/_court_years_list.html' %} + {% endblock %} + {% block month_list %} + {% include 'peachjam/_court_months_list.html' %} + {% endblock %} {% endblock %} {% block content %} {% if grouped_documents %} diff --git a/peachjam/templates/peachjam/court_registry_detail.html b/peachjam/templates/peachjam/court_registry_detail.html index 2722c6e29..2c5247f49 100644 --- a/peachjam/templates/peachjam/court_registry_detail.html +++ b/peachjam/templates/peachjam/court_registry_detail.html @@ -1,18 +1,8 @@ {% extends 'peachjam/court_detail.html' %} {% load i18n %} -{% block title %}{{ formatted_court_name }}{% endblock %} -{% block breadcrumbs %} -
      - -
      +{% block year_list %} + {% include "peachjam/_court_registries_years_list.html" %} +{% endblock %} +{% block month_list %} + {% include "peachjam/_court_registries_months_list.html" %} {% endblock %} diff --git a/peachjam/tests/test_views.py b/peachjam/tests/test_views.py index c77ab2d17..fe91e57b2 100644 --- a/peachjam/tests/test_views.py +++ b/peachjam/tests/test_views.py @@ -55,13 +55,8 @@ def test_court_listing(self): "Ababacar and Ors vs Senegal [2018] ECOWASCJ 17 (29 June 2018)", documents, ) - self.assertEqual( - response.context["years"], - [ - {"url": "/judgments/ECOWASCJ/2018/", "year": 2018}, - {"url": "/judgments/ECOWASCJ/2016/", "year": 2016}, - ], - ) + self.assertContains(response, "/judgments/ECOWASCJ/2018/") + self.assertContains(response, "/judgments/ECOWASCJ/2016/") self.assertNotIn("years", response.context["facet_data"], [2016, 2018]) def test_court_year_listing(self): @@ -79,13 +74,8 @@ def test_court_year_listing(self): documents, ) self.assertEqual(response.context["year"], 2016) - self.assertEqual( - response.context["years"], - [ - {"url": "/judgments/ECOWASCJ/2018/", "year": 2018}, - {"url": "/judgments/ECOWASCJ/2016/", "year": 2016}, - ], - ) + self.assertContains(response, "/judgments/ECOWASCJ/2018/") + self.assertContains(response, "/judgments/ECOWASCJ/2016/") self.assertNotIn("years", response.context["facet_data"], [2016, 2018]) def test_judgment_detail(self): diff --git a/peachjam/urls.py b/peachjam/urls.py index 269bae934..677027791 100644 --- a/peachjam/urls.py +++ b/peachjam/urls.py @@ -36,6 +36,9 @@ ArticleYearArchiveView, AuthorDetailView, BookListView, + CourtClassDetailView, + CourtClassMonthView, + CourtClassYearView, CourtDetailView, CourtMonthView, CourtRegistryDetailView, @@ -93,6 +96,21 @@ name="author", ), path("judgments/", JudgmentListView.as_view(), name="judgment_list"), + path( + "judgments/court-class//", + CourtClassDetailView.as_view(), + name="court_class", + ), + path( + "judgments/court-class///", + CourtClassYearView.as_view(), + name="court_class_year", + ), + path( + "judgments/court-class////", + CourtClassMonthView.as_view(), + name="court_class_month", + ), path( "judgments//", CourtDetailView.as_view(), diff --git a/peachjam/views/courts.py b/peachjam/views/courts.py index 48d881f4c..665528595 100644 --- a/peachjam/views/courts.py +++ b/peachjam/views/courts.py @@ -1,69 +1,39 @@ +from functools import cached_property from itertools import groupby +from django.http import Http404 from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.dates import MONTHS +from django.utils.text import gettext_lazy as _ from peachjam.helpers import chunks, lowercase_alphabet -from peachjam.models import Court, CourtRegistry, Judgment +from peachjam.models import Court, CourtClass, CourtRegistry, Judgment from peachjam.views.generic_views import FilteredDocumentListView -class CourtDetailView(FilteredDocumentListView): - """List View class for filtering a court's judgments. - - This view may be restricted to a specific year. - """ +class FilteredJudgmentView(FilteredDocumentListView): + """Base List View class for filtering judgments.""" model = Judgment - template_name = "peachjam/court_detail.html" navbar_link = "judgments" - queryset = Judgment.objects.prefetch_related("labels") - - def get_base_queryset(self): - qs = super().get_base_queryset().filter(court=self.court) - if "year" in self.kwargs: - qs = qs.filter(date__year=self.kwargs["year"]) - - if "month" in self.kwargs: - qs = qs.filter(date__month=self.kwargs["month"]) + queryset = Judgment.objects.prefetch_related("judges", "labels") - return qs + def base_view_name(self): + return _("Judgments") - def get_queryset(self): - self.court = get_object_or_404(Court, code=self.kwargs["code"]) - return super().get_queryset() + def page_title(self): + return self.base_view_name() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["doc_type"] = "Judgment" - context["court"] = self.court - context["formatted_court_name"] = self.formatted_court_name() - context["registry_label_plural"] = CourtRegistry.model_label_plural - context["registries"] = self.court.registries.exclude( - judgments__isnull=True - ) # display registries with judgments only - context["registry_groups"] = list(chunks(context["registries"], 2)) + context["page_title"] = self.page_title() if not self.form.cleaned_data.get("alphabet"): context["grouped_documents"] = self.grouped_judgments(context["documents"]) - if "year" in self.kwargs: - context["year"] = self.kwargs["year"] - - if "month" in self.kwargs and self.kwargs["month"] in set(range(1, 13)): - context["month"] = MONTHS[self.kwargs["month"]] - context[ - "formatted_court_name" - ] = f"{context['formatted_court_name']} - {context['month']} {self.kwargs['year']}" - else: - context[ - "formatted_court_name" - ] = f"{context['formatted_court_name']} - {self.kwargs['year']}" - - self.populate_months(context) - self.populate_years(context) self.populate_facets(context) @@ -110,118 +80,183 @@ def populate_facets(self, context): "order_outcomes": order_outcomes, } - def formatted_court_name(self): - return self.court.name - def populate_years(self, context): - years = self.queryset.filter(court=self.court).dates( + context["years"] = self.get_base_queryset(exclude=["year", "month"]).dates( "date", "year", order="DESC" ) - context["years"] = [ - { - "url": reverse("court_year", args=[self.court.code, year.year]), - "year": year.year, - } - for year in years - ] - context["all_years_url"] = reverse("court", args=[self.court.code]) + + def grouped_judgments(self, documents): + """Group the judgments by month and return a list of dicts with the month name and judgments for that month""" + # Group documents by month + groups = groupby(documents, lambda d: f"{MONTHS[d.date.month]} {d.date.year}") + + return [{"key": key, "judgments": list(group)} for key, group in groups] + + +class CourtDetailView(FilteredJudgmentView): + template_name = "peachjam/court_detail.html" + + @cached_property + def court(self): + return get_object_or_404(Court, code=self.kwargs.get("code")) + + def base_view_name(self): + return self.court.name + + def get_base_queryset(self, exclude=None): + qs = super().get_base_queryset(exclude=exclude).filter(court=self.court) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["court"] = self.court + context["registry_label_plural"] = CourtRegistry.model_label_plural + context["registries"] = self.court.registries.exclude( + judgments__isnull=True + ) # display registries with judgments only + context["registry_groups"] = list(chunks(context["registries"], 2)) + context["all_years_url"] = self.court.get_absolute_url() + return context + + +class YearMixin: + @property + def year(self): + return self.kwargs["year"] + + def page_title(self): + return f"{super().page_title()} - {self.year}" + + def get_base_queryset(self, exclude=None): + qs = super().get_base_queryset() + if exclude is None: + exclude = [] + if "year" not in exclude: + qs = qs.filter(date__year=self.kwargs["year"]) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["year"] = self.year + self.populate_months(context) + return context def populate_months(self, context): - year = self.kwargs["year"] - months = self.queryset.filter(court=self.court, date__year=year).dates( + context["months"] = self.get_base_queryset(exclude=["month"]).dates( "date", "month", order="ASC" ) - context["months"] = [ - { - "url": reverse("court_month", args=[self.court.code, year, item.month]), - "month": MONTHS[item.month], - } - for item in months - ] + + +class CourtYearView(YearMixin, CourtDetailView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context["all_months_url"] = reverse( - "court_year", args=[self.court.code, self.kwargs["year"]] + "court_year", args=[self.court.code, self.year] ) + return context - def grouped_judgments(self, documents): - """Group the judgments by month and return a list of dicts with the month name and judgments for that month""" - # Group documents by month - groups = groupby(documents, lambda d: f"{MONTHS[d.date.month]} {d.date.year}") - return [{"key": key, "judgments": list(group)} for key, group in groups] +class MonthMixin: + @property + def month(self): + if self.kwargs["month"] not in set(range(1, 13)): + raise Http404("Invalid month") + return self.kwargs["month"] + def page_title(self): + return f"{super().page_title()} {MONTHS[self.month]}" -class CourtYearView(CourtDetailView): - pass + def get_base_queryset(self, exclude=None): + if exclude is None: + exclude = [] + qs = super().get_base_queryset(exclude=exclude) + if "month" not in exclude: + qs = qs.filter(date__month=self.month) + return qs + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["month"] = MONTHS[self.month] + return context -class CourtMonthView(CourtDetailView): +class CourtMonthView(MonthMixin, CourtYearView): pass -class CourtRegistryDetailView(CourtDetailView): - queryset = Judgment.objects.prefetch_related("judges", "labels") +class RegistryMixin: template_name = "peachjam/court_registry_detail.html" - def get_base_queryset(self): - return super().get_base_queryset().filter(registry=self.registry) + def base_view_name(self): + return self.registry.name - def get_queryset(self): - self.court = get_object_or_404(Court, code=self.kwargs["code"]) - self.registry = get_object_or_404( - CourtRegistry, code=self.kwargs["registry_code"] - ) - return super().get_queryset() + @cached_property + def registry(self): + return get_object_or_404(CourtRegistry, code=self.kwargs.get("registry_code")) + + def get_base_queryset(self, *args, **kwargs): + return super().get_base_queryset(*args, **kwargs).filter(registry=self.registry) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["registry"] = self.registry + context["all_years_url"] = self.registry.get_absolute_url() return context - def formatted_court_name(self): - return self.registry.name - def populate_years(self, context): - years = self.queryset.filter(registry=self.registry).dates( - "date", "year", order="DESC" - ) - context["years"] = [ - { - "url": reverse( - "court_registry_year", - args=[self.court.code, self.registry.code, year.year], - ), - "year": year.year, - } - for year in years - ] - context["all_years_url"] = reverse( - "court_registry", args=[self.court.code, self.registry.code] - ) +class CourtRegistryDetailView(RegistryMixin, CourtDetailView): + pass - def populate_months(self, context): - year = self.kwargs["year"] - months = self.queryset.filter(registry=self.registry, date__year=year).dates( - "date", "month", order="ASC" - ) - context["months"] = [ - { - "url": reverse( - "court_registry_month", - args=[self.court.code, self.registry.code, year, item.month], - ), - "month": MONTHS[item.month], - } - for item in months - ] + +class CourtRegistryYearView(YearMixin, CourtRegistryDetailView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context["all_months_url"] = reverse( - "court_registry_year", - args=[self.court.code, self.registry.code, self.kwargs["year"]], + "court_registry_year", args=[self.court.code, self.registry.code, self.year] ) + return context -class CourtRegistryYearView(CourtRegistryDetailView): +class CourtRegistryMonthView(MonthMixin, CourtRegistryYearView): pass -class CourtRegistryMonthView(CourtRegistryDetailView): +class CourtClassDetailView(FilteredJudgmentView): + template_name = "peachjam/court_class_detail.html" + + def base_view_name(self): + return self.court_class.name + + @cached_property + def court_class(self): + return get_object_or_404(CourtClass, slug=self.kwargs["court_class"]) + + def get_base_queryset(self, exclude=None): + return ( + super() + .get_base_queryset(exclude=exclude) + .filter(court__court_class=self.court_class) + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["court_class"] = self.court_class + context["registries"] = Court.objects.filter(court_class=self.court_class) + context["registry_label_plural"] = _("Courts") + context["registry_groups"] = list(chunks(context["registries"], 2)) + context["all_years_url"] = self.court_class.get_absolute_url() + + return context + + +class CourtClassYearView(YearMixin, CourtClassDetailView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["all_months_url"] = reverse( + "court_class_year", args=[self.court_class.slug, self.year] + ) + return context + + +class CourtClassMonthView(MonthMixin, CourtClassYearView): pass diff --git a/peachjam/views/generic_views.py b/peachjam/views/generic_views.py index 5ff7da9aa..0be56bee4 100644 --- a/peachjam/views/generic_views.py +++ b/peachjam/views/generic_views.py @@ -34,7 +34,7 @@ class DocumentListView(ListView): "nature", "work", "jurisdiction", "locality" ) - def get_base_queryset(self): + def get_base_queryset(self, *args, **kwargs): qs = self.queryset if self.queryset is not None else self.model.objects return qs.filter(published=True)