Compare commits

...

27 Commits

Author SHA1 Message Date
Oliver Zander 3fe9b08925 fixed form data for project funding 2025-11-20 12:23:45 +00:00
Oliver Zander 7fd35636b1 fixed validation 2025-11-20 12:23:45 +00:00
Oliver Zander 6932b9413c removed unnecessary code 2025-11-20 12:23:45 +00:00
Oliver Zander 3e9859c1a8 improved literature form validation 2025-11-20 12:23:45 +00:00
Oliver Zander 5ba6195a82 properly clean selfbuy data 2025-11-20 12:23:45 +00:00
Oliver Zander db8f5b1bef added test for failed extern redirect 2025-11-20 12:23:45 +00:00
Oliver Zander feafada61d fixed typing for help texts in application type 2025-11-20 12:23:45 +00:00
Oliver Zander 361e3252f2 added test for unknown applicant name 2025-11-20 12:23:45 +00:00
Oliver Zander bf16383c3f WM-18: improved mails & added full form data 2025-11-20 12:23:45 +00:00
Oliver Zander 45b910a768 WM-13: made denied projects re-grantable 2025-11-20 12:23:45 +00:00
Oliver Zander e92029db83 WM-12: bring back opacity for admin 2025-11-20 12:23:45 +00:00
Oliver Zander d9091c818a WM-13: removed decision mails 2025-11-20 12:23:45 +00:00
Oliver Zander 9e30415003 WM-12: use textarea for other fields 2025-11-20 12:23:45 +00:00
Oliver Zander 7bb4f1a841 WM-11: left align applications 2025-11-20 12:23:45 +00:00
Oliver Zander 5e77900daf renamed blocks to applications 2025-11-20 12:23:45 +00:00
Oliver Zander 984c7807cb only use german 2025-11-20 12:23:45 +00:00
Oliver Zander c828964ee6 WM-12: hide other value row when not checked 2025-11-20 12:23:45 +00:00
Oliver Zander 341c24e651 WM-16: reorder fields in admin 2025-11-20 12:23:45 +00:00
Oliver Zander 966bb8e7a6 WM-19: added username to projects 2025-11-20 12:23:45 +00:00
Oliver Zander 3356bcef4f WM-17: added verbose names for models 2025-11-20 12:23:45 +00:00
Oliver Zander 5d6295358b removed duplicate trailing colons in labels 2025-11-20 12:23:45 +00:00
Oliver Zander 513671bef0 WM-12: added back button 2025-11-20 12:23:45 +00:00
Oliver Zander 336428bcb6 added titles 2025-11-20 12:23:45 +00:00
Oliver Zander ec60aa0550 validate checkin & checkout 2025-11-20 12:23:45 +00:00
Oliver Zander 03dc9c0775 removed initial value for travel cost 2025-11-20 12:23:45 +00:00
Oliver Zander a084eb6664 WM-12: changed some labels & verbose names 2025-11-20 12:23:45 +00:00
Oliver Zander 1a299fd10b WM-11: switch service list to radios with info links 2025-11-20 12:23:45 +00:00
39 changed files with 529 additions and 399 deletions

View File

@ -121,6 +121,9 @@ 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, transaction from django.db import models
from django.http import HttpResponse from django.http import HttpResponse
from input.utils.mail import send_decision_mails from input.utils.list import reorder_value
from .forms import BaseProjectForm from .forms import BaseProjectForm
from .models import ( from .models import (
@ -24,24 +24,21 @@ from .models import (
BusinessCard, BusinessCard,
List, List,
Literature, Literature,
TYPE_PROJ,
) )
class RequestURLBeforeInternNotesMixin: class WMDEAdmin(admin.ModelAdmin):
"""
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)] fields = super().get_fields(request, obj=obj)
fields.remove('request_url') if 'username' in fields:
fields = reorder_value(fields, 'username', after='email')
index = fields.index('intern_notes') fields = reorder_value(fields, 'request_url', before='intern_notes')
fields.insert(index, 'request_url') if 'terms_accepted' in fields:
fields = reorder_value(fields, 'terms_accepted', before='request_url')
return fields return fields
@ -114,6 +111,7 @@ class BaseProjectAdmin(admin.ModelAdmin):
('Kontakt', {'fields': ( ('Kontakt', {'fields': (
'realname', 'realname',
'email', 'email',
'username',
)}), )}),
('Projekt', {'fields': ( ('Projekt', {'fields': (
'name', 'name',
@ -175,16 +173,6 @@ 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):
@ -193,12 +181,9 @@ 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(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class BusinessCardAdmin(WMDEAdmin):
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')
@ -212,7 +197,7 @@ class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(Literature) @admin.register(Literature)
class LiteratureAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class LiteratureAdmin(WMDEAdmin):
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')
@ -242,7 +227,7 @@ class HonoraryCertificateAdmin(admin.ModelAdmin):
@admin.register(Library, ELiterature, Software) @admin.register(Library, ELiterature, Software)
class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class LibraryAdmin(WMDEAdmin):
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')
@ -269,7 +254,7 @@ class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(IFG) @admin.register(IFG)
class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class IFGAdmin(WMDEAdmin):
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')
@ -282,7 +267,7 @@ class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(Travel) @admin.register(Travel)
class TravelAdmin(admin.ModelAdmin): class TravelAdmin(WMDEAdmin):
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',
@ -297,7 +282,7 @@ class TravelAdmin(admin.ModelAdmin):
@admin.register(Email) @admin.register(Email)
class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class EmailAdmin(WMDEAdmin):
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')
@ -311,7 +296,7 @@ class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(List) @admin.register(List)
class ListAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin): class ListAdmin(WMDEAdmin):
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,6 +5,7 @@ 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,
@ -125,6 +126,11 @@ 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,
@ -161,6 +167,7 @@ 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
@ -178,6 +185,10 @@ 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,
@ -202,6 +213,9 @@ 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)
@ -250,10 +264,6 @@ 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 = [
@ -272,6 +282,24 @@ 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

@ -0,0 +1,57 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,23 @@
# 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,6 +100,10 @@ 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}'
@ -142,8 +146,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):
@ -184,7 +188,7 @@ class ProjectCategoryField(models.ManyToManyField):
super().__init__(**kwargs) super().__init__(**kwargs)
self.other_field = models.CharField(max_length=200, blank=True) self.other_field = models.TextField(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)
@ -213,6 +217,8 @@ 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)
@ -347,8 +353,12 @@ 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'Certificate for {self.realname}' return f'Bescheinigung für {self.realname}'
TRANSPORT_CHOICES = { TRANSPORT_CHOICES = {
@ -371,15 +381,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')
@ -387,6 +397,21 @@ 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):
@ -400,7 +425,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
@ -443,6 +468,10 @@ 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
@ -460,6 +489,8 @@ class ELiterature(Library):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = 'eLiteraturstipendium'
verbose_name_plural = 'eLiteraturstipendien'
class Software(Library): class Software(Library):
@ -470,6 +501,8 @@ class Software(Library):
class Meta: class Meta:
proxy = True proxy = True
verbose_name = 'Softwarestipendium'
verbose_name_plural = 'Softwarestipendien'
SELFBUY_CHOICES = { SELFBUY_CHOICES = {
@ -492,14 +525,22 @@ 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 'IFG-Anfrage von ' + self.realname return f'IFG-Anfrage von {self.realname}'
DOMAIN_CHOICES = { DOMAIN_CHOICES = {
@ -523,7 +564,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 = {
@ -542,6 +583,10 @@ 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',
@ -549,6 +594,10 @@ 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',
@ -593,6 +642,10 @@ 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,6 +6,16 @@
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,6 +6,7 @@
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');
@ -14,6 +15,7 @@
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,32 +1,44 @@
{% extends "input/base.html" %} {% extends 'input/base.html' %}
{% load i18n %} {% load i18n %}
{% block content %}
<div class="page-centered">
<table class="wm-table">
<tr>
<th class="col-request">Was möchtest du beantragen?</th>
<td>
<strong>Projektförderung</strong>
<ul>
<li>
<a href="{% url 'extern' type='projektfoerderung' %}">Projektförderung</a>
mit einer Gesamtsumme unter 1.000,— EUR
</li>
<li>
<a href="{% url 'projektfoerderung-ab-1000' %}">Projektförderung</a>
mit einer Gesamtsumme ab 1.000,— EUR
</li>
</ul>
<strong>Serviceleistungen</strong> {% block head_extra %}
<ul> <title>Was möchtest du beantragen?</title>
{% for info in services %} {% endblock %}
<li><a href="{% url 'extern' type=info.path %}">{{ info.label|striptags }}</a></li>
{% endfor %} {% block content %}
</ul> <form class="page-centered" method="post">
</td> {% csrf_token %}
</tr>
</table> <table class="wm-table start">
</div> <tbody class="applications">
<tr>
<th class="col-request">Was möchtest du beantragen?</th>
<td>
{% for title, services in applications %}
<strong>{{ title }}</strong>
<ul>
{% for service in services %}
<li>
<label>
<input type="radio" name="url" value="{% url 'extern' type=service.path %}" />
<span>{{ service.label|striptags }}</span>
</label>
<span>(<a href="{{ service.url }}" target="_blank">mehr erfahren</a>)</span>
</li>
{% endfor %}
</ul>
{% endfor %}
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="2">
<button type="submit">Beantragen</button>
</td>
</tr>
</tbody>
</table>
</form>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,10 @@
{% 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,4 +21,7 @@
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

@ -1,9 +0,0 @@
<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

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

View File

@ -1,7 +0,0 @@
<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

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

View File

@ -1,10 +0,0 @@
<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

@ -1,7 +0,0 @@
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

@ -1,7 +0,0 @@
<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

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

View File

@ -1,60 +1,26 @@
<html> <html lang="de">
<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: {{ data.type_label|striptags }}</p> <p>{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}</p>
{% if data.choice in data.grant %}<br> <p>
Vorraussichtliche Kosten: {{data.cost}}<br> {% for label, value in form_data.items %}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br> {{ label }}: {{ value }} <br />
Domain: <a href="{{data.domain}}">{{data.domain}}</a><br> {% endfor %}
Adressenbestandteil: {{data.address}} <br> {% endif %} {% if data.choice == 'BIB' %} </p>
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: <p>Zum Eintrag in der Förderdatenbank: <a href="{{ urls.admin }}">{{ urls.admin }}</a></p>
{% 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> {% if urls.authorize %}
{% elif data.choice == 'ELIT'%} <p>Zum Genehmigen hier klicken: <a href="{{ urls.authorize }}">{{ urls.authorize }}</a></p>
<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 %} {% endif %}
</p>
<p>Zum Genehmigen hier klicken: {% if urls.deny %}
<a href="{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}">{{data.url_prefix}}{% url 'authorize' data.choice data.pk %}</a> <p>Zum Ablehnen hier klicken: <a href="{{ urls.deny }}">{{ urls.deny }}</a></p>
</p> {% endif %}
<p>Zum Ablehnen hier klicken:
<a href="{{data.url_prefix}}{% url 'deny' data.choice data.pk %}">{{data.url_prefix}}{% url 'deny' data.choice data.pk %}</a>
</p>
</body> </body>
</html> </html>

View File

@ -1,52 +1,12 @@
Hallo Team Communitys und Engagement, Hallo Team Community-Konferenzen & Förderung,
es gab einen neuen Antrag von {{data.realname}}. es gibt eine neue Anfrage von {{ data.realname }}.
Der Nutzer mit dem Username {{data.username}} ({{data.email}}) fragt ein_e {{data.type_label|striptags}} an. {{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}
{% 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 %}
Zum Eintrag in der Förderdatenbank: {% for label, value in form_data.items %}{{ label }}: {{ value }}
{% if data.choice == 'BIB' %} {% endfor %}
<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 }}
Zum Genehmigen hier klicken: {{data.url_prefix}}{% url 'authorize' data.choice data.pk %} {% if urls.authorize %}Zum Genehmigen hier klicken: {{ urls.authorize }}{% endif %}
{% 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,29 +1,14 @@
<html> <html lang="de">
<body> <body>
<p>Hallo {{ data.username|default:data.realname }},</p> <p>Hallo {{ applicant_name }},</p>
<p>vielen Dank für deine Anfrage ({{ data.type_label|striptags }}), die bei uns eingegangen ist.</p> <p>vielen Dank für deine Anfrage ({{ type_label }}), 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>
Vorraussichtliche Kosten: {{data.cost}}<br> {% for label, value in form_data.items %}
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br> {{ label }}: {{ value }} <br />
Domain: <a href="{{data.domain}}">{{data.domain}}</a><br> {% endfor %}
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>
@ -44,4 +29,4 @@ Sendungsadrese: {{data.send_to}} {% endif %}<br>
<p>Datenschutzerklärung:<br> <p>Datenschutzerklärung:<br>
Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer <a href="https://www.wikimedia.de/datenschutz/">Datenschutzerklärung </a>.</p> Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer <a href="https://www.wikimedia.de/datenschutz/">Datenschutzerklärung </a>.</p>
</body> </body>
</html> </html>

View File

@ -1,29 +1,22 @@
Hallo {{data.realname}}, Hallo {{ applicant_name }},
wir haben Deine Anfrage ({{data.type_label|striptags}}) erhalten. vielen Dank für deine Anfrage ({{type_label}}), die bei uns eingegangen ist.
{% 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 %}
Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern {% for label, value in form_data.items %}{{ label }}: {{ value }}
und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben, {% endfor %}
wende dich gern an community@wikimedia.de.
Viele Grüße, dein freundliches aber komplett unmenschliches automatisches 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.
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,6 +1,5 @@
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
@ -109,7 +108,4 @@ 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,11 +1,17 @@
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 input.models import Library, TYPE_PROJ from foerderbarometer.constants import *
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 TYPES from input.views import PROJECT_FUNDING, TYPES, ApplicationView
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):
@ -18,6 +24,15 @@ 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 {
@ -50,15 +65,15 @@ class AnonymousViewTestCase(TestCase):
def test_extern_types(self): def test_extern_types(self):
types = [ types = [
('BIB', 'Bibliotheksausweis'), (TYPE_BIB, 'Bibliotheksausweis'),
('ELIT', 'Online-Ressource'), (TYPE_ELIT, 'Online-Ressource'),
('MAIL', 'Mailadresse beantragen'), (TYPE_MAIL, 'Mailadresse beantragen'),
('IFG', 'gewonnenen Informationen'), (TYPE_IFG, 'gewonnenen Informationen'),
('LIT', 'Literatur verwenden'), (TYPE_LIT, 'Literatur verwenden'),
('LIST', 'Mailingliste beantragen'), (TYPE_LIST, 'Mailingliste beantragen'),
('TRAV', 'Transportmittel'), (TYPE_TRAV, 'Transportmittel'),
('SOFT', 'Lizenz'), (TYPE_SOFT, 'Lizenz'),
('VIS', 'DIN 5008'), (TYPE_VIS, 'DIN 5008'),
(TYPE_PROJ, 'Projektförderung'), (TYPE_PROJ, 'Projektförderung'),
] ]
@ -70,7 +85,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('TRAV', 'Transportmittel', { self.helper_extern(TYPE_TRAV, 'Transportmittel', {
'project_name': 'Test', 'project_name': 'Test',
'transport': 'BAHN', 'transport': 'BAHN',
'travelcost': 10, 'travelcost': 10,
@ -81,27 +96,27 @@ class AnonymousViewTestCase(TestCase):
}) })
def test_extern_lit(self): def test_extern_lit(self):
self.helper_extern('LIT', 'Literatur verwenden', { self.helper_extern(TYPE_LIT, 'Literatur verwenden', {
'cost': 20, 'cost': 20,
'info': 'Test', 'info': 'Test',
'source': 'Test', 'source': 'Test',
'notes': '', 'notes': '',
'selfbuy': 'TRUE', 'selfbuy': 'FALSE',
'selfbuy_data': 'NONE', 'selfbuy_data': 'Test',
'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('LIT', 'Literatur verwenden', { response = self.helper_extern_base(TYPE_LIT, 'Literatur verwenden', {
'cost': 20, 'cost': 20,
'info': 'Test', 'info': 'Test',
'source': 'Test', 'source': 'Test',
'notes': '', 'notes': '',
'selfbuy': 'TRUE', 'selfbuy': 'TRUE',
'selfbuy_data': 'NONE', 'selfbuy_data': '',
'selfbuy_give_data': True, 'selfbuy_give_data': False,
'check': False, 'check': False,
}) })
@ -115,9 +130,32 @@ 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):

24
input/utils/list.py Normal file
View File

@ -0,0 +1,24 @@
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,9 +10,6 @@ __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',
] ]
@ -35,49 +32,3 @@ 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,10 +1,20 @@
from smtplib import SMTPException import datetime
from typing import NamedTuple
from django.shortcuts import render from dataclasses import dataclass
from smtplib import SMTPException
from typing import Optional
from urllib.parse import urljoin
from django.forms import ChoiceField, Field
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
@ -12,6 +22,7 @@ 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 (
@ -41,6 +52,8 @@ from .models import (
TYPE_SOFT, TYPE_SOFT,
TYPE_TRAV, TYPE_TRAV,
TYPE_VIS, TYPE_VIS,
Project,
ProductCategoryFormField,
) )
HELP_TEXTS = { HELP_TEXTS = {
@ -67,35 +80,49 @@ HELP_TEXTS = {
}, },
} }
@dataclass
class ApplicationType(NamedTuple): class ApplicationType:
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 label(self): def url(self):
return TYPE_CHOICES[self.code] return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}'
@property
def help_texts(self):
return HELP_TEXTS.get(self.code)
PROJECT_FUNDING = [ PROJECT_FUNDING = [
ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm), ApplicationType(TYPE_PROJ, 'projektfoerderung', ProjectForm, 'Projektplanung',
'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), ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm, 'Zugang_zu_Fachliteratur#Bibliotheksstipendium'),
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm), ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm, 'Zugang_zu_Fachliteratur#eLiteraturstipendium'),
ApplicationType(TYPE_MAIL, 'email', EmailForm), ApplicationType(TYPE_MAIL, 'email', EmailForm, 'E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen'),
ApplicationType(TYPE_IFG, 'ifg', IFGForm), ApplicationType(TYPE_IFG, 'ifg', IFGForm, 'Geb%C3%BChrenerstattungen_f%C3%BCr_Beh%C3%B6rdenanfragen'),
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm), ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm, 'Zugang_zu_Fachliteratur#Literaturstipendium'),
ApplicationType(TYPE_LIST, 'mailingliste', ListForm), ApplicationType(TYPE_LIST, 'mailingliste', ListForm, 'E-Mail-Adressen_und_Visitenkarten#Mailinglisten'),
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm), ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm, 'Reisekostenerstattungen'),
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm), ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm, 'Software-Stipendien'),
ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm), ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm, 'E-Mail-Adressen_und_Visitenkarten#Visitenkarten'),
]
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}
@ -147,7 +174,14 @@ def index(request):
class ApplicationStartView(TemplateView): class ApplicationStartView(TemplateView):
template_name = 'input/forms/extern.html' template_name = 'input/forms/extern.html'
extra_context = {'services': SERVICES} extra_context = {'applications': APPLICATIONS}
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):
@ -220,14 +254,7 @@ 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
@ -263,19 +290,9 @@ 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
type_label_html = self.type_info.label context = self.get_email_context(obj, data)
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)
@ -287,6 +304,59 @@ 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)