foerderbarometer/input/models.py

568 lines
28 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
'CLOSE': 'die Projektabschlussmail wurde versendet',
'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'}
class TermsConsentMixin(models.Model):
"""Abstract mixin to add a terms_accepted field for documenting user consent."""
terms_accepted = models.BooleanField(default=False, verbose_name="Nutzungsbedingungen zugestimmt")
class Meta:
abstract = True
class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name='Realname',
help_text='Bitte gib deinen Vornamen und deinen Nachnamen ein.', default='')
email = models.EmailField(max_length=200, null=True, verbose_name='E-Mail-Adresse',
help_text=mark_safe('Bitte gib deine E-Mail-Adresse ein, damit dich<br>Wikimedia Deutschland bei Rückfragen oder für<br>die Zusage kontaktieren kann.'))
# the following Fields are not supposed to be edited by users
granted = models.BooleanField(null=True, verbose_name='bewilligt')
granted_date = models.DateField(null=True, verbose_name='bewilligt am')
survey_mail_date = models.DateField(verbose_name='Umfragemail wurde verschickt am', null=True, blank=True)
mail_state = models.CharField(max_length=6, choices=EMAIL_STATES.items(), default='NONE')
survey_mail_send = models.BooleanField(default=False, verbose_name='Keine Umfragemail schicken')
@classmethod
def set_granted(cl, key, b):
obj = cl.objects.get(pk=key)
obj.granted = b
obj.granted_date = date.today()
obj.save()
class Meta:
abstract = True
class Extern(Volunteer):
''' abstract basis class for all data entered by extern volunteers '''
username = models.CharField(max_length=200, null=True, verbose_name='Benutzer_innenname',
help_text=mark_safe('Wikimedia Benutzer_innenname'))
# the following Fields are not supposed to be edited by users
service_id = models.CharField(max_length=15, null=True, blank=True)
def save(self,*args,**kwargs):
# we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# but maybe there is a better solution?
super().save()
self.service_id = type(self).__name__ + str(self.pk)
super().save()
class Meta:
abstract = True
class ConcreteExtern(Extern):
''' needed because we can't initiate abstract base classes in the view'''
pass
class Account(models.Model):
code = models.CharField('Kostenstelle', max_length=5, default='DEF',
null=False, primary_key = True)
description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
def __str__(self):
return f'{self.code} {self.description}'
class Project(Volunteer):
end_mail_send = models.BooleanField(default=False, verbose_name='Keine Projektabschlussmail schicken')
name = models.CharField(max_length=200, verbose_name='Name des Projekts')
description = models.CharField(max_length=500, verbose_name='Kurzbeschreibung', null=True)
start = models.DateField('Startdatum', null=True)
end = models.DateField('Erwartetes Projektende', null=True)
otrs = models.URLField(max_length=300, null=True, verbose_name='OTRS-Link')
plan = models.URLField(max_length=2000, null=True, blank=True, verbose_name='Link zum Förderplan')
page = models.URLField(max_length=2000, null=True, blank=True, verbose_name='Link zur Projektseite')
urls = models.CharField(max_length=2000, null=True, blank=True, verbose_name='Weitere Links')
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/Adresse/Location')
participants_estimated = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende angefragt')
participants_real = models.IntegerField(blank=True, null=True, verbose_name='Teilnehmende ausgezählt')
insurance = models.BooleanField(default=False, verbose_name='Haftpflichtversicherung')
insurance_technic = models.BooleanField(default=False, verbose_name='Technikversicherung Ausland')
support = models.CharField(max_length=300, blank=True, null=True, verbose_name='Betreuungsperson und Vertretung')
cost = models.IntegerField(blank=True, null=True, verbose_name='Kosten')
account = models.ForeignKey('Account', on_delete=models.CASCADE, null=True, to_field='code', db_constraint = False)
granted_from = models.CharField(max_length=100,null=True,verbose_name='Bewilligt von')
notes = models.TextField(max_length=1000,null=True,blank=True,verbose_name='Anmerkungen')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
# the following Fields are not supposed to be edited by users
pid = models.CharField(max_length=15, null=True, blank=True)
status = models.CharField(max_length=3,choices=(('RUN', 'läuft'),('END','beendet'),('NOT','nicht stattgefunden')),default='RUN')
finance_id = models.CharField(max_length=15, null= True, blank=True)
project_of_year = models.IntegerField(default=0)
end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name='Quartal Projekt Ende')
def save(self,*args,**kwargs):
generate_finance_id=False
'''we generate the autogenerated fields here'''
# we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# but maybe there is a better solution?
if not self.pk:
print ('NO PK THERE');
generate_finance_id=True
super().save()
else:
orig = type(self).objects.get(pk=self.pk) # Originaldaten aus der DB abrufen
if orig.start.year != self.start.year:
generate_finance_id=True
if orig.account.code != self.account.code:
if str(self.account.code) == '21111':
generate_finance_id=True
else:
self.finance_id = str(self.account.code)
if generate_finance_id:
print ('MUST GENERATE FINANCE ID')
year = self.start.year
projects = Project.objects.filter(start__year=year)
if not projects:
self.project_of_year = 1
#self.pid = str(self.start.year) + '-' + str(self.account.code) + str(self.project_of_year).zfill(3)
else:
# get the project of year number of latest entry
projects = projects.order_by('-project_of_year')[0]
# add one to value of latest entry
self.project_of_year = int(projects.project_of_year) + 1
# self.pid = str(self.start.year) + '-' + str(self.account.code) + str(self.project_of_year).zfill(3)
if str(self.account.code) == '21111':
self.finance_id = str(self.account.code) + '-' + str(self.project_of_year).zfill(3)
else:
self.finance_id = str(self.account.code)
# print (('Current PID',self.pid))
if not self.pid:
self.pid = str(self.account.code) + str(self.pk).zfill(8)
# self.pid = str(self.account.code) + str(self.pk).zfill(3)
print (('Hallo Leute! Ich save jetzt mal MIT PID DANN!!!',self.pid))
if self.end:
self.end_quartal = f'Q{self.end.month // 4 + 1}'
super().save()
def __str__(self):
return f'{self.pid} {self.name}'
class Intern(Volunteer):
'''abstract base class for data entry from /intern (except Project)'''
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
abstract = True
class ConcreteVolunteer(Volunteer):
''' needed because we can't initiate abstract base classes in the view'''
pass
class HonoraryCertificate(Intern):
''' this class is also used for accreditations '''
project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.SET_NULL)
def __str__(self):
return 'Certificate for ' + self.realname
TRANSPORT_CHOICES = {'BAHN': 'Bahn',
'NONE': 'Keine Fahrtkosten',
'OTHER': 'Sonstiges (mit Begründung)'}
PAYEDBY_CHOICES = {'WMDE': 'WMDE',
'REQU': 'Antragstellender Mensch'}
HOTEL_CHOICES = {'TRUE': mark_safe('Hotelzimmer benötigt'),
'FALSE': mark_safe('Kein Hotelzimmer benötigt')
}
from django.contrib.contenttypes.models import ContentType
class Travel(Extern):
# project variable is now null true and blank true, which means it can be saved without project id to be later on filled out by admins
project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True)
project_name = models.CharField(max_length=50, null=True, blank=True, verbose_name='Projektname:')
transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel:')
other_transport = models.CharField(max_length=200, null=True, blank=True, verbose_name='Sonstige Transportmittel (mit Begründung)')
travelcost = models.CharField(max_length=10, default='0', verbose_name='Fahrtkosten')
checkin = models.DateField(blank=True, null=True, verbose_name='Anreise')
checkout = models.DateField(blank=True, null=True, verbose_name='Abreise')
payed_for_hotel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Hotel durch')
payed_for_travel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Fahrt durch')
hotel = models.CharField(max_length=10, choices=HOTEL_CHOICES.items(), verbose_name='Hotelzimmer benötigt:')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen')
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
project_end = models.DateField(blank=True, null=True, verbose_name='Projektende')
# use content type model to get the end date for the project foreign key
project_end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name='Quartal Projekt Ende')
from django.db.models.signals import pre_save
from django.dispatch import receiver
@receiver(pre_save, sender=Travel, dispatch_uid='get_project_end')
def getProjectEnd(sender, instance, **kwargs):
#instance.project_end = instance.project.end
if instance.project:
instance.project_end = instance.project.end
instance.project_end_quartal = instance.project.end_quartal
# using pre save instead
# def save(self,*args,**kwargs):
# '''we generate the autogenerated fields here'''
# # we don't call save with args/kwargs to avoid UNIQUE CONSTRAINT errors
# # but maybe there is a better solution?
# intern_notes
# project_end = self.checkout
# super(Travel, self).save(*args,**kwargs)
#abstract base class for Library and IFG
class Grant(Extern):
cost = models.CharField(max_length=10, verbose_name='Kosten',
help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen',
help_text='Bitte gib an wofür Du das Stipendium verwenden willst.')
class Meta:
abstract = True
def type_link(path, label):
return format_html(
format_string='<a href="{href}" target="_blank" rel="noopener">{label}</a>',
href=f'https://de.wikipedia.org/wiki/Wikipedia:Förderung/{path}',
label=label,
)
TYPE_BIB = 'BIB'
TYPE_ELIT = 'ELIT'
TYPE_MAIL = 'MAIL'
TYPE_IFG = 'IFG'
TYPE_LIT = 'LIT'
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'),
TYPE_ELIT: type_link('Zugang_zu_Fachliteratur#eLiteraturstipendium', 'eLiteraturstipendium'),
TYPE_MAIL: type_link('E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen', 'E-Mail-Adresse'),
TYPE_IFG: type_link('Gebührenerstattungen_für_Behördenanfragen', 'Kostenübernahme IFG-Anfrage'),
TYPE_LIT: type_link('Zugang_zu_Fachliteratur#Literaturstipendium', 'Literaturstipendium'),
TYPE_LIST: type_link('E-Mail-Adressen_und_Visitenkarten#Mailinglisten', 'Mailingliste'),
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
LIBRARY_TYPE_CHOICES = [(choice, TYPE_CHOICES[choice]) for choice in LIBRARY_TYPES]
# same model is used for Library, ELitStip and Software!
class Library(Grant):
TYPE = TYPE_BIB
LIBRARY_LABEL = 'Bibliothek'
LIBRARY_HELP_TEXT = 'Für welche Bibliothek gilt das Stipendium?'
DURATION_HELP_TEXT = mark_safe('In welchem Zeitraum möchtest du recherchieren oder<br>wie lange ist der Bibliotheksausweis gültig?')
type = models.CharField(max_length=4, choices=LIBRARY_TYPE_CHOICES, default=TYPE_BIB)
library = models.CharField(max_length=200)
duration = models.CharField(max_length=100, verbose_name='Dauer')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
def __str__(self):
return self.library
def save(self, **kwargs):
self.type = self.TYPE
return super().save(**kwargs)
class ELiterature(Library):
TYPE = TYPE_ELIT
LIBRARY_LABEL = 'Datenbank/Online-Ressource'
LIBRARY_HELP_TEXT = 'Für welche Datenbank/Online-Ressource gilt das Stipendium?'
DURATION_HELP_TEXT = 'Wie lange gilt der Zugang?'
class Meta:
proxy = True
class Software(Library):
TYPE = TYPE_SOFT
LIBRARY_LABEL = 'Software'
LIBRARY_HELP_TEXT = 'Für welche Software gilt das Stipendium?'
DURATION_HELP_TEXT = 'Wie lange gilt die Lizenz?'
class Meta:
proxy = True
SELFBUY_CHOICES = {'TRUE': mark_safe('Ich möchte das Werk selbst kaufen und per Kostenerstattung bei Wikimedia Deutschland abrechnen.'),
'FALSE': mark_safe('Ich möchte, dass Wikimedia Deutschland das Werk für mich kauft'),
}
class Literature(TermsConsentMixin, Grant):
info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=mark_safe('Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)'))
source = models.CharField(max_length=200, verbose_name='Bezugsquelle',
help_text='Bitte gib an, wo du das Werk kaufen möchtest.')
selfbuy = models.CharField( max_length=10, verbose_name='Selbstkauf?', choices=SELFBUY_CHOICES.items(), default='TRUE')
selfbuy_give_data = models.BooleanField(verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Ich stimme der Weitergabe meiner Daten (Name, Postadresse) an den von mir angegebenen Anbieter/Dienstleister zu.'))
selfbuy_data = models.TextField(max_length=1000, verbose_name='Persönliche Daten sowie Adresse', default='',\
help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, die wir benötigen, um das Werk<br>\
für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift, <br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name='URL',
help_text='Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
def __str__(self):
return 'IFG-Anfrage von ' + self.realname
DOMAIN_CHOICES = {'PEDIA': '@wikipedia.de',
'BOOKS': '@wikibooks.de',
'QUOTE': '@wikiquote.de',
'SOURCE': '@wikisource.de',
'VERSITY': '@wikiversity.de',}
class Domain(Extern):
domain = models.CharField(max_length=10,
choices=DOMAIN_CHOICES.items(),
default='PEDIA')
class Meta:
abstract = True
MAIL_CHOICES = {'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username',
'OTHER': 'Sonstiges:'}
ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.')
}
class Email(TermsConsentMixin, Domain):
address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil',
help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.'))
other = models.CharField(max_length=50,blank=True,null=True, verbose_name='Sonstiges')
adult = models.CharField( max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name='Adressbestandteil für Projektmailingliste',
help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
'SOURCE': 'Wikisource',
'BOOKS': 'Wikibooks',
'QUOTE': 'Wikiquote',
'VERSITY': 'Wikiversity',
'VOYAGE': 'Wikivoyage',
'DATA': 'Wikidata',
'NEWS': 'Wikinews',
'COMMONS': 'Wikimedia Commons'}
BC_VARIANT = {'PIC': 'mit Bild',
'NOPIC': 'ohne Bild'}
class BusinessCard(TermsConsentMixin, Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')
data = models.TextField(max_length=1000, verbose_name='Persönliche Daten für die Visitenkarten', default='',
help_text=mark_safe('Bitte gib hier alle persönlichen Daten an, und zwar genau so,<br>\
wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen<br>\
(z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,<br>\
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.<br>\
Hinweis: Telefonnummern bilden wir üblicherweise im internationalen Format gemäß<br>\
DIN 5008 ab. Als anzugebende E-Mail-Adresse empfehlen wir dir eine Wikimedia-Projekt-<br>\
Adresse, die du ebenfalls beantragen kannst, sofern du nicht bereits eine besitzt.'))
variant = models.CharField(max_length=5, choices=BC_VARIANT.items(),
default='NOPIC', verbose_name='Variante',
help_text=mark_safe('so sehen die Varianten aus: <a href="https://upload.wikimedia.org/wikipedia/commons/c/cd/Muster_Visitenkarten_WMDE_2018.jpg">\
mit Bild</a> <a href="https://upload.wikimedia.org/wikipedia/commons/d/d3/Muster_Visitenkarte_WMDE.png">ohne Bild</a>' ))
url_of_pic = models.CharField(max_length=200, verbose_name='Url des Bildes', default='', help_text='Bitte gib die Wikimedia-Commons-URL des Bildes an.')
sent_to = models.TextField(max_length=1000, verbose_name='Versandadresse',
default='', help_text='Bitte gib den Namen und die vollständige Adresse ein, an welche die Visitenkarten geschickt werden sollen.')
send_data_to_print = models.BooleanField(default=False, verbose_name=mark_safe('Datenweitergabe erlauben'), help_text=mark_safe('Hiermit erlaube ich die Weitergabe meiner Daten (Name, Postadresse) an den von Wikimedia<br> Deutschland ausgewählten Dienstleister (z. B. <a href="wir-machen-druck.de">wir-machen-druck.de</a>) zum Zwecke des direkten <br> Versands der Druckerzeugnisse an mich.'))
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,
TYPE_MAIL: Email,
TYPE_IFG: IFG,
TYPE_LIT: Literature,
TYPE_LIST: List,
TYPE_TRAV: Travel,
TYPE_SOFT: Software,
TYPE_VIS: BusinessCard,
}