Coverage for website/registrations/forms.py: 100.00%
104 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +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 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
128 self.cleaned_data["length"]
129 ]
131 return self.cleaned_data
134class BenefactorRegistrationForm(BaseRegistrationForm):
135 """Form for benefactor registrations."""
137 icis_employee = forms.BooleanField(
138 required=False, label=_("I am an employee of iCIS")
139 )
141 contribution = forms.DecimalField(
142 required=True,
143 max_digits=5,
144 decimal_places=2,
145 )
147 class Meta:
148 model = Registration
149 widgets = {
150 "signature": SignatureWidget(),
151 }
152 fields = (
153 "length",
154 "first_name",
155 "last_name",
156 "birthday",
157 "email",
158 "phone_number",
159 "student_number",
160 "address_street",
161 "address_street2",
162 "address_postal_code",
163 "address_city",
164 "address_country",
165 "optin_birthday",
166 "optin_mailinglist",
167 "contribution",
168 "membership_type",
169 "direct_debit",
170 "initials",
171 "iban",
172 "bic",
173 "signature",
174 "optin_thabloid",
175 )
178class NewYearForm(forms.Form):
179 privacy_policy = forms.BooleanField(
180 required=True,
181 )
183 extension = forms.BooleanField(
184 required=True,
185 label="I am still a student and I want to extend my "
186 "membership until the end of the next academic year.",
187 )
189 def __init__(self, *args, **kwargs):
190 super().__init__(*args, **kwargs)
191 self.fields["privacy_policy"].label = format_html(
192 'I accept the <a href="{}">privacy policy</a>.',
193 reverse_lazy("singlepages:privacy-policy"),
194 )
197class RenewalForm(forms.ModelForm):
198 """Form for membership renewals."""
200 privacy_policy = forms.BooleanField(
201 required=True,
202 )
204 icis_employee = forms.BooleanField(
205 required=False, label=_("I am an employee of iCIS")
206 )
208 contribution = forms.DecimalField(
209 required=False,
210 max_digits=5,
211 decimal_places=2,
212 )
214 def __init__(self, *args, **kwargs):
215 super().__init__(*args, **kwargs)
216 self.fields["privacy_policy"].label = mark_safe(
217 _('I accept the <a href="{}">privacy policy</a>.').format(
218 reverse_lazy("singlepages:privacy-policy")
219 )
220 )
221 self.fields["length"].help_text = (
222 "A discount of €7,50 will be applied if you upgrade your (active) year membership "
223 "to a membership until graduation. You will only have to pay €22,50 in that case."
224 )
226 class Meta:
227 model = Renewal
228 fields = (
229 "member",
230 "length",
231 "contribution",
232 "membership_type",
233 "no_references",
234 "remarks",
235 )
237 def clean(self):
238 super().clean()
239 if self.cleaned_data["member"].profile.is_minimized:
240 raise ValidationError(
241 "It's not possible to renew a membership using an incomplete profile."
242 )
243 if (
244 self.cleaned_data["member"].latest_membership
245 and self.cleaned_data["member"].latest_membership.study_long
246 and self.cleaned_data["membership_type"] != Membership.BENEFACTOR
247 ):
248 raise ValidationError("It's not possible to renew a study long membership.")
250 if self.cleaned_data["length"] == Renewal.MEMBERSHIP_STUDY:
251 now = timezone.now()
252 if Membership.objects.filter(
253 user=self.cleaned_data["member"],
254 type=Membership.MEMBER,
255 until__gte=now - timedelta(days=366),
256 since__lte=now,
257 ).exists():
258 # The membership upgrade discount applies if, at the time a Renewal is
259 # created, the user has an active 'member' type membership for a year.
260 self.cleaned_data["contribution"] = (
261 settings.MEMBERSHIP_PRICES[Renewal.MEMBERSHIP_STUDY]
262 - settings.MEMBERSHIP_PRICES[Renewal.MEMBERSHIP_YEAR]
263 )
264 else:
265 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
266 Renewal.MEMBERSHIP_STUDY
267 ]
268 elif self.cleaned_data["membership_type"] == Membership.MEMBER:
269 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[
270 self.cleaned_data["length"]
271 ]
273 return self.cleaned_data
276class ReferenceForm(forms.ModelForm):
277 def clean(self):
278 super().clean()
279 membership = self.cleaned_data["member"].current_membership
280 if membership and membership.type == Membership.BENEFACTOR:
281 raise ValidationError(_("Benefactors cannot give references."))
283 membership = self.cleaned_data["member"].latest_membership
284 today = timezone.now().date()
285 lecture_year = datetime_to_lectureyear(today)
286 if today.month == 8:
287 lecture_year += 1
289 if (
290 membership
291 and membership.until
292 and membership.until <= date(lecture_year, 9, 1)
293 ):
294 raise ValidationError(
295 "It's not possible to give references for memberships "
296 "that start after your own membership's end."
297 )
299 class Meta:
300 model = Reference
301 fields = "__all__"
302 error_messages = {
303 NON_FIELD_ERRORS: {
304 "unique_together": _(
305 "You've already given a reference for this person."
306 ),
307 }
308 }