From 6eeb7769a478833dfc106f07d345b0f2518a6e6a Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Tue, 6 Jan 2026 17:45:06 +0100 Subject: [PATCH] ruff check; update ValidationError params --- fet2020/fet2020/utils.py | 78 +-- fet2020/finance/forms.py | 7 +- fet2020/finance/models.py | 18 +- fet2020/gallery/utils.py | 6 +- fet2020/gallery/views.py | 132 ++-- fet2020/intern/models.py | 424 ++++++------ fet2020/members/admin.py | 462 ++++++------- fet2020/members/models.py | 417 ++++++----- fet2020/members/templatetags/job_groups.py | 64 +- fet2020/members/validators.py | 48 +- fet2020/minecraft/views.py | 5 +- fet2020/posts/managers.py | 6 +- fet2020/posts/models.py | 760 +++++++++++---------- fet2020/posts/search_indexes.py | 72 +- fet2020/posts/views.py | 23 +- fet2020/rental/models.py | 5 +- 16 files changed, 1282 insertions(+), 1245 deletions(-) diff --git a/fet2020/fet2020/utils.py b/fet2020/fet2020/utils.py index 36b6da90..dc3022c6 100644 --- a/fet2020/fet2020/utils.py +++ b/fet2020/fet2020/utils.py @@ -1,39 +1,39 @@ -# util functions for all apps -import uuid - -from django.contrib.admin.utils import construct_change_message - - -def add_log_action(request, form, app_label, model, add=True): - from django.contrib.admin.models import ADDITION, CHANGE, LogEntry - from django.contrib.contenttypes.models import ContentType - - obj = form.save() - content_type = ContentType.objects.get(app_label=app_label, model=model) - change_message = construct_change_message(form, None, add) - action_flag = ADDITION if add else CHANGE - - LogEntry.objects.log_action( - user_id=request.user.pk, - content_type_id=content_type.pk, - object_id=obj.pk, - object_repr=str(obj), - action_flag=action_flag, - change_message=change_message, - ) - - -def create_perms(sender, **kwargs): - from django.contrib.auth.models import Group, Permission - from django.contrib.contenttypes.models import ContentType - - group, created = Group.objects.get_or_create(name=sender.label) - - content_types = ContentType.objects.filter(app_label=sender.label) - for content_type in content_types: - permissions = Permission.objects.filter(content_type=content_type) - [group.permissions.add(permission) for permission in permissions] - - -def create_random_id(): - return str(uuid.uuid4())[:8] +# util functions for all apps +import uuid + +from django.contrib.admin.utils import construct_change_message + + +def add_log_action(request, form, app_label, model, add=True): + from django.contrib.admin.models import ADDITION, CHANGE, LogEntry + from django.contrib.contenttypes.models import ContentType + + obj = form.save() + content_type = ContentType.objects.get(app_label=app_label, model=model) + change_message = construct_change_message(form, None, add) + action_flag = ADDITION if add else CHANGE + + LogEntry.objects.log_action( + user_id=request.user.pk, + content_type_id=content_type.pk, + object_id=obj.pk, + object_repr=str(obj), + action_flag=action_flag, + change_message=change_message, + ) + + +def create_perms(sender, **kwargs): + from django.contrib.auth.models import Group, Permission + from django.contrib.contenttypes.models import ContentType + + group, _ = Group.objects.get_or_create(name=sender.label) + + content_types = ContentType.objects.filter(app_label=sender.label) + for content_type in content_types: + permissions = Permission.objects.filter(content_type=content_type) + [group.permissions.add(permission) for permission in permissions] + + +def create_random_id(): + return str(uuid.uuid4())[:8] diff --git a/fet2020/finance/forms.py b/fet2020/finance/forms.py index 77003fec..d061a061 100644 --- a/fet2020/finance/forms.py +++ b/fet2020/finance/forms.py @@ -1,9 +1,8 @@ -import datetime import decimal from dateutil.relativedelta import relativedelta from django import forms -from django.core.validators import ValidationError +from django.core.exceptions import ValidationError from django.db.models import Count, Q from django.forms import DateInput from django.utils import timezone @@ -451,8 +450,8 @@ class ResolutionAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # to get the self.fields set - budget = decimal.Decimal(0.0) - total = decimal.Decimal(0.0) + budget = decimal.Decimal("0.0") + total = decimal.Decimal("0.0") if resolution := kwargs.get("instance"): for elem in Bill.objects.filter(resolution=resolution): total += elem.amount diff --git a/fet2020/finance/models.py b/fet2020/finance/models.py index 394d8c39..31cf8dd8 100644 --- a/fet2020/finance/models.py +++ b/fet2020/finance/models.py @@ -1,6 +1,8 @@ +import logging from pathlib import Path -from django.core.validators import FileExtensionValidator, ValidationError +from django.core.exceptions import ValidationError +from django.core.validators import FileExtensionValidator from django.db import models from django.urls import reverse @@ -8,6 +10,8 @@ from members.models import Member from .validators import validate_bill_file_extension +logger = logging.getLogger(__name__) + class BankData(models.Model): # members can be deleted but never their bank datas @@ -86,8 +90,18 @@ class Resolution(models.Model): if not Resolution.objects.filter(id=_id).exists(): break else: + msg = ( + "Es wurden zu viele Beschlüsse in dieser Woche %(week)s vom Jahr %(year)s " + "erstellt." + ) + logger.error( + "Too many resolutions created in week %(week)s of year %(year)s.", + extra={"week": week, "year": year}, + ) raise ValidationError( - f"Es wurden zu viele Beschlüsse in dieser Woche angelegt. (ID: {_id})" + msg, + code="too_many_resolutions", + params={"week": week, "year": year}, ) self.id = _id diff --git a/fet2020/gallery/utils.py b/fet2020/gallery/utils.py index 9068bc05..9af657cb 100644 --- a/fet2020/gallery/utils.py +++ b/fet2020/gallery/utils.py @@ -27,11 +27,11 @@ def get_image_list(folder_name: str) -> list: Path(thumb_path).mkdir(exist_ok=True) - for _file in os.listdir(image_path): - if Path(_file).suffix.lower()[1:] not in get_available_image_extensions(): + for _file in image_path.iterdir(): + if _file.suffix.lower()[1:] not in get_available_image_extensions(): continue - thumb_file_path = Path(thumb_path) / f"thumb_{_file}" + thumb_file_path = Path(thumb_path) / f"thumb_{_file.name}" if not Path(thumb_file_path).exists(): with Image.open(Path(image_path) / _file, "r") as im: if im._getexif() is not None: diff --git a/fet2020/gallery/views.py b/fet2020/gallery/views.py index 4a229cab..ed485c92 100644 --- a/fet2020/gallery/views.py +++ b/fet2020/gallery/views.py @@ -1,65 +1,67 @@ -import logging -from collections import deque - -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import Http404 -from django.shortcuts import render -from django.utils.text import slugify -from django.views.generic.detail import DetailView - -from .models import Album -from .utils import get_folder_list - -logger = logging.getLogger(__name__) - - -def index(request): - if request.user.is_authenticated: - albums = deque(Album.objects.all()) - - # Get albums that are in the server but not in the db. - for folder in get_folder_list(): - if not Album.objects.filter(folder_name=folder): - albums.append( - Album(title=folder, slug=slugify(folder), folder_name=folder, event_date=None) - ) - - else: - # Show only PUBLIC albums. - albums = Album.objects.public() - - context = {"albums": albums} - - return render(request, "gallery/index.html", context) - - -class AlbumDetailView(DetailView): - model = Album - template_name = "gallery/album.html" - - def get_queryset(self): - return ( - Album.objects.public() - if not self.request.user.is_authenticated - else Album.objects.all() - ) - - -class DraftAlbumDetailView(LoginRequiredMixin, DetailView): - model = Album - template_name = "gallery/album.html" - - def get_object(self, queryset=None): - slug = self.kwargs.get(self.slug_url_kwarg) - - if not (album := Album.objects.filter(slug=slug).first()): - for folder in get_folder_list(): - if slug == slugify(folder): - album = Album( - title=folder, slug=slugify(folder), folder_name=folder, event_date=None - ) - break - else: - raise Http404("Album slug not found.") - - return album +import logging +from collections import deque + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import Http404 +from django.shortcuts import render +from django.utils.text import slugify +from django.views.generic.detail import DetailView + +from .models import Album +from .utils import get_folder_list + +logger = logging.getLogger(__name__) + + +def index(request): + if request.user.is_authenticated: + albums = deque(Album.objects.all()) + + # Get albums that are in the server but not in the db. + for folder in get_folder_list(): + if not Album.objects.filter(folder_name=folder): + albums.append( + Album(title=folder, slug=slugify(folder), folder_name=folder, event_date=None) + ) + + else: + # Show only PUBLIC albums. + albums = Album.objects.public() + + context = {"albums": albums} + + return render(request, "gallery/index.html", context) + + +class AlbumDetailView(DetailView): + model = Album + template_name = "gallery/album.html" + + def get_queryset(self): + return ( + Album.objects.public() + if not self.request.user.is_authenticated + else Album.objects.all() + ) + + +class DraftAlbumDetailView(LoginRequiredMixin, DetailView): + model = Album + template_name = "gallery/album.html" + + def get_object(self, queryset=None): + slug = self.kwargs.get(self.slug_url_kwarg) + + if not (album := Album.objects.filter(slug=slug).first()): + for folder in get_folder_list(): + if slug == slugify(folder): + album = Album( + title=folder, slug=slugify(folder), folder_name=folder, event_date=None + ) + break + else: + msg = f"Album mit dem Slug '{slug}' nicht gefunden." + logger.error("Album with slug '%s' not found.", slug) + raise Http404(msg) + + return album diff --git a/fet2020/intern/models.py b/fet2020/intern/models.py index 669633bd..cd380cf7 100644 --- a/fet2020/intern/models.py +++ b/fet2020/intern/models.py @@ -1,205 +1,219 @@ -import logging -from datetime import date -from pathlib import Path - -from django.core.validators import ValidationError -from django.db import models -from django.db.models.constraints import UniqueConstraint -from django.urls import reverse -from django.utils.text import slugify - -from documents.api import ep_create_new_pad, ep_get_html, ep_get_url -from fet2020.utils import create_random_id - -logger = logging.getLogger(__name__) - - -class TopicGroup(models.Model): - title = models.CharField(verbose_name="Titel", max_length=128) - - shortterm = models.CharField(max_length=128, unique=True, blank=True) - slug = models.SlugField(unique=True) - - short_description = models.TextField(blank=True, default="") - - order = models.PositiveSmallIntegerField( - verbose_name="Reihenfolge", - unique=True, - null=True, - blank=True, - ) - - objects = models.Manager() - - class Meta: - verbose_name = "Themenbereich" - verbose_name_plural = "Themenbereiche" - - def __str__(self): - return self.title - - def get_absolute_url(self): - return reverse("intern:index") + "#" + self.slug - - def clean(self, *args, **kwargs): - if not self.shortterm: - self.shortterm = self.title - self.slug = slugify(self.shortterm) - - -class Topic(models.Model): - title = models.CharField(max_length=128, verbose_name="Titel") - slug = models.SlugField() - - archive = models.BooleanField(default=False, verbose_name="Archiv") - - description = models.TextField(blank=True, default="") - - topic_group = models.ForeignKey( - TopicGroup, - on_delete=models.CASCADE, - verbose_name="Themenbereich", - ) - - objects = models.Manager() - - class Meta: - verbose_name = "Thema" - verbose_name_plural = "Themen" - - constraints = [ - UniqueConstraint(fields=["slug", "topic_group"], name="unique_intern_slug_topic_group"), - UniqueConstraint( - fields=["title", "topic_group"], - name="unique_intern_title_topic_group", - ), - ] - - def __str__(self): - return self.title - - def get_absolute_url(self): - kwargs = { - "topic_group_slug": self.topic_group.slug, - "slug": self.slug, - } - return reverse("intern:topic", kwargs=kwargs) - - def clean(self, *args, **kwargs): - self.slug = slugify(self.title) - - -class Attachment(models.Model): - title = models.CharField(max_length=128, verbose_name="Titel") - - slug = models.SlugField() - - description = models.TextField(blank=True, default="") - - topic = models.ForeignKey(Topic, on_delete=models.CASCADE, verbose_name="Thema") - - objects = models.Manager() - - class Meta: - verbose_name = "Anhang Ordner" - verbose_name_plural = "Anhang Ordner" - - constraints = [ - UniqueConstraint(fields=["slug", "topic"], name="unique_intern_slug_topic"), - UniqueConstraint(fields=["title", "topic"], name="unique_intern_title_topic"), - ] - - def __str__(self): - return self.topic.title + " / " + self.title - - def get_absolute_url(self): - kwargs = { - "topic_group_slug": self.topic.topic_group.slug, - "topic_slug": self.topic.slug, - "slug": self.slug, - } - return reverse("intern:attachment", kwargs=kwargs) - - def clean(self, *args, **kwargs): - self.slug = slugify(self.title) - - -class Etherpad(models.Model): - title = models.CharField(max_length=128, verbose_name="Titel") - - slug_id = models.CharField(default=create_random_id, editable=False, max_length=8, unique=True) - etherpad_key = models.CharField(blank=True, max_length=50) - date = models.DateField(default=date.today, verbose_name="Datum") - - attachment = models.ForeignKey( - Attachment, - on_delete=models.CASCADE, - verbose_name="Anhang Ordner", - ) - - objects = models.Manager() - - class Meta: - verbose_name = "Etherpad" - verbose_name_plural = "Etherpads" - - constraints = [ - UniqueConstraint(fields=["title", "date", "attachment"], name="unique_intern_etherpad"), - ] - - def __str__(self): - return self.title - - def get_absolute_url(self): - return ep_get_url(self.etherpad_key) - - def clean(self): - pad_name = slugify(str(self.slug_id) + "-" + self.title[:40]) - if len(pad_name) > 50: - raise ValidationError( - ( - "Name zum Erstellen des Etherpads ist zu lange - max. 50 Zeichen. (" - "Länge: %(length)s, Name: %(pad_name)s)" - ), - params={"length": len(pad_name), "pad_name": pad_name}, - ) - - if self.etherpad_key == "": - if ep_create_new_pad(pad_name): - self.etherpad_key = pad_name - else: - raise ValidationError( - f"Etherpad '{pad_name}' konnte nicht erstellt werden. This should never happen!" - ) - - @property - def etherpad_html(self): - return ep_get_html(self.etherpad_key) - - def get_model_name(self): - return self._meta.model_name - - -class FileUpload(models.Model): - title = models.CharField(blank=True, max_length=128, verbose_name="Titel") - file_field = models.FileField(upload_to="uploads/intern/files/", verbose_name="Dokument") - date = models.DateField(default=date.today, verbose_name="Datum") - - attachment = models.ForeignKey( - Attachment, - on_delete=models.CASCADE, - verbose_name="Anhang Ordner", - ) - - objects = models.Manager() - - class Meta: - verbose_name = "Datei" - verbose_name_plural = "Dateien" - - def __str__(self): - return self.title - - def clean(self): - if not self.title: - self.title = Path(self.file_field.name).stem +import logging +from datetime import date +from pathlib import Path + +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models.constraints import UniqueConstraint +from django.urls import reverse +from django.utils.text import slugify + +from documents.api import ep_create_new_pad, ep_get_html, ep_get_url +from fet2020.utils import create_random_id + +logger = logging.getLogger(__name__) + + +class TopicGroup(models.Model): + title = models.CharField(verbose_name="Titel", max_length=128) + + shortterm = models.CharField(max_length=128, unique=True, blank=True) + slug = models.SlugField(unique=True) + + short_description = models.TextField(blank=True, default="") + + order = models.PositiveSmallIntegerField( + verbose_name="Reihenfolge", + unique=True, + null=True, + blank=True, + ) + + objects = models.Manager() + + class Meta: + verbose_name = "Themenbereich" + verbose_name_plural = "Themenbereiche" + + def __str__(self): + return self.title + + def get_absolute_url(self): + return reverse("intern:index") + "#" + self.slug + + def clean(self, *args, **kwargs): + if not self.shortterm: + self.shortterm = self.title + self.slug = slugify(self.shortterm) + + +class Topic(models.Model): + title = models.CharField(max_length=128, verbose_name="Titel") + slug = models.SlugField() + + archive = models.BooleanField(default=False, verbose_name="Archiv") + + description = models.TextField(blank=True, default="") + + topic_group = models.ForeignKey( + TopicGroup, + on_delete=models.CASCADE, + verbose_name="Themenbereich", + ) + + objects = models.Manager() + + class Meta: + verbose_name = "Thema" + verbose_name_plural = "Themen" + + constraints = [ + UniqueConstraint(fields=["slug", "topic_group"], name="unique_intern_slug_topic_group"), + UniqueConstraint( + fields=["title", "topic_group"], + name="unique_intern_title_topic_group", + ), + ] + + def __str__(self): + return self.title + + def get_absolute_url(self): + kwargs = { + "topic_group_slug": self.topic_group.slug, + "slug": self.slug, + } + return reverse("intern:topic", kwargs=kwargs) + + def clean(self, *args, **kwargs): + self.slug = slugify(self.title) + + +class Attachment(models.Model): + title = models.CharField(max_length=128, verbose_name="Titel") + + slug = models.SlugField() + + description = models.TextField(blank=True, default="") + + topic = models.ForeignKey(Topic, on_delete=models.CASCADE, verbose_name="Thema") + + objects = models.Manager() + + class Meta: + verbose_name = "Anhang Ordner" + verbose_name_plural = "Anhang Ordner" + + constraints = [ + UniqueConstraint(fields=["slug", "topic"], name="unique_intern_slug_topic"), + UniqueConstraint(fields=["title", "topic"], name="unique_intern_title_topic"), + ] + + def __str__(self): + return self.topic.title + " / " + self.title + + def get_absolute_url(self): + kwargs = { + "topic_group_slug": self.topic.topic_group.slug, + "topic_slug": self.topic.slug, + "slug": self.slug, + } + return reverse("intern:attachment", kwargs=kwargs) + + def clean(self, *args, **kwargs): + self.slug = slugify(self.title) + + +class Etherpad(models.Model): + title = models.CharField(max_length=128, verbose_name="Titel") + + slug_id = models.CharField(default=create_random_id, editable=False, max_length=8, unique=True) + etherpad_key = models.CharField(blank=True, max_length=50) + date = models.DateField(default=date.today, verbose_name="Datum") + + attachment = models.ForeignKey( + Attachment, + on_delete=models.CASCADE, + verbose_name="Anhang Ordner", + ) + + objects = models.Manager() + + class Meta: + verbose_name = "Etherpad" + verbose_name_plural = "Etherpads" + + constraints = [ + UniqueConstraint(fields=["title", "date", "attachment"], name="unique_intern_etherpad"), + ] + + def __str__(self): + return self.title + + def get_absolute_url(self): + return ep_get_url(self.etherpad_key) + + def clean(self): + pad_name = slugify(str(self.slug_id) + "-" + self.title[:40]) + if len(pad_name) > 50: + msg = ( + "Name ist zum Erstellen des Etherpads zu lange - max. 50 Zeichen. (Länge: " + "%(length)s, Name: %(pad_name)s)" + ) + logger.error( + ( + "Name '%(pad_name)s' is too long for creating a new etherpad - max. 50 " + "characters. (Length: %(length)s)", + ), + extra={"length": len(pad_name), "pad_name": pad_name}, + ) + raise ValidationError( + msg, + code="pad_name_too_long", + params={"length": len(pad_name), "pad_name": pad_name}, + ) + + if self.etherpad_key == "": + if ep_create_new_pad(pad_name): + self.etherpad_key = pad_name + else: + msg = "Etherpad '%(pad_name)s' konnte nicht erstellt werden." + logger.error( + "Etherpad '%(pad_name)s' could not be created. This should never happen!", + extra={"pad_name": pad_name}, + ) + raise ValidationError( + msg, code="pad_creation_failed", params={"pad_name": pad_name} + ) + + @property + def etherpad_html(self): + return ep_get_html(self.etherpad_key) + + def get_model_name(self): + return self._meta.model_name + + +class FileUpload(models.Model): + title = models.CharField(blank=True, max_length=128, verbose_name="Titel") + file_field = models.FileField(upload_to="uploads/intern/files/", verbose_name="Dokument") + date = models.DateField(default=date.today, verbose_name="Datum") + + attachment = models.ForeignKey( + Attachment, + on_delete=models.CASCADE, + verbose_name="Anhang Ordner", + ) + + objects = models.Manager() + + class Meta: + verbose_name = "Datei" + verbose_name_plural = "Dateien" + + def __str__(self): + return self.title + + def clean(self): + if not self.title: + self.title = Path(self.file_field.name).stem diff --git a/fet2020/members/admin.py b/fet2020/members/admin.py index 4ed1ce2b..d03bacb9 100644 --- a/fet2020/members/admin.py +++ b/fet2020/members/admin.py @@ -1,230 +1,232 @@ -from django.contrib import admin - -from .forms import ( - ActiveMemberForm, - InactiveMemberForm, - JobForm, - JobGroupForm, - JobInlineForm, - MemberForm, -) -from .models import Job, JobGroup, JobMember, Member - - -class MemberRoleFilter(admin.SimpleListFilter): - title = "Rolle" - parameter_name = "role" - - def lookups(self, request, model_admin): - return ( - ("A", "Aktiv"), - ("P", "Pension"), - ) - - def queryset(self, request, queryset): - if self.value() in Member.MemberRole: - return queryset.filter(role=self.value()) - - -class JobMemberInline(admin.TabularInline): - model = JobMember - extra = 0 - - -class JobOverviewInline(JobMemberInline): - verbose_name = "Tätigkeit" - verbose_name_plural = "Tätigkeitsübersicht" - - def get_queryset(self, request): - return JobMember.members.get_all_jobs() - - -class ActiveMemberInline(JobMemberInline): - form = ActiveMemberForm - verbose_name = "Mitglied" - verbose_name_plural = "Aktive Mitglieder Liste" - - def get_queryset(self, request): - return JobMember.active_member.get_queryset() - - -class InactiveMemberInline(JobMemberInline): - form = InactiveMemberForm - verbose_name = "Mitglied" - verbose_name_plural = "Inaktive Mitglieder Liste" - - def get_queryset(self, request): - return JobMember.inactive_member.get_queryset() - - -class JobInline(admin.TabularInline): - form = JobInlineForm - model = Job - extra = 0 - show_change_link = True - - -@admin.register(Member) -class MemberAdmin(admin.ModelAdmin): - form = MemberForm - model = Member - - fieldsets = ( - ( - None, - { - "fields": ( - ( - "firstname", - "surname", - ), - "nickname", - "mailaccount", - "role", - "image", - "description", - ), - }, - ), - ) - inlines = (JobOverviewInline,) - - list_display = ["nickname", "firstname", "surname", "mailaccount", "role"] - list_filter = [MemberRoleFilter] - ordering = ["firstname"] - search_fields = ["firstname", "surname", "nickname", "mailaccount"] - - def add_view(self, request, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().add_view( - request, - form_url, - extra_context=extra_context, - ) - - def change_view(self, request, object_id, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().change_view( - request, - object_id, - form_url, - extra_context=extra_context, - ) - - def save_model(self, request, obj, form, change): - obj.author = request.user - super().save_model(request, obj, form, change) - - -@admin.register(Job) -class JobAdmin(admin.ModelAdmin): - form = JobForm - model = Job - - list_display = ["name", "job_group"] - ordering = ["name"] - search_fields = ["name"] - - fieldsets = ( - ( - None, - { - "fields": ( - "name", - "job_group", - ), - }, - ), - ( - "Permalink", - { - "fields": ( - "shortterm", - "slug", - ), - }, - ), - ) - inlines = (ActiveMemberInline, InactiveMemberInline) - readonly_fields = ["slug"] - - def add_view(self, request, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().add_view( - request, - form_url, - extra_context=extra_context, - ) - - def change_view(self, request, object_id, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichfelder." - return super().change_view( - request, - object_id, - form_url, - extra_context=extra_context, - ) - - def save_model(self, request, obj, form, change): - obj.author = request.user - super().save_model(request, obj, form, change) - - -@admin.register(JobGroup) -class JobGroupAdmin(admin.ModelAdmin): - form = JobGroupForm - model = JobGroup - - list_display = ["name"] - ordering = ["name"] - search_fields = ["name"] - - fieldsets = ( - ( - None, - { - "fields": ( - "name", - "description", - ), - }, - ), - ( - "Permalink", - { - "fields": ( - "shortterm", - "slug", - ), - }, - ), - ) - inlines = (JobInline,) - readonly_fields = ["slug"] - - def add_view(self, request, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." - return super().add_view( - request, - form_url, - extra_context=extra_context, - ) - - def change_view(self, request, object_id, form_url="", extra_context=None): - extra_context = extra_context or {} - extra_context["help_text"] = "Fette Schriften sind Pflichfelder." - return super().change_view( - request, - object_id, - form_url, - extra_context=extra_context, - ) - - def save_model(self, request, obj, form, change): - obj.author = request.user - super().save_model(request, obj, form, change) +from django.contrib import admin + +from .forms import ( + ActiveMemberForm, + InactiveMemberForm, + JobForm, + JobGroupForm, + JobInlineForm, + MemberForm, +) +from .models import Job, JobGroup, JobMember, Member + + +class MemberRoleFilter(admin.SimpleListFilter): + title = "Rolle" + parameter_name = "role" + + def lookups(self, request, model_admin): + return ( + ("A", "Aktiv"), + ("P", "Pension"), + ) + + def queryset(self, request, queryset): + if self.value() in Member.MemberRole: + return queryset.filter(role=self.value()) + + return queryset + + +class JobMemberInline(admin.TabularInline): + model = JobMember + extra = 0 + + +class JobOverviewInline(JobMemberInline): + verbose_name = "Tätigkeit" + verbose_name_plural = "Tätigkeitsübersicht" + + def get_queryset(self, request): + return JobMember.members.get_all_jobs() + + +class ActiveMemberInline(JobMemberInline): + form = ActiveMemberForm + verbose_name = "Mitglied" + verbose_name_plural = "Aktive Mitglieder Liste" + + def get_queryset(self, request): + return JobMember.active_member.get_queryset() + + +class InactiveMemberInline(JobMemberInline): + form = InactiveMemberForm + verbose_name = "Mitglied" + verbose_name_plural = "Inaktive Mitglieder Liste" + + def get_queryset(self, request): + return JobMember.inactive_member.get_queryset() + + +class JobInline(admin.TabularInline): + form = JobInlineForm + model = Job + extra = 0 + show_change_link = True + + +@admin.register(Member) +class MemberAdmin(admin.ModelAdmin): + form = MemberForm + model = Member + + fieldsets = ( + ( + None, + { + "fields": ( + ( + "firstname", + "surname", + ), + "nickname", + "mailaccount", + "role", + "image", + "description", + ), + }, + ), + ) + inlines = (JobOverviewInline,) + + list_display = ["nickname", "firstname", "surname", "mailaccount", "role"] + list_filter = [MemberRoleFilter] + ordering = ["firstname"] + search_fields = ["firstname", "surname", "nickname", "mailaccount"] + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view( + request, + form_url, + extra_context=extra_context, + ) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().change_view( + request, + object_id, + form_url, + extra_context=extra_context, + ) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) + + +@admin.register(Job) +class JobAdmin(admin.ModelAdmin): + form = JobForm + model = Job + + list_display = ["name", "job_group"] + ordering = ["name"] + search_fields = ["name"] + + fieldsets = ( + ( + None, + { + "fields": ( + "name", + "job_group", + ), + }, + ), + ( + "Permalink", + { + "fields": ( + "shortterm", + "slug", + ), + }, + ), + ) + inlines = (ActiveMemberInline, InactiveMemberInline) + readonly_fields = ["slug"] + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view( + request, + form_url, + extra_context=extra_context, + ) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichfelder." + return super().change_view( + request, + object_id, + form_url, + extra_context=extra_context, + ) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) + + +@admin.register(JobGroup) +class JobGroupAdmin(admin.ModelAdmin): + form = JobGroupForm + model = JobGroup + + list_display = ["name"] + ordering = ["name"] + search_fields = ["name"] + + fieldsets = ( + ( + None, + { + "fields": ( + "name", + "description", + ), + }, + ), + ( + "Permalink", + { + "fields": ( + "shortterm", + "slug", + ), + }, + ), + ) + inlines = (JobInline,) + readonly_fields = ["slug"] + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichtfelder." + return super().add_view( + request, + form_url, + extra_context=extra_context, + ) + + def change_view(self, request, object_id, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["help_text"] = "Fette Schriften sind Pflichfelder." + return super().change_view( + request, + object_id, + form_url, + extra_context=extra_context, + ) + + def save_model(self, request, obj, form, change): + obj.author = request.user + super().save_model(request, obj, form, change) diff --git a/fet2020/members/models.py b/fet2020/members/models.py index 47bf60eb..f2f0337f 100644 --- a/fet2020/members/models.py +++ b/fet2020/members/models.py @@ -1,210 +1,207 @@ -import logging - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.validators import ValidationError, validate_email -from django.db import models -from django.db.models import F -from django.urls import reverse -from django.utils.text import slugify -from easy_thumbnails.fields import ThumbnailerImageField - -from .managers import ( - ActiveJobMemberManager, - InactiveJobMemberManager, - JobMemberManager, - MemberManager, -) -from .validators import ( - validate_domainonly_email, - validate_file_size, - validate_image_dimension, -) - -fet_logo_url = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png" -logger = logging.getLogger(__name__) - - -class Member(models.Model): - firstname = models.CharField("Vorname", max_length=128) - surname = models.CharField("Nachname", max_length=128) - nickname = models.CharField("Spitzname", max_length=128) - - # LDAP Username - username = models.CharField(max_length=128, blank=True) - - # fet mail account - mailaccount = models.CharField( - "Mailadresse", - unique=True, - max_length=128, - validators=[validate_email, validate_domainonly_email], - error_messages={ - "unique": "Diese Mailadresse existiert schon.", - }, - ) - - class MemberRole(models.TextChoices): - ACTIVE = "A", "Active" - PENSION = "P", "Pension" - - role = models.CharField( - "Rolle", - max_length=1, - choices=MemberRole.choices, - default=MemberRole.ACTIVE, - ) - - description = models.TextField(blank=True, default="") - - image = ThumbnailerImageField( - upload_to="uploads/members/image/", - validators=[validate_file_size, validate_image_dimension], - ) - - date_modified = models.DateTimeField(auto_now=True) - date_created = models.DateTimeField(auto_now_add=True) - - # Managers - objects = models.Manager() - all_members = MemberManager() - - class Meta: - verbose_name = "Mitglied" - verbose_name_plural = "Mitglieder" - - def __str__(self): - return self.firstname + " " + self.surname - - # need to have 'View on site' link in admin app - def get_absolute_url(self): - return reverse("members:member", kwargs={"pk": self.pk}) - - def clean(self): - if not self.image: - raise ValidationError("Es fehlt das Profilbild.") - - if self.username: - try: - user = User.objects.get(username=self.username.lower()) - except User.DoesNotExist as e: - logger.info("Username not found. Error: %s", e) - else: - user.first_name = self.firstname - user.save() - - def get_model_name(self): - return self._meta.model_name - - @property - def image_url(self): - return self.image.url if self.image else fet_logo_url - - @property - def avatar_url(self): - return self.image["avatar"].url if self.image else fet_logo_url - - @property - def portrait_url(self): - return self.image["portrait"].url if self.image else fet_logo_url - - @property - def thumb_url(self): - return self.image["thumb"].url if self.image else fet_logo_url - - -class JobGroup(models.Model): - name = models.CharField(max_length=128) - - shortterm = models.CharField(max_length=128, unique=True, blank=True) - slug = models.SlugField(unique=True, null=True, blank=True) - - description = models.TextField(blank=True, default="") - - # Managers - objects = models.Manager() - - class Meta: - verbose_name = "Tätigkeitsbereich" - verbose_name_plural = "Tätigkeitsbereiche" - - def __str__(self): - return self.name - - # need to have 'View on site' link in admin app - def get_absolute_url(self): - return reverse("members:jobs", kwargs={"slug": self.slug}) - - def clean(self): - if not self.shortterm: - self.shortterm = slugify(self.name) - self.slug = slugify(self.shortterm) - - -class Job(models.Model): - name = models.CharField(max_length=128) - - shortterm = models.CharField(max_length=128, unique=True, blank=True) - slug = models.SlugField(unique=True, null=True, blank=True) - - order = models.PositiveSmallIntegerField(null=True, blank=True) - - job_group = models.ForeignKey( - JobGroup, - on_delete=models.CASCADE, - verbose_name="Job Gruppe", - ) - - # Managers - objects = models.Manager() - - class Meta: - ordering = (F("order").asc(nulls_last=True), "name") - - verbose_name = "Tätigkeit" - verbose_name_plural = "Tätigkeiten" - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse("members:jobs", kwargs={"slug": self.job_group.slug}) + "#" + self.slug - - def clean(self): - if not self.shortterm: - self.shortterm = slugify(self.name) - self.slug = slugify(self.shortterm) - - -class JobMember(models.Model): - member = models.ForeignKey( - Member, - on_delete=models.CASCADE, - verbose_name="Mitglied", - ) - job = models.ForeignKey( - Job, - on_delete=models.CASCADE, - verbose_name="Tätigkeit", - ) - - job_start = models.DateField("Job Start") - job_end = models.DateField("Job Ende", null=True, blank=True) - - class JobRole(models.TextChoices): - PRESIDENT = ("10", "Vorsitz") - VICE_PRESIDENT = ("20", "Stv. Vorsitz") - SECOND_VICE_PRESIDENT = ("30", "2. Stv. Vorsitz") - PERSON_RESPONSIBLE = ("40", "Verantwortliche_r") - MEMBER = ("50", "Mitglied") - SUBSTITUTE_MEMBER = ("60", "Ersatzmitglied") - - job_role = models.CharField(max_length=2, choices=JobRole.choices, default=JobRole.MEMBER) - - objects = models.Manager() - members = JobMemberManager() - active_member = ActiveJobMemberManager() - inactive_member = InactiveJobMemberManager() - - def __str__(self): - return self.job.name +import logging + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.validators import validate_email +from django.db import models +from django.db.models import F +from django.urls import reverse +from django.utils.text import slugify +from easy_thumbnails.fields import ThumbnailerImageField + +from .managers import ( + ActiveJobMemberManager, + InactiveJobMemberManager, + JobMemberManager, + MemberManager, +) +from .validators import ( + validate_domainonly_email, + validate_file_size, + validate_image_dimension, +) + +fet_logo_url = settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png" +logger = logging.getLogger(__name__) + + +class Member(models.Model): + firstname = models.CharField("Vorname", max_length=128) + surname = models.CharField("Nachname", max_length=128) + nickname = models.CharField("Spitzname", max_length=128) + + # LDAP Username + username = models.CharField(max_length=128, blank=True) + + # fet mail account + mailaccount = models.CharField( + "Mailadresse", + unique=True, + max_length=128, + validators=[validate_email, validate_domainonly_email], + error_messages={ + "unique": "Diese Mailadresse existiert schon.", + }, + ) + + class MemberRole(models.TextChoices): + ACTIVE = "A", "Active" + PENSION = "P", "Pension" + + role = models.CharField( + "Rolle", + max_length=1, + choices=MemberRole.choices, + default=MemberRole.ACTIVE, + ) + + description = models.TextField(blank=True, default="") + + image = ThumbnailerImageField( + upload_to="uploads/members/image/", + validators=[validate_file_size, validate_image_dimension], + ) + + date_modified = models.DateTimeField(auto_now=True) + date_created = models.DateTimeField(auto_now_add=True) + + # Managers + objects = models.Manager() + all_members = MemberManager() + + class Meta: + verbose_name = "Mitglied" + verbose_name_plural = "Mitglieder" + + def __str__(self): + return self.firstname + " " + self.surname + + # need to have 'View on site' link in admin app + def get_absolute_url(self): + return reverse("members:member", kwargs={"pk": self.pk}) + + def clean(self): + if self.username: + try: + user = User.objects.get(username=self.username.lower()) + except User.DoesNotExist as e: + logger.info("Username not found. Error: %s", e) + else: + user.first_name = self.firstname + user.save() + + def get_model_name(self): + return self._meta.model_name + + @property + def image_url(self): + return self.image.url if self.image else fet_logo_url + + @property + def avatar_url(self): + return self.image["avatar"].url if self.image else fet_logo_url + + @property + def portrait_url(self): + return self.image["portrait"].url if self.image else fet_logo_url + + @property + def thumb_url(self): + return self.image["thumb"].url if self.image else fet_logo_url + + +class JobGroup(models.Model): + name = models.CharField(max_length=128) + + shortterm = models.CharField(max_length=128, unique=True, blank=True) + slug = models.SlugField(unique=True, null=True, blank=True) + + description = models.TextField(blank=True, default="") + + # Managers + objects = models.Manager() + + class Meta: + verbose_name = "Tätigkeitsbereich" + verbose_name_plural = "Tätigkeitsbereiche" + + def __str__(self): + return self.name + + # need to have 'View on site' link in admin app + def get_absolute_url(self): + return reverse("members:jobs", kwargs={"slug": self.slug}) + + def clean(self): + if not self.shortterm: + self.shortterm = slugify(self.name) + self.slug = slugify(self.shortterm) + + +class Job(models.Model): + name = models.CharField(max_length=128) + + shortterm = models.CharField(max_length=128, unique=True, blank=True) + slug = models.SlugField(unique=True, null=True, blank=True) + + order = models.PositiveSmallIntegerField(null=True, blank=True) + + job_group = models.ForeignKey( + JobGroup, + on_delete=models.CASCADE, + verbose_name="Job Gruppe", + ) + + # Managers + objects = models.Manager() + + class Meta: + ordering = (F("order").asc(nulls_last=True), "name") + + verbose_name = "Tätigkeit" + verbose_name_plural = "Tätigkeiten" + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse("members:jobs", kwargs={"slug": self.job_group.slug}) + "#" + self.slug + + def clean(self): + if not self.shortterm: + self.shortterm = slugify(self.name) + self.slug = slugify(self.shortterm) + + +class JobMember(models.Model): + member = models.ForeignKey( + Member, + on_delete=models.CASCADE, + verbose_name="Mitglied", + ) + job = models.ForeignKey( + Job, + on_delete=models.CASCADE, + verbose_name="Tätigkeit", + ) + + job_start = models.DateField("Job Start") + job_end = models.DateField("Job Ende", null=True, blank=True) + + class JobRole(models.TextChoices): + PRESIDENT = ("10", "Vorsitz") + VICE_PRESIDENT = ("20", "Stv. Vorsitz") + SECOND_VICE_PRESIDENT = ("30", "2. Stv. Vorsitz") + PERSON_RESPONSIBLE = ("40", "Verantwortliche_r") + MEMBER = ("50", "Mitglied") + SUBSTITUTE_MEMBER = ("60", "Ersatzmitglied") + + job_role = models.CharField(max_length=2, choices=JobRole.choices, default=JobRole.MEMBER) + + objects = models.Manager() + members = JobMemberManager() + active_member = ActiveJobMemberManager() + inactive_member = InactiveJobMemberManager() + + def __str__(self): + return self.job.name diff --git a/fet2020/members/templatetags/job_groups.py b/fet2020/members/templatetags/job_groups.py index ae778a94..6a350d0d 100644 --- a/fet2020/members/templatetags/job_groups.py +++ b/fet2020/members/templatetags/job_groups.py @@ -1,33 +1,31 @@ -from collections import deque - -from django import template -from django.db.models import F - -from members.models import JobGroup, JobMember - -register = template.Library() - - -@register.inclusion_tag("members/partials/jobs_sidebar.html") -def get_jobs_sidebar(slug): - job_groups = deque(JobGroup.objects.all()) - - # remove job group if there is no longer an active member - for elem in job_groups.copy(): - job_members = JobMember.active_member.get_all(slug=elem.slug) - if not job_members: - job_groups.remove(elem) - - job_members = JobMember.active_member.get_all(slug=slug).order_by( - F("job__order").asc(nulls_last=True), - "job__name", - ) - active_job_group = JobGroup.objects.filter(slug=slug).first() - - context = { - "job_groups": job_groups, - "job_members": job_members, - "active_job_group": active_job_group, - } - - return context +from collections import deque + +from django import template +from django.db.models import F + +from members.models import JobGroup, JobMember + +register = template.Library() + + +@register.inclusion_tag("members/partials/jobs_sidebar.html") +def get_jobs_sidebar(slug): + job_groups = deque(JobGroup.objects.all()) + + # remove job group if there is no longer an active member + for elem in job_groups.copy(): + job_members = JobMember.active_member.get_all(slug=elem.slug) + if not job_members: + job_groups.remove(elem) + + job_members = JobMember.active_member.get_all(slug=slug).order_by( + F("job__order").asc(nulls_last=True), + "job__name", + ) + active_job_group = JobGroup.objects.filter(slug=slug).first() + + return { + "job_groups": job_groups, + "job_members": job_members, + "active_job_group": active_job_group, + } diff --git a/fet2020/members/validators.py b/fet2020/members/validators.py index 6c31f5d0..d74aa3ad 100644 --- a/fet2020/members/validators.py +++ b/fet2020/members/validators.py @@ -1,22 +1,26 @@ -from django.core.validators import ValidationError - - -def validate_domainonly_email(value): - if "fet.at" not in value: - raise ValidationError("In der Mailadresse fehlt die richtige Domäne.") - - -def validate_file_size(value): - if value.size > 10 * 1024 * 1024: - raise ValidationError("Die maximale Dateigröße ist 10MB.") - - -def validate_image_dimension(value): - if value.height < 150 or value.width < 150: - raise ValidationError( - "Das Bild ist zu klein. (Höhe: %(height)s, Breite: %(width)s)", - params={ - "height": value.height, - "width": value.width, - }, - ) +from django.core.exceptions import ValidationError + + +def validate_domainonly_email(value): + if "fet.at" not in value: + msg = "In der Mailadresse fehlt die richtige Domäne." + raise ValidationError(msg, code="invalid_domain") + + +def validate_file_size(value): + if value.size > 10 * 1024 * 1024: + msg = "Die maximale Dateigröße ist 10MB." + raise ValidationError(msg, code="file_too_large") + + +def validate_image_dimension(value): + if value.height < 150 or value.width < 150: + msg = "Das Bild ist zu klein. (Höhe: %(height)s, Breite: %(width)s)" + raise ValidationError( + msg, + code="image_too_small", + params={ + "height": value.height, + "width": value.width, + }, + ) diff --git a/fet2020/minecraft/views.py b/fet2020/minecraft/views.py index cee1ff47..32762ef5 100644 --- a/fet2020/minecraft/views.py +++ b/fet2020/minecraft/views.py @@ -23,10 +23,7 @@ def create_token(username, masterpassword): @authenticated_user def index(request): - context = { - "mctoken": "", - "valid_master_pwd": True - } + context = {"mctoken": "", "valid_master_pwd": True} masterpassword = settings.MC_MASTERPASSWORD diff --git a/fet2020/posts/managers.py b/fet2020/posts/managers.py index ff4d40b1..ed4087e6 100644 --- a/fet2020/posts/managers.py +++ b/fet2020/posts/managers.py @@ -179,8 +179,7 @@ class EventManager(PublishedManager, models.Manager): def past_events(self, public=True): date_today = timezone.now().date() - qs = self.published(public).filter(event_start__lt=date_today) - return qs + return self.published(public).filter(event_start__lt=date_today) class FetMeetingManager(PublishedManager, models.Manager): @@ -204,5 +203,4 @@ class FetMeetingManager(PublishedManager, models.Manager): def past_events(self): date_today = timezone.now().date() - qs = self.published().filter(event_start__lt=date_today) - return qs + return self.published().filter(event_start__lt=date_today) diff --git a/fet2020/posts/models.py b/fet2020/posts/models.py index f3140778..75648e42 100644 --- a/fet2020/posts/models.py +++ b/fet2020/posts/models.py @@ -1,376 +1,384 @@ -import logging -from datetime import timedelta - -from django.conf import settings -from django.contrib.auth.models import User -from django.core.validators import ValidationError -from django.db import models -from django.urls import reverse -from django.utils import timezone -from django.utils.text import slugify -from taggit.managers import TaggableManager - -from core.models import CustomFlatPage -from documents.api import ep_create_new_pad, ep_get_html, ep_get_url, ep_pad_exists, ep_set_html - -from .choices import PostType, Status -from .managers import ( - AllEventManager, - ArticleManager, - EventManager, - FetMeetingManager, - NewsManager, - PostManager, -) - -logger = logging.getLogger(__name__) - - -def create_pad_for_post(slug, item="agenda"): - logger.info("Pad-Type: %s", item) - - pad_id = slug + "-" + item - if not ep_create_new_pad(pad_id): - return "" - - # Set template into the newly created pad if it exists. - if page := CustomFlatPage.objects.filter(title__iexact=item).first(): - ep_set_html(pad_id, page.content) - logger.info("Template '%s' is set.", page.title) - - return pad_id - - -class Post(models.Model): - # legacy id is for the posts from the old website - legacy_id = models.IntegerField(null=True, blank=True) - - title = models.CharField(verbose_name="Titel", max_length=200) - subtitle = models.CharField(max_length=500, blank=True, default="") - tags = TaggableManager(blank=True) - - slug = models.SlugField(unique=True, blank=True) - - body = models.TextField(blank=True, default="") - image = models.ImageField(null=True, blank=True) - - author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) - public_date = models.DateField( - verbose_name="Veröffentlichung", - null=True, - blank=True, - default=timezone.now, - ) - - post_type = models.CharField(max_length=1, choices=PostType.choices, editable=True) - - status = models.CharField( - verbose_name="Status", max_length=2, choices=Status.choices, default=Status.DRAFT - ) - - # post is pinned at main page - is_pinned = models.BooleanField(verbose_name="ANGEHEFTET", default=False) - - # addional infos for events - event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True) - event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True) - event_place = models.CharField(max_length=200, blank=True, default="") - - # protocol for fet meeting - has_protocol = models.BooleanField(default=False) - has_agenda = models.BooleanField(default=False) - protocol_key = models.CharField(max_length=200, blank=True, default="") - agenda_key = models.CharField(max_length=200, blank=True, default="") - - # TimeStamps - date_modified = models.DateTimeField(auto_now=True) - date_created = models.DateTimeField(auto_now_add=True) - - # Managers - objects = PostManager() - articles = ArticleManager() - - def __str__(self): - return "Post ({}, {}): {}".format( - self.slug, - self.public_date.strftime("%d.%m.%Y"), - self.title, - ) - - def save(self, *args, **kwargs): - # save the post with some defaults - if not self.public_date: - self.public_date = timezone.now().date() - - if not self.slug: - self.slug = slugify(self.public_date) + "-" + slugify(self.title) - - super().save(*args, **kwargs) - - def get_absolute_url(self): - return reverse("posts:post", kwargs={"slug": self.slug}) - - # "#" for backward compatibility - _possible_empty_key_value = ["#", "", None] - - @property - def agenda_html(self) -> str | None: - if self.agenda_key in self._possible_empty_key_value: - return None - - return ep_get_html(self.agenda_key) - - @agenda_html.setter - def agenda_html(self, value: str) -> str | None: - if self.agenda_key in self._possible_empty_key_value: - self.create_agenda_key() - - if not value or not self.agenda_key: - return None - - ep_set_html(self.agenda_key, value) - logger.info("Set agenda etherpad '%s' for post '%s'.", self.agenda_key, self.slug) - return value - - @property - def protocol_html(self) -> str | None: - if self.protocol_key in self._possible_empty_key_value: - return None - - return ep_get_html(self.protocol_key) - - @protocol_html.setter - def protocol_html(self, value: str) -> str | None: - if self.protocol_key in self._possible_empty_key_value: - self.create_protocol_key() - - if not value or not self.protocol_key: - return None - - ep_set_html(self.protocol_key, value) - logger.info("Set protocol etherpad '%s' for post '%s'.", self.protocol_key, self.slug) - return value - - _agenda_filename = None - _agenda_url = None - - @property - def agenda_url(self) -> str | None: - if not self.has_agenda: - self._agenda_url = None - self._agenda_filename = None - return self._agenda_url - - if self._agenda_url: - return self._agenda_url - - if url := ep_get_url(self.agenda_key): - self._agenda_url = url - self._agenda_filename = self.slug + "-agenda.pdf" - else: - self._agenda_url = None - self._agenda_filename = None - - return self._agenda_url - - @property - def agenda_filename(self) -> str | None: - # TODO: fix pdf render - # if self._agenda_filename: - # return self._agenda_filename - - # if self.has_agenda and self.agenda_url: - # return self.slug + "-agenda.pdf" - - return None - - _protocol_filename = None - _protocol_url = None - - @property - def protocol_url(self) -> str | None: - if not self.has_protocol: - self._protocol_url = None - self._protocol_filename = None - return self._protocol_url - - if self._protocol_url: - return self._protocol_url - - if url := ep_get_url(self.protocol_key): - self._protocol_url = url - self._protocol_filename = self.slug + "-protokoll.pdf" - else: - self._protocol_url = None - self._protocol_filename = None - - return self._protocol_url - - @property - def protocol_filename(self) -> str | None: - # TODO: fix pdf render - # if self._protocol_filename: - # return self._protocol_filename - - # if self.has_protocol and self.protocol_url: - # return self.slug + "-protokoll.pdf" - - return None - - def create_agenda_key(self) -> None: - """ - Create a Etherpad Id for the Pad associated to this post. - Create the pad if it doesn't exist. - """ - if self.slug: - self.agenda_key = create_pad_for_post(self.slug, "agenda") - - def create_protocol_key(self) -> None: - """ - Create a Etherpad Id for the Pad associated to this post. - Create the pad if it doesn't exist. - """ - if self.slug: - self.protocol_key = create_pad_for_post(self.slug, "protocol") - - def get_model_name(self): - return self._meta.model_name - - @property - def three_tag_names(self): - return self.tags.names()[:3] - - @property - def tag_names(self): - return [t for t in self.tags.names()] - - @property - def imageurl(self) -> str: - return ( - self.image.url if self.image else settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png" - ) - - def clean(self): - if self.event_end and self.event_end < self.event_start: - raise ValidationError("Das Ende des Events liegt vor dem Beginn.") - if self.event_start and self.post_type not in ["E", "F"]: - raise ValidationError("Für diesen Post Typ ist kein Event Start zulässig.") - - @property - def published(self): - return self.status == Status.PUBLIC - - -class News(Post): - objects = NewsManager() - - class Meta: - proxy = True - - verbose_name = "News" - verbose_name_plural = "News" - - def save(self, *args, **kwargs): - if not self.post_type: - self.post_type = "N" - - super().save(*args, **kwargs) - - -class Event(Post): - only_events = EventManager() - all_events = AllEventManager() - - class Meta: - proxy = True - - verbose_name = "Event" - verbose_name_plural = "Events" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.post_type = "E" - - def save(self, *args, **kwargs): - if not self.post_type: - self.post_type = "E" - if not self.event_end: - self.event_end = self.event_start + timedelta(hours=2) - - super().save(*args, **kwargs) - - def clean(self): - super().clean() - if not self.event_start: - raise ValidationError("Das Datum des Events fehlt.") - - -class FetMeeting(Event): - objects = FetMeetingManager() - - class Meta: - proxy = True - - verbose_name = "Fet Sitzung" - verbose_name_plural = "Fet Sitzungen" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.post_type = "F" - - def save(self, *args, **kwargs): - self.title = "Fachschaftssitzung" - if not self.slug: - self.slug = self.__get_slug() - - if not ep_pad_exists(self.agenda_key) or self.agenda_key in self._possible_empty_key_value: - self.create_agenda_key() - if self.agenda_key: - self.has_agenda = True - - if ( - not ep_pad_exists(self.protocol_key) - or self.protocol_key in self._possible_empty_key_value - ): - self.create_protocol_key() - if self.protocol_key: - self.has_protocol = True - - if not self.post_type: - self.post_type = "F" - - if not self.event_place: - self.event_place = "FET" - - # make duration 2 hours if not specified otherwise - if not self.event_end: - self.event_end = self.event_start + timedelta(hours=2) - - # set FET Meeting always public - self.status = Status.PUBLIC - - super().save(*args, **kwargs) - - def __get_slug(self) -> str: - slug = slugify(self.event_start.date()) + "-" + slugify("Fachschaftssitzung") - - if Post.objects.filter(slug=slug).exists() and Post.objects.get(slug=slug).id != self.id: - raise ValidationError("Es existiert bereits eine Sitzung mit demselben Datum.") - - return slug - - def clean(self): - super().clean() - if not self.slug: - self.slug = self.__get_slug() - - -class FileUpload(models.Model): - title = models.CharField(verbose_name="Titel", max_length=200) - file_field = models.FileField(verbose_name="Dokument", upload_to="uploads/posts/files/") - post = models.ForeignKey(Post, on_delete=models.CASCADE) - - objects = models.Manager() - - def __str__(self): - return self.title +import logging +from datetime import timedelta + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db import models +from django.urls import reverse +from django.utils import timezone +from django.utils.text import slugify +from taggit.managers import TaggableManager + +from core.models import CustomFlatPage +from documents.api import ep_create_new_pad, ep_get_html, ep_get_url, ep_pad_exists, ep_set_html + +from .choices import PostType, Status +from .managers import ( + AllEventManager, + ArticleManager, + EventManager, + FetMeetingManager, + NewsManager, + PostManager, +) + +logger = logging.getLogger(__name__) + + +def create_pad_for_post(slug, item="agenda"): + logger.info("Pad-Type: %s", item) + + pad_id = slug + "-" + item + if not ep_create_new_pad(pad_id): + return "" + + # Set template into the newly created pad if it exists. + if page := CustomFlatPage.objects.filter(title__iexact=item).first(): + ep_set_html(pad_id, page.content) + logger.info("Template '%s' is set.", page.title) + + return pad_id + + +class Post(models.Model): + # legacy id is for the posts from the old website + legacy_id = models.IntegerField(null=True, blank=True) + + title = models.CharField(verbose_name="Titel", max_length=200) + subtitle = models.CharField(max_length=500, blank=True, default="") + tags = TaggableManager(blank=True) + + slug = models.SlugField(unique=True, blank=True) + + body = models.TextField(blank=True, default="") + image = models.ImageField(null=True, blank=True) + + author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + public_date = models.DateField( + verbose_name="Veröffentlichung", + null=True, + blank=True, + default=timezone.now, + ) + + post_type = models.CharField(max_length=1, choices=PostType.choices, editable=True) + + status = models.CharField( + verbose_name="Status", max_length=2, choices=Status.choices, default=Status.DRAFT + ) + + # post is pinned at main page + is_pinned = models.BooleanField(verbose_name="ANGEHEFTET", default=False) + + # addional infos for events + event_start = models.DateTimeField(verbose_name="Event Start", null=True, blank=True) + event_end = models.DateTimeField(verbose_name="Event Ende", null=True, blank=True) + event_place = models.CharField(max_length=200, blank=True, default="") + + # protocol for fet meeting + has_protocol = models.BooleanField(default=False) + has_agenda = models.BooleanField(default=False) + protocol_key = models.CharField(max_length=200, blank=True, default="") + agenda_key = models.CharField(max_length=200, blank=True, default="") + + # TimeStamps + date_modified = models.DateTimeField(auto_now=True) + date_created = models.DateTimeField(auto_now_add=True) + + # Managers + objects = PostManager() + articles = ArticleManager() + + def __str__(self): + return "Post ({}, {}): {}".format( + self.slug, + self.public_date.strftime("%d.%m.%Y"), + self.title, + ) + + def save(self, *args, **kwargs): + # save the post with some defaults + if not self.public_date: + self.public_date = timezone.now().date() + + if not self.slug: + self.slug = slugify(self.public_date) + "-" + slugify(self.title) + + super().save(*args, **kwargs) + + def get_absolute_url(self): + return reverse("posts:post", kwargs={"slug": self.slug}) + + # "#" for backward compatibility + _possible_empty_key_value = ["#", "", None] + + @property + def agenda_html(self) -> str | None: + if self.agenda_key in self._possible_empty_key_value: + return None + + return ep_get_html(self.agenda_key) + + @agenda_html.setter + def agenda_html(self, value: str) -> str | None: + if self.agenda_key in self._possible_empty_key_value: + self.create_agenda_key() + + if not value or not self.agenda_key: + return None + + ep_set_html(self.agenda_key, value) + logger.info("Set agenda etherpad '%s' for post '%s'.", self.agenda_key, self.slug) + return value + + @property + def protocol_html(self) -> str | None: + if self.protocol_key in self._possible_empty_key_value: + return None + + return ep_get_html(self.protocol_key) + + @protocol_html.setter + def protocol_html(self, value: str) -> str | None: + if self.protocol_key in self._possible_empty_key_value: + self.create_protocol_key() + + if not value or not self.protocol_key: + return None + + ep_set_html(self.protocol_key, value) + logger.info("Set protocol etherpad '%s' for post '%s'.", self.protocol_key, self.slug) + return value + + _agenda_filename = None + _agenda_url = None + + @property + def agenda_url(self) -> str | None: + if not self.has_agenda: + self._agenda_url = None + self._agenda_filename = None + return self._agenda_url + + if self._agenda_url: + return self._agenda_url + + if url := ep_get_url(self.agenda_key): + self._agenda_url = url + self._agenda_filename = self.slug + "-agenda.pdf" + else: + self._agenda_url = None + self._agenda_filename = None + + return self._agenda_url + + @property + def agenda_filename(self) -> str | None: + # TODO: fix pdf render + # if self._agenda_filename: + # return self._agenda_filename + + # if self.has_agenda and self.agenda_url: + # return self.slug + "-agenda.pdf" + + return None + + _protocol_filename = None + _protocol_url = None + + @property + def protocol_url(self) -> str | None: + if not self.has_protocol: + self._protocol_url = None + self._protocol_filename = None + return self._protocol_url + + if self._protocol_url: + return self._protocol_url + + if url := ep_get_url(self.protocol_key): + self._protocol_url = url + self._protocol_filename = self.slug + "-protokoll.pdf" + else: + self._protocol_url = None + self._protocol_filename = None + + return self._protocol_url + + @property + def protocol_filename(self) -> str | None: + # TODO: fix pdf render + # if self._protocol_filename: + # return self._protocol_filename + + # if self.has_protocol and self.protocol_url: + # return self.slug + "-protokoll.pdf" + + return None + + def create_agenda_key(self) -> None: + """ + Create a Etherpad Id for the Pad associated to this post. + Create the pad if it doesn't exist. + """ + if self.slug: + self.agenda_key = create_pad_for_post(self.slug, "agenda") + + def create_protocol_key(self) -> None: + """ + Create a Etherpad Id for the Pad associated to this post. + Create the pad if it doesn't exist. + """ + if self.slug: + self.protocol_key = create_pad_for_post(self.slug, "protocol") + + def get_model_name(self): + return self._meta.model_name + + @property + def three_tag_names(self): + return self.tags.names()[:3] + + @property + def tag_names(self): + return [t for t in self.tags.names()] + + @property + def imageurl(self) -> str: + return ( + self.image.url if self.image else settings.STATIC_URL + "img/FET-Logo-2014-quadrat.png" + ) + + def clean(self): + if self.event_end and self.event_end < self.event_start: + msg = "Das Ende des Events liegt vor dem Beginn." + raise ValidationError(msg, code="invalid_event_end") + if self.event_start and self.post_type not in ["E", "F"]: + msg = "Für diesen Post Typ ist kein Event Start zulässig." + raise ValidationError(msg, code="invalid_event_start") + + @property + def published(self): + return self.status == Status.PUBLIC + + +class News(Post): + objects = NewsManager() + + class Meta: + proxy = True + + verbose_name = "News" + verbose_name_plural = "News" + + def save(self, *args, **kwargs): + if not self.post_type: + self.post_type = "N" + + super().save(*args, **kwargs) + + +class Event(Post): + only_events = EventManager() + all_events = AllEventManager() + + class Meta: + proxy = True + + verbose_name = "Event" + verbose_name_plural = "Events" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.post_type = "E" + + def save(self, *args, **kwargs): + if not self.post_type: + self.post_type = "E" + if not self.event_end: + self.event_end = self.event_start + timedelta(hours=2) + + super().save(*args, **kwargs) + + def clean(self): + super().clean() + if not self.event_start: + msg = "Das Datum des Events fehlt." + raise ValidationError(msg, code="missing_event_start") + + +class FetMeeting(Event): + objects = FetMeetingManager() + + class Meta: + proxy = True + + verbose_name = "Fet Sitzung" + verbose_name_plural = "Fet Sitzungen" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.post_type = "F" + + def save(self, *args, **kwargs): + self.title = "Fachschaftssitzung" + if not self.slug: + self.slug = self.__get_slug() + + if not ep_pad_exists(self.agenda_key) or self.agenda_key in self._possible_empty_key_value: + self.create_agenda_key() + if self.agenda_key: + self.has_agenda = True + + if ( + not ep_pad_exists(self.protocol_key) + or self.protocol_key in self._possible_empty_key_value + ): + self.create_protocol_key() + if self.protocol_key: + self.has_protocol = True + + if not self.post_type: + self.post_type = "F" + + if not self.event_place: + self.event_place = "FET" + + # make duration 2 hours if not specified otherwise + if not self.event_end: + self.event_end = self.event_start + timedelta(hours=2) + + # set FET Meeting always public + self.status = Status.PUBLIC + + super().save(*args, **kwargs) + + def __get_slug(self) -> str: + slug = slugify(self.event_start.date()) + "-" + slugify("Fachschaftssitzung") + + if Post.objects.filter(slug=slug).exists() and Post.objects.get(slug=slug).id != self.id: + msg = "Es existiert bereits eine Sitzung mit demselben Datum." + logger.error( + "A fet meeting with same date (slug: %(slug)s) is already existing.", + extra={"slug": slug}, + ) + raise ValidationError(msg, code="duplicate_fet_meeting") + + return slug + + def clean(self): + super().clean() + if not self.slug: + self.slug = self.__get_slug() + + +class FileUpload(models.Model): + title = models.CharField(verbose_name="Titel", max_length=200) + file_field = models.FileField(verbose_name="Dokument", upload_to="uploads/posts/files/") + post = models.ForeignKey(Post, on_delete=models.CASCADE) + + objects = models.Manager() + + def __str__(self): + return self.title diff --git a/fet2020/posts/search_indexes.py b/fet2020/posts/search_indexes.py index 667d28a6..60f0b8c0 100644 --- a/fet2020/posts/search_indexes.py +++ b/fet2020/posts/search_indexes.py @@ -1,36 +1,36 @@ -from haystack import indexes -from html2text import html2text - -from .models import Post - - -class PostIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True) - title = indexes.EdgeNgramField(model_attr="title") - body = indexes.EdgeNgramField(model_attr="body", null=True) - status = indexes.CharField(model_attr="status") - date = indexes.DateField() - agenda = indexes.EdgeNgramField() - protocol = indexes.EdgeNgramField() - - def get_model(self): - return Post - - def index_queryset(self, using=None): - return self.get_model().objects.date_sorted(public=False) - - def prepare_date(self, obj): - if obj.post_type == "N": - return obj.public_date - elif obj.post_type == "E" or obj.post_type == "F": - return obj.event_start.date() - - def prepare_agenda(self, obj): - if obj.has_agenda and obj.agenda_html: - return html2text(obj.agenda_html) - return None - - def prepare_protocol(self, obj): - if obj.has_protocol and obj.protocol_html: - return html2text(obj.protocol_html) - return None +from haystack import indexes +from html2text import html2text + +from .models import Post + + +class PostIndex(indexes.SearchIndex, indexes.Indexable): + text = indexes.CharField(document=True) + title = indexes.EdgeNgramField(model_attr="title") + body = indexes.EdgeNgramField(model_attr="body", null=True) + status = indexes.CharField(model_attr="status") + date = indexes.DateField() + agenda = indexes.EdgeNgramField() + protocol = indexes.EdgeNgramField() + + def get_model(self): + return Post + + def index_queryset(self, using=None): + return self.get_model().objects.date_sorted(public=False) + + def prepare_date(self, obj): + if obj.post_type == "E" or obj.post_type == "F": + return obj.event_start.date() + + return obj.public_date # obj.post_type == "N" + + def prepare_agenda(self, obj): + if obj.has_agenda and obj.agenda_html: + return html2text(obj.agenda_html) + return None + + def prepare_protocol(self, obj): + if obj.has_protocol and obj.protocol_html: + return html2text(obj.protocol_html) + return None diff --git a/fet2020/posts/views.py b/fet2020/posts/views.py index 0f74063d..9468e9d5 100644 --- a/fet2020/posts/views.py +++ b/fet2020/posts/views.py @@ -117,15 +117,17 @@ class PostDetailView(DetailView): if not obj.published: related_posts.remove(obj) - context = { - "post": self.object, - "files": files, - "author": author, - "author_image": author_image, - "next": self.post_next(), - "prev": self.post_prev(), - "related_posts": related_posts[:4], - } + context.update( + { + "post": self.object, + "files": files, + "author": author, + "author_image": author_image, + "next": self.post_next(), + "prev": self.post_prev(), + "related_posts": related_posts[:4], + } + ) return context @@ -309,7 +311,8 @@ def show_pdf(request, html, filename): html = html[:idx] + rendered + html[idx:] if not (pdf := render_to_pdf(html)): - raise Http404("can't create pdf file.") + msg = "PDF konnte nicht erstellt werden." + raise Http404(msg) response = HttpResponse(pdf, content_type="application/pdf") diff --git a/fet2020/rental/models.py b/fet2020/rental/models.py index 8af9d580..b6d38ab7 100644 --- a/fet2020/rental/models.py +++ b/fet2020/rental/models.py @@ -1,6 +1,6 @@ +from django.core.exceptions import ValidationError from django.core.validators import FileExtensionValidator from django.db import models -from django.forms import ValidationError from .mails import send_mail_approved, send_mail_rejected, send_mail_returned from .managers import RentalItemsManager @@ -116,7 +116,8 @@ class Rental(models.Model): self.date_end = self.date_start if self.date_start > self.date_end: - raise ValidationError("Das Abholdatum muss vor dem Rückgabedatum liegen.") + msg = "Das Abholdatum muss vor dem Rückgabedatum liegen." + raise ValidationError(msg, code="invalid_date_range") def calc_total_deposit(self) -> int: total_deposit = 0