Compare commits

..

No commits in common. "3fe9b0892584b52d3f2a51b11a38a9f0da3f0545" and "ebd7ebd3fd9087c9bb06c2c36e4d76f281adccf2" have entirely different histories.

39 changed files with 395 additions and 525 deletions

View File

@ -121,9 +121,6 @@ AUTH_PASSWORD_VALIDATORS = password_validators(
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
LANGUAGE_CODE = env('LANGUAGE_CODE', 'de') LANGUAGE_CODE = env('LANGUAGE_CODE', 'de')
LANGUAGES = [
('de', 'Deutsch'),
]
USE_TZ = True USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'Europe/Berlin') TIME_ZONE = env('TIME_ZONE', 'Europe/Berlin')

View File

@ -1,10 +1,10 @@
import csv import csv
from django.contrib import admin from django.contrib import admin
from django.db import models from django.db import models, transaction
from django.http import HttpResponse from django.http import HttpResponse
from input.utils.list import reorder_value from input.utils.mail import send_decision_mails
from .forms import BaseProjectForm from .forms import BaseProjectForm
from .models import ( from .models import (
@ -24,21 +24,24 @@ from .models import (
BusinessCard, BusinessCard,
List, List,
Literature, Literature,
TYPE_PROJ,
) )
class WMDEAdmin(admin.ModelAdmin): class RequestURLBeforeInternNotesMixin:
"""
Ensures that 'request_url' appears directly before 'intern_notes'.
Works whether 'fields' is explicitly defined or derived from the Model/Form.
"""
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
fields = super().get_fields(request, obj=obj) fields = [*super().get_fields(request, obj)]
if 'username' in fields: fields.remove('request_url')
fields = reorder_value(fields, 'username', after='email')
fields = reorder_value(fields, 'request_url', before='intern_notes') index = fields.index('intern_notes')
if 'terms_accepted' in fields: fields.insert(index, 'request_url')
fields = reorder_value(fields, 'terms_accepted', before='request_url')
return fields return fields
@ -111,7 +114,6 @@ class BaseProjectAdmin(admin.ModelAdmin):
('Kontakt', {'fields': ( ('Kontakt', {'fields': (
'realname', 'realname',
'email', 'email',
'username',
)}), )}),
('Projekt', {'fields': ( ('Projekt', {'fields': (
'name', 'name',
@ -173,6 +175,16 @@ class ProjectAdmin(BaseProjectAdmin):
class ProjectRequestAdmin(BaseProjectAdmin): class ProjectRequestAdmin(BaseProjectAdmin):
granted = None granted = None
def save_model(self, request, obj: ProjectRequest, form: ProjectAdminForm, change: bool):
super().save_model(request, obj, form, change)
if obj.granted is None:
return None
transaction.on_commit(lambda: send_decision_mails(obj))
return obj.granted
@admin.register(ProjectDeclined) @admin.register(ProjectDeclined)
class ProjectDeclinedAdmin(BaseProjectAdmin): class ProjectDeclinedAdmin(BaseProjectAdmin):
@ -181,9 +193,12 @@ class ProjectDeclinedAdmin(BaseProjectAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(BusinessCard) @admin.register(BusinessCard)
class BusinessCardAdmin(WMDEAdmin): class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
@ -197,7 +212,7 @@ class BusinessCardAdmin(WMDEAdmin):
@admin.register(Literature) @admin.register(Literature)
class LiteratureAdmin(WMDEAdmin): class LiteratureAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -227,7 +242,7 @@ class HonoraryCertificateAdmin(admin.ModelAdmin):
@admin.register(Library, ELiterature, Software) @admin.register(Library, ELiterature, Software)
class LibraryAdmin(WMDEAdmin): class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date')
@ -254,7 +269,7 @@ class LibraryAdmin(WMDEAdmin):
@admin.register(IFG) @admin.register(IFG)
class IFGAdmin(WMDEAdmin): class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date')
@ -267,7 +282,7 @@ class IFGAdmin(WMDEAdmin):
@admin.register(Travel) @admin.register(Travel)
class TravelAdmin(WMDEAdmin): class TravelAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid'] search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid']
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project', list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project',
@ -282,7 +297,7 @@ class TravelAdmin(WMDEAdmin):
@admin.register(Email) @admin.register(Email)
class EmailAdmin(WMDEAdmin): class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -296,7 +311,7 @@ class EmailAdmin(WMDEAdmin):
@admin.register(List) @admin.register(List)
class ListAdmin(WMDEAdmin): class ListAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')

View File

@ -5,7 +5,6 @@ from django.forms import ModelForm
from django.forms.renderers import DjangoTemplates from django.forms.renderers import DjangoTemplates
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as trans
from .models import ( from .models import (
Project, Project,
@ -126,11 +125,6 @@ class ProjectForm(BaseProjectForm, BaseApplicationForm):
'insurance', 'insurance',
'notes', 'notes',
] ]
labels = {
'cost': 'Kosten in Euro',
'insurance': 'Haftpflicht- und Unfallversicherung gewünscht',
'participants_estimated': 'Voraussichtliche Zahl der Teilnehmenden',
}
widgets = { widgets = {
'start': AdminDateWidget, 'start': AdminDateWidget,
'end': AdminDateWidget, 'end': AdminDateWidget,
@ -167,7 +161,6 @@ class TravelForm(BaseApplicationForm):
self.fields['project_name'].required = True self.fields['project_name'].required = True
self.fields['transport'].required = True self.fields['transport'].required = True
self.fields['travelcost'].required = True self.fields['travelcost'].required = True
self.fields['travelcost'].initial = None
self.fields['checkin'].required = True self.fields['checkin'].required = True
self.fields['checkout'].required = True self.fields['checkout'].required = True
self.fields['hotel'].required = True self.fields['hotel'].required = True
@ -185,10 +178,6 @@ class TravelForm(BaseApplicationForm):
'hotel', 'hotel',
'notes', 'notes',
] ]
labels = {
'checkin': 'Datum der Anreise',
'checkout': 'Datum der Abreise',
}
widgets = { widgets = {
'checkin': AdminDateWidget, 'checkin': AdminDateWidget,
'checkout': AdminDateWidget, 'checkout': AdminDateWidget,
@ -213,9 +202,6 @@ class LibraryForm(BaseApplicationForm):
'duration', 'duration',
'notes', 'notes',
] ]
labels = {
'cost': 'Kosten in Euro',
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -264,6 +250,10 @@ class TermsForm(BaseApplicationForm):
class LiteratureForm(TermsForm): class LiteratureForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selfbuy_give_data'].required = True
class Meta: class Meta:
model = Literature model = Literature
fields = [ fields = [
@ -282,24 +272,6 @@ class LiteratureForm(TermsForm):
class Media: class Media:
js = ('dropdown/js/literature.js',) js = ('dropdown/js/literature.js',)
def clean(self):
cleaned_data = TermsForm.clean(self)
if self.errors:
return cleaned_data
if cleaned_data['selfbuy'] == 'TRUE':
cleaned_data['selfbuy_data'] = ''
cleaned_data['selfbuy_give_data'] = False
return cleaned_data
for field in 'selfbuy_data', 'selfbuy_give_data':
if not cleaned_data.get(field):
self.add_error(field, trans('This field is required.'))
return cleaned_data
ADULT_CHOICES = { ADULT_CHOICES = {
'TRUE': mark_safe('Ich bin volljährig.'), 'TRUE': mark_safe('Ich bin volljährig.'),

View File

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
('survey_mail_send', models.BooleanField(null=True)), ('survey_mail_send', models.BooleanField(null=True)),
('username', models.CharField(max_length=200, null=True)), ('username', models.CharField(max_length=200, null=True)),
('domain', models.CharField(choices=[('PEDIA', '@wikipedia.de'), ('BOOKS', '@wikibooks.de'), ('QUOTE', '@wikiquote.de'), ('SOURCE', '@wikisource.de'), ('VERSITY', '@wikiversity.de')], default='PEDIA', max_length=10)), ('domain', models.CharField(choices=[('PEDIA', '@wikipedia.de'), ('BOOKS', '@wikibooks.de'), ('QUOTE', '@wikiquote.de'), ('SOURCE', '@wikisource.de'), ('VERSITY', '@wikiversity.de')], default='PEDIA', max_length=10)),
('adress', models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', max_length=50)), ('adress', models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', max_length=50)),
('other', models.CharField(blank=True, max_length=50, null=True)), ('other', models.CharField(blank=True, max_length=50, null=True)),
], ],
options={ options={

View File

@ -45,7 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='email', model_name='email',
name='address', name='address',
field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', max_length=50, verbose_name='Adressbestandteil'), field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', max_length=50, verbose_name='Adressbestandteil'),
), ),
migrations.AddField( migrations.AddField(
model_name='list', model_name='list',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='email', model_name='email',
name='address', name='address',
field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges')], default='USERNAME', help_text='Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.', max_length=50, verbose_name='Adressbestandteil'), field=models.CharField(choices=[('REALNAME', 'Vorname.Nachname'), ('USERNAME', 'Username'), ('OTHER', 'Sonstiges:')], default='USERNAME', help_text='Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.', max_length=50, verbose_name='Adressbestandteil'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='email', model_name='email',

View File

@ -18,12 +18,12 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ifg', model_name='ifg',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='literature', model_name='literature',
name='notes', name='notes',
field=models.TextField(blank=True, help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'), field=models.TextField(blank=True, help_text='Bitte gib an wofür Du das Stipendium verwenden willst.', max_length=1000, verbose_name='Anmerkungen'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='project', model_name='project',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='travel', model_name='travel',
name='project_name', name='project_name',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Projektname'), field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Projektname:'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='library', model_name='library',

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='travel', model_name='travel',
name='hotel', name='hotel',
field=models.BooleanField(default=False, verbose_name='Hotelzimmer benötigt'), field=models.BooleanField(default=False, verbose_name='Hotelzimmer benötigt:'),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='travel', model_name='travel',
name='transport', name='transport',
field=models.CharField(choices=[('BAHN', 'Bahn'), ('NONE', 'Keine Fahrtkosten'), ('OTHER', 'Sonstiges (mit Begründung)')], default='BAHN', max_length=5, verbose_name='Transportmittel'), field=models.CharField(choices=[('BAHN', 'Bahn'), ('NONE', 'Keine Fahrtkosten'), ('OTHER', 'Sonstiges (mit Begründung)')], default='BAHN', max_length=5, verbose_name='Transportmittel:'),
), ),
] ]

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='travel', model_name='travel',
name='hotel', name='hotel',
field=models.CharField(choices=[('TRUE', 'Hotelzimmer benötigt'), ('FALSE', 'Kein Hotelzimmer benötigt')], max_length=10, verbose_name='Hotelzimmer benötigt'), field=models.CharField(choices=[('TRUE', 'Hotelzimmer benötigt'), ('FALSE', 'Kein Hotelzimmer benötigt')], max_length=10, verbose_name='Hotelzimmer benötigt:'),
), ),
] ]

View File

@ -65,8 +65,8 @@ class Migration(migrations.Migration):
('order', models.PositiveIntegerField(verbose_name='Reihenfolge')), ('order', models.PositiveIntegerField(verbose_name='Reihenfolge')),
], ],
options={ options={
'verbose_name': 'Wikimedia-Projekt', 'verbose_name': 'Wikimedia Projekt',
'verbose_name_plural': 'Wikimedia-Projekte', 'verbose_name_plural': 'Wikimedia Projekte',
'ordering': ['order'], 'ordering': ['order'],
}, },
), ),

View File

@ -24,11 +24,11 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='project', model_name='project',
name='wikimedia_projects', name='wikimedia_projects',
field=input.models.ProjectCategoryField(related_name='projects', to='input.wikimediaproject', verbose_name='Wikimedia-Projekte'), field=input.models.ProjectCategoryField(related_name='projects', to='input.wikimediaproject', verbose_name='Wikimedia Projekte'),
), ),
migrations.AddField( migrations.AddField(
model_name='project', model_name='project',
name='wikimedia_projects_other', name='wikimedia_projects_other',
field=models.CharField(blank=True, max_length=200, verbose_name='Wikimedia-Projekte (Anderes)'), field=models.CharField(blank=True, max_length=200, verbose_name='Wikimedia Projekte (Anderes)'),
), ),
] ]

View File

@ -1,57 +0,0 @@
# Generated by Django 5.2.5 on 2025-11-07 15:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('input', '0104_alter_project_required_fields'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'verbose_name': 'Kostenstelle', 'verbose_name_plural': 'Kostenstellen'},
),
migrations.AlterModelOptions(
name='businesscard',
options={'verbose_name': 'Visitenkarte', 'verbose_name_plural': 'Visitenkarten'},
),
migrations.AlterModelOptions(
name='eliterature',
options={'verbose_name': 'eLiteraturstipendium', 'verbose_name_plural': 'eLiteraturstipendien'},
),
migrations.AlterModelOptions(
name='email',
options={'verbose_name': 'E-Mail-Adresse', 'verbose_name_plural': 'E-Mail-Adressen'},
),
migrations.AlterModelOptions(
name='honorarycertificate',
options={'verbose_name': 'Bescheinigung', 'verbose_name_plural': 'Bescheinigungen'},
),
migrations.AlterModelOptions(
name='ifg',
options={'verbose_name': 'IFG-Anfrage', 'verbose_name_plural': 'IFG-Anfragen'},
),
migrations.AlterModelOptions(
name='library',
options={'verbose_name': 'Bibliotheksstipendium', 'verbose_name_plural': 'Bibliotheksstipendien'},
),
migrations.AlterModelOptions(
name='list',
options={'verbose_name': 'Mailingliste', 'verbose_name_plural': 'Mailinglisten'},
),
migrations.AlterModelOptions(
name='literature',
options={'verbose_name': 'Literaturstipendium', 'verbose_name_plural': 'Literaturstipendien'},
),
migrations.AlterModelOptions(
name='software',
options={'verbose_name': 'Softwarestipendium', 'verbose_name_plural': 'Softwarestipendien'},
),
migrations.AlterModelOptions(
name='travel',
options={'verbose_name': 'Reisekosten', 'verbose_name_plural': 'Reisekosten'},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.5 on 2025-11-07 15:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0105_add_verbose_names'),
]
operations = [
migrations.AddField(
model_name='project',
name='username',
field=models.CharField(help_text='Wikimedia Benutzer_innenname', max_length=200, blank=True, verbose_name='Benutzer_innenname'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.2.5 on 2025-11-10 10:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('input', '0106_project_username'),
]
operations = [
migrations.AlterField(
model_name='project',
name='categories_other',
field=models.TextField(blank=True, verbose_name='Projektkategorien (Sonstiges)'),
),
migrations.AlterField(
model_name='project',
name='wikimedia_projects_other',
field=models.TextField(blank=True, verbose_name='Wikimedia-Projekte (Anderes)'),
),
]

View File

@ -100,10 +100,6 @@ class Account(models.Model):
description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION') description = models.CharField('Beschreibung', max_length=60, default='NO DESCRIPTION')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Kostenstelle'
verbose_name_plural = 'Kostenstellen'
def __str__(self): def __str__(self):
return f'{self.code} {self.description}' return f'{self.code} {self.description}'
@ -146,8 +142,8 @@ class WikimediaProject(BaseProjectCategory):
OTHER = 'Anderes' OTHER = 'Anderes'
class Meta(BaseProjectCategory.Meta): class Meta(BaseProjectCategory.Meta):
verbose_name = 'Wikimedia-Projekt' verbose_name = 'Wikimedia Projekt'
verbose_name_plural = 'Wikimedia-Projekte' verbose_name_plural = 'Wikimedia Projekte'
class ProductCategoryChoiceIterator(ModelMultipleChoiceField.iterator): class ProductCategoryChoiceIterator(ModelMultipleChoiceField.iterator):
@ -188,7 +184,7 @@ class ProjectCategoryField(models.ManyToManyField):
super().__init__(**kwargs) super().__init__(**kwargs)
self.other_field = models.TextField(blank=True) self.other_field = models.CharField(max_length=200, blank=True)
def contribute_to_class(self, cls, name, **kwargs): def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs) super().contribute_to_class(cls, name, **kwargs)
@ -217,8 +213,6 @@ class ProjectCategoryField(models.ManyToManyField):
class Project(Volunteer): class Project(Volunteer):
username = models.CharField(max_length=200, blank=True, verbose_name='Benutzer_innenname',
help_text=mark_safe('Wikimedia Benutzer_innenname'))
end_mail_send = models.BooleanField(default=False, verbose_name='Keine Projektabschlussmail schicken') end_mail_send = models.BooleanField(default=False, verbose_name='Keine Projektabschlussmail schicken')
name = models.CharField(max_length=200, verbose_name='Name des Projekts') name = models.CharField(max_length=200, verbose_name='Name des Projekts')
description = models.CharField(max_length=500, verbose_name='Kurzbeschreibung', null=True) description = models.CharField(max_length=500, verbose_name='Kurzbeschreibung', null=True)
@ -353,12 +347,8 @@ class HonoraryCertificate(Intern):
project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.SET_NULL) project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
verbose_name = 'Bescheinigung'
verbose_name_plural = 'Bescheinigungen'
def __str__(self): def __str__(self):
return f'Bescheinigung für {self.realname}' return f'Certificate for {self.realname}'
TRANSPORT_CHOICES = { TRANSPORT_CHOICES = {
@ -381,15 +371,15 @@ HOTEL_CHOICES = {
class Travel(Extern): 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 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 = 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') 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') 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)') 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') travelcost = models.CharField(max_length=10, default='0', verbose_name='Fahrtkosten')
checkin = models.DateField(blank=True, null=True, verbose_name='Anreise') checkin = models.DateField(blank=True, null=True, verbose_name='Anreise')
checkout = models.DateField(blank=True, null=True, verbose_name='Abreise') 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_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') 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') 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') notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen')
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)') request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
@ -397,21 +387,6 @@ class Travel(Extern):
# use content type model to get the end date for the project foreign key # 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') project_end_quartal = models.CharField(max_length=15, null=True, blank=True, verbose_name='Quartal Projekt Ende')
class Meta:
verbose_name = 'Reisekosten'
verbose_name_plural = 'Reisekosten'
def __str__(self):
return f'Reisekosten für {self.realname}'
def clean(self):
if (self.checkin and self.checkout) and (self.checkout < self.checkin):
raise forms.ValidationError({
'checkout': [
forms.ValidationError('Das Datum der Abreise muss nach dem Datum der Anreise liegen.'),
],
})
@receiver(pre_save, sender=Travel, dispatch_uid='get_project_end') @receiver(pre_save, sender=Travel, dispatch_uid='get_project_end')
def get_project_end(sender, instance, **kwargs): def get_project_end(sender, instance, **kwargs):
@ -425,7 +400,7 @@ class Grant(RequestUrlMixin, Extern):
cost = models.CharField(max_length=10, verbose_name='Kosten', cost = models.CharField(max_length=10, verbose_name='Kosten',
help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.') help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen', notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen',
help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.') help_text='Bitte gib an wofür Du das Stipendium verwenden willst.')
class Meta: class Meta:
abstract = True abstract = True
@ -468,10 +443,6 @@ class Library(Grant):
duration = models.CharField(max_length=100, verbose_name='Dauer') duration = models.CharField(max_length=100, verbose_name='Dauer')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Bibliotheksstipendium'
verbose_name_plural = 'Bibliotheksstipendien'
def __str__(self): def __str__(self):
return self.library return self.library
@ -489,8 +460,6 @@ class ELiterature(Library):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = 'eLiteraturstipendium'
verbose_name_plural = 'eLiteraturstipendien'
class Software(Library): class Software(Library):
@ -501,8 +470,6 @@ class Software(Library):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = 'Softwarestipendium'
verbose_name_plural = 'Softwarestipendien'
SELFBUY_CHOICES = { SELFBUY_CHOICES = {
@ -525,22 +492,14 @@ class Literature(TermsConsentMixin, Grant):
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.')) 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') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Literaturstipendium'
verbose_name_plural = 'Literaturstipendien'
class IFG(Grant): class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name='URL', url = models.URLField(max_length=2000, verbose_name='URL',
help_text='Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.') 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') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'IFG-Anfrage'
verbose_name_plural = 'IFG-Anfragen'
def __str__(self): def __str__(self):
return f'IFG-Anfrage von {self.realname}' return 'IFG-Anfrage von ' + self.realname
DOMAIN_CHOICES = { DOMAIN_CHOICES = {
@ -564,7 +523,7 @@ class Domain(RequestUrlMixin, Extern):
MAIL_CHOICES = { MAIL_CHOICES = {
'REALNAME': 'Vorname.Nachname', 'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username', 'USERNAME': 'Username',
'OTHER': 'Sonstiges', 'OTHER': 'Sonstiges:',
} }
ADULT_CHOICES = { ADULT_CHOICES = {
@ -583,10 +542,6 @@ class Email(TermsConsentMixin, Domain):
adult = models.CharField(max_length=10, verbose_name='Volljährigkeit', choices=ADULT_CHOICES.items(), default='FALSE') 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') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'E-Mail-Adresse'
verbose_name_plural = 'E-Mail-Adressen'
class List(TermsConsentMixin, Domain): class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS', address = models.CharField(max_length=50, default='NO_ADDRESS',
@ -594,10 +549,6 @@ class List(TermsConsentMixin, Domain):
help_text=mark_safe('Bitte gib hier den gewünschten Adressbestandteil an,<br>der sich vor der Domain befinden soll.')) 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') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Mailingliste'
verbose_name_plural = 'Mailinglisten'
PROJECT_CHOICE = { PROJECT_CHOICE = {
'PEDIA': 'Wikipedia', 'PEDIA': 'Wikipedia',
@ -642,10 +593,6 @@ class BusinessCard(RequestUrlMixin, TermsConsentMixin, Extern):
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.')) 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') intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Visitenkarte'
verbose_name_plural = 'Visitenkarten'
MODELS = { MODELS = {
TYPE_BIB: Library, TYPE_BIB: Library,

View File

@ -6,16 +6,6 @@
margin: 0 auto; margin: 0 auto;
} }
.wm-table.start {
td, th {
border: 0;
}
.applications {
text-align: left;
}
}
.col-request { .col-request {
width: 40%; width: 40%;
} }

View File

@ -6,7 +6,6 @@
const otherInput = $(otherInputSelector); const otherInput = $(otherInputSelector);
const otherLabelSelector = 'label'.concat('[for="', this.id, '_other"]'); const otherLabelSelector = 'label'.concat('[for="', this.id, '_other"]');
const otherLabel = $(otherLabelSelector); const otherLabel = $(otherLabelSelector);
const otherTableRow = otherInput.parents('tr');
const toggle = function () { const toggle = function () {
const checked = otherCheckbox.prop('checked'); const checked = otherCheckbox.prop('checked');
@ -15,7 +14,6 @@
otherInput.prop('required', checked); otherInput.prop('required', checked);
otherLabel.toggleClass('required', checked); otherLabel.toggleClass('required', checked);
otherLabel.css('opacity', checked ? 1 : 0.3); otherLabel.css('opacity', checked ? 1 : 0.3);
otherTableRow.css('visibility', checked ? 'visible' : 'collapse');
if (checked) { if (checked) {
otherInput.focus(); otherInput.focus();

View File

@ -1,44 +1,32 @@
{% extends 'input/base.html' %} {% extends "input/base.html" %}
{% load i18n %} {% load i18n %}
{% block head_extra %}
<title>Was möchtest du beantragen?</title>
{% endblock %}
{% block content %} {% block content %}
<form class="page-centered" method="post"> <div class="page-centered">
{% csrf_token %} <table class="wm-table">
<table class="wm-table start">
<tbody class="applications">
<tr> <tr>
<th class="col-request">Was möchtest du beantragen?</th> <th class="col-request">Was möchtest du beantragen?</th>
<td> <td>
{% for title, services in applications %} <strong>Projektförderung</strong>
<strong>{{ title }}</strong>
<ul> <ul>
{% for service in services %}
<li> <li>
<label> <a href="{% url 'extern' type='projektfoerderung' %}">Projektförderung</a>
<input type="radio" name="url" value="{% url 'extern' type=service.path %}" /> mit einer Gesamtsumme unter 1.000,— EUR
<span>{{ service.label|striptags }}</span>
</label>
<span>(<a href="{{ service.url }}" target="_blank">mehr erfahren</a>)</span>
</li> </li>
<li>
<a href="{% url 'projektfoerderung-ab-1000' %}">Projektförderung</a>
mit einer Gesamtsumme ab 1.000,— EUR
</li>
</ul>
<strong>Serviceleistungen</strong>
<ul>
{% for info in services %}
<li><a href="{% url 'extern' type=info.path %}">{{ info.label|striptags }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endfor %}
</td> </td>
</tr> </tr>
</tbody>
<tbody>
<tr>
<td colspan="2">
<button type="submit">Beantragen</button>
</td>
</tr>
</tbody>
</table> </table>
</form> </div>
{% endblock %} {% endblock %}

View File

@ -1,10 +1,6 @@
{% extends "input/base.html" %} {% extends "input/base.html" %}
{% load static %} {% load static %}
{% block head_extra %}
<title>{{ type_label|striptags }}</title>
{% endblock %}
{% block content %} {% block content %}
{{ form.media }} {{ form.media }}

View File

@ -21,7 +21,4 @@
Für Fragen steht dir das Team Community-Konferenzen &amp; Förderung gern unter Für Fragen steht dir das Team Community-Konferenzen &amp; Förderung gern unter
<a href="mailto:community@wikimedia.de">community@wikimedia.de</a> zur Verfügung. <a href="mailto:community@wikimedia.de">community@wikimedia.de</a> zur Verfügung.
</p> </p>
<div class="page-centered">
<button type="button" onclick="history.back()">Zurück</button>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,9 @@
<html>
<body>
<p>Hallo {{ data.realname }},</p>
<p>Deine Förderanfrage {{project.name}} wurde leider abgelehnt.</p>
<p>Fragen? <a href="mailto:community@wikimedia.de">community@wikimedia.de</a></p>
</body>
</html>

View File

@ -0,0 +1,5 @@
Hallo {{ data.realname }},
deine Förderanfrage {{project.name}} wurde leider abgelehnt.
Fragen? community@wikimedia.de

View File

@ -0,0 +1,7 @@
<html>
<body>
<p>Hallo Team Community-Konferenzen &amp; Förderung,</p>
<p>Die Förderanfrage {{project.name}} von {{ data.realname }} wurde abgelehnt.</p>
</body>
</html>

View File

@ -0,0 +1,3 @@
Hallo Team Community-Konferenzen & Förderung,
die Förderanfrage {{project.name}} von {{ data.realname }} wurde abgelehnt.

View File

@ -0,0 +1,10 @@
<html>
<body>
<p>Hallo {{ data.realname }},</p>
<p>Deine Förderanfrage {{project.name}} wurde bewilligt.</p>
<p>Das Team Community-Konferenzen &amp; Förderung meldet sich bald bei dir.<br>
Fragen? <a href="mailto:community@wikimedia.de">community@wikimedia.de</a></p>
</body>
</html>

View File

@ -0,0 +1,7 @@
Hallo {{ data.realname }},
deine Förderanfrage {{project.name}} wurde bewilligt.
Das Team Community-Konferenzen & Förderung meldet sich bald bei dir.
Fragen? community@wikimedia.de

View File

@ -0,0 +1,7 @@
<html>
<body>
<p>Hallo Team Community-Konferenzen &amp; Förderung,</p>
<p>Die Förderanfrage {{project.name}} von {{ data.realname }} wurde bewilligt.</p>
</body>
</html>

View File

@ -0,0 +1,3 @@
Hallo Team Community-Konferenzen & Förderung,
die Förderanfrage {{project.name}} von {{ data.realname }} wurde bewilligt.

View File

@ -1,26 +1,60 @@
<html lang="de"> <html>
<body> <body>
<p>Hallo Team Community-Konferenzen &amp; Förderung,</p> <p>Hallo Team Community-Konferenzen &amp; Förderung,</p>
<p>es gibt eine neue Anfrage von {{ data.realname }}.</p> <p>es gibt eine neue Anfrage von {{ data.realname }}.</p>
<p>{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}</p> <p>{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ data.type_label|striptags }}</p>
<p> {% if data.choice in data.grant %}<br>
{% for label, value in form_data.items %} Vorraussichtliche Kosten: {{data.cost}}<br>
{{ label }}: {{ value }} <br /> Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
{% endfor %} Domain: <a href="{{data.domain}}">{{data.domain}}</a><br>
Adressenbestandteil: {{data.address}} <br> {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'SOFT' %}
Software: {{data.library}}<br>
Dauer: {{data.duration}} <br> {% elif data.choice == 'IFG'%}
Anfrage-URL: <a href="{{data.url}}">{{data.url}}</a> <br> {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}<br>
Bezugsquelle: {{data.source}} <br> {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} <br> {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}<br>
Persönliche Daten: {{data.data}}<br>
Variante: {{data.variant}}<br>
Sendungsadrese: {{data.send_to}} <br> {% endif %}
<p>Zum Eintrag in der Förderdatenbank:
{% if data.choice == 'BIB' %}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%}
<a href="{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change">{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%}
<a href="{{data.url_prefix}}/admin/input/email/{{data.pk}}/change">{{data.url_prefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%}
<a href="{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change">{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%}
<a href="{{data.url_prefix}}/admin/input/list/{{data.pk}}/change">{{data.url_prefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%}
<a href="{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change">{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%}
<a href="{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %}
</p> </p>
<p>Zum Eintrag in der Förderdatenbank: <a href="{{ urls.admin }}">{{ urls.admin }}</a></p> <p>Zum Genehmigen hier klicken:
<a href="{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}">{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}</a>
</p>
{% if urls.authorize %} <p>Zum Ablehnen hier klicken:
<p>Zum Genehmigen hier klicken: <a href="{{ urls.authorize }}">{{ urls.authorize }}</a></p> <a href="{{data.url_prefix}}{% url 'deny' data.choice data.pk %}">{{data.url_prefix}}{% url 'deny' data.choice data.pk %}</a>
{% endif %} </p>
{% if urls.deny %}
<p>Zum Ablehnen hier klicken: <a href="{{ urls.deny }}">{{ urls.deny }}</a></p>
{% endif %}
</body> </body>
</html> </html>

View File

@ -1,12 +1,52 @@
Hallo Team Community-Konferenzen & Förderung, Hallo Team Communitys und Engagement,
es gibt eine neue Anfrage von {{ data.realname }}. es gab einen neuen Antrag von {{data.realname}}.
{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }} Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.type_label|striptags}} an.
{% if data.choice in data.grant %}
Vorraussichtliche Kosten: {{data.cost}}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
Domain: {{data.domain}}
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
Software: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}
Persönliche Daten: {{data.data}}
Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}} {% endif %}
{% for label, value in form_data.items %}{{ label }}: {{ value }} Zum Eintrag in der Förderdatenbank:
{% endfor %} {% if data.choice == 'BIB' %}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'ELIT'%}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'LIT'%}
<a href="{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change">{{data.url_prefix}}/admin/input/literature/{{data.pk}}/change</a>
{% elif data.choice == 'MAIL'%}
<a href="{{data.url_prefix}}/admin/input/email/{{data.pk}}/change">{{data.url_prefix}}/admin/input/email/{{data.pk}}/change</a>
{% elif data.choice == 'IFG'%}
<a href="{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change">{{data.url_prefix}}/admin/input/ifg/{{data.pk}}/change</a>
{% elif data.choice == 'LIST'%}
<a href="{{data.url_prefix}}/admin/input/list/{{data.pk}}/change">{{data.url_prefix}}/admin/input/list/{{data.pk}}/change</a>
{% elif data.choice == 'TRAV'%}
<a href="{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change">{{data.url_prefix}}/admin/input/travel/{{data.pk}}/change</a>
{% elif data.choice == 'SOFT'%}
<a href="{{data.url_prefix}}/admin/input/library/{{data.pk}}/change">{{data.url_prefix}}/admin/input/library/{{data.pk}}/change</a>
{% elif data.choice == 'VIS'%}
<a href="{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change">{{data.url_prefix}}/admin/input/businesscard/{{data.pk}}/change</a>
{% endif %}
Zum Eintrag in der Förderdatenbank: {{ urls.admin }}
{% if urls.authorize %}Zum Genehmigen hier klicken: {{ urls.authorize }}{% endif %} Zum Genehmigen hier klicken: {{data.url_prefix}}{% url 'authorize' data.choice data.pk %}
{% if urls.deny %}Zum Ablehnen hier klicken: {{ urls.deny }}{% endif %}
Zu Ablehnen hier klicken: {{data.url_prefix}}{% url 'deny' data.choice data.pk %}
Stets zu Diensten, Deine Förderdatenbank

View File

@ -1,14 +1,29 @@
<html lang="de"> <html>
<body> <body>
<p>Hallo {{ applicant_name }},</p> <p>Hallo {{ data.username|default:data.realname }},</p>
<p>vielen Dank für deine Anfrage ({{ type_label }}), die bei uns eingegangen ist.</p> <p>vielen Dank für deine Anfrage ({{ data.type_label|striptags }}), die bei uns eingegangen ist.</p>
Dies ist eine automatisch generierte E-Mail. Im Folgenden findest du deine Formulareingaben nochmals zu deiner Übersicht:<br> Dies ist eine automatisch generierte E-Mail. Im Folgenden findest du deine Formulareingaben nochmals zu deiner Übersicht:<br>
{% if data.choice in data.grant %}<br>
{% for label, value in form_data.items %} Vorraussichtliche Kosten: {{data.cost}}<br>
{{ label }}: {{ value }} <br /> Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
{% endfor %} Domain: <a href="{{data.domain}}">{{data.domain}}</a><br>
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}<br>
Bibliothek: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}<br>
Datenbank: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}<br>
Software: {{data.library}}<br>
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}<br>
Anfrage-URL: <a href="{{data.url}}">{{data.url}}</a> {% elif data.choice == 'LIT'%}<br>
Info zum Werk: {{data.info}}<br>
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}<br>
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}<br>
Wikimedia-Projekt: {{data.project}}<br>
Persönliche Daten: {{data.data}}<br>
Variante: {{data.variant}}<br>
Sendungsadrese: {{data.send_to}} {% endif %}<br>
<p>Das Team Community-Konferenzen &amp; Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an <a href="mailto:community@wikimedia.de">community@wikimedia.de</a>.</p> <p>Das Team Community-Konferenzen &amp; Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an <a href="mailto:community@wikimedia.de">community@wikimedia.de</a>.</p>

View File

@ -1,22 +1,29 @@
Hallo {{ applicant_name }}, Hallo {{data.realname}},
vielen Dank für deine Anfrage ({{type_label}}), die bei uns eingegangen ist. wir haben Deine Anfrage ({{data.type_label|striptags}}) erhalten.
{% if data.choice in data.grant %}
Vorraussichtliche Kosten: {{data.cost}}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}
Domain: {{data.domain}}
Adressenbestandteil: {{data.address}} {% endif %} {% if data.choice == 'BIB' %}
Bibliothek: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'ELIT' %}
Datenbank: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'SOFT' %}
Software: {{data.library}}
Dauer: {{data.duration}} {% elif data.choice == 'IFG'%}
Anfrage-URL: {{data.url}} {% elif data.choice == 'LIT'%}
Info zum Werk: {{data.info}}
Bezugsquelle: {{data.source}} {% elif data.choice == 'MAIL'%}
Adressenbestandteil frei gewählt: {{data.other}} {% elif data.choice == 'VIS'%}
Wikimedia-Projekt: {{data.project}}
Persönliche Daten: {{data.data}}
Variante: {{data.variant}}
Sendungsadrese: {{data.send_to}} {% endif %}
{% for label, value in form_data.items %}{{ label }}: {{ value }} Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern
{% endfor %} und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben,
wende dich gern an community@wikimedia.de.
Das Team Community-Konferenzen & Förderung wird sich um deine Anfrage kümmern und sich in den nächsten Tagen bei dir melden. Wenn du Fragen hast, wende dich gern jederzeit an community@wikimedia.de. Viele Grüße, dein freundliches aber komplett unmenschliches automatisches
Formularbeantwortungssystem.
--
Wikimedia Deutschland e. V. | Tempelhofer Ufer 2324 | 10963 Berlin
Zentrale: +49 30 5771162-0
https://wikimedia.de
Unsere Vision ist eine Welt, in der alle Menschen am Wissen der Menschheit teilhaben, es nutzen und mehren können. Helfen Sie uns dabei!
https://spenden.wikimedia.de
Wikimedia Deutschland Gesellschaft zur Förderung Freien Wissens e. V. Eingetragen im Vereinsregister des Amtsgerichts Charlottenburg, VR 23855 B. Als gemeinnützig anerkannt durch das Finanzamt für Körperschaften I Berlin, Steuernummer 27/029/42207. Geschäftsführende Vorständin: Franziska Heine.
Datenschutzerklärung:
Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer Datenschutzerklärung (https://www.wikimedia.de/datenschutz/).

View File

@ -1,5 +1,6 @@
import datetime import datetime
from django.core import mail
from django.forms import model_to_dict from django.forms import model_to_dict
from django.test import TestCase from django.test import TestCase
@ -108,4 +109,7 @@ class AdminTestCase(TestCase):
if data[key] is None: if data[key] is None:
data.pop(key) data.pop(key)
with self.captureOnCommitCallbacks(execute=True):
request(self, url, expected_url=expected_url, data=data) request(self, url, expected_url=expected_url, data=data)
self.assertEqual(len(mail.outbox), 2)

View File

@ -1,17 +1,11 @@
import random
from django.forms import model_to_dict
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.test import TestCase from django.test import TestCase
from foerderbarometer.constants import * from input.models import Library, TYPE_PROJ
from input.models import Library, ProjectCategory, WikimediaProject
from input.utils.testing import create_superuser, login, request from input.utils.testing import create_superuser, login, request
from input.views import PROJECT_FUNDING, TYPES, ApplicationView from input.views import TYPES
PATHS = {TYPES[path].code: path for path in TYPES} PATHS = {TYPES[path].code: path for path in TYPES}
PATHS[TYPE_PROJ] = PROJECT_FUNDING[0].path
CODES = list(PATHS)
class AnonymousViewTestCase(TestCase): class AnonymousViewTestCase(TestCase):
@ -24,15 +18,6 @@ class AnonymousViewTestCase(TestCase):
def test_extern(self): def test_extern(self):
request(self, 'extern') request(self, 'extern')
def test_extern_post(self):
code = random.choice(CODES)
url = self.helper_url(code)
request(self, 'extern', expected_url=url, data={'url': url})
def test_extern_invalid_url(self):
request(self, 'extern', data={'url': 'https://domain.not/allowed/to/be/redirected/'})
@classmethod @classmethod
def get_step_data(cls, choice, **data): def get_step_data(cls, choice, **data):
return { return {
@ -65,15 +50,15 @@ class AnonymousViewTestCase(TestCase):
def test_extern_types(self): def test_extern_types(self):
types = [ types = [
(TYPE_BIB, 'Bibliotheksausweis'), ('BIB', 'Bibliotheksausweis'),
(TYPE_ELIT, 'Online-Ressource'), ('ELIT', 'Online-Ressource'),
(TYPE_MAIL, 'Mailadresse beantragen'), ('MAIL', 'Mailadresse beantragen'),
(TYPE_IFG, 'gewonnenen Informationen'), ('IFG', 'gewonnenen Informationen'),
(TYPE_LIT, 'Literatur verwenden'), ('LIT', 'Literatur verwenden'),
(TYPE_LIST, 'Mailingliste beantragen'), ('LIST', 'Mailingliste beantragen'),
(TYPE_TRAV, 'Transportmittel'), ('TRAV', 'Transportmittel'),
(TYPE_SOFT, 'Lizenz'), ('SOFT', 'Lizenz'),
(TYPE_VIS, 'DIN 5008'), ('VIS', 'DIN 5008'),
(TYPE_PROJ, 'Projektförderung'), (TYPE_PROJ, 'Projektförderung'),
] ]
@ -85,7 +70,7 @@ class AnonymousViewTestCase(TestCase):
self.assertContains(response, text) self.assertContains(response, text)
def test_extern_travel(self): def test_extern_travel(self):
self.helper_extern(TYPE_TRAV, 'Transportmittel', { self.helper_extern('TRAV', 'Transportmittel', {
'project_name': 'Test', 'project_name': 'Test',
'transport': 'BAHN', 'transport': 'BAHN',
'travelcost': 10, 'travelcost': 10,
@ -96,27 +81,27 @@ class AnonymousViewTestCase(TestCase):
}) })
def test_extern_lit(self): def test_extern_lit(self):
self.helper_extern(TYPE_LIT, 'Literatur verwenden', { self.helper_extern('LIT', 'Literatur verwenden', {
'cost': 20, 'cost': 20,
'info': 'Test', 'info': 'Test',
'source': 'Test', 'source': 'Test',
'notes': '', 'notes': '',
'selfbuy': 'FALSE', 'selfbuy': 'TRUE',
'selfbuy_data': 'Test', 'selfbuy_data': 'NONE',
'selfbuy_give_data': True, 'selfbuy_give_data': True,
'check': True, 'check': True,
'terms_accepted': True, 'terms_accepted': True,
}) })
def test_extern_lit_without_consent_fails(self): def test_extern_lit_without_consent_fails(self):
response = self.helper_extern_base(TYPE_LIT, 'Literatur verwenden', { response = self.helper_extern_base('LIT', 'Literatur verwenden', {
'cost': 20, 'cost': 20,
'info': 'Test', 'info': 'Test',
'source': 'Test', 'source': 'Test',
'notes': '', 'notes': '',
'selfbuy': 'TRUE', 'selfbuy': 'TRUE',
'selfbuy_data': '', 'selfbuy_data': 'NONE',
'selfbuy_give_data': False, 'selfbuy_give_data': True,
'check': False, 'check': False,
}) })
@ -130,32 +115,9 @@ class AnonymousViewTestCase(TestCase):
'notes': '', 'notes': '',
}) })
def test_extern_proj(self):
category = ProjectCategory.objects.order_by('?').first()
wikimedia_project = WikimediaProject.objects.order_by('?').first()
self.helper_extern(TYPE_PROJ, 'Projektförderung', {
'name': 'Test',
'description': 'Test',
'categories': [category.id, 0],
'categories_other': 'Test',
'wikimedia_projects': [wikimedia_project.id, 0],
'wikimedia_projects_other': 'Test',
'start': '2025-01-01',
'end': '2025-01-02',
'participants_estimated': 1,
'cost': 20,
})
def test_extern_invalid_code(self): def test_extern_invalid_code(self):
request(self, 'extern', args=['invalid'], status_code=404) request(self, 'extern', args=['invalid'], status_code=404)
def test_unknown_name(self):
obj = Library(type=Library.TYPE)
data = model_to_dict(obj)
name = ApplicationView.get_recipient_name(obj, data)
self.assertEqual(name, 'Unbekannt')
class AuthenticatedViewTestCase(TestCase): class AuthenticatedViewTestCase(TestCase):

View File

@ -1,24 +0,0 @@
from typing import Iterable
def reorder_value(values: Iterable, value, *, after=None, before=None):
"""
Reorders a value after or before another value in the given list.
Does not work properly for duplicate or None values.
Raises ValueError when any of the values is not contained in the list.
"""
assert (after is None) != (before is None), 'Either after or before is needed but not both.'
values = list(values)
values.remove(value)
if after is None:
index = values.index(before)
else:
index = values.index(after) + 1
values.insert(index, value)
return values

View File

@ -10,6 +10,9 @@ __all__ = [
'build_email', 'build_email',
'send_email', 'send_email',
'collect_and_attach', 'collect_and_attach',
'send_applicant_decision_mail',
'send_staff_decision_mail',
'send_decision_mails',
] ]
@ -32,3 +35,49 @@ def build_email(template_name: str, context: dict, subject: str, *recipients: st
def send_email(template_name: str, context: dict, subject: str, *recipients: str, fail_silently=False, **kwargs): def send_email(template_name: str, context: dict, subject: str, *recipients: str, fail_silently=False, **kwargs):
return build_email(template_name, context, subject, *recipients, **kwargs).send(fail_silently) return build_email(template_name, context, subject, *recipients, **kwargs).send(fail_silently)
def get_decision_mail_context(obj: Project):
"""
Build a minimal, consistent context for decision mails (applicant & staff).
"""
return {
'project': obj,
'data': {
'realname': obj.realname or obj.email,
'name': obj.name,
},
}
def send_base_decision_mail(obj: Project, scope: str, subject: str, recipient: str):
context = get_decision_mail_context(obj)
decision = 'granted' if obj.granted else 'denied'
decision_label = 'bewilligt' if obj.granted else 'abgelehnt'
subject = subject.format(name=obj.name, decision=decision_label)
return send_email(f'approval_{decision}_{scope}', context, subject, recipient)
def send_applicant_decision_mail(obj: Project):
"""
Send a decision email to the applicant after manual approval/denial.
"""
if recipient := obj.email:
return send_base_decision_mail(obj, 'applicant', 'Deine Förderanfrage „{name} {decision}', recipient)
return 0
def send_staff_decision_mail(obj: Project):
"""
Send a decision email to the internal team (staff) after approval/denial.
"""
return send_base_decision_mail(obj, 'staff', 'Entscheidung: {name} ({decision})', settings.IF_EMAIL)
def send_decision_mails(obj: Project):
return send_applicant_decision_mail(obj) + send_staff_decision_mail(obj)

View File

@ -1,20 +1,10 @@
import datetime
from dataclasses import dataclass
from smtplib import SMTPException from smtplib import SMTPException
from typing import Optional from typing import NamedTuple
from urllib.parse import urljoin
from django.forms import ChoiceField, Field from django.shortcuts import render
from django.shortcuts import render, redirect
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.urls import reverse
from django.utils.choices import flatten_choices
from django.utils.formats import date_format
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import get_text_list
from django.core.mail import BadHeaderError from django.core.mail import BadHeaderError
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -22,7 +12,6 @@ from django.views.generic import TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django.utils.html import strip_tags from django.utils.html import strip_tags
from input.utils.admin import admin_url_name
from input.utils.mail import build_email, collect_and_attach from input.utils.mail import build_email, collect_and_attach
from .forms import ( from .forms import (
@ -52,8 +41,6 @@ from .models import (
TYPE_SOFT, TYPE_SOFT,
TYPE_TRAV, TYPE_TRAV,
TYPE_VIS, TYPE_VIS,
Project,
ProductCategoryFormField,
) )
HELP_TEXTS = { HELP_TEXTS = {
@ -80,49 +67,35 @@ HELP_TEXTS = {
}, },
} }
@dataclass
class ApplicationType: class ApplicationType(NamedTuple):
code: str code: str
path: str path: str
form_class: type[BaseApplicationForm] form_class: type[BaseApplicationForm]
link: str
label: Optional[str] = None
help_texts: Optional[dict] = None
def __post_init__(self):
if self.label is None:
self.label = TYPE_CHOICES[self.code]
if self.help_texts is None: # pragma: no branch
self.help_texts = HELP_TEXTS.get(self.code)
@property @property
def url(self): def label(self):
return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}' return TYPE_CHOICES[self.code]
@property
def help_texts(self):
return HELP_TEXTS.get(self.code)
PROJECT_FUNDING = [ PROJECT_FUNDING = [
ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm, 'Projektplanung', ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm),
'Projektförderung mit einer Gesamtsumme unter 1.000,— EUR'),
ApplicationType(TYPE_PROJ, 'projektfoerderung-ab-1000', ProjectForm, 'Projektplanung',
'Projektförderung mit einer Gesamtsumme ab 1.000,— EUR'),
] ]
SERVICES = [ SERVICES = [
ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm, 'Zugang_zu_Fachliteratur#Bibliotheksstipendium'), ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm),
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm, 'Zugang_zu_Fachliteratur#eLiteraturstipendium'), ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm),
ApplicationType(TYPE_MAIL, 'email', EmailForm, 'E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen'), ApplicationType(TYPE_MAIL, 'email', EmailForm),
ApplicationType(TYPE_IFG, 'ifg', IFGForm, 'Geb%C3%BChrenerstattungen_f%C3%BCr_Beh%C3%B6rdenanfragen'), ApplicationType(TYPE_IFG, 'ifg', IFGForm),
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm, 'Zugang_zu_Fachliteratur#Literaturstipendium'), ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm),
ApplicationType(TYPE_LIST, 'mailingliste', ListForm, 'E-Mail-Adressen_und_Visitenkarten#Mailinglisten'), ApplicationType(TYPE_LIST, 'mailingliste', ListForm),
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm, 'Reisekostenerstattungen'), ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm),
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm, 'Software-Stipendien'), ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm),
ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm, 'E-Mail-Adressen_und_Visitenkarten#Visitenkarten'), ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm),
]
APPLICATIONS = [
('Projektförderung', PROJECT_FUNDING),
('Serviceleistungen', SERVICES),
] ]
TYPES = {info.path: info for info in PROJECT_FUNDING + SERVICES} TYPES = {info.path: info for info in PROJECT_FUNDING + SERVICES}
@ -174,14 +147,7 @@ def index(request):
class ApplicationStartView(TemplateView): class ApplicationStartView(TemplateView):
template_name = 'input/forms/extern.html' template_name = 'input/forms/extern.html'
extra_context = {'applications': APPLICATIONS} extra_context = {'services': SERVICES}
def post(self, request, *args, **kwargs):
if url := request.POST.get('url'):
if url_has_allowed_host_and_scheme(url, None):
return redirect(url)
return self.get(request, *args, **kwargs)
class ProjectFundingInfoView(TemplateView): class ProjectFundingInfoView(TemplateView):
@ -254,7 +220,14 @@ class ApplicationView(FormView):
def prepare_data(self, form): def prepare_data(self, form):
# Collect cleaned data and mark the current type # Collect cleaned data and mark the current type
return {**form.cleaned_data, 'choice': self.type_code}
data = {**form.cleaned_data, 'choice': self.type_code}
# Special rule for literature applications
if self.type_code == TYPE_LIT and data.get('selfbuy') == 'TRUE':
data['selfbuy_give_data'] = 'False'
return data
def save_obj(self, form, data): def save_obj(self, form, data):
# Save model instance # Save model instance
@ -290,9 +263,19 @@ class ApplicationView(FormView):
def send_mail(self, obj, data): def send_mail(self, obj, data):
# Prepare minimal mail context and send mails # Prepare minimal mail context and send mails
context = self.get_email_context(obj, data) type_label_html = self.type_info.label
type_label_plain = strip_tags(type_label_html)
data['pk'] = obj.pk
data['url_prefix'] = settings.EMAIL_URL_PREFIX
data['type_label'] = type_label_html
context = {'data': data}
applicant_name = self.get_recipient_name(obj, data)
applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland' applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland'
staff_subject = 'Anfrage {type_label} von {applicant_name}'.format(**context)
staff_subject = f'Anfrage {type_label_plain} von {applicant_name}'
try: try:
self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context) self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context)
@ -304,59 +287,6 @@ class ApplicationView(FormView):
obj.delete() obj.delete()
return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!') return HttpResponse('Error in sending mails (probably wrong address?). Data not saved!')
def get_email_context(self, obj, data):
return {
'data': data,
'urls': self.get_urls(obj),
'form_data': self.get_form_data(obj, data),
'applicant_name': self.get_recipient_name(obj, data),
'type_label': self.sanitize_label(self.type_info.label),
}
def get_urls(self, obj, **urls):
urls['admin'] = self.get_absolute_url(admin_url_name(obj, 'change'), obj.id)
if isinstance(obj, Project):
urls['authorize'] = urls['deny'] = None
else:
urls['authorize'] = self.get_absolute_url('authorize', self.type_info.code, obj.id)
urls['deny'] = self.get_absolute_url('deny', self.type_info.code, obj.id)
return urls
@staticmethod
def get_absolute_url(view, *args):
return urljoin(settings.EMAIL_URL_PREFIX, reverse(view, args=args))
def get_form_data(self, obj, data):
return {
self.sanitize_label(field.label): self.format_value(field.field, field.initial)
for field in self.type_info.form_class(initial=data)
}
@staticmethod
def sanitize_label(label: str):
label = strip_tags(label)
words = str.split(label)
return ' '.join(words)
@staticmethod
def format_value(field: Field, value):
if isinstance(field, ProductCategoryFormField):
value = get_text_list(value, 'und')
elif isinstance(field, ChoiceField):
choices = flatten_choices(field.choices)
value = dict(choices).get(value, value)
elif isinstance(value, bool):
value = '' if value else ''
elif isinstance(value, datetime.date):
value = date_format(value)
elif value in field.empty_values:
value = ''
return value
def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False): def send_email(self, kind, template_name, subject, recipient, context, *, fail_silently=False):
email = build_email(template_name, context, subject, recipient) email = build_email(template_name, context, subject, recipient)