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_L10N = True
LANGUAGE_CODE = env('LANGUAGE_CODE', 'de')
LANGUAGES = [
('de', 'Deutsch'),
]
USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'Europe/Berlin')

View File

@ -1,10 +1,10 @@
import csv
from django.contrib import admin
from django.db import models, transaction
from django.db import models
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 .models import (
@ -24,24 +24,21 @@ from .models import (
BusinessCard,
List,
Literature,
TYPE_PROJ,
)
class RequestURLBeforeInternNotesMixin:
"""
Ensures that 'request_url' appears directly before 'intern_notes'.
Works whether 'fields' is explicitly defined or derived from the Model/Form.
"""
class WMDEAdmin(admin.ModelAdmin):
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
@ -114,6 +111,7 @@ class BaseProjectAdmin(admin.ModelAdmin):
('Kontakt', {'fields': (
'realname',
'email',
'username',
)}),
('Projekt', {'fields': (
'name',
@ -175,16 +173,6 @@ class ProjectAdmin(BaseProjectAdmin):
class ProjectRequestAdmin(BaseProjectAdmin):
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)
class ProjectDeclinedAdmin(BaseProjectAdmin):
@ -193,12 +181,9 @@ class ProjectDeclinedAdmin(BaseProjectAdmin):
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(BusinessCard)
class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class BusinessCardAdmin(WMDEAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
@ -212,7 +197,7 @@ class BusinessCardAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(Literature)
class LiteratureAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class LiteratureAdmin(WMDEAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -242,7 +227,7 @@ class HonoraryCertificateAdmin(admin.ModelAdmin):
@admin.register(Library, ELiterature, Software)
class LibraryAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class LibraryAdmin(WMDEAdmin):
save_as = True
search_fields = ('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)
class IFGAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class IFGAdmin(WMDEAdmin):
save_as = True
search_fields = ('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)
class TravelAdmin(admin.ModelAdmin):
class TravelAdmin(WMDEAdmin):
save_as = True
search_fields = ['realname', 'service_id', 'granted_date', 'project__name', 'project__pid']
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project_end', 'project',
@ -297,7 +282,7 @@ class TravelAdmin(admin.ModelAdmin):
@admin.register(Email)
class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class EmailAdmin(WMDEAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
@ -311,7 +296,7 @@ class EmailAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
@admin.register(List)
class ListAdmin(RequestURLBeforeInternNotesMixin, admin.ModelAdmin):
class ListAdmin(WMDEAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
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.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as trans
from .models import (
Project,
@ -125,6 +126,11 @@ class ProjectForm(BaseProjectForm, BaseApplicationForm):
'insurance',
'notes',
]
labels = {
'cost': 'Kosten in Euro',
'insurance': 'Haftpflicht- und Unfallversicherung gewünscht',
'participants_estimated': 'Voraussichtliche Zahl der Teilnehmenden',
}
widgets = {
'start': AdminDateWidget,
'end': AdminDateWidget,
@ -161,6 +167,7 @@ class TravelForm(BaseApplicationForm):
self.fields['project_name'].required = True
self.fields['transport'].required = True
self.fields['travelcost'].required = True
self.fields['travelcost'].initial = None
self.fields['checkin'].required = True
self.fields['checkout'].required = True
self.fields['hotel'].required = True
@ -178,6 +185,10 @@ class TravelForm(BaseApplicationForm):
'hotel',
'notes',
]
labels = {
'checkin': 'Datum der Anreise',
'checkout': 'Datum der Abreise',
}
widgets = {
'checkin': AdminDateWidget,
'checkout': AdminDateWidget,
@ -202,6 +213,9 @@ class LibraryForm(BaseApplicationForm):
'duration',
'notes',
]
labels = {
'cost': 'Kosten in Euro',
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -250,10 +264,6 @@ class TermsForm(BaseApplicationForm):
class LiteratureForm(TermsForm):
terms_accepted_url = settings.NUTZUNGSBEDINGUNGEN_LITERATURSTIPENDIUM
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selfbuy_give_data'].required = True
class Meta:
model = Literature
fields = [
@ -272,6 +282,24 @@ class LiteratureForm(TermsForm):
class Media:
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 = {
'TRUE': mark_safe('Ich bin volljährig.'),

View File

@ -38,7 +38,7 @@ class Migration(migrations.Migration):
('survey_mail_send', models.BooleanField(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)),
('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)),
],
options={

View File

@ -45,7 +45,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='email',
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(
model_name='list',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='email',
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(
model_name='email',

View File

@ -18,12 +18,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ifg',
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(
model_name='library',
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(
model_name='library',
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='literature',
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(
model_name='project',

View File

@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='travel',
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(
model_name='library',

View File

@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='travel',
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(
model_name='travel',
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(
model_name='travel',
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')),
],
options={
'verbose_name': 'Wikimedia Projekt',
'verbose_name_plural': 'Wikimedia Projekte',
'verbose_name': 'Wikimedia-Projekt',
'verbose_name_plural': 'Wikimedia-Projekte',
'ordering': ['order'],
},
),

View File

@ -24,11 +24,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='project',
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(
model_name='project',
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')
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):
return f'{self.code} {self.description}'
@ -142,8 +146,8 @@ class WikimediaProject(BaseProjectCategory):
OTHER = 'Anderes'
class Meta(BaseProjectCategory.Meta):
verbose_name = 'Wikimedia Projekt'
verbose_name_plural = 'Wikimedia Projekte'
verbose_name = 'Wikimedia-Projekt'
verbose_name_plural = 'Wikimedia-Projekte'
class ProductCategoryChoiceIterator(ModelMultipleChoiceField.iterator):
@ -184,7 +188,7 @@ class ProjectCategoryField(models.ManyToManyField):
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):
super().contribute_to_class(cls, name, **kwargs)
@ -213,6 +217,8 @@ class ProjectCategoryField(models.ManyToManyField):
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')
name = models.CharField(max_length=200, verbose_name='Name des Projekts')
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)
class Meta:
verbose_name = 'Bescheinigung'
verbose_name_plural = 'Bescheinigungen'
def __str__(self):
return f'Certificate for {self.realname}'
return f'Bescheinigung für {self.realname}'
TRANSPORT_CHOICES = {
@ -371,15 +381,15 @@ HOTEL_CHOICES = {
class Travel(Extern):
# project variable is now null true and blank true, which means it can be saved without project id to be later on filled out by admins
project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True)
project_name = models.CharField(max_length=50, null=True, blank=True, verbose_name='Projektname:')
transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel:')
project_name = models.CharField(max_length=50, null=True, blank=True, verbose_name='Projektname')
transport = models.CharField(max_length=5, choices=TRANSPORT_CHOICES.items(), default='BAHN', verbose_name='Transportmittel')
other_transport = models.CharField(max_length=200, null=True, blank=True, verbose_name='Sonstige Transportmittel (mit Begründung)')
travelcost = models.CharField(max_length=10, default='0', verbose_name='Fahrtkosten')
checkin = models.DateField(blank=True, null=True, verbose_name='Anreise')
checkout = models.DateField(blank=True, null=True, verbose_name='Abreise')
payed_for_hotel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Hotel durch')
payed_for_travel_by = models.CharField(max_length=4, choices=PAYEDBY_CHOICES.items(), blank=True, null=True, verbose_name='Kostenauslage Fahrt durch')
hotel = models.CharField(max_length=10, choices=HOTEL_CHOICES.items(), verbose_name='Hotelzimmer benötigt:')
hotel = models.CharField(max_length=10, choices=HOTEL_CHOICES.items(), verbose_name='Hotelzimmer benötigt')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen')
request_url = models.URLField(max_length=2000, verbose_name='Antrag (URL)')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
@ -387,6 +397,21 @@ class Travel(Extern):
# 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')
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')
def get_project_end(sender, instance, **kwargs):
@ -400,7 +425,7 @@ class Grant(RequestUrlMixin, Extern):
cost = models.CharField(max_length=10, verbose_name='Kosten',
help_text='Bitte gib die ungefähr zu erwartenden Kosten in Euro an.')
notes = models.TextField(max_length=1000, blank=True, verbose_name='Anmerkungen',
help_text='Bitte gib an wofür Du das Stipendium verwenden willst.')
help_text='Bitte gib an, wofür Du das Stipendium verwenden willst.')
class Meta:
abstract = True
@ -443,6 +468,10 @@ class Library(Grant):
duration = models.CharField(max_length=100, verbose_name='Dauer')
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):
return self.library
@ -460,6 +489,8 @@ class ELiterature(Library):
class Meta:
proxy = True
verbose_name = 'eLiteraturstipendium'
verbose_name_plural = 'eLiteraturstipendien'
class Software(Library):
@ -470,6 +501,8 @@ class Software(Library):
class Meta:
proxy = True
verbose_name = 'Softwarestipendium'
verbose_name_plural = 'Softwarestipendien'
SELFBUY_CHOICES = {
@ -492,14 +525,22 @@ class Literature(TermsConsentMixin, Grant):
Telefonnummer, E-Mail-Adresse usw.). Trenne die einzelnen Angaben durch Zeilenumbrüche.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Literaturstipendium'
verbose_name_plural = 'Literaturstipendien'
class IFG(Grant):
url = models.URLField(max_length=2000, verbose_name='URL',
help_text='Bitte gib den Link zu deiner Anfrage bei Frag den Staat an.')
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'IFG-Anfrage'
verbose_name_plural = 'IFG-Anfragen'
def __str__(self):
return 'IFG-Anfrage von ' + self.realname
return f'IFG-Anfrage von {self.realname}'
DOMAIN_CHOICES = {
@ -523,7 +564,7 @@ class Domain(RequestUrlMixin, Extern):
MAIL_CHOICES = {
'REALNAME': 'Vorname.Nachname',
'USERNAME': 'Username',
'OTHER': 'Sonstiges:',
'OTHER': 'Sonstiges',
}
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')
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):
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.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Mailingliste'
verbose_name_plural = 'Mailinglisten'
PROJECT_CHOICE = {
'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.'))
intern_notes = models.TextField(max_length=1000, blank=True, verbose_name='interne Anmerkungen')
class Meta:
verbose_name = 'Visitenkarte'
verbose_name_plural = 'Visitenkarten'
MODELS = {
TYPE_BIB: Library,

View File

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

View File

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

View File

@ -1,32 +1,44 @@
{% extends "input/base.html" %}
{% extends 'input/base.html' %}
{% 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>
<ul>
{% for info in services %}
<li><a href="{% url 'extern' type=info.path %}">{{ info.label|striptags }}</a></li>
{% endfor %}
</ul>
</td>
</tr>
</table>
</div>
{% block head_extra %}
<title>Was möchtest du beantragen?</title>
{% endblock %}
{% block content %}
<form class="page-centered" method="post">
{% csrf_token %}
<table class="wm-table start">
<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 %}

View File

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

View File

@ -21,4 +21,7 @@
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.
</p>
<div class="page-centered">
<button type="button" onclick="history.back()">Zurück</button>
</div>
{% 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>
<p>Hallo Team Community-Konferenzen &amp; Förderung,</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>
Vorraussichtliche Kosten: {{data.cost}}<br>
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
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>
{% for label, value in form_data.items %}
{{ label }}: {{ value }} <br />
{% endfor %}
</p>
<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>
<p>Zum Eintrag in der Förderdatenbank: <a href="{{ urls.admin }}">{{ urls.admin }}</a></p>
{% if urls.authorize %}
<p>Zum Genehmigen hier klicken: <a href="{{ urls.authorize }}">{{ urls.authorize }}</a></p>
{% endif %}
</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>
<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>
{% if urls.deny %}
<p>Zum Ablehnen hier klicken: <a href="{{ urls.deny }}">{{ urls.deny }}</a></p>
{% endif %}
</body>
</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.
{% 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 %}
{{ data.username|default:data.realname }} ({{ data.email }}) fragt an: {{ type_label }}
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 %}
{% for label, value in form_data.items %}{{ label }}: {{ value }}
{% endfor %}
Zum Genehmigen hier klicken: {{data.url_prefix}}{% url 'authorize' data.choice data.pk %}
Zu Ablehnen hier klicken: {{data.url_prefix}}{% url 'deny' data.choice data.pk %}
Stets zu Diensten, Deine Förderdatenbank
Zum Eintrag in der Förderdatenbank: {{ urls.admin }}
{% if urls.authorize %}Zum Genehmigen hier klicken: {{ urls.authorize }}{% endif %}
{% if urls.deny %}Zum Ablehnen hier klicken: {{ urls.deny }}{% endif %}

View File

@ -1,29 +1,14 @@
<html>
<html lang="de">
<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>
{% if data.choice in data.grant %}<br>
Vorraussichtliche Kosten: {{data.cost}}<br>
Anmerkungen: {{data.notes}} {% endif %} {% if data.choice in data.domain %}<br>
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>
{% for label, value in form_data.items %}
{{ label }}: {{ value }} <br />
{% endfor %}
<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>
Soweit Sie uns personenbezogene Daten mitteilen, verarbeiten wir diese Daten gemäß unserer <a href="https://www.wikimedia.de/datenschutz/">Datenschutzerklärung </a>.</p>
</body>
</html>
</html>

View File

@ -1,29 +1,22 @@
Hallo {{data.realname}},
Hallo {{ applicant_name }},
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 %}
vielen Dank für deine Anfrage ({{type_label}}), die bei uns eingegangen ist.
Das Team Comunitys und Engagement wird sich um die Bearbeitung deiner Anfrage kümmern
und sich in den nächsten Tagen bei dir melden. Solltest du Rückfragen haben,
wende dich gern an community@wikimedia.de.
{% for label, value in form_data.items %}{{ label }}: {{ value }}
{% endfor %}
Viele Grüße, dein freundliches aber komplett unmenschliches automatisches
Formularbeantwortungssystem.
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.
--
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
from django.core import mail
from django.forms import model_to_dict
from django.test import TestCase
@ -109,7 +108,4 @@ class AdminTestCase(TestCase):
if data[key] is None:
data.pop(key)
with self.captureOnCommitCallbacks(execute=True):
request(self, url, expected_url=expected_url, data=data)
self.assertEqual(len(mail.outbox), 2)
request(self, url, expected_url=expected_url, data=data)

View File

@ -1,11 +1,17 @@
import random
from django.forms import model_to_dict
from django.shortcuts import resolve_url
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.views import TYPES
from input.views import PROJECT_FUNDING, TYPES, ApplicationView
PATHS = {TYPES[path].code: path for path in TYPES}
PATHS[TYPE_PROJ] = PROJECT_FUNDING[0].path
CODES = list(PATHS)
class AnonymousViewTestCase(TestCase):
@ -18,6 +24,15 @@ class AnonymousViewTestCase(TestCase):
def test_extern(self):
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
def get_step_data(cls, choice, **data):
return {
@ -50,15 +65,15 @@ class AnonymousViewTestCase(TestCase):
def test_extern_types(self):
types = [
('BIB', 'Bibliotheksausweis'),
('ELIT', 'Online-Ressource'),
('MAIL', 'Mailadresse beantragen'),
('IFG', 'gewonnenen Informationen'),
('LIT', 'Literatur verwenden'),
('LIST', 'Mailingliste beantragen'),
('TRAV', 'Transportmittel'),
('SOFT', 'Lizenz'),
('VIS', 'DIN 5008'),
(TYPE_BIB, 'Bibliotheksausweis'),
(TYPE_ELIT, 'Online-Ressource'),
(TYPE_MAIL, 'Mailadresse beantragen'),
(TYPE_IFG, 'gewonnenen Informationen'),
(TYPE_LIT, 'Literatur verwenden'),
(TYPE_LIST, 'Mailingliste beantragen'),
(TYPE_TRAV, 'Transportmittel'),
(TYPE_SOFT, 'Lizenz'),
(TYPE_VIS, 'DIN 5008'),
(TYPE_PROJ, 'Projektförderung'),
]
@ -70,7 +85,7 @@ class AnonymousViewTestCase(TestCase):
self.assertContains(response, text)
def test_extern_travel(self):
self.helper_extern('TRAV', 'Transportmittel', {
self.helper_extern(TYPE_TRAV, 'Transportmittel', {
'project_name': 'Test',
'transport': 'BAHN',
'travelcost': 10,
@ -81,27 +96,27 @@ class AnonymousViewTestCase(TestCase):
})
def test_extern_lit(self):
self.helper_extern('LIT', 'Literatur verwenden', {
self.helper_extern(TYPE_LIT, 'Literatur verwenden', {
'cost': 20,
'info': 'Test',
'source': 'Test',
'notes': '',
'selfbuy': 'TRUE',
'selfbuy_data': 'NONE',
'selfbuy': 'FALSE',
'selfbuy_data': 'Test',
'selfbuy_give_data': True,
'check': True,
'terms_accepted': True,
})
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,
'info': 'Test',
'source': 'Test',
'notes': '',
'selfbuy': 'TRUE',
'selfbuy_data': 'NONE',
'selfbuy_give_data': True,
'selfbuy_data': '',
'selfbuy_give_data': False,
'check': False,
})
@ -115,9 +130,32 @@ class AnonymousViewTestCase(TestCase):
'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):
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):

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',
'send_email',
'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):
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
from typing import NamedTuple
import datetime
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.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.http import url_has_allowed_host_and_scheme
from django.utils.safestring import mark_safe
from django.utils.text import get_text_list
from django.core.mail import BadHeaderError
from django.conf import settings
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.utils.html import strip_tags
from input.utils.admin import admin_url_name
from input.utils.mail import build_email, collect_and_attach
from .forms import (
@ -41,6 +52,8 @@ from .models import (
TYPE_SOFT,
TYPE_TRAV,
TYPE_VIS,
Project,
ProductCategoryFormField,
)
HELP_TEXTS = {
@ -67,35 +80,49 @@ HELP_TEXTS = {
},
}
class ApplicationType(NamedTuple):
@dataclass
class ApplicationType:
code: str
path: str
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
def label(self):
return TYPE_CHOICES[self.code]
@property
def help_texts(self):
return HELP_TEXTS.get(self.code)
def url(self):
return f'https://de.wikipedia.org/wiki/Wikipedia:F%C3%B6rderung/{self.link}'
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 = [
ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm),
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm),
ApplicationType(TYPE_MAIL, 'email', EmailForm),
ApplicationType(TYPE_IFG, 'ifg', IFGForm),
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm),
ApplicationType(TYPE_LIST, 'mailingliste', ListForm),
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm),
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm),
ApplicationType(TYPE_VIS, 'visitenkarten', BusinessCardForm),
ApplicationType(TYPE_BIB, 'bibliotheksstipendium', LibraryForm, 'Zugang_zu_Fachliteratur#Bibliotheksstipendium'),
ApplicationType(TYPE_ELIT, 'eliteraturstipendium', ELiteratureForm, 'Zugang_zu_Fachliteratur#eLiteraturstipendium'),
ApplicationType(TYPE_MAIL, 'email', EmailForm, 'E-Mail-Adressen_und_Visitenkarten#E-Mail-Adressen'),
ApplicationType(TYPE_IFG, 'ifg', IFGForm, 'Geb%C3%BChrenerstattungen_f%C3%BCr_Beh%C3%B6rdenanfragen'),
ApplicationType(TYPE_LIT, 'literaturstipendium', LiteratureForm, 'Zugang_zu_Fachliteratur#Literaturstipendium'),
ApplicationType(TYPE_LIST, 'mailingliste', ListForm, 'E-Mail-Adressen_und_Visitenkarten#Mailinglisten'),
ApplicationType(TYPE_TRAV, 'reisekosten', TravelForm, 'Reisekostenerstattungen'),
ApplicationType(TYPE_SOFT, 'softwarestipendium', SoftwareForm, 'Software-Stipendien'),
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}
@ -147,7 +174,14 @@ def index(request):
class ApplicationStartView(TemplateView):
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):
@ -220,14 +254,7 @@ class ApplicationView(FormView):
def prepare_data(self, form):
# Collect cleaned data and mark the current type
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
return {**form.cleaned_data, 'choice': self.type_code}
def save_obj(self, form, data):
# Save model instance
@ -263,19 +290,9 @@ class ApplicationView(FormView):
def send_mail(self, obj, data):
# Prepare minimal mail context and send mails
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)
context = self.get_email_context(obj, data)
applicant_subject = 'Deine Förderanfrage bei Wikimedia Deutschland'
staff_subject = f'Anfrage {type_label_plain} von {applicant_name}'
staff_subject = 'Anfrage {type_label} von {applicant_name}'.format(**context)
try:
self.send_email('applicant', 'ifg_volunteer_mail', applicant_subject, data['email'], context)
@ -287,6 +304,59 @@ class ApplicationView(FormView):
obj.delete()
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):
email = build_email(template_name, context, subject, recipient)