Coverage for website/registrations/forms.py: 100.00%
107 statements
« prev ^ index » next coverage.py v7.6.12, created at 2026-06-21 23:59 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2026-06-21 23:59 +0000
1from datetime import date, timedelta
3from django import forms
4from django.conf import settings
5from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
6from django.forms import HiddenInput, TypedChoiceField
7from django.urls import reverse_lazy
8from django.utils import timezone
9from django.utils.html import format_html
10from django.utils.safestring import mark_safe
11from django.utils.text import capfirst
12from django.utils.translation import gettext_lazy as _
14from members.models import Membership
15from payments.widgets import SignatureWidget
16from utils.snippets import datetime_to_lectureyear
18from .models import Reference, Registration, Renewal
21class BaseRegistrationForm(forms.ModelForm):
22 """Base form for membership registrations.
24 Subclasses must implement setting the right contribution.
25 """
27 birthday = forms.DateField(
28 label=capfirst(_("birthday")),
29 )
31 privacy_policy = forms.BooleanField(
32 required=True,
33 )
35 direct_debit = forms.BooleanField(
36 required=False,
37 label=_("Pay via direct debit"),
38 help_text=_(
39 "This will allow you to sign a Direct Debit mandate, allowing Thalia to withdraw the membership fees from your bank account. Also, you will be able to use this bank account for future payments to Thalia via Thalia Pay."
40 ),
41 )
43 contribution = forms.DecimalField(required=False, widget=HiddenInput())
45 def __init__(self, *args, **kwargs):
46 super().__init__(*args, **kwargs)
47 self.fields["privacy_policy"].label = format_html(
48 'I accept the <a href="{}">privacy policy</a>.',
49 reverse_lazy("singlepages:privacy-policy"),
50 )
51 self.fields["birthday"].widget.input_type = "date"
52 self.fields["length"].help_text = None
54 def clean(self):
55 if self.cleaned_data.get("phone_number") is not None: # pragma: no cover
56 self.cleaned_data["phone_number"] = self.cleaned_data[
57 "phone_number"
58 ].replace(" ", "")
59 super().clean()
62class RegistrationAdminForm(forms.ModelForm):
63 """Custom admin form for Registration model to add the widget for the signature."""
65 class Meta:
66 fields = "__all__"
67 model = Registration
68 widgets = {
69 "signature": SignatureWidget(),
70 }
73class MemberRegistrationForm(BaseRegistrationForm):
74 """Form for member registrations."""
76 this_year = timezone.now().year
77 years = reversed(
78 [(x, f"{x} - {x + 1}") for x in range(this_year - 20, this_year + 1)]
79 )
81 starting_year = TypedChoiceField(
82 choices=years,
83 coerce=int,
84 empty_value=this_year,
85 required=False,
86 help_text=_("What lecture year did you start studying at Radboud University?"),
87 )
89 def __init__(self, *args, **kwargs):
90 super().__init__(*args, **kwargs)
91 self.fields["student_number"].required = True
92 self.fields["programme"].required = True
93 self.fields["starting_year"].required = True
95 class Meta:
96 model = Registration
97 widgets = {"signature": SignatureWidget()}
98 fields = (
99 "length",
100 "first_name",
101 "last_name",
102 "birthday",
103 "email",
104 "phone_number",
105 "student_number",
106 "programme",
107 "starting_year",
108 "address_street",
109 "address_street2",
110 "address_postal_code",
111 "address_city",
112 "address_country",
113 "optin_birthday",
114 "optin_mailinglist",
115 "contribution",
116 "membership_type",
117 "direct_debit",
118 "initials",
119 "iban",
120 "bic",
121 "signature",
122 "optin_thabloid",
123 )
125 def clean(self):
126 super().clean()
127 if "length" not in self.cleaned_data:
128 # This is not valid, but already validated automatically.
129 return self.cleaned_data
131 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
132 self.cleaned_data["length"]
133 ]
135 return self.cleaned_data
138class BenefactorRegistrationForm(BaseRegistrationForm):
139 """Form for benefactor registrations."""
141 icis_employee = forms.BooleanField(
142 required=False, label=_("I am an employee of iCIS")
143 )
145 contribution = forms.DecimalField(
146 required=True,
147 max_digits=5,
148 decimal_places=2,
149 )
151 class Meta:
152 model = Registration
153 widgets = {
154 "signature": SignatureWidget(),
155 }
156 fields = (
157 "length",
158 "first_name",
159 "last_name",
160 "birthday",
161 "email",
162 "phone_number",
163 "student_number",
164 "address_street",
165 "address_street2",
166 "address_postal_code",
167 "address_city",
168 "address_country",
169 "optin_birthday",
170 "optin_mailinglist",
171 "contribution",
172 "membership_type",
173 "direct_debit",
174 "initials",
175 "iban",
176 "bic",
177 "signature",
178 "optin_thabloid",
179 )
182class NewYearForm(forms.Form):
183 privacy_policy = forms.BooleanField(
184 required=True,
185 )
187 extension = forms.BooleanField(
188 required=True,
189 label="I am still a student and I want to extend my "
190 "membership until the end of the next academic year.",
191 )
193 def __init__(self, *args, **kwargs):
194 super().__init__(*args, **kwargs)
195 self.fields["privacy_policy"].label = format_html(
196 'I accept the <a href="{}">privacy policy</a>.',
197 reverse_lazy("singlepages:privacy-policy"),
198 )
201class RenewalForm(forms.ModelForm):
202 """Form for membership renewals."""
204 privacy_policy = forms.BooleanField(
205 required=True,
206 )
208 icis_employee = forms.BooleanField(
209 required=False, label=_("I am an employee of iCIS")
210 )
212 contribution = forms.DecimalField(
213 required=False,
214 max_digits=5,
215 decimal_places=2,
216 )
218 def __init__(self, *args, **kwargs):
219 super().__init__(*args, **kwargs)
220 self.fields["privacy_policy"].label = mark_safe(
221 _('I accept the <a href="{}">privacy policy</a>.').format(
222 reverse_lazy("singlepages:privacy-policy")
223 )
224 )
225 self.fields["length"].help_text = (
226 "A discount of €7,50 will be applied if you upgrade your (active) year membership "
227 "to a membership until graduation. You will only have to pay €22,50 in that case."
228 )
230 class Meta:
231 model = Renewal
232 fields = (
233 "member",
234 "length",
235 "contribution",
236 "membership_type",
237 "no_references",
238 "remarks",
239 )
241 def clean(self):
242 if "length" not in self.cleaned_data:
243 # This is not valid, but already validated automatically.
244 return self.cleaned_data
246 if self.cleaned_data["member"].profile.is_minimized:
247 raise ValidationError(
248 "It's not possible to renew a membership using an incomplete profile."
249 )
250 if (
251 self.cleaned_data["member"].latest_membership
252 and self.cleaned_data["member"].latest_membership.study_long
253 and self.cleaned_data["membership_type"] != Membership.BENEFACTOR
254 ):
255 raise ValidationError("It's not possible to renew a study long membership.")
257 if self.cleaned_data["length"] == Renewal.MEMBERSHIP_STUDY:
258 now = timezone.now()
259 if Membership.objects.filter(
260 user=self.cleaned_data["member"],
261 type=Membership.MEMBER,
262 until__gte=now - timedelta(days=366),
263 since__lte=now,
264 ).exists():
265 # The membership upgrade discount applies if, at the time a Renewal is
266 # created, the user has an active 'member' type membership for a year.
267 self.cleaned_data["contribution"] = (
268 settings.MEMBERSHIP_PRICES[Renewal.MEMBERSHIP_STUDY]
269 - settings.MEMBERSHIP_PRICES[Renewal.MEMBERSHIP_YEAR]
270 )
271 else:
272 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
273 Renewal.MEMBERSHIP_STUDY
274 ]
275 elif self.cleaned_data["membership_type"] == Membership.MEMBER:
276 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
277 self.cleaned_data["length"]
278 ]
280 return self.cleaned_data
283class ReferenceForm(forms.ModelForm):
284 def clean(self):
285 super().clean()
286 membership = self.cleaned_data["member"].current_membership
287 if membership and membership.type == Membership.BENEFACTOR:
288 raise ValidationError(_("Benefactors cannot give references."))
290 membership = self.cleaned_data["member"].latest_membership
291 today = timezone.now().date()
292 lecture_year = datetime_to_lectureyear(today)
293 if today.month == 8:
294 lecture_year += 1
296 if (
297 membership
298 and membership.until
299 and membership.until <= date(lecture_year, 9, 1)
300 ):
301 raise ValidationError(
302 "It's not possible to give references for memberships "
303 "that start after your own membership's end."
304 )
306 class Meta:
307 model = Reference
308 fields = "__all__"
309 error_messages = {
310 NON_FIELD_ERRORS: {
311 "unique_together": _(
312 "You've already given a reference for this person."
313 ),
314 }
315 }