diff --git a/input/models.py b/input/models.py index c0a8949..551965f 100755 --- a/input/models.py +++ b/input/models.py @@ -3,6 +3,8 @@ from datetime import date from django.db import models from django.utils.html import format_html from django.utils.safestring import mark_safe +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator, MaxValueValidator EMAIL_STATES = {'NONE': 'noch keine Mail versendet', 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet', @@ -261,6 +263,7 @@ TYPE_LIST = 'LIST' TYPE_TRAV = 'TRAV' TYPE_SOFT = 'SOFT' TYPE_VIS = 'VIS' +TYPE_PROJ_LT_1000 = 'PROJ_LT_1000' TYPE_CHOICES = { TYPE_BIB: type_link('Zugang_zu_Fachliteratur#Bibliotheksstipendium', 'Bibliotheksstipendium'), @@ -272,6 +275,7 @@ TYPE_CHOICES = { TYPE_TRAV: type_link('Reisekostenerstattungen', 'Reisekosten'), TYPE_SOFT: type_link('Software-Stipendien', 'Softwarestipendium'), TYPE_VIS: type_link('E-Mail-Adressen_und_Visitenkarten#Visitenkarten', 'Visitenkarten'), + TYPE_PROJ_LT_1000: type_link('Projektplanung', 'Projektförderung unter 1000 EUR'), } LIBRARY_TYPES = TYPE_BIB, TYPE_ELIT, TYPE_SOFT @@ -422,6 +426,124 @@ class BusinessCard(Extern): intern_notes = models.TextField(max_length=1000, blank=True, verbose_name="interne Anmerkungen") +PROJECT_CATEGORIES = [ + 'Erstellung und Weiterentwicklung von Inhalten für die Wikimedia-Projekte', + 'Aufklärung über die Wikimedia-Projekte', + 'Formate zur Ansprache, Gewinnung und Bindung von Ehrenamtlichen für die Wikimedia-Projekte', + 'Beteiligung von Menschen, die einen erschwerten Zugang zum Engagement in den Wikimedia-Projekten haben', + 'Vernetzung und Austausch innerhalb der Communitys oder zwischen den Communitys und externen Partner*innen', + 'Vermittlung von Kompetenzen, die die ehrenamtliche Arbeit stärken', + 'Stärkung einer respektvollen, konstruktiven Kommunikationskultur und der Wertschätzung in den Wikimedia-Projekten', + 'Verbesserung der Selbstorganisation in Bezug auf interne Regeln, Strukturen und Prozesse der Wikimedia-Projektcommunitys', + 'Ehrenamtliche Aktivitäten, die der Erstellung, Pflege und Weiterentwicklung von Tools oder sonstigen technischen Verbesserungen dienen', + 'Sonstiges' +] + +WIKIMEDIA_CHOICES = ['Wikipedia', 'Wikimedia Commons', 'Wikidata', 'Anderes'] + + +class Decision(models.TextChoices): + OPEN = 'OPEN', 'offen' + APPROVED = 'APPROVED', 'bewilligt' + DECLINED = 'DECLINED', 'abgelehnt' + + +# Application for project funding < 1000 EUR +class ProjectRequest(Volunteer): + name = models.CharField(max_length=200, verbose_name='Name des Projekts') + description = models.TextField(max_length=500, verbose_name='Kurzbeschreibung des Projekts') + + # Multi-select: stored pragmatically as JSON + categories = models.JSONField(default=list, verbose_name='Projektkategorie') + categories_other = models.CharField(max_length=200, null=True, blank=True, + verbose_name='Projektkategorie: Sonstiges (kurz)') + wikimedia_projects = models.JSONField(default=list, verbose_name='Wikimedia Projekt(e)') + wikimedia_other = models.CharField(max_length=200, null=True, blank=True, + verbose_name='Wikimedia-Projekt: Anderes (kurz)' + ) + + start = models.DateField('Startdatum') + end = models.DateField('Erwartetes Projektende') + participants_estimated = models.IntegerField(verbose_name='Zahl der Teilnehmenden', + validators=[MinValueValidator(0)]) + + page = models.URLField(max_length=2000, null=True, blank=True, verbose_name='Link zur Projektseite') + group = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Mitorganisierende') + location = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Ort') + + cost = models.IntegerField(verbose_name='Höhe der Projektkosten', + validators=[MinValueValidator(0), MaxValueValidator(1000)]) + insurance = models.BooleanField(default=False, verbose_name='Versicherung gewünscht?') + notes = models.TextField(max_length=2000, null=True, blank=True, verbose_name='Anmerkungen') + + # Workflow fields (used only for the request process) + decision = models.CharField( + max_length=10, choices=Decision.choices, default=Decision.OPEN, db_index=True + ) + decision_date = models.DateField(null=True, blank=True, db_index=True) + decided_by = models.CharField(max_length=100, null=True, blank=True, verbose_name='Entschieden von') + + class Meta: + verbose_name = 'Projektförderungs-Antrag (< 1000 EUR)' + verbose_name_plural = 'Projects_requested' + ordering = ('-id',) + + def __str__(self): + return f"[Antrag] {self.name} ({self.realname})" + + def clean(self): + super().clean() + + # 1) Additional guard if MaxValueValidator is removed + if self.cost is not None and self.cost > 1000: + raise ValidationError({ + 'cost': ('Bitte beachte, dass für Projektkosten über 1.000 EUR ' + 'ein öffentlicher Projektplan erforderlich ist ' + '(siehe Wikipedia:Förderung/Projektplanung).') + }) + + # 2) Required and allowed values + if not self.categories: + raise ValidationError({'categories': 'Bitte wähle mindestens eine Projektkategorie.'}) + unknown = set(self.categories) - set(PROJECT_CATEGORIES) + if unknown: + raise ValidationError({'categories': f'Unzulässige Kategorie(n): {", ".join(unknown)}'}) + + if not self.wikimedia_projects: + raise ValidationError({'wikimedia_projects': 'Bitte wähle mindestens ein Wikimedia-Projekt.'}) + unknown_w = set(self.wikimedia_projects) - set(WIKIMEDIA_CHOICES) + if unknown_w: + raise ValidationError({'wikimedia_projects': f'Ungültige Auswahl: {", ".join(unknown_w)}'}) + + # 3) Require short text for “Sonstiges/Anderes” + if 'Sonstiges' in self.categories and not (self.categories_other and self.categories_other.strip()): + raise ValidationError({'categories_other': 'Bitte kurz beschreiben (Sonstiges).'}) + + if 'Anderes' in self.wikimedia_projects and not (self.wikimedia_other and self.wikimedia_other.strip()): + raise ValidationError({'wikimedia_other': 'Bitte kurz angeben (Anderes).'}) + + # 4) Date consistency + if self.start and self.end and self.end < self.start: + raise ValidationError({'end': 'Erwartetes Projektende darf nicht vor dem Startdatum liegen.'}) + + +# Archive table for declined applications (no PID/Finance-ID) +class ProjectsDeclined(models.Model): + original_request_id = models.PositiveIntegerField() + name = models.CharField(max_length=200) + realname = models.CharField(max_length=200) + email = models.EmailField() + decision_date = models.DateField() + reason = models.TextField(null=True, blank=True) + + class Meta: + verbose_name_plural = 'Projects_declined' + ordering = ('-decision_date', '-id') + + def __str__(self): + return f"[Abgelehnt] {self.name} – {self.decision_date}" + + MODELS = { TYPE_BIB: Library, TYPE_ELIT: ELiterature,