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