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

1from datetime import date, timedelta 

2 

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 _ 

13 

14from members.models import Membership 

15from payments.widgets import SignatureWidget 

16from utils.snippets import datetime_to_lectureyear 

17 

18from .models import Reference, Registration, Renewal 

19 

20 

21class BaseRegistrationForm(forms.ModelForm): 

22 """Base form for membership registrations. 

23 

24 Subclasses must implement setting the right contribution. 

25 """ 

26 

27 birthday = forms.DateField( 

28 label=capfirst(_("birthday")), 

29 ) 

30 

31 privacy_policy = forms.BooleanField( 

32 required=True, 

33 ) 

34 

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 ) 

42 

43 contribution = forms.DecimalField(required=False, widget=HiddenInput()) 

44 

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 

53 

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() 

60 

61 

62class RegistrationAdminForm(forms.ModelForm): 

63 """Custom admin form for Registration model to add the widget for the signature.""" 

64 

65 class Meta: 

66 fields = "__all__" 

67 model = Registration 

68 widgets = { 

69 "signature": SignatureWidget(), 

70 } 

71 

72 

73class MemberRegistrationForm(BaseRegistrationForm): 

74 """Form for member registrations.""" 

75 

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 ) 

80 

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 ) 

88 

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 

94 

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 ) 

124 

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 

130 

131 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[ 

132 self.cleaned_data["length"] 

133 ] 

134 

135 return self.cleaned_data 

136 

137 

138class BenefactorRegistrationForm(BaseRegistrationForm): 

139 """Form for benefactor registrations.""" 

140 

141 icis_employee = forms.BooleanField( 

142 required=False, label=_("I am an employee of iCIS") 

143 ) 

144 

145 contribution = forms.DecimalField( 

146 required=True, 

147 max_digits=5, 

148 decimal_places=2, 

149 ) 

150 

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 ) 

180 

181 

182class NewYearForm(forms.Form): 

183 privacy_policy = forms.BooleanField( 

184 required=True, 

185 ) 

186 

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 ) 

192 

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 ) 

199 

200 

201class RenewalForm(forms.ModelForm): 

202 """Form for membership renewals.""" 

203 

204 privacy_policy = forms.BooleanField( 

205 required=True, 

206 ) 

207 

208 icis_employee = forms.BooleanField( 

209 required=False, label=_("I am an employee of iCIS") 

210 ) 

211 

212 contribution = forms.DecimalField( 

213 required=False, 

214 max_digits=5, 

215 decimal_places=2, 

216 ) 

217 

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 ) 

229 

230 class Meta: 

231 model = Renewal 

232 fields = ( 

233 "member", 

234 "length", 

235 "contribution", 

236 "membership_type", 

237 "no_references", 

238 "remarks", 

239 ) 

240 

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 

245 

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.") 

256 

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 ] 

279 

280 return self.cleaned_data 

281 

282 

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.")) 

289 

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 

295 

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 ) 

305 

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 }