# 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}"