Source code for eucrim.issue.models

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

import datetime

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
# from django.utils.dates import MONTHS

from django.shortcuts import render

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

from eucrim.core.abstracts import BasePage
from eucrim.core.models.mixins import HideShowInMenusField

from ..core.storage import OverwriteStorage
from .forms import IssueForm
from .utils import canonical_issue_filename, get_wordcloud_bitmap


[docs] class IssueIndexPage(RoutablePageMixin, Page): parent_page_types = ["core.HomePage"] subpage_types = ["IssuePage"] max_count = 1 show_in_menus = True intro = RichTextField(blank=True) wordcloud_stopwords = models.TextField( blank=True, help_text="Words that should be ignored when creating a wordcloud." "One word (case does not matter) per line.", ) @property def issues(self): issues = ( IssuePage.public_objects.live() .descendant_of(self) .select_related("cover_image") ) return issues
[docs] def get_context(self, request): context = super(IssueIndexPage, self).get_context(request) context["issues"] = self.issues context["issue_index_page"] = self return context
[docs] @route(r"^issue-details/(\d+)/$", name="issue_details") def issue_details(self, request, issue_pk): """HTMX partial: article listing + wordcloud for a single issue.""" from eucrim.article.models import ArticlePage, article_authors_prefetch issue = IssuePage.objects.get(pk=issue_pk) articles = ( ArticlePage.objects.filter(issue=issue) .order_by("issue_page_from") .prefetch_related(article_authors_prefetch()) ) return render( request, "issue/partials/issue_details.html", { "issue": issue, "articles": articles, }, )
search_fields = Page.search_fields + [ index.SearchField("intro"), ] content_panels = Page.content_panels + [ FieldPanel("intro", classname="full"), ] settings_panels = Page.settings_panels + [ FieldPanel("wordcloud_stopwords"), ] promote_panels = HideShowInMenusField.promote_panels class Meta: verbose_name = "Issues Listing" verbose_name_plural = "Issues Listing"
[docs] def issue_pdf_path(instance, filename): # file will be uploaded to MEDIA_ROOT/issue/pdf/<filename> issue_path = canonical_issue_filename( instance.year, instance.issue_number, "pdf", ) issue_path = "issue/pdf/{}".format(issue_path) return issue_path
[docs] def get_current_year(): return datetime.datetime.now().year + 1
[docs] class IssuePageManager(PageManager): # pylint: disable=too-few-public-methods """Custom manager for Issue pages"""
[docs] def get_queryset(self): return super().get_queryset().order_by("-year", "-issue_number")
[docs] class PublicIssuePageManager(PageManager): # pylint: disable=too-few-public-methods """ Custom manager for Issue pages, only listing live/published issues """
[docs] def get_queryset(self): return super().get_queryset().live().order_by("-year", "-issue_number")
[docs] class IssuePage(BasePage): parent_page_types = ["issue.IssueIndexPage"] subpage_types = [] show_in_menus = False YEAR_CHOICES = [] for year in reversed(range(2006, 2030, 1)): YEAR_CHOICES.append((year, year)) ISSUE_NUMBER_CHOICES = ( (1, "01"), (2, "02"), (3, "03"), (4, "04"), ) focus_en = models.CharField( max_length=255, blank=True, verbose_name="Focus title (English)" ) focus_fr = models.CharField( max_length=255, blank=True, verbose_name="Focus title (French)" ) focus_ge = models.CharField( max_length=255, blank=True, verbose_name="Focus title (German)" ) year = models.PositiveSmallIntegerField( _("year"), choices=YEAR_CHOICES, default=get_current_year ) # month = models.PositiveSmallIntegerField(choices=MONTHS.items(), blank=True, null=True) issue_number = models.PositiveSmallIntegerField(choices=ISSUE_NUMBER_CHOICES) legacy_toc = RichTextField( blank=True, help_text="Manual listing of articles, will not be displayed when" "articles are available for this specific issue.", ) pdf = models.FileField( # we are handling the filename generation in utils.py upload_to=issue_pdf_path, # upload_to='issue/pdf/', storage=OverwriteStorage(), null=True, blank=True, ) cover_image = models.ForeignKey( "wagtailimages.Image", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) start_page_numbering_at = models.PositiveIntegerField( default=0, help_text="First page number in printed issue." ) objects = IssuePageManager() # The default manager. public_objects = PublicIssuePageManager() @property def get_official_notation(self): return "{}/{}".format(self.issue_number, self.year) @property def get_official_notation_w_volume(self): return f"{self.year}, Vol. {self.get_volume}({self.issue_number})" @property def get_wordcloud(self): # print('Searching wordcloud bitmap for "', self, '"...') return get_wordcloud_bitmap(self) @property def get_articles(self): from eucrim.article.models import ArticlePage, article_authors_prefetch return ( ArticlePage.objects.filter(issue=self) .prefetch_related(article_authors_prefetch()) .order_by("issue_page_from") ) @property def get_obj_og_description(self): return f"{self.focus_en} - {self.focus_fr} - {self.focus_ge}" @property def get_obj_og_image(self): return self.cover_image @property def get_obj_og_title(self): return f"Issue {self.get_official_notation}" @property def get_volume(self, start_year=2006): """ Calculate the volume number for a given year. :param year: The year for which to calculate the volume. :param start_year: The year the journal started (default is 2006). :return: The volume number for the given year. """ year = int(self.year) if year < start_year: raise ValueError( f"get_volume for issue {self}: Year cannot be earlier than the journal's start year." ) return (year - start_year) + 1 @property def pdf_file_size(self): """Return size in bytes without leaving open handles.""" if not self.pdf: return 0 try: return self.pdf.storage.size(self.pdf.name) except Exception: # fallback for storages that don't implement size() try: with self.pdf.open("rb") as f: f.seek(0, 2) return f.tell() except Exception: return 0 # content_panels = Page.content_panels + [ content_panels = [ # FieldPanel('focus_en', classname='full'), # FieldPanel('focus_fr', classname='full'), # FieldPanel('focus_ge', classname='full'), MultiFieldPanel( [ FieldRowPanel( [ FieldPanel("year", classname="col4"), # FieldPanel('month', classname="col4"), FieldPanel("issue_number", classname="col4"), ] ), ] ), FieldPanel("pdf"), FieldPanel("start_page_numbering_at"), FieldPanel("legacy_toc"), # FieldPanel('cover_image'), # FieldPanel('pdf'), ] promote_panels = [ MultiFieldPanel( [ # FieldPanel("slug"), FieldPanel("seo_title"), FieldPanel("search_description"), ], _("For search engines"), ), ] search_fields = Page.search_fields + [ index.SearchField("focus_en"), index.SearchField("focus_fr"), index.SearchField("focus_ge"), index.SearchField("legacy_toc"), ] # We are providing our own, extended form for this section base_form_class = IssueForm
[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 # issue values. Also we need to hide the promote_panel which # contains the slug field. if not self.title: self.title = f"{self.get_official_notation}" self.slug = f"{slugify(self.title.replace('/', '-'))}"
[docs] def save(self, *args, **kwargs): super().save(*args, **kwargs)
class Meta: unique_together = ("year", "issue_number") verbose_name = "Issue" verbose_name_plural = "Issues" ordering = ["-year", "-issue_number"] def __str__(self): released_statement = " (not published)" if not self.live else "" return f"{self.year}-{self.issue_number}{released_statement}"