from contextlib import suppress from datetime import date from functools import partial from django import forms from django.contrib.contenttypes.models import ContentType from django.core.validators import MaxValueValidator from django.db import models from django.db.models.signals import pre_save from django.dispatch import receiver from django.forms import ModelMultipleChoiceField, CheckboxSelectMultiple from django.forms.models import ModelChoiceIterator from django.utils.functional import cached_property, classproperty from django.utils.html import format_html from django.utils.safestring import mark_safe 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
Wikimedia Deutschland bei Rückfragen oder für
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 BaseProjectCategory(models.Model): OTHER: str name = models.CharField('Name', max_length=200) order = models.PositiveIntegerField('Reihenfolge') class Meta: abstract = True ordering = ['order'] def __str__(self): return self.name @cached_property def project_count(self): return self.projects.count() @classproperty def other(cls): return cls(id=0, name=cls.OTHER) class ProjectCategory(BaseProjectCategory): OTHER = 'Sonstiges' class Meta(BaseProjectCategory.Meta): verbose_name = 'Projektkategorie' verbose_name_plural = 'Projektkategorien' class WikimediaProject(BaseProjectCategory): OTHER = 'Anderes' class Meta(BaseProjectCategory.Meta): verbose_name = 'Wikimedia Projekt' verbose_name_plural = 'Wikimedia Projekte' class ProductCategoryChoiceIterator(ModelChoiceIterator): def __iter__(self): yield from ModelChoiceIterator.__iter__(self) yield f'{self.field.other.id}', self.field.other.name class ProductCategoryFormField(ModelMultipleChoiceField): widget = CheckboxSelectMultiple iterator = ProductCategoryChoiceIterator def __init__(self, *, other, **kwargs): super().__init__(**kwargs) self.other = other def _check_values(self, value, *, other=False): with suppress(TypeError): value = set(value) if other := f'{self.other.id}' in value: value.remove(f'{self.other.id}') queryset = super()._check_values(value) if other: return [*queryset, self.other] return list(queryset) class ProjectCategoryField(models.ManyToManyField): def __init__(self, to, **kwargs): kwargs['to'] = to kwargs['related_name'] = 'projects' super().__init__(**kwargs) self.other_field = models.CharField(max_length=200, blank=True) def contribute_to_class(self, cls, name, **kwargs): super().contribute_to_class(cls, name, **kwargs) model, other_field = self.remote_field.model, self.other_field if not isinstance(model, str): self.verbose_name = self._verbose_name = verbose_name = model._meta.verbose_name_plural other_field.verbose_name = other_field._verbose_name = f'{verbose_name} ({model.OTHER})' other_field.contribute_to_class(cls, f'{name}_other') def formfield(self, **kwargs): kwargs['form_class'] = ProductCategoryFormField kwargs['other'] = self.remote_field.model.other return super().formfield(**kwargs) 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') categories = ProjectCategoryField(ProjectCategory) wikimedia_projects = ProjectCategoryField(WikimediaProject) # 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}' def clean(self): if (self.start and self.end) and (self.end < self.start): raise forms.ValidationError({ 'end': [ forms.ValidationError('Das erwartete Projektende muss nach dem Startdatum liegen.'), ], }) 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'), } 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') @receiver(pre_save, sender=Travel, dispatch_uid='get_project_end') def get_project_end(sender, instance, **kwargs): if instance.project: instance.project_end = instance.project.end instance.project_end_quartal = instance.project.end_quartal # 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='{label}', 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 = 'PROJ' 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: 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
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,
\ 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
\ für dich zu kaufen und es dir anschließend zu schicken (z.B. Vorname Nachname, Anschrift,
\ 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,
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,
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,
\ wie sie (auch in der entsprechenden Reihenfolge) auf den Visitenkarten stehen sollen
\ (z.B. Vorname Nachname, Benutzer:/Benutzerin:, Benutzer-/-innenname, Anschrift,
\ Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.
\ Hinweis: Telefonnummern bilden wir üblicherweise im internationalen Format gemäß
\ DIN 5008 ab. Als anzugebende E-Mail-Adresse empfehlen wir dir eine Wikimedia-Projekt-
\ 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: \ mit Bild ohne Bild')) 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
Deutschland ausgewählten Dienstleister (z. B. wir-machen-druck.de) zum Zwecke des direkten
Versands der Druckerzeugnisse an mich.')) intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') class Decision(models.TextChoices): OPEN = 'OPEN', 'offen' APPROVED = 'APPROVED', 'bewilligt' DECLINED = 'DECLINED', 'abgelehnt' validate_cost = MaxValueValidator( limit_value=100, message=( 'Bitte beachte, dass für Projektkosten über 1.000 EUR ' 'ein öffentlicher Projektplan erforderlich ist ' '(siehe Wikipedia:Förderung/Projektplanung).' ), ) 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, }