diff --git a/aleksis/apps/resint/admin.py b/aleksis/apps/resint/admin.py index 98ac6723126b5eecefe06db48f336bb27abab8b2..2f35aabaa9857262caad319dff5d37286ab05f3b 100644 --- a/aleksis/apps/resint/admin.py +++ b/aleksis/apps/resint/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin -from .models import Poster +from .models import Poster, PosterGroup +admin.site.register(PosterGroup) admin.site.register(Poster) diff --git a/aleksis/apps/resint/default.odt b/aleksis/apps/resint/default.odt deleted file mode 100644 index 9b52ee32db4631bc506c29cb64d8ba0ea8c24a6d..0000000000000000000000000000000000000000 Binary files a/aleksis/apps/resint/default.odt and /dev/null differ diff --git a/aleksis/apps/resint/default.pdf b/aleksis/apps/resint/default.pdf deleted file mode 100644 index 397f82391e8bad15e66ffc8df6845e5f854948f3..0000000000000000000000000000000000000000 Binary files a/aleksis/apps/resint/default.pdf and /dev/null differ diff --git a/aleksis/apps/resint/forms.py b/aleksis/apps/resint/forms.py index e4587daaf49a319a602a0658cd9ffc7232957def..5a0ae9822616b4aa78fb6e4851311ea82432940b 100644 --- a/aleksis/apps/resint/forms.py +++ b/aleksis/apps/resint/forms.py @@ -1,31 +1,37 @@ from django import forms -from django.core.validators import FileExtensionValidator -from django.utils import timezone from material import Layout, Row -from .models import Poster +from .models import Poster, PosterGroup -current_year = timezone.datetime.now().year -options_for_year = [(current_year, current_year), (current_year + 1, current_year + 1)] -calendar_weeks = [(cw, str(cw)) for cw in range(1, 53)] - - -class PosterUploadForm(forms.ModelForm): - calendar_week = forms.ChoiceField(choices=calendar_weeks, initial=timezone.datetime.now().isocalendar()[1]) - year = forms.ChoiceField( - initial=timezone.datetime.now().year, choices=options_for_year - ) - pdf = forms.FileField( - validators=[FileExtensionValidator(allowed_extensions=["pdf"])], - ) +class PosterGroupForm(forms.ModelForm): + """Form to manage poster groups.""" layout = Layout( - Row("calendar_week", "year"), - Row("pdf") + Row("slug"), + Row("name"), + Row("publishing_day", "publishing_time"), + Row("default_pdf"), + Row("show_in_menu", "public"), ) + class Meta: + model = PosterGroup + fields = [ + "slug", + "name", + "publishing_day", + "publishing_time", + "default_pdf", + "show_in_menu", + "public", + ] + + +class PosterUploadForm(forms.ModelForm): + """Form for uploading new posters.""" + class Meta: model = Poster - fields = ("calendar_week", "year", "pdf") + fields = ["group", "week", "year", "pdf"] diff --git a/aleksis/apps/resint/menus.py b/aleksis/apps/resint/menus.py index 799d4a6784998373612145be688f8645509053dc..22c93b0d971fe6b88d4d236ef2a9982d79032d64 100644 --- a/aleksis/apps/resint/menus.py +++ b/aleksis/apps/resint/menus.py @@ -1,5 +1,30 @@ +from typing import Any, Dict, List + +from django.apps import apps +from django.urls import reverse +from django.utils.functional import lazy from django.utils.translation import ugettext_lazy as _ + +def _get_menu_entries() -> List[Dict[str, Any]]: + """Build menu entries for all poster groups. + + This will include only poster groups where ``show_in_menu`` is enabled. + """ + PosterGroup = apps.get_model("resint", "PosterGroup") + return [ + { + "name": group.name, + "url": reverse("poster_show_current", args=[group.slug]), + "icon": "picture_as_pdf", + "validators": ["menu_generator.validators.is_authenticated"], + } + for group in PosterGroup.objects.filter(show_in_menu=True) + ] + + +get_menu_entries_lazy = lazy(_get_menu_entries, list) + MENUS = { "NAV_MENU_CORE": [ { @@ -7,23 +32,22 @@ MENUS = { "url": "#", "icon": "open_in_browser", "root": True, - "validators": [ - "menu_generator.validators.is_authenticated", - ], + "validators": ["menu_generator.validators.is_authenticated",], "submenu": [ { - "name": _("Current poster"), - "url": "poster_show_current", - "icon": "picture_as_pdf", + "name": _("Manage posters"), + "url": "poster_index", + "icon": "file_upload", "validators": ["menu_generator.validators.is_authenticated"], }, { - "name": _("Upload poster"), - "url": "poster_index", - "icon": "file_upload", + "name": _("Poster groups"), + "url": "poster_group_list", + "icon": "topic", "validators": ["menu_generator.validators.is_authenticated"], }, - ], + ] + + get_menu_entries_lazy(), } ] } diff --git a/aleksis/apps/resint/migrations/0001_initial.py b/aleksis/apps/resint/migrations/0001_initial.py index 58c47437aa94116ed6b542c580624c2617242ae5..eb439275a716d70c0da8b4d893da1c6aeba25b47 100644 --- a/aleksis/apps/resint/migrations/0001_initial.py +++ b/aleksis/apps/resint/migrations/0001_initial.py @@ -1,8 +1,11 @@ -# Generated by Django 3.0.4 on 2020-03-29 16:02 +# Generated by Django 3.2.4 on 2021-06-30 18:23 import aleksis.apps.resint.models -import django.contrib.postgres.fields.jsonb +import calendarweek.calendarweek +import django.contrib.sites.managers +import django.core.validators from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -10,22 +13,61 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('sites', '0002_alter_domain_unique'), ] operations = [ + migrations.CreateModel( + name='PosterGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('slug', models.SlugField(help_text="If you use 'example', the filename will be 'example.pdf'.", verbose_name='Slug used in URL name')), + ('name', models.CharField(max_length=255, verbose_name='Name')), + ('publishing_day', models.PositiveSmallIntegerField(choices=[(0, 'Montag'), (1, 'Dienstag'), (2, 'Mittwoch'), (3, 'Donnerstag'), (4, 'Freitag'), (5, 'Samstag'), (6, 'Sonntag')], verbose_name='Publishing weekday')), + ('publishing_time', models.TimeField(verbose_name='Publishing time')), + ('default_pdf', models.FileField(help_text='This PDF file will be shown if there is no current PDF.', upload_to='default_posters/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf'])], verbose_name='Default 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')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ], + options={ + 'verbose_name': 'Poster group', + 'verbose_name_plural': 'Poster groups', + }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), migrations.CreateModel( name='Poster', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extended_data', django.contrib.postgres.fields.jsonb.JSONField(default=dict, editable=False)), - ('calendar_week', models.IntegerField(verbose_name='CW')), - ('year', models.IntegerField(verbose_name='Year')), - ('pdf', models.FileField(upload_to=aleksis.apps.resint.models.path_and_rename_poster, verbose_name='PDF')), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extended_data', models.JSONField(default=dict, editable=False)), + ('week', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5'), (6, '6'), (7, '7'), (8, '8'), (9, '9'), (10, '10'), (11, '11'), (12, '12'), (13, '13'), (14, '14'), (15, '15'), (16, '16'), (17, '17'), (18, '18'), (19, '19'), (20, '20'), (21, '21'), (22, '22'), (23, '23'), (24, '24'), (25, '25'), (26, '26'), (27, '27'), (28, '28'), (29, '29'), (30, '30'), (31, '31'), (32, '32'), (33, '33'), (34, '34'), (35, '35'), (36, '36'), (37, '37'), (38, '38'), (39, '39'), (40, '40'), (41, '41'), (42, '42'), (43, '43'), (44, '44'), (45, '45'), (46, '46'), (47, '47'), (48, '48'), (49, '49'), (50, '50'), (51, '51'), (52, '52')], default=calendarweek.calendarweek.CalendarWeek.current_week, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(53)], verbose_name='Calendar week')), + ('year', models.PositiveSmallIntegerField(default=aleksis.apps.resint.models._get_current_year, verbose_name='Year')), + ('pdf', models.FileField(upload_to='posters/', validators=[django.core.validators.FileExtensionValidator(allowed_extensions=['pdf'])], verbose_name='PDF')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posters', to='resint.postergroup', verbose_name='Poster group')), + ('site', models.ForeignKey(default=1, editable=False, on_delete=django.db.models.deletion.CASCADE, to='sites.site')), ], options={ 'verbose_name': 'Poster', 'verbose_name_plural': 'Posters', - 'unique_together': {('calendar_week', 'year')}, }, + managers=[ + ('objects', django.contrib.sites.managers.CurrentSiteManager()), + ], + ), + migrations.AddConstraint( + model_name='postergroup', + constraint=models.UniqueConstraint(fields=('site_id', 'name'), name='unique_site_name'), + ), + migrations.AddConstraint( + model_name='postergroup', + constraint=models.UniqueConstraint(fields=('site_id', 'slug'), name='unique_site_slug'), + ), + migrations.AddConstraint( + model_name='poster', + constraint=models.UniqueConstraint(fields=('site_id', 'week', 'year'), name='unique_site_week_year'), ), ] diff --git a/aleksis/apps/resint/models.py b/aleksis/apps/resint/models.py index 53dca0d8f550ceec0a04c4d5a62203c294aa2374..2a84336e1a46614d1833785be62a9be83716f565 100644 --- a/aleksis/apps/resint/models.py +++ b/aleksis/apps/resint/models.py @@ -1,23 +1,137 @@ +from datetime import datetime +from typing import Optional + +from django.core.validators import FileExtensionValidator, MaxValueValidator, MinValueValidator from django.db import models +from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from calendarweek import CalendarWeek +from calendarweek.django import i18n_day_name_choices_lazy + from aleksis.core.mixins import ExtensibleModel -from aleksis.core.util.core_helpers import path_and_rename -def path_and_rename_poster(instance, filename: str) -> str: - return path_and_rename(instance, filename, upload_to="poster") +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"), + ] + + 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): - calendar_week = models.IntegerField(verbose_name=_("CW")) - year = models.IntegerField(verbose_name=_("Year")) - pdf = models.FileField(upload_to=path_and_rename_poster, verbose_name=_("PDF")) + """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"])], + ) class Meta: - unique_together = ("calendar_week", "year") + constraints = [ + models.UniqueConstraint( + fields=["site_id", "week", "year"], name="unique_site_week_year" + ) + ] verbose_name = _("Poster") verbose_name_plural = _("Posters") - def __str__(self): - return "{} {}/{}".format(_("CW"), self.calendar_week, self.year) + 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) diff --git a/aleksis/apps/resint/settings.py b/aleksis/apps/resint/settings.py deleted file mode 100644 index ed40ecdd3440579f339929d0617da61c4308380f..0000000000000000000000000000000000000000 --- a/aleksis/apps/resint/settings.py +++ /dev/null @@ -1,13 +0,0 @@ -import os -from datetime import time - -from django.utils.translation import gettext_lazy as _ - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -CONSTANCE_CONFIG = { - "RESINT_NEW_WEEK_DAY": (4, _("Weekday at which the poster of the next week is to be shown"), "weekday_field"), - "RESINT_NEW_WEEK_TIME": (time(14, 00), _("Time at which the poster of the next week is to be shown"), time) -} -CONSTANCE_CONFIG_FIELDSETS = { - "Resint settings": ("RESINT_NEW_WEEK_DAY", "RESINT_NEW_WEEK_TIME"), -} diff --git a/aleksis/apps/resint/templates/resint/group/create.html b/aleksis/apps/resint/templates/resint/group/create.html new file mode 100644 index 0000000000000000000000000000000000000000..67dd26368e9245198f1f468b7e000b6bd5e7ce9b --- /dev/null +++ b/aleksis/apps/resint/templates/resint/group/create.html @@ -0,0 +1,17 @@ +{% extends 'core/base.html' %} +{% load material_form i18n %} + +{% block browser_title %} + {% blocktrans %}Create poster group{% endblocktrans %} +{% endblock %} +{% block page_title %} + {% blocktrans %}Create poster group{% endblocktrans %} +{% endblock %} + +{% block content %} + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> +{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/group/edit.html b/aleksis/apps/resint/templates/resint/group/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..a811160fcf01ab2a88d58ddb8cbf616cc32b11f7 --- /dev/null +++ b/aleksis/apps/resint/templates/resint/group/edit.html @@ -0,0 +1,17 @@ +{% extends 'core/base.html' %} +{% load material_form i18n %} + +{% block page_title %} + {% trans "Edit poster group" %} +{% endblock %} +{% block browser_title %} + {% trans "Edit poster group" %} +{% endblock %} + +{% block content %} + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + {% include "core/partials/save_button.html" %} + </form> +{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/group/list.html b/aleksis/apps/resint/templates/resint/group/list.html new file mode 100644 index 0000000000000000000000000000000000000000..00b2d3c1ce5d2f790d3403392192339a62cc1565 --- /dev/null +++ b/aleksis/apps/resint/templates/resint/group/list.html @@ -0,0 +1,59 @@ +{% extends 'core/base.html' %} +{% load material_form i18n %} + +{% block browser_title %}{% blocktrans %}Poster groups{% endblocktrans %}{% endblock %} + +{% block content %} + <a class="waves-effect waves-light btn green modal-trigger right" href="{% url "create_poster_group" %}"> + <i class="material-icons left">add</i>{% blocktrans %}Create new poster group{% endblocktrans %} + </a> + + <h1>{% blocktrans %}Poster groups{% endblocktrans %}</h1> + + <table> + <thead> + <tr> + <th>{% blocktrans %}Name{% endblocktrans %}</th> + <th>{% blocktrans %}Filename{% endblocktrans %}</th> + <th>{% blocktrans %}Publishing day{% endblocktrans %}</th> + <th>{% blocktrans %}Publishing time{% endblocktrans %}</th> + <th>{% blocktrans %}Default PDF file{% endblocktrans %}</th> + <th>{% blocktrans %}Actions{% endblocktrans %}</th> + </tr> + </thead> + <tbody> + {% for poster_group in postergroup_list %} + <tr> + <td>{{ poster_group.name }}</td> + <td> + <a href="{% url "poster_show_current" poster_group.slug %}"><code>{{ poster_group.filename }}</code></a> + </td> + <td>{{ poster_group.publishing_day_name }}</td> + <td>{{ poster_group.publishing_time }}</td> + <td> + <a href="{{ poster_group.default_pdf.url }}" class="btn-flat" target="_blank"> + <i class="material-icons left">picture_as_pdf</i> + {% trans "Open" %} + </a> + </td> + <td> + <a href="{% url 'edit_poster_group' poster_group.id %}" + class="waves-effect waves-light btn-flat orange-text"> + <i class="material-icons left">edit</i> + {% trans "Edit" %} + </a> + <a href="{% url 'delete_poster_group' poster_group.id %}" + class="waves-effect waves-light btn-flat red-text"> + <i class="material-icons left">delete</i> + {% trans "Delete" %} + </a> + </td> + </tr> + {% empty %} + <tr> + <td colspan="4">{% blocktrans %}There are no poster groups available.{% endblocktrans %}</td> + </tr> + {% endfor %} + </tbody> + </table> +{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/index.html b/aleksis/apps/resint/templates/resint/index.html deleted file mode 100644 index 90dc986206617ede153f486e3717871b3920652d..0000000000000000000000000000000000000000 --- a/aleksis/apps/resint/templates/resint/index.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "core/base.html" %} -{% load msg_box static i18n %} - -{% block content %} - <a class="waves-effect waves-light btn green" href="{% url "poster_upload" %}"><i class="material-icons left">add</i> - {% trans "Upload new poster" %} - </a> - <a class="waves-effect waves-light btn orange" href="{% url "poster_show_current" %}"><i class="material-icons left">picture_as_pdf</i> - {% trans "Show current poster" %} - </a> - - <h5>{% trans "All uploaded posters" %}</h5> - - <ul class="collection"> - {% for poster in posters %} - <li class="collection-item "> - <span class="title">{{ poster }}</span> - <p> - <a class="btn-flat waves-effect waves-green" href="{% get_media_prefix %}{{ poster.pdf }}" target="_blank"> - <i class="material-icons left">picture_as_pdf</i> {% trans "Show" %} - </a> - <a class="btn-flat delete-poster waves-effect waves-red" href="{% url "poster_delete" poster.id %}"> - <i class="material-icons left">delete</i> {% trans "Delete" %} - </a> - </p> - </li> - {% endfor %} - </ul> - - <script type="text/javascript"> - $(".delete-poster").click(function (e) { - if (!confirm("Wirklich löschen?")) { - e.preventDefault(); - } - }) - </script> -{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/poster/list.html b/aleksis/apps/resint/templates/resint/poster/list.html new file mode 100644 index 0000000000000000000000000000000000000000..3970ffe8a4b46df69a0050c9f5a0c2588496a565 --- /dev/null +++ b/aleksis/apps/resint/templates/resint/poster/list.html @@ -0,0 +1,77 @@ +{% extends "core/base.html" %} +{% load static i18n %} + +{% block content %} + + + <h1>{% trans "Posters" %}</h1> + <div class="row"> + {% for group in poster_groups %} + <div class="col s12 m6 l4 xl3"> + <div class="card"> + <div class="card-content"> + <div class="card-title">{{ group.name }}</div> + {% with current_poster=group.current_poster %} + {% if current_poster %} + <p class="margin-bottom"> + <i class="material-icons left">picture_as_pdf</i> + <a href="{{ current_poster.pdf.url }}"> + {% blocktrans with week=current_poster.week year=current_poster.year %} + Week {{ week }}/{{ year }} + {% endblocktrans %} + </a> + </p> + <p> + <i class="material-icons left">schedule</i> + {{ current_poster.valid_from }}–{{ current_poster.valid_to }} + </p> + {% else %} + <p> + <i class="material-icons left">picture_as_pdf</i> + {% trans "There is no poster for this week." %} + </p> + {% endif %} + {% endwith %} + </div> + <div class="card-action"> + <a href="{% url "poster_show_current" group.slug %}"> + {% trans "Show current PDF" %} + </a> + </div> + </div> + </div> + {% endfor %} + </div> + <a class="waves-effect waves-light btn green right" href="{% url "poster_upload" %}"> + <i class="material-icons left">add</i> + {% trans "Upload new poster" %} + </a> + <h2>{% trans "All uploaded posters" %}</h2> + <table> + <thead> + <tr> + <th>{% trans "Group" %}</th> + <th>{% trans "Week" %}</th> + <th>{% trans "Valid from ... to" %}</th> + <th>{% trans "Actions" %}</th> + </tr> + </thead> + {% for poster in poster_list %} + <tr> + <td>{{ poster.group }}</td> + <td>{{ poster.week }}/{{ poster.year }}</td> + <td>{{ poster.valid_from }}–{{ poster.valid_to }}</td> + <td> + <a class="btn-flat waves-effect waves-green" href="{{ poster.pdf.url }}" target="_blank"> + <i class="material-icons left">picture_as_pdf</i> {% trans "Show" %} + </a> + <a class="btn-flat red-text waves-effect waves-red" href="{% url "poster_delete" poster.id %}"> + <i class="material-icons left">delete</i> {% trans "Delete" %} + </a> + </td> + </tr> + {% endfor %} + </table> + + +{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/poster/upload.html b/aleksis/apps/resint/templates/resint/poster/upload.html new file mode 100644 index 0000000000000000000000000000000000000000..9a24ec2d42e63c4f84555a41625d9c803d4829c8 --- /dev/null +++ b/aleksis/apps/resint/templates/resint/poster/upload.html @@ -0,0 +1,14 @@ +{% extends 'core/base.html' %} +{% load material_form i18n %} + +{% block content %} + <h4>{% blocktrans %}Upload poster{% endblocktrans %}</h4> + + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {% form form=form %}{% endform %} + <button type="submit" class="waves-effect waves-light btn green"> + <i class="material-icons left">save</i>{% blocktrans %}Upload poster{% endblocktrans %} + </button> + </form> +{% endblock %} diff --git a/aleksis/apps/resint/templates/resint/upload.html b/aleksis/apps/resint/templates/resint/upload.html deleted file mode 100644 index ca07c32682967d1e4c40367358b04ccd8084380d..0000000000000000000000000000000000000000 --- a/aleksis/apps/resint/templates/resint/upload.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "core/base.html" %} -{% load msg_box i18n material_form %} - -{% block page_title %}{% trans "Upload poster" %}{% endblock %} - -{% block content %} - <form method="post" enctype="multipart/form-data"> - {% csrf_token %} - - {% form form=form %}{% endform %} - - <button class="waves-effect waves-light btn green" type="submit"> - <i class="material-icons left">cloud_upload</i> - {% trans "Upload and publish poster" %} - </button> - </form> - - <p> - <a href="{% url 'poster_index' %}" class="waves-effect waves-teal btn-flat">{% trans "Back to overview" %}</a> - </p> -{% endblock %} diff --git a/aleksis/apps/resint/urls.py b/aleksis/apps/resint/urls.py index 2830be5752035e33d9b7f6ee3d6deb43aef6280b..86c2fc88a018231af60ab19772cf1786945883fe 100644 --- a/aleksis/apps/resint/urls.py +++ b/aleksis/apps/resint/urls.py @@ -1,11 +1,23 @@ from django.urls import path -from . import views +from .views import ( + PosterCurrentView, + PosterDeleteView, + PosterGroupCreateView, + PosterGroupDeleteView, + PosterGroupEditView, + PosterGroupListView, + PosterListView, + PosterUploadView, +) urlpatterns = [ - path('', views.index, name="poster_index"), - path('upload/', views.upload, name="poster_upload"), - path('delete/<int:id>', views.delete, name="poster_delete"), - path('current.pdf', views.show_current, name="poster_show_current"), - path('<str:msg>', views.index, name="poster_index_msg"), + path("", PosterListView.as_view(), name="poster_index"), + path("upload/", PosterUploadView.as_view(), name="poster_upload"), + path("<int:pk>/delete/", PosterDeleteView.as_view(), name="poster_delete"), + path("<str:slug>.pdf", PosterCurrentView.as_view(), name="poster_show_current"), + path("groups/", PosterGroupListView.as_view(), name="poster_group_list"), + path("groups/create/", PosterGroupCreateView.as_view(), name="create_poster_group"), + path("groups/<int:pk>/edit/", PosterGroupEditView.as_view(), name="edit_poster_group"), + path("groups/<int:pk>/delete/", PosterGroupDeleteView.as_view(), name="delete_poster_group"), ] diff --git a/aleksis/apps/resint/views.py b/aleksis/apps/resint/views.py index d5fea916e300b59b47cb223f05390ab69f451d60..4b1be59cdcf42c61520e84e8f7193c07bbb8833a 100644 --- a/aleksis/apps/resint/views.py +++ b/aleksis/apps/resint/views.py @@ -1,87 +1,96 @@ -import os +from typing import Any, Dict -from django.conf import settings -from django.contrib.auth.decorators import login_required, permission_required -from django.http import FileResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django.db.models import QuerySet +from django.http import FileResponse, HttpRequest +from django.urls import reverse_lazy +from django.utils.translation import gettext as _ +from django.views import View +from django.views.generic.detail import SingleObjectMixin +from django.views.generic.list import ListView -from calendarweek import CalendarWeek -from constance import config +from aleksis.core.mixins import AdvancedCreateView, AdvancedDeleteView, AdvancedEditView -from aleksis.core.util import messages +from .forms import PosterGroupForm, PosterUploadForm +from .models import Poster, PosterGroup -from .forms import PosterUploadForm -from .models import Poster -from .settings import BASE_DIR +class PosterGroupListView(ListView): + """Show a list of all poster groups.""" -@login_required -@permission_required("resint.add_poster") -def upload(request): - if request.method == 'POST': - form = PosterUploadForm(request.POST, request.FILES) - if form.is_valid(): - form.save() + template_name = "resint/group/list.html" + model = PosterGroup - messages.success(request, _("The poster was uploaded successfully.")) - return redirect('poster_index') - else: - form = PosterUploadForm() - return render(request, 'resint/upload.html', { - 'form': form - }) +class PosterGroupCreateView(AdvancedCreateView): + """Create a new poster group.""" -@login_required -@permission_required("resint.add_poster") -def delete(request, id): - poster = get_object_or_404(Poster, pk=id) - poster.delete() + model = PosterGroup + success_url = reverse_lazy("poster_group_list") + template_name = "resint/group/create.html" + success_message = _("The poster group has been saved.") + form_class = PosterGroupForm - messages.success(request, _("The poster was deleted successfully.")) - return redirect("poster_index") +class PosterGroupEditView(AdvancedEditView): + """Edit an existing poster group.""" -@login_required -@permission_required("poster.add_poster") -def index(request): - posters = Poster.objects.all().order_by("calendar_week", "year") - return render(request, 'resint/index.html', {"posters": posters}) + model = PosterGroup + success_url = reverse_lazy("poster_group_list") + template_name = "resint/group/edit.html" + success_message = _("The poster group has been saved.") + form_class = PosterGroupForm -def return_pdf(filename): - """Read and response a PDF file""" +class PosterGroupDeleteView(AdvancedDeleteView): + """Delete a poster group.""" - file = open(filename, "rb") - return FileResponse(file, content_type="application/pdf") + model = PosterGroup + success_url = reverse_lazy("poster_group_list") + success_message = _("The poster group has been deleted.") + template_name = "core/pages/delete.html" -def return_default_pdf(): - """Response the default PDF""" +class PosterListView(ListView): + """Show a list of all uploaded posters.""" - return return_pdf(os.path.join(BASE_DIR, "default.pdf")) + template_name = "resint/poster/list.html" + model = Poster + def get_queryset(self) -> QuerySet: + return Poster.objects.all().order_by("-year", "-week") -def show_current(request): - # Get current date with year and calendar week - current_date = timezone.datetime.now() - cw = CalendarWeek.from_date(current_date) + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + context["poster_groups"] = PosterGroup.objects.all().order_by("name") + return context - # Create datetime with the friday of the week and the toggle time - friday = cw[int(config.RESINT_NEW_WEEK_DAY)] - friday = timezone.datetime.combine(friday, config.RESINT_NEW_WEEK_TIME) - # Check whether to show the poster of the next week or the current week - if current_date > friday: - cw += 1 +class PosterUploadView(AdvancedCreateView): + """Upload a new poster.""" - # Look for matching PDF in DB - try: - obj = Poster.objects.get(year=cw.year, calendar_week=cw.week) - return return_pdf(os.path.join(settings.MEDIA_ROOT, str(obj.pdf))) + model = Poster + success_url = reverse_lazy("poster_index") + template_name = "resint/poster/upload.html" + success_message = _("The poster has been uploaded.") + form_class = PosterUploadForm - # Or show the default PDF - except Poster.DoesNotExist: - return return_default_pdf() + +class PosterDeleteView(AdvancedDeleteView): + """Delete an uploaded poster.""" + + model = Poster + success_url = reverse_lazy("poster_index") + success_message = _("The poster has been deleted.") + template_name = "core/pages/delete.html" + + +class PosterCurrentView(SingleObjectMixin, View): + """Show the poster which is currently valid.""" + + model = PosterGroup + + def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> FileResponse: + group = self.get_object() + current_poster = group.current_poster + file = current_poster.pdf if current_poster else group.default_pdf + return FileResponse(file, content_type="application/pdf")