Merge branch 'feature/terms-consent' into 'cosmocode'

Store user agreement to terms

See merge request wikimedia/foerderbarometer!4
This commit is contained in:
Oliver Zander 2025-09-29 16:31:06 +02:00
commit aa5c981872
6 changed files with 96 additions and 30 deletions

View File

@ -119,7 +119,7 @@ AUTH_PASSWORD_VALIDATORS = password_validators(
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
LANGUAGE_CODE = env('LANGUAGE_CODE', 'en-us') LANGUAGE_CODE = env('LANGUAGE_CODE', 'de')
USE_TZ = True USE_TZ = True
TIME_ZONE = env('TIME_ZONE', 'UTC') TIME_ZONE = env('TIME_ZONE', 'UTC')

View File

@ -57,7 +57,7 @@ class ProjectAdmin(admin.ModelAdmin):
class BusinessCardAdmin(admin.ModelAdmin): class BusinessCardAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project') search_fields = ('realname', 'service_id', 'granted', 'granted_date', 'project')
list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'project', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
# action = ['export_as_csv'] # action = ['export_as_csv']
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
@ -69,7 +69,7 @@ class BusinessCardAdmin(admin.ModelAdmin):
class LiteratureAdmin(admin.ModelAdmin): class LiteratureAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']
@ -142,7 +142,7 @@ class TravelAdmin(admin.ModelAdmin):
class EmailAdmin(admin.ModelAdmin): class EmailAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
radio_fields = {'adult': admin.VERTICAL} radio_fields = {'adult': admin.VERTICAL}
@ -155,7 +155,7 @@ class EmailAdmin(admin.ModelAdmin):
class ListAdmin(admin.ModelAdmin): class ListAdmin(admin.ModelAdmin):
save_as = True save_as = True
search_fields = ('realname', 'service_id', 'granted', 'granted_date') search_fields = ('realname', 'service_id', 'granted', 'granted_date')
list_display = ('realname', 'service_id', 'granted', 'granted_date') list_display = ('realname', 'service_id', 'granted', 'granted_date', 'terms_accepted')
list_display_links = ('realname', 'service_id') list_display_links = ('realname', 'service_id')
date_hierarchy = 'granted_date' date_hierarchy = 'granted_date'
readonly_fields = ['service_id'] readonly_fields = ['service_id']

View File

@ -156,22 +156,16 @@ class CheckForm(FdbForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['check'] = BooleanField(
required=True, # Check if the model field 'terms_accepted' is present
label=format_html( 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 <a href='{}'>Nutzungsbedingungen</a> zu", "Ich stimme den <a href='{}'>Nutzungsbedingungen</a> zu",
self.termstoaccept 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 <a href='{}'>Nutzungsbedingungen</a> zu",
# termstoaccept))
# NUTZUNGSBEDINGUNGEN))
class LiteratureForm(CheckForm): class LiteratureForm(CheckForm):
@ -182,7 +176,7 @@ class LiteratureForm(CheckForm):
self.fields['selfbuy_give_data'].required = True self.fields['selfbuy_give_data'].required = True
class Meta: class Meta:
model = Literature 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'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media: class Media:
js = ('dropdown/js/literature.js',) js = ('dropdown/js/literature.js',)
@ -209,7 +203,7 @@ class EmailForm(CheckForm):
# TODO: add some javascript to show/hide other-field # TODO: add some javascript to show/hide other-field
class Meta: class Meta:
model = Email model = Email
fields = ['domain', 'address', 'other', 'adult'] fields = ['domain', 'address', 'other', 'adult', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] exclude = ['intern_notes', 'survey_mail_send', 'mail_state']
class Media: class Media:
js = ('dropdown/js/mail.js',) js = ('dropdown/js/mail.js',)
@ -227,7 +221,7 @@ class BusinessCardForm(CheckForm):
class Meta: class Meta:
model = BusinessCard model = BusinessCard
exclude = ['intern_notes', 'survey_mail_send', 'mail_state'] 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: class Media:
js = ('dropdown/js/businessCard.js',) js = ('dropdown/js/businessCard.js',)
@ -236,5 +230,5 @@ class ListForm(CheckForm):
termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN termstoaccept = settings.NUTZUNGSBEDINGUNGEN_MAILINGLISTEN
class Meta: class Meta:
model = List model = List
fields = ['domain', 'address'] fields = ['domain', 'address', 'terms_accepted']
exclude = ['intern_notes', 'survey_mail_send','mail_state'] exclude = ['intern_notes', 'survey_mail_send','mail_state']

View File

@ -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'),
),
]

View File

@ -5,9 +5,19 @@ from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
EMAIL_STATES = {'NONE': 'noch keine Mail versendet', EMAIL_STATES = {'NONE': 'noch keine Mail versendet',
'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet', 'INF': 'die Benachrichtigung zur Projektabschlussmail wurde versendet',
'CLOSE': 'die Projektabschlussmail wurde versendet', 'CLOSE': 'die Projektabschlussmail wurde versendet',
'END': 'alle automatischen Mails, auch surveyMail, wurden 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): class Volunteer(models.Model):
realname = models.CharField(max_length=200, null=True, verbose_name="Realname", 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', info = models.CharField(max_length=500, verbose_name='Informationen zum Werk',
help_text=mark_safe("Bitte gib alle Informationen zum benötigten Werk an,<br>\ help_text=mark_safe("Bitte gib alle Informationen zum benötigten Werk an,<br>\
die eine eindeutige Identifizierung ermöglichen (Autor, Titel, Verlag, ISBN, ...)")) 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.') 'FALSE': mark_safe('Ich bin noch nicht volljährig.')
} }
class Email(Domain): class Email(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, address = models.CharField(max_length=50,
choices=MAIL_CHOICES.items(), choices=MAIL_CHOICES.items(),
default='USERNAME', verbose_name='Adressbestandteil', 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') 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 List(Domain): class List(TermsConsentMixin, Domain):
address = models.CharField(max_length=50, default='NO_ADDRESS', address = models.CharField(max_length=50, default='NO_ADDRESS',
verbose_name="Adressbestandteil für Projektmailingliste", verbose_name="Adressbestandteil für Projektmailingliste",
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."))
@ -396,7 +406,7 @@ PROJECT_CHOICE = {'PEDIA': 'Wikipedia',
BC_VARIANT = {'PIC': 'mit Bild', BC_VARIANT = {'PIC': 'mit Bild',
'NOPIC': 'ohne Bild'} 'NOPIC': 'ohne Bild'}
class BusinessCard(Extern): class BusinessCard(TermsConsentMixin, Extern):
project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(), project = models.CharField(max_length=20, choices=PROJECT_CHOICE.items(),
default='PEDIA', verbose_name='Wikimedia-Projekt', default='PEDIA', verbose_name='Wikimedia-Projekt',
help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?') help_text='Für welches Wikimedia-Projekt möchtest Du Visitenkarten?')

View File

@ -9,7 +9,7 @@ class AnonymousViewTestCase(TestCase):
def test_index(self): def test_index(self):
response = request(self, 'index') response = request(self, 'index')
self.assertContains(response,'<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a>') self.assertContains(response, '<a href="https://srcsrv.wikimedia.de/beba/foerderbarometer">Sourcecode</a>')
def test_extern(self): def test_extern(self):
request(self, 'extern') request(self, 'extern')
@ -131,3 +131,32 @@ class AuthenticatedViewTestCase(TestCase):
def test_deny_error(self): def test_deny_error(self):
self.helper_auth_deny_error('deny') 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.')