diff --git a/foerderbarometer/settings.py b/foerderbarometer/settings.py
index 7261d32..5fe2c46 100644
--- a/foerderbarometer/settings.py
+++ b/foerderbarometer/settings.py
@@ -119,7 +119,7 @@ AUTH_PASSWORD_VALIDATORS = password_validators(
USE_I18N = True
USE_L10N = True
-LANGUAGE_CODE = env('LANGUAGE_CODE', 'en-us')
+LANGUAGE_CODE = env('LANGUAGE_CODE', 'de')
USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'UTC')
diff --git a/input/admin.py b/input/admin.py
index 7da1e6f..6c8b0a8 100755
--- a/input/admin.py
+++ b/input/admin.py
@@ -57,7 +57,7 @@ class ProjectAdmin(admin.ModelAdmin):
class BusinessCardAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
- list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project')
+ list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
list_display_links = ('realname', 'service_id')
# action = ['export_as_csv']
date_hierarchy = 'granted_date'
@@ -69,7 +69,7 @@ class BusinessCardAdmin(admin.ModelAdmin):
class LiteratureAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
- list_display = ('realname', 'service_id', 'granted', 'granted_date')
+ list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
readonly_fields = ['service_id']
@@ -142,7 +142,7 @@ class TravelAdmin(admin.ModelAdmin):
class EmailAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
- list_display = ('realname', 'service_id', 'granted', 'granted_date')
+ list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL}
@@ -155,7 +155,7 @@ class EmailAdmin(admin.ModelAdmin):
class ListAdmin(admin.ModelAdmin):
save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date')
- list_display = ('realname', 'service_id', 'granted', 'granted_date')
+ list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date'
readonly_fields = ['service_id']
diff --git a/input/forms.py b/input/forms.py
index 32d40d6..bdc71ee 100755
--- a/input/forms.py
+++ b/input/forms.py
@@ -156,22 +156,16 @@ class CheckForm(FdbForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.fields['check'] = BooleanField(
- required=True,
- label=format_html(
+
+ # Check if the model field 'terms_accepted' is present
+ if 'terms_accepted' in self.fields:
+ # Make the field required (HTML5 validation)
+ self.fields['terms_accepted'].required = True
+ # Set custom label with link to terms
+ self.fields['terms_accepted'].label = format_html(
"Ich stimme den Nutzungsbedingungen zu",
self.termstoaccept
)
- )
-
-
-
- """Baseclass for all classes which need a check for Nutzungsbedingungen"""
-# def __init__(self, *args, **kwargs):
-# check = BooleanField(required=True,
-# label=format_html("Ich stimme den Nutzungsbedingungen zu",
-# termstoaccept))
-# NUTZUNGSBEDINGUNGEN))
class LiteratureForm(CheckForm):
@@ -182,7 +176,7 @@ class LiteratureForm(CheckForm):
self.fields['selfbuy_give_data'].required = True
class Meta:
model = Literature
- fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data']
+ fields = ['cost', 'info', 'source', 'notes', 'selfbuy', 'selfbuy_data', 'selfbuy_give_data', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media:
js = ('dropdown/js/literature.js',)
@@ -209,7 +203,7 @@ class EmailForm(CheckForm):
# TODO: add some javascript to show/hide other-field
class Meta:
model = Email
- fields = ['domain', 'address', 'other', 'adult']
+ fields = ['domain', 'address', 'other', 'adult', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media:
js = ('dropdown/js/mail.js',)
@@ -227,7 +221,7 @@ class BusinessCardForm(CheckForm):
class Meta:
model = BusinessCard
exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
- fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to']
+ fields = ['project', 'data', 'variant', 'url_of_pic', 'send_data_to_print', 'sent_to', 'terms_accepted']
class Media:
js = ('dropdown/js/businessCard.js',)
@@ -236,5 +230,5 @@ class ListForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta:
model = List
- fields = ['domain', 'address']
+ fields = ['domain', 'address', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send','mail_state']
diff --git a/input/migrations/0099_businesscard_terms_accepted_email_terms_accepted_and_more.py b/input/migrations/0099_businesscard_terms_accepted_email_terms_accepted_and_more.py
new file mode 100644
index 0000000..180ada9
--- /dev/null
+++ b/input/migrations/0099_businesscard_terms_accepted_email_terms_accepted_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 5.2.5 on 2025-08-26 11:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('input', '0098_add_eliterature_and_software_proxies'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='businesscard',
+ name='terms_accepted',
+ field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
+ ),
+ migrations.AddField(
+ model_name='email',
+ name='terms_accepted',
+ field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
+ ),
+ migrations.AddField(
+ model_name='list',
+ name='terms_accepted',
+ field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
+ ),
+ migrations.AddField(
+ model_name='literature',
+ name='terms_accepted',
+ field=models.BooleanField(default=False, verbose_name='Nutzungsbedingungen zugestimmt'),
+ ),
+ ]
diff --git a/input/models.py b/input/models.py
index c0a8949..3c8b91c 100755
--- a/input/models.py
+++ b/input/models.py
@@ -5,9 +5,19 @@ from django.utils.html import format_html
from django.utils.safestring import mark_safe
EMAIL_STATES = {'NONE': 'noch keine Mail versendet',
- 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
- 'CLOSE': 'die Projektabschlussmail wurde versendet',
- 'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'}
+ 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
+ 'CLOSE': 'die Projektabschlussmail wurde versendet',
+ 'END': 'alle automatischen Mails, auch surveyMail, wurden versendet'}
+
+
+class TermsConsentMixin(models.Model):
+ """Abstract mixin to add a terms_accepted field for documenting user consent."""
+
+ terms_accepted = models.BooleanField(default=False, verbose_name="Nutzungsbedingungen zugestimmt")
+
+ class Meta:
+ abstract = True
+
class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name="Realname",
@@ -323,7 +333,7 @@ SELFBUY_CHOICES = {'TRUE': mark_safe('Ich möchte das Werk selbst kaufen und per
}
-class Literature(Grant):
+class Literature(TermsConsentMixin, Grant):
info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=mark_safe("Bitte gib alle Informationen zum benötigten Werk an,
\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)"))
@@ -367,7 +377,7 @@ ADULT_CHOICES = {'TRUE': mark_safe('Ich bin volljährig.'),
'FALSE': mark_safe('Ich bin noch nicht volljährig.')
}
-class Email(Domain):
+class Email(TermsConsentMixin, Domain):
address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil',
@@ -377,7 +387,7 @@ class Email(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 List(Domain):
+class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name="Adressbestandteil für Projektmailingliste",
help_text=mark_safe("Bitte gib hier den gewünschten Adressbestandteil an,
der sich vor der Domain befinden soll."))
@@ -396,7 +406,7 @@ PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
BC_VARIANT = {'PIC': 'mit Bild',
'NOPIC': 'ohne Bild'}
-class BusinessCard(Extern):
+class BusinessCard(TermsConsentMixin, Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')
diff --git a/input/tests/views.py b/input/tests/views.py
index 53101d5..4cbd3a5 100644
--- a/input/tests/views.py
+++ b/input/tests/views.py
@@ -9,7 +9,7 @@ class AnonymousViewTestCase(TestCase):
def test_index(self):
response = request(self, 'index')
- self.assertContains(response,'Sourcecode')
+ self.assertContains(response, 'Sourcecode')
def test_extern(self):
request(self, 'extern')
@@ -131,3 +131,32 @@ class AuthenticatedViewTestCase(TestCase):
def test_deny_error(self):
self.helper_auth_deny_error('deny')
+
+
+class TermsConsentViewTests(AnonymousViewTestCase):
+
+ def test_extern_lit(self):
+ """
+ Negative case: Literature form without terms_accepted must not succeed
+ and should display a required field error.
+ """
+ # Step 1
+ resp = request(self, 'extern', data=self.get_first_step_data('LIT'))
+ self.assertContains(resp, 'Literatur verwenden')
+
+ # Step 2: submit without terms_accepted
+ payload = self.get_step_data(1, {
+ 'cost': 20,
+ 'info': 'Test',
+ 'source': 'Test',
+ 'notes': '',
+ 'selfbuy': 'TRUE',
+ 'selfbuy_data': 'NONE',
+ 'selfbuy_give_data': 'on', # checkbox checked
+ # terms_accepted intentionally omitted
+ })
+ resp = request(self, 'extern', data=payload)
+
+ # Expect to remain on step 2 with required field error
+ self.assertNotContains(resp, 'Deine Anfrage wurde gesendet.')
+ self.assertContains(resp, 'Dieses Feld ist zwingend erforderlich.')
\ No newline at end of file