Coverage for website/events/services.py: 73.43%

215 statements  

« prev     ^ index     » next       coverage.py v7.6.7, created at 2025-08-14 10:31 +0000

1from collections import OrderedDict 

2from datetime import date, timedelta 

3 

4from django.db.models import Q 

5from django.utils import timezone 

6from django.utils.formats import localize 

7from django.utils.translation import gettext_lazy as _ 

8 

9from events import emails, signals 

10from events.exceptions import RegistrationError 

11from events.models import ( 

12 Event, 

13 EventRegistration, 

14 RegistrationInformationField, 

15 categories, 

16 status, 

17) 

18from utils.snippets import datetime_to_lectureyear 

19 

20 

21def is_user_registered(member, event): 

22 """Return if the user is registered for the specified event. 

23 

24 :param member: the user 

25 :param event: the event 

26 :return: None if registration is not required or no member else True/False 

27 """ 

28 if not member.is_authenticated: 

29 return None 

30 

31 return event.registrations.filter(member=member, date_cancelled=None).exists() 

32 

33 

34def cancel_status(event: Event, registration: EventRegistration): 

35 if event.after_cancel_deadline: 

36 # Deadline passed 

37 if registration and registration.queue_position: 

38 return status.CANCEL_WAITINGLIST 

39 return status.CANCEL_LATE 

40 

41 if not event.registration_allowed and not event.optional_registrations: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true

42 return status.CANCEL_FINAL 

43 return status.CANCEL_NORMAL 

44 

45 

46def cancel_info_string(event: Event, cancel_status, reg_status): 

47 if reg_status not in [ 

48 status.STATUS_OPEN, 

49 status.STATUS_WAITINGLIST, 

50 status.STATUS_REGISTERED, 

51 ]: 

52 return "" 

53 infos = { 

54 status.CANCEL_NORMAL: _(""), 

55 status.CANCEL_FINAL: _( 

56 "Note: if you cancel, you will not be able to re-register." 

57 ), 

58 status.CANCEL_LATE: _( 

59 "Cancellation is not allowed anymore without having to pay the full costs of €{fine}. You will also not be able to re-register." 

60 ), 

61 status.CANCEL_WAITINGLIST: _( 

62 "Cancellation while on the waiting list will not result in a fine. However, you will not be able to re-register." 

63 ), 

64 } 

65 # No str.format(), see https://github.com/svthalia/concrexit/security/advisories/GHSA-vf8w-xr57-hw87. 

66 return infos[cancel_status].replace("{fine}", str(event.fine)) 

67 

68 

69def registration_status(event: Event, registration: EventRegistration, member): 

70 if not event.registration_required and not event.optional_registration_allowed: 

71 return status.STATUS_NONE 

72 

73 if not member or not member.is_authenticated: 73 ↛ 74line 73 didn't jump to line 74 because the condition on line 73 was never true

74 if event.optional_registration_allowed: 

75 return status.STATUS_OPTIONAL 

76 return status.STATUS_LOGIN 

77 

78 if registration: 

79 if registration.date_cancelled: 

80 if event.optional_registration_allowed: 80 ↛ 82line 80 didn't jump to line 82 because the condition on line 80 was never true

81 # Optional registrations are not meaningfully cancelled 

82 return status.STATUS_OPTIONAL 

83 if registration.is_late_cancellation(): 

84 return status.STATUS_CANCELLED_LATE 

85 if event.registration_allowed: 85 ↛ 87line 85 didn't jump to line 87 because the condition on line 85 was always true

86 return status.STATUS_CANCELLED 

87 return status.STATUS_CANCELLED_FINAL 

88 

89 if registration.queue_position: 

90 return status.STATUS_WAITINGLIST 

91 if event.optional_registration_allowed: 

92 return status.STATUS_OPTIONAL_REGISTERED 

93 

94 return status.STATUS_REGISTERED 

95 if event.optional_registration_allowed: 

96 return status.STATUS_OPTIONAL 

97 

98 if event.reached_participants_limit(): 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 return status.STATUS_FULL 

100 if event.registration_allowed: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true

101 return status.STATUS_OPEN 

102 

103 if not event.registration_started: 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true

104 return status.STATUS_WILL_OPEN 

105 if not event.registration_allowed: 105 ↛ 108line 105 didn't jump to line 108 because the condition on line 105 was always true

106 return status.STATUS_EXPIRED 

107 

108 raise ValueError("invalid/unexpected registration status") 

109 

110 

111def show_cancel_status(registration_status): 

112 return registration_status not in [ 

113 status.STATUS_CANCELLED, 

114 status.STATUS_CANCELLED_LATE, 

115 status.STATUS_LOGIN, 

116 ] 

117 

118 

119def registration_status_string(status, event: Event, registration: EventRegistration): 

120 if not status: 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true

121 return None 

122 

123 status_msg = getattr( 

124 event, 

125 event.STATUS_MESSAGE_FIELDS.get(status) or "", 

126 event.DEFAULT_STATUS_MESSAGE[status], 

127 ) 

128 if not status_msg: 128 ↛ 132line 128 didn't jump to line 132 because the condition on line 128 was always true

129 status_msg = event.DEFAULT_STATUS_MESSAGE[status] 

130 

131 # registration = event.registrations.get(member=member, date_cancelled=None) 

132 if registration: 

133 queue_pos = registration.queue_position 

134 else: 

135 queue_pos = None 

136 

137 # Replace placeholders in the status message, but not using str.format(), 

138 # which is vulnerable to injection attacks that could leak secrets. 

139 # See https://github.com/svthalia/concrexit/security/advisories/GHSA-vf8w-xr57-hw87. 

140 return ( 

141 status_msg.replace("{fine}", str(event.fine)) 

142 .replace("{pos}", str(queue_pos)) 

143 .replace("{regstart}", localize(timezone.localtime(event.registration_start))) 

144 ) 

145 

146 

147def user_registration_pending(member, event): 

148 """Return if the user is in the queue, but not yet registered for, the specific event. 

149 

150 :param member: the user 

151 :param event: the event 

152 :return: None if not authenticated, else False or the queue position 

153 """ 

154 if not event.registration_required: 

155 return False 

156 if not member.is_authenticated: 

157 return None 

158 if event.max_participants is None: 

159 return False 

160 

161 try: 

162 registration = event.registrations.get(member=member, date_cancelled=None) 

163 if registration.queue_position: 

164 return registration.queue_position 

165 return False 

166 except EventRegistration.DoesNotExist: 

167 return False 

168 

169 

170def event_permissions(member, event: Event, name=None, registration_prefetch=False): 

171 """Return a dictionary with the available event permissions of the user. 

172 

173 :param member: the user 

174 :param event: the event 

175 :param name: the name of a non member registration 

176 :param registration_prefetch: if the registrations for the member are already prefetched into an attribute "member_registration" 

177 :return: the permission dictionary 

178 """ 

179 perms = { 

180 "create_registration": False, 

181 "create_registration_when_open": False, 

182 "cancel_registration": False, 

183 "update_registration": False, 

184 "manage_event": is_organiser(member, event), 

185 } 

186 if not member: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true

187 return perms 

188 if not (member.is_authenticated or name): 

189 return perms 

190 

191 registration = None 

192 if registration_prefetch: 

193 if len(event.member_registration) > 0: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true

194 registration = event.member_registration[-1] 

195 else: 

196 try: 

197 registration = EventRegistration.objects.get( 

198 event=event, member=member, name=name 

199 ) 

200 except EventRegistration.DoesNotExist: 

201 pass 

202 

203 now = timezone.now() 

204 

205 perms["create_registration"] = ( 

206 (registration is None or registration.date_cancelled is not None) 

207 and ( 

208 event.registration_allowed 

209 or (event.optional_registration_allowed and not event.registration_required) 

210 ) 

211 and ( 

212 name 

213 or member.can_attend_events 

214 or ( 

215 event.registration_without_membership 

216 and member.can_attend_events_without_membership 

217 ) 

218 ) 

219 ) 

220 perms["create_registration_when_open"] = ( 

221 (registration is None or registration.date_cancelled is not None) 

222 and ( 

223 event.registration_start is not None 

224 and ( 

225 now < event.registration_start 

226 and now > event.registration_start - timedelta(hours=2) 

227 ) 

228 ) 

229 and ( 

230 name 

231 or member.can_attend_events 

232 or ( 

233 event.registration_without_membership 

234 and member.can_attend_events_without_membership 

235 ) 

236 ) 

237 ) 

238 perms["cancel_registration"] = ( 

239 registration is not None 

240 and registration.date_cancelled is None 

241 and ( 

242 event.cancellation_allowed 

243 or name 

244 or (event.optional_registration_allowed and not event.registration_required) 

245 ) 

246 and registration.payment is None 

247 ) 

248 perms["update_registration"] = ( 

249 registration is not None 

250 and registration.date_cancelled is None 

251 and event.has_fields 

252 and ( 

253 event.registration_allowed 

254 or (event.optional_registration_allowed and not event.registration_required) 

255 or (event.update_deadline is not None and event.update_deadline >= now) 

256 ) 

257 and ( 

258 name 

259 or member.can_attend_events 

260 or ( 

261 event.registration_without_membership 

262 and member.can_attend_events_without_membership 

263 ) 

264 ) 

265 ) 

266 return perms 

267 

268 

269def is_organiser(member, event): 

270 if member and member.is_authenticated: 

271 if member.is_superuser or member.has_perm("events.override_organiser"): 

272 return True 

273 

274 if event: 

275 return ( 

276 member.get_member_groups() 

277 .filter(pk__in=event.organisers.values_list("pk")) 

278 .exists() 

279 ) 

280 

281 return False 

282 

283 

284def create_registration(member, event): 

285 """Create a new user registration for an event. 

286 

287 :param member: the user 

288 :param event: the event 

289 :return: Return the registration if successful 

290 """ 

291 if event_permissions(member, event)["create_registration"]: 

292 registration = None 

293 try: 

294 registration = EventRegistration.objects.get(event=event, member=member) 

295 except EventRegistration.DoesNotExist: 

296 pass 

297 

298 if registration is None: 

299 return EventRegistration.objects.create(event=event, member=member) 

300 if registration.date_cancelled is not None: 

301 if registration.is_late_cancellation(): 

302 raise RegistrationError( 

303 _( 

304 "You cannot re-register anymore " 

305 "since you've cancelled after the " 

306 "deadline." 

307 ) 

308 ) 

309 registration.date = timezone.now() 

310 registration.date_cancelled = None 

311 registration.save() 

312 

313 return registration 

314 if event_permissions(member, event)["cancel_registration"]: 

315 raise RegistrationError(_("You were already registered.")) 

316 raise RegistrationError(_("You may not register.")) 

317 

318 

319def cancel_registration(member, event): 

320 """Cancel a user registration for an event. 

321 

322 :param member: the user 

323 :param event: the event 

324 """ 

325 registration = None 

326 try: 

327 registration = EventRegistration.objects.get(event=event, member=member) 

328 except EventRegistration.DoesNotExist: 

329 pass 

330 

331 if event_permissions(member, event)["cancel_registration"] and registration: 

332 if not registration.queue_position: 332 ↛ 353line 332 didn't jump to line 353 because the condition on line 332 was always true

333 if ( 

334 event.max_participants is not None 

335 and event.eventregistration_set.filter(date_cancelled=None).count() 

336 > event.max_participants 

337 ): 

338 first_waiting: EventRegistration = event.eventregistration_set.filter( 

339 date_cancelled=None 

340 ).order_by("date")[event.max_participants] 

341 emails.notify_first_waiting(event, first_waiting) 

342 signals.user_left_queue.send( 

343 sender=None, event=event, first_waiting=first_waiting 

344 ) 

345 

346 if event.send_cancel_email and event.after_cancel_deadline: 

347 emails.notify_organiser(event, registration) 

348 

349 # Note that this doesn"t remove the values for the 

350 # information fields that the user entered upon registering. 

351 # But this is regarded as a feature, not a bug. Especially 

352 # since the values will still appear in the backend. 

353 registration.date_cancelled = timezone.now() 

354 registration.save() 

355 else: 

356 raise RegistrationError(_("You are not allowed to deregister for this event.")) 

357 

358 

359def update_registration( 

360 member=None, event=None, name=None, registration=None, field_values=None, actor=None 

361): 

362 """Update a user registration of an event. 

363 

364 :param member: the user 

365 :param event: the event 

366 :param name: the name of a registration not associated with a user 

367 :param registration: the registration 

368 :param field_values: values for the information fields 

369 :param actor: Member executing this action 

370 """ 

371 if not registration: 371 ↛ 381line 371 didn't jump to line 381 because the condition on line 371 was always true

372 try: 

373 registration = EventRegistration.objects.get( 

374 event=event, member=member, name=name 

375 ) 

376 except EventRegistration.DoesNotExist as error: 

377 raise RegistrationError( 

378 _("You are not registered for this event.") 

379 ) from error 

380 else: 

381 member = registration.member 

382 event = registration.event 

383 name = registration.name 

384 

385 if not actor: 385 ↛ 388line 385 didn't jump to line 388 because the condition on line 385 was always true

386 actor = member 

387 

388 permissions = event_permissions(actor, event, name) 

389 

390 if not field_values: 

391 return 

392 if not (permissions["update_registration"] or permissions["manage_event"]): 392 ↛ 393line 392 didn't jump to line 393 because the condition on line 392 was never true

393 raise RegistrationError(_("You are not allowed to update this registration.")) 

394 

395 for field_id, field_value in field_values: 

396 field = RegistrationInformationField.objects.get( 

397 id=field_id.replace("info_field_", "") 

398 ) 

399 

400 if ( 

401 field.type == RegistrationInformationField.INTEGER_FIELD 

402 and field_value is None 

403 ): 

404 field_value = 0 

405 elif ( 

406 field.type == RegistrationInformationField.BOOLEAN_FIELD 

407 and field_value is None 

408 ): 

409 field_value = False 

410 elif ( 

411 field.type == RegistrationInformationField.TEXT_FIELD 

412 and field_value is None 

413 ): 

414 field_value = "" 

415 

416 field.set_value_for(registration, field_value) 

417 

418 

419def registration_fields(request, member=None, event=None, registration=None, name=None): 

420 """Return information about the registration fields of a registration. 

421 

422 :param member: the user (optional if registration provided) 

423 :param name: the name of a non member registration 

424 (optional if registration provided) 

425 :param event: the event (optional if registration provided) 

426 :param registration: the registration (optional if member & event provided) 

427 :return: the fields 

428 """ 

429 if registration is None: 

430 try: 

431 registration = EventRegistration.objects.get( 

432 event=event, member=member, name=name 

433 ) 

434 except EventRegistration.DoesNotExist as error: 

435 raise RegistrationError( 

436 _("You are not registered for this event.") 

437 ) from error 

438 except EventRegistration.MultipleObjectsReturned as error: 

439 raise RegistrationError( 

440 _("Unable to find the right registration.") 

441 ) from error 

442 

443 member = registration.member 

444 event = registration.event 

445 name = registration.name 

446 

447 perms = event_permissions(member, event, name)[ 

448 "update_registration" 

449 ] or is_organiser(request.member, event) 

450 if perms and registration: 

451 information_fields = registration.information_fields 

452 fields = OrderedDict() 

453 

454 for information_field in information_fields: 

455 field = information_field["field"] 

456 

457 fields[f"info_field_{field.id}"] = { 

458 "type": field.type, 

459 "label": field.name, 

460 "description": field.description, 

461 "value": information_field["value"], 

462 "required": field.required, 

463 } 

464 

465 return fields 

466 raise RegistrationError(_("You are not allowed to update this registration.")) 

467 

468 

469def generate_category_statistics() -> dict: 

470 """Generate statistics about events per category.""" 

471 current_year = datetime_to_lectureyear(timezone.now()) 

472 

473 data: dict[str, list] = { 

474 "labels": [str(current_year - 4 + i) for i in range(5)], 

475 "datasets": [ 

476 {"label": str(display), "data": []} 

477 for _, display in categories.EVENT_CATEGORIES 

478 ], 

479 } 

480 

481 for index, (key, category) in enumerate(categories.EVENT_CATEGORIES): 

482 for i in range(5): 

483 year_start = date(year=current_year - 4 + i, month=9, day=1) 

484 year_end = date(year=current_year - 3 + i, month=9, day=1) 

485 

486 data["datasets"][index]["data"].append( 

487 Event.objects.filter( 

488 category=key, start__gte=year_start, end__lte=year_end 

489 ).count() 

490 ) 

491 

492 return data 

493 

494 

495def execute_data_minimisation(dry_run=False): 

496 """Delete information about very old events.""" 

497 # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late 

498 deletion_period = timezone.now().date() - timezone.timedelta(days=365 * 5) 

499 

500 queryset = EventRegistration.objects.filter(event__end__lte=deletion_period).filter( 

501 Q(payment__isnull=False) | Q(member__isnull=False) | ~Q(name__exact="<removed>") 

502 ) 

503 if not dry_run: 

504 queryset.update(payment=None, member=None, name="<removed>") 

505 return queryset.all() 

506 

507 

508def is_eventdocument_owner(member, event_doc): 

509 if member and member.is_authenticated: 

510 if member.is_superuser or member.has_perm("documents.override_owner"): 

511 return True 

512 

513 if event_doc and member.has_perm("documents.change_document"): 

514 return member.get_member_groups().filter(pk=event_doc.owner.pk).exists() 

515 

516 return False