Source code for eucrim.news.models

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

from datetime import timedelta, datetime

from django.conf import settings
from django import forms
from django.db import models
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
from django.contrib.auth.models import Group
from django.utils.text import slugify

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

from modelcluster.fields import ParentalKey, ParentalManyToManyField
from bs4 import BeautifulSoup

from ..core.models.mixins import RelatedLinkMixin, HideShowInMenusField
from ..core.abstracts import RelatedLink, AbstractPublicationPage


[docs] class NewsIndexPage(RoutablePageMixin, Page): parent_page_types = ["core.HomePage"] subpage_types = ["NewsPage"] max_count = 1 show_in_menus = True paginate_news_at = models.PositiveSmallIntegerField( blank=True, null=True, default=16, help_text="Show X news on paginated view (default: 16). Should be an even number.", ) # feed_class = EventsFeed settings_panels = Page.settings_panels + [ FieldPanel("paginate_news_at"), ] promote_panels = HideShowInMenusField.promote_panels
[docs] @route(r"^$") def filternews(self, request, *args, **kwargs): from .filters import NewsFilter latest_spotlight_news = NewsPage.featurednews_objects.live()[:4] from django.db.models import Prefetch from ..profile.models import ProfilePage all_news = ( NewsPage.objects.live() .select_related("issue") .prefetch_related( Prefetch( "authors", queryset=ProfilePage.objects.select_related("avatar"), ), "categories_region", "categories_foundation", "categories_institution", "categories_areacrime", "categories_cooperation", "categories_procedural", ) ) _request_copy = request.GET.copy() parameters = _request_copy.pop("page", True) and _request_copy.urlencode() filter = NewsFilter(request.GET, queryset=all_news) filtered_qs = NewsFilter(request.GET, queryset=all_news).qs paginator = Paginator(filtered_qs, self.paginate_news_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( { "parameters": parameters, "filter": filter, "filter_qs_count": filter.qs.count(), "items": items, "all_news": all_news, "all_news_count": all_news.count(), "latest_spotlight_news": latest_spotlight_news, "news_index_page": self, } ) return render(request, self.template, context, *args, **kwargs)
class Meta: verbose_name = "News" verbose_name_plural = "News"
[docs] class DefaultNewsManager(PageManager): # pylint: disable=too-few-public-methods """Custom sorting for all news"""
[docs] def get_queryset(self): return super().get_queryset().order_by("-publication_date", "-pk")
[docs] class FeaturedNewsManager(DefaultNewsManager): # pylint: disable=too-few-public-methods """Custom manager for featured news"""
[docs] def get_queryset(self): return super().get_queryset().live().filter(is_featured_news=True)
[docs] class StandardNewsManager(DefaultNewsManager): # pylint: disable=too-few-public-methods """Custom manager for standard news"""
[docs] def get_queryset(self): return super().get_queryset().live().filter(is_featured_news=False)
[docs] class NewsPage(HideShowInMenusField, AbstractPublicationPage): parent_page_types = ["NewsIndexPage"] subpage_types = [] # Model managers: objects = DefaultNewsManager() standardnews_objects = StandardNewsManager() featurednews_objects = FeaturedNewsManager() CATEGORY_FIELDS = { "Foundation": "categories_foundation", "Institution": "categories_institution", "Area of crime": "categories_areacrime", "Procedural": "categories_procedural", "Cooperation": "categories_cooperation", "Region": "categories_region", } NEWS = "NW" REPORT = "RP" NEWS_TYPE_CHOICES = ( (NEWS, "News"), (REPORT, "Report"), ) GERMAN = "GE" ENGLISH = "EN" FRENCH = "FR" LANGUAGE_CHOICES = ( (GERMAN, "German"), (ENGLISH, "English"), (FRENCH, "French"), ) parent_page_types = ["news.NewsIndexPage"] subpage_types = [] # common news fields: news_type = models.CharField( max_length=2, choices=NEWS_TYPE_CHOICES, default=NEWS, help_text="Available news types: News, Report", ) is_featured_news = models.BooleanField( default=False, verbose_name="Featured news/Spotlight", help_text='Featured news/spotlights will be highlighted and will be "sticky" on ' "the homepages news listing.", ) language = models.CharField( max_length=2, choices=LANGUAGE_CHOICES, default=ENGLISH, help_text="Language of this news.", ) issue = models.ForeignKey( "issue.IssuePage", on_delete=models.SET_NULL, null=True, blank=True, related_name="news", help_text="Printed issue, in which this news will be/was published.", ) issue_page_from = models.PositiveIntegerField( blank=True, null=True, help_text="On which page this news starts in the issue pdf file", ) issue_page_to = models.PositiveIntegerField( blank=True, null=True, help_text="On which page this news ends in the issue pdf file", ) publication_date = models.DateField( blank=True, null=True, help_text="If blank, the date of the first publication process is used.", ) def _limit_author_choices(): authors = {} try: news_author_group = Group.objects.get( name=settings.NEWS_AUTHOR_GROUP_NAME ).id authors = {"user__groups__id__contains": news_author_group} except Group.DoesNotExist: pass return authors authors = ParentalManyToManyField( "profile.ProfilePage", blank=True, related_name="news", limit_choices_to=_limit_author_choices, ) subtitle = models.CharField( max_length=255, blank=True, help_text="An optional subtitle.", ) body = RichTextField( features=[ "h3", "h4", "h5", "h6", "bold", "italic", "ol", "ul", "link", "document-link", "image", ], verbose_name="News body text", ) excerpt = models.TextField( verbose_name="Excerpt/Abstract", blank=True, max_length=500, help_text="Entry excerpt to be displayed on entries list. If this " "field is not filled, a truncate version of body text will " "be used. Please stick to no more than five lines.", ) # Internal news spcific fields # Order matters! The ordering of this category fields sets the ordering for # the issue news exporter. This order should align with the order of # categories in the printed issue. 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_procedural = ParentalManyToManyField( "core.CategoryProceduralCriminalLaw", blank=True, verbose_name="Procedural Criminal Law", ) categories_cooperation = ParentalManyToManyField( "core.CategoryCooperation", blank=True, verbose_name="Cooperation" ) categories_region = ParentalManyToManyField( "core.CategoryRegion", blank=True, verbose_name="Region" )
[docs] def get_all_categories_static(): categories = [] for field_name, field in NewsPage.CATEGORY_FIELDS.items(): field_instance = NewsPage._meta.get_field(field) to_model = field_instance.remote_field.model categories.extend( [ (f"{slugify(field_name)}---{slug}", f"{field_name} / {title}") for slug, title in to_model.objects.values_list("slug", "title") ] ) return categories
category_news_export = models.CharField( max_length=255, blank=True, choices=get_all_categories_static ) @property def get_category_news_export_label(self): """ Reverse the slugified category_news_export field to a human readable: category_news_export='area-of-crime---cybercrime' -> 'Area of crime / Cybercrime' """ if self.category_news_export: category_slug, category_title = self.category_news_export.split("---") return f"{category_slug.replace('-', ' ').title()} / {category_title.replace('-', ' ').title()}" @property def has_categories(self): return ( bool(self.categories_region.all()) or 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_all_categories(self): categories = {} for field_name, field in self.CATEGORY_FIELDS.items(): categories.update( { field_name: [opt.title for opt in getattr(self, field).all()], } ) return categories @property def get_body_with_author(self): authors = "/".join( f"{author.first_name[0]}{author.last_name[0]}" for author in self.authors.all() ) if authors: # Given the following body: `<p>whatever</p><p>Some <i>boing</i> text</p>`. How do I insert a new tag (eg. `<span>foo</span>`) before the last closing tag? # I want to get `<p>Some <i>boing</i> text<span>foo</span></p>`. soup = BeautifulSoup(self.body, "html.parser") # Create the new tag new_tag = soup.new_tag("span") new_tag.string = f" ({authors})" # Find all root-level tags and get the last one outermost_tag = list(soup.children)[-1] # Insert the new tag before the last closing tag outermost_tag.append(new_tag) return str(soup) return self.body @property def news_index_page(self): return self.get_ancestors().type(NewsIndexPage).first() # @property # def get_publication_date(self): # return self.publication_date if self.publication_date == datetime(1970, 1, 1) else self.first_published_at @property def has_been_modified(self): if self.last_published_at and self.first_published_at: if (self.last_published_at - self.first_published_at) > timedelta( minutes=60 ): return True
[docs] def arcana_markdown(self): from .export_markdown import export_news_to_markdown return export_news_to_markdown(self)
[docs] def get_absolute_url(self): """Get_absolute_url, needed by syndication framework, see feeds.py""" return self.full_url
@property def get_backlinks(self): """ Get related_links from other NewsPages pointing to this NewsPage but only get backlink if not already present as a related link. """ _current_related_links_page_ids = list( self.related_links.all().values_list("link_page_id", flat=True) ) # print(f"{_current_related_links_page_ids=}") # print(f"{self.pk=}") return ( NewsPageRelatedLink.objects.filter(link_page_id=self.id) # .exclude(page__pk__in=_current_related_links_page_ids) ) @property def get_obj_og_description(self): return self.excerpt if self.excerpt else self.body # customizing wagtail editing interface content_panels = Page.content_panels + [ MultiFieldPanel( [ FieldPanel("subtitle"), FieldPanel("body", classname="full"), FieldPanel("excerpt"), ], heading="Common news fields", classname="collapsible", ), MultiFieldPanel( [ FieldPanel("issue", widget=forms.Select), FieldRowPanel( [ FieldPanel("issue_page_from"), FieldPanel("issue_page_to"), ] ), ], heading="Issue", classname="collapsible", ), ] settings_panels = Page.settings_panels + [ FieldPanel("news_type"), FieldPanel("publication_date"), FieldPanel("is_featured_news"), ] search_fields = Page.search_fields + [ index.AutocompleteField("subtitle"), index.AutocompleteField("body"), index.AutocompleteField("excerpt"), ] authors_panels = [ FieldPanel( "authors", classname="col12", widget=forms.SelectMultiple( attrs={ "class": "is-tom-select", } ), # widget=forms.CheckboxSelectMultiple, # widget=Select2MultipleWidget( # attrs={ # 'data-allow-clear': 'true', # 'data-width': '100%', # }, # ), ), ] related_panels = [ FieldPanel("category_news_export"), MultiFieldPanel( [ FieldPanel( "categories_foundation", widget=forms.CheckboxSelectMultiple ), FieldPanel( "categories_institution", widget=forms.CheckboxSelectMultiple ), FieldPanel("categories_areacrime", widget=forms.CheckboxSelectMultiple), FieldPanel( "categories_procedural", widget=forms.CheckboxSelectMultiple ), FieldPanel( "categories_cooperation", widget=forms.CheckboxSelectMultiple ), FieldPanel("categories_region", widget=forms.CheckboxSelectMultiple), ], heading="News fields", classname="collapsible", ), InlinePanel("related_links", label="Related links"), ] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Content"), ObjectList(authors_panels, heading="Authors"), ObjectList(related_panels, heading="Related"), ObjectList(HideShowInMenusField.promote_panels, heading="Promote"), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) class Meta: verbose_name = "News" verbose_name_plural = "News" ordering = ["-last_published_at"]
[docs] def save(self, *args, **kwargs): """ In case ``publication_date`` is not set or is 1970-01-01 (our import default value), set ``publication_date`` to ``first_published_at`` date. This ensures we could use ``publication_date`` for all orderings etc. """ if self.publication_date == datetime(1970, 1, 1).date(): self.publication_date = self.first_published_at elif not self.publication_date: self.publication_date = self.first_published_at super().save(*args, **kwargs)