Add ProjectRequest and ProjectsDeclined models for project funding under 1000 EUR

This commit is contained in:
Roman 2025-09-29 00:18:40 +02:00
parent a239922e14
commit 011e262df6
1 changed files with 122 additions and 0 deletions

View File

@ -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,