Newer
Older
from datetime import datetime
from typing import Any, Optional
from django.core.validators import FileExtensionValidator, MaxValueValidator, MinValueValidator
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
import reversion
from calendarweek import CalendarWeek
from calendarweek.django import i18n_day_name_choices_lazy
from celery.result import allow_join_result
from celery.states import SUCCESS
from reversion.models import Revision, Version
from aleksis.core.mixins import ExtensibleModel, ExtensiblePolymorphicModel
from aleksis.core.util.pdf import generate_pdf_from_template
class PosterGroup(ExtensibleModel):
"""Group for time-based documents, called posters."""
slug = models.SlugField(
verbose_name=_("Slug used in URL name"),
help_text=_("If you use 'example', the filename will be 'example.pdf'."),
)
name = models.CharField(max_length=255, verbose_name=_("Name"))
publishing_day = models.PositiveSmallIntegerField(
verbose_name=_("Publishing weekday"), choices=i18n_day_name_choices_lazy()
)
publishing_time = models.TimeField(verbose_name=_("Publishing time"))
default_pdf = models.FileField(
upload_to="default_posters/",
verbose_name=_("Default PDF"),
help_text=_("This PDF file will be shown if there is no current PDF."),
validators=[FileExtensionValidator(allowed_extensions=["pdf"])],
)
show_in_menu = models.BooleanField(default=True, verbose_name=_("Show in menu"))
public = models.BooleanField(default=False, verbose_name=_("Show for not logged-in users"))
class Meta:
verbose_name = _("Poster group")
verbose_name_plural = _("Poster groups")
constraints = [
models.UniqueConstraint(fields=["site_id", "name"], name="unique_site_name"),
models.UniqueConstraint(fields=["site_id", "slug"], name="unique_site_slug"),
]
permissions = [
("view_poster_of_group", _("Can view all posters of this group")),
("upload_poster_to_group", _("Can upload new posters to this group")),
("change_poster_of_group", _("Can change all posters of this group")),
("delete_poster_of_group", _("Can delete all posters of this group")),
]
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
def __str__(self) -> str:
return f"{self.name} ({self.publishing_day_name}, {self.publishing_time})"
@property
def publishing_day_name(self) -> str:
"""Return the full name of the publishing day (e. g. Monday)."""
return i18n_day_name_choices_lazy()[self.publishing_day][1]
@property
def filename(self) -> str:
"""Return the filename for the currently valid PDF file."""
return f"{self.slug}.pdf"
@property
def current_poster(self) -> Optional["Poster"]:
"""Get the currently valid poster."""
# Get current date with year and calendar week
current = timezone.datetime.now()
cw = CalendarWeek.from_date(current)
# Create datetime with the friday of the week and the toggle time
day = cw[self.publishing_day]
day_and_time = timezone.datetime.combine(day, self.publishing_time)
# Check whether to show the poster of the next week or the current week
if current > day_and_time:
cw += 1
# Look for matching PDF in DB
try:
obj = self.posters.get(year=cw.year, week=cw.week)
return obj
# Or show the default PDF
except Poster.DoesNotExist:
return None
def _get_current_year() -> int:
"""Get the current year."""
return timezone.now().year
calendar_weeks = [(cw, str(cw)) for cw in range(1, 53)]
class Poster(ExtensibleModel):
"""A time-based document."""
group = models.ForeignKey(
to=PosterGroup,
related_name="posters",
on_delete=models.CASCADE,
verbose_name=_("Poster group"),
)
week = models.PositiveSmallIntegerField(
verbose_name=_("Calendar week"),
validators=[MinValueValidator(1), MaxValueValidator(53)],
default=CalendarWeek.current_week,
choices=calendar_weeks,
)
year = models.PositiveSmallIntegerField(verbose_name=_("Year"), default=_get_current_year)
pdf = models.FileField(
upload_to="posters/",
verbose_name=_("PDF"),
validators=[FileExtensionValidator(allowed_extensions=["pdf"])],
)
constraints = [
models.UniqueConstraint(
fields=["site_id", "week", "year", "group"], name="unique_site_week_year"
)
]
verbose_name = _("Poster")
verbose_name_plural = _("Posters")
def __str__(self) -> str:
return f"{self.group.name}: {self.week}/{self.year}"
@property
def valid_from(self) -> datetime:
"""Return the time this poster is valid from."""
cw = CalendarWeek(week=self.week, year=self.year) - 1
day = cw[self.group.publishing_day]
return timezone.datetime.combine(day, self.group.publishing_time)
@property
def valid_to(self) -> datetime:
"""Return the time this poster is valid to."""
cw = CalendarWeek(week=self.week, year=self.year)
day = cw[self.group.publishing_day]
return timezone.datetime.combine(day, self.group.publishing_time)
class LiveDocument(ExtensiblePolymorphicModel):
"""Model for periodically/automatically updated files."""
SCOPE_PREFIX = "live_document_pdf"
template = None
slug = models.SlugField(
verbose_name=_("Slug"),
help_text=_("This will be used for the name of the current PDF file."),
)
name = models.CharField(max_length=255, verbose_name=_("Name"))
current_file = models.FileField(
upload_to="live_documents/",
blank=True,
verbose_name=_("Current file"),
editable=False,
)
last_update_triggered_manually = models.BooleanField(
default=False, verbose_name=_("Was the last update triggered manually?"), editable=False
)
@property
def last_version(self) -> Optional[Revision]:
"""Get django-reversion version of last file update."""
versions = Version.objects.get_for_object(self).order_by("revision__date_created")
if versions.exists():
return versions.last()
return None
@property
def last_update(self) -> Optional[datetime]:
"""Get datetime of last file update."""
last_version = self.last_version
if last_version:
return last_version.revision.date_created
return None
def get_current_file(self) -> Optional[File]:
"""Get current file."""
if not self.current_file:
self.update()
return self.current_file
@property
def filename(self) -> str:
"""Get the filename without path of the PDF file."""
return f"{self.slug}.pdf"
@property
def scope(self) -> str:
"""Return OAuth2 scope name to access PDF file via API."""
return f"{self.SCOPE_PREFIX}_{self.slug}"
def save(self, *args, **kwargs):
with reversion.create_revision():
super().save(*args, **kwargs)
def get_context_data(self) -> dict[str, Any]:
"""Get context to pass to the PDF template."""
return {}
def update(self, triggered_manually: bool = True):
"""Update the file with a new version.
Has to be implemented by subclasses.
"""
if not self.template:
raise NotImplementedError("Subclasses of LiveDocument must implement update()")
file_object, result = generate_pdf_from_template(self.template, self.get_context_data())
with allow_join_result():
result.wait()
file_object.refresh_from_db()
if result.status == SUCCESS and file_object.file:
self.last_update_triggered_manually = triggered_manually
self.current_file.save(self.filename, file_object.file.file)
self.save()
def __str__(self) -> str:
return self.name
class Meta:
verbose_name = _("Live document")
verbose_name_plural = _("Live documents")