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

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 self.cleaned_data["contribution"] = settings.MEMBERSHIP_PRICES[ 

128 self.cleaned_data["length"] 

129 ] 

130 

131 return self.cleaned_data 

132 

133 

134class BenefactorRegistrationForm(BaseRegistrationForm): 

135 """Form for benefactor registrations.""" 

136 

137 icis_employee = forms.BooleanField( 

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

139 ) 

140 

141 contribution = forms.DecimalField( 

142 required=True, 

143 max_digits=5, 

144 decimal_places=2, 

145 ) 

146 

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 ) 

176 

177 

178class NewYearForm(forms.Form): 

179 privacy_policy = forms.BooleanField( 

180 required=True, 

181 ) 

182 

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 ) 

188 

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 ) 

195 

196 

197class RenewalForm(forms.ModelForm): 

198 """Form for membership renewals.""" 

199 

200 privacy_policy = forms.BooleanField( 

201 required=True, 

202 ) 

203 

204 icis_employee = forms.BooleanField( 

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

206 ) 

207 

208 contribution = forms.DecimalField( 

209 required=False, 

210 max_digits=5, 

211 decimal_places=2, 

212 ) 

213 

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 ) 

225 

226 class Meta: 

227 model = Renewal 

228 fields = ( 

229 "member", 

230 "length", 

231 "contribution", 

232 "membership_type", 

233 "no_references", 

234 "remarks", 

235 ) 

236 

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

249 

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 ] 

272 

273 return self.cleaned_data 

274 

275 

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

282 

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 

288 

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 ) 

298 

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 }