Source code for eucrim.procedure.models

# SPDX-FileCopyrightText: 2024 Thomas Breitner <t.breitner@csl.mpg.de>
#
# SPDX-License-Identifier: EUPL-1.2

from django.db import models
from django.forms import CheckboxSelectMultiple
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from django.template.defaultfilters import truncatewords_html
from django.views.defaults import bad_request
from django.core.validators import RegexValidator
from django.utils.text import slugify

from wagtail.search import index
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField
from wagtail.admin.panels import (
    FieldPanel,
    FieldRowPanel,
    HelpPanel,
    InlinePanel,
    MultiFieldPanel,
    TabbedInterface,
    ObjectList,
)
from wagtail.contrib.routable_page.models import RoutablePageMixin, route

from modelcluster.fields import ParentalKey, ParentalManyToManyField

from ..core.models.mixins import RelatedLinkMixin, HideShowInMenusField
from ..core.abstracts import RelatedLink
from .forms import ProcedurePageForm






[docs] class ProcedurePage(HideShowInMenusField, Page): parent_page_types = ["procedure.ProcedureIndexPage"] subpage_types = [] show_in_menus = False celex_section = models.PositiveIntegerField( blank=False, default=6, help_text="Celex section. Defaults to 'Case-law' (sector 6).", )
[docs] class CelexCaseType(models.TextChoices): C_CASE = "C", "ECJ (C-Case)" T_CASE = "T", "General Court (T-Case)"
celex_case_type = models.CharField( max_length=1, choices=CelexCaseType.choices, default=CelexCaseType.C_CASE, help_text="Celex document type / descriptor", ) celex_year = models.PositiveIntegerField( blank=False, help_text="Year (YYYY) in which the case entered the Court’s register.", validators=[ RegexValidator( regex=r"^\d\d\d\d$", message="Enter a valid year in the format YYYY.", code="invalid_year", ), ], ) celex_case = models.PositiveIntegerField( blank=False, help_text="Number (NNNN) of case entered in the register in the concering year.", ) referring_court = models.TextField( blank=True, ) subject = RichTextField( blank=True, ) party = models.TextField( blank=True, ) ppu = models.BooleanField( default=False, verbose_name="PPU", help_text='"Eilverfahren" (valid translation not available atm)', ) appeal_court = models.BooleanField( default=False, verbose_name="P", help_text='"Kassationsverfahren" for EuGH', ) # backend: neues BooleanFeld "appeal_court", label="P", help_text="kassationsverfahren for eugh"; "P" wird an title angehängt # p_case = models.BooleanField( # default=False, # )
[docs] class TypeOfProceedingChoiceses(models.TextChoices): REFERENCE_FOR_PRELIMINARY_RULING = ( "Request", "Reference for a preliminary ruling", ) ACTION_FOR_FAILURE_TO_FULFIL_OBLIGATIONS = ( "Action", "Actions for failure to fulfil obligations", ) ACTION_FOR_ANNULMENT = "Application", "Action for annulment" APPEAL = "Appeal", "Appeal" APPLICATION_SEEKING_COMPENSATION = ( "Application compensation", "Application seeking compensation", )
type_of_proceeding = models.CharField( max_length=255, choices=TypeOfProceedingChoiceses.choices, default=TypeOfProceedingChoiceses.REFERENCE_FOR_PRELIMINARY_RULING, ) # The following URL fields should get populated by our huey task # Hints: # - Field ordering matters, as we display these links in the order # they are defined in this model. # - The `help_text` will be used as the link text on the frontend # C-Cases: celex_link_cn = models.URLField( blank=True, verbose_name="EUR-Lex Link CN", help_text="Communication: new case; Request for a preliminary ruling", ) celex_link_cc = models.URLField( blank=True, verbose_name="EUR-Lex Link CC", help_text="Opinion (AG)", ) celex_link_cp = models.URLField( blank=True, verbose_name="EUR-Lex Link CP", help_text="View", ) celex_link_cj = models.URLField( blank=True, verbose_name="EUR-Lex Link CJ", help_text="Judgment", ) celex_link_co = models.URLField( blank=True, verbose_name="EUR-Lex Link CO", help_text="Order", ) celex_link_cv = models.URLField( blank=True, verbose_name="EUR-Lex Link CV", help_text="Opinion (Court)", ) celex_link_cx = models.URLField( blank=True, verbose_name="EUR-Lex Link CX", help_text="Ruling", ) celex_link_cd = models.URLField( blank=True, verbose_name="EUR-Lex Link CD", help_text="Decision", ) # T-Cases: celex_link_tn = models.URLField( blank=True, verbose_name="EUR-Lex Link TN", help_text="Communication: new case", ) celex_link_tc = models.URLField( blank=True, verbose_name="EUR-Lex Link TC", help_text="Opinion (AG)", ) celex_link_tj = models.URLField( blank=True, verbose_name="EUR-Lex Link TJ", help_text="Judgement", ) celex_link_to = models.URLField( blank=True, verbose_name="EUR-Lex Link TO", help_text="Order", ) last_update = models.DateTimeField( null=True, blank=True, help_text="Last time celex links updates were found.", ) # internal news spcific fields: categories_foundation = ParentalManyToManyField( "core.CategoryFoundation", blank=True, verbose_name="Foundations" ) categories_institution = ParentalManyToManyField( "core.CategoryInstitution", blank=True, verbose_name="Institutions" ) categories_areacrime = ParentalManyToManyField( "core.CategoryAreasCrime", blank=True, verbose_name="Specific Areas of Crime/Substantive Criminial Law", ) # noqa:E501 categories_cooperation = ParentalManyToManyField( "core.CategoryCooperation", blank=True, verbose_name="Cooperation" ) categories_procedural = ParentalManyToManyField( "core.CategoryProceduralCriminalLaw", blank=True, verbose_name="Procedural Criminal Law", ) # noqa:E501 class Meta: unique_together = [ "celex_section", "celex_case_type", "celex_year", "celex_case", ] # constraints = [ # models.CheckConstraint( # name="%(app_label)s_%(class)s_t_case_and_court", # check = ( # Q(Q(celex_case_type="T") & Q(referring_court="")) # ) # ) # ] @property def has_categories(self): return ( bool(self.categories_foundation.all()) or bool(self.categories_institution.all()) or bool(self.categories_areacrime.all()) or bool(self.categories_cooperation.all()) or bool(self.categories_procedural.all()) ) @property def get_celex_case_string(self): """ Provide a unified string representation for a case/act. """ case_as_string = "" # Only try to generate a valid case string if all mandatory fields # are available. This is not always true for previewing the page # in Wagtail admin while drafting a page. if all( [ self.celex_case_type, self.celex_case, self.celex_year, ] ): case_as_string = "{case_type}-{case:03d}/{year}{ppu}{appeal_court}".format( case_type=self.celex_case_type.upper(), case=self.celex_case if self.celex_case else "", year=str(self.celex_year)[2:] if self.celex_year else "", ppu=" PPU" if self.ppu else "", appeal_court=" P" if self.appeal_court else "", ) return case_as_string @property def get_celex_links_rendered(self): link_fields = [] for field in self._meta.fields: if field.name.startswith("celex_link_") and getattr(self, field.name): linktext = field.help_text if field.name in ["celex_link_cn", "celex_link_tn"]: linktext = self.type_of_proceeding link_fields.append( { "celex_url": f"{getattr(self, field.name)}", "linktext": linktext, } ) return link_fields
[docs] @classmethod def get_celex_fields(cls, case_type): """ C-cases should only check `celex_link_c*` URLs, and T-cases only `celex_link_t*` URLs. """ pattern = f"celex_link_{case_type.lower()}" celex_fields = [] for field in cls._meta.fields: if field.name.startswith(pattern): celex_fields.append(field) return celex_fields
@property def is_subject_truncated(self): truncate_threshold = ( ProcedureIndexPage.objects.live().first().truncate_subject_at ) truncated = truncatewords_html(self.subject, truncate_threshold) if self.subject != truncated: return truncated else: return False @property def get_procedure_index_page(self): return ProcedureIndexPage.objects.live().first()
[docs] @classmethod def get_year_choices(cls): all_year_values = cls.objects.live().values_list("celex_year", flat=True) all_year_values = list(set(all_year_values)) all_year_values.sort(reverse=True) return [(y, y) for y in all_year_values]
content_panels = [ HelpPanel( heading="Help about CELEX IDs", content="<p>The title field is not relevant here, but you are advised" 'to enter the case ("C-517/16") here. From the case the CELEX' 'number could be constructed: the first part ("C") is the section' 'the second part ("571") is the case number and the third part' '("16") is the year. Use these values to insert the CELEX id.</p>' '<p><a href="https://eur-lex.europa.eu/content/tools/HowCelexNumbersAreComposed.pdf">Reference document "HowCelexNumbersAreComposed.pdf"</a></p>', ), MultiFieldPanel( [ FieldRowPanel( [ # FieldPanel('celex_section'), # We always use the default section 6, no need to expose this FieldPanel("celex_case_type"), FieldPanel("celex_year"), FieldPanel("celex_case"), ] ), FieldRowPanel( [ FieldPanel("type_of_proceeding"), ] ), ], heading="Celex number", ), FieldPanel("subject"), FieldPanel("party"), MultiFieldPanel( [ FieldPanel("referring_court"), FieldPanel("ppu"), FieldPanel("appeal_court"), ], heading="C-Case specific fields", classname="collapsible collapsed", ), # Readonly panels for automatically updated link fields MultiFieldPanel( [ FieldPanel( "last_update", read_only=False, ), FieldPanel( "celex_link_cc", read_only=False, ), FieldPanel( "celex_link_cj", read_only=False, ), FieldPanel( "celex_link_cn", read_only=False, ), FieldPanel( "celex_link_co", read_only=False, ), FieldPanel( "celex_link_cv", read_only=False, ), FieldPanel( "celex_link_cp", read_only=False, ), FieldPanel( "celex_link_cx", read_only=False, ), FieldPanel( "celex_link_cd", read_only=False, ), FieldPanel( "celex_link_tj", read_only=False, ), FieldPanel( "celex_link_to", read_only=False, ), FieldPanel( "celex_link_tc", read_only=False, ), FieldPanel( "celex_link_tn", read_only=False, ), ], heading="Auto-populated fields", classname="collapsible collapsed", ), ] settings_panels = ( Page.settings_panels + [ # Moved celex-link-fields to content panel ] ) related_panels = [ MultiFieldPanel( [ FieldPanel("categories_foundation", widget=CheckboxSelectMultiple), FieldPanel("categories_institution", widget=CheckboxSelectMultiple), FieldPanel("categories_areacrime", widget=CheckboxSelectMultiple), FieldPanel("categories_cooperation", widget=CheckboxSelectMultiple), FieldPanel("categories_procedural", widget=CheckboxSelectMultiple), ], heading="News fields", classname="collapsible", ), InlinePanel("related_links", label="Related links"), ] search_fields = Page.search_fields + [ index.SearchField("celex_case"), index.SearchField("get_celex_case_string"), ] # Not using HideShowInMenusField.promote_panels here, as not only # show_in_menus must be hidden, but also slug. promote_panels = [] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Content"), ObjectList(related_panels, heading="Related"), # ObjectList(promote_panels, heading='Promote'), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) preview_modes = [] base_form_class = ProcedurePageForm
[docs] def full_clean(self, *args, **kwargs): # First call the built-in cleanups (including default slug generation) super().full_clean(*args, **kwargs) # Automatically set page title and page slug based on # celex values. Also we need to hide the promote_panel which # contains the slug field. if not self.title: self.title = f"{self.get_celex_case_string}" self.slug = f"{slugify(self.title.replace('/', '-'))}"
[docs] class ProcedureIndexPage(HideShowInMenusField, RoutablePageMixin, Page): parent_page_types = ["core.HomePage", "core.Standardpage"] subpage_types = ["procedure.ProcedurePage"] max_count = 1 show_in_menus = True intro = RichTextField(blank=True) paginate_at = models.PositiveSmallIntegerField( blank=True, null=True, default=16, help_text="Show X procedures on paginated view (default: 16). Should be an even number.", ) truncate_subject_at = models.PositiveSmallIntegerField( blank=True, null=True, default=50, help_text='Truncate subjects after N words (default: 50) and display a "More" link.', ) content_panels = Page.content_panels + [ FieldPanel("intro", classname="full"), ] settings_panels = Page.settings_panels + [ FieldPanel("paginate_at"), FieldPanel("truncate_subject_at"), ] promote_panels = HideShowInMenusField.promote_panels qs = ( ProcedurePage.objects.live() .select_related("latest_revision", "live_revision") .prefetch_related( "categories_foundation", "categories_institution", "categories_areacrime", "categories_cooperation", "categories_procedural", "related_links", ) .order_by("-celex_year", "-celex_case") )
[docs] @route(r"^$") def filter_procedures(self, request, *args, **kwargs): from .filters import ProcedurePageFilter _request_copy = request.GET.copy() # Validate some filter request parameters: # categories_-filter must be convetable to type int # Todo: This should be handeled sooner, see: https://stackoverflow.com/q/70659936 try: _categories_values = [ v for k, v in _request_copy.items() if k.startswith(("categories_", "year")) ] _intvalues = [int(v) for v in _categories_values] except ValueError as e: print(f"{e=}") return bad_request(request, "Suspicious query parameter") parameters = _request_copy.pop("page", True) and _request_copy.urlencode() filter = ProcedurePageFilter(request.GET, queryset=self.qs) filtered_qs = filter.qs paginator = Paginator(filtered_qs, self.paginate_at) page = request.GET.get("page", 1) try: items = paginator.page(page) except PageNotAnInteger: items = paginator.page(1) except EmptyPage: items = paginator.page(paginator.num_pages) context = super().get_context(request) context.update( { "items": items, "procedures_count": self.qs.count(), "parameters": parameters, "filter": filter, "filter_qs_count": filter.qs.count(), "truncate_subject_at": self.truncate_subject_at, } ) return render(request, self.template, context, *args, **kwargs)