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
« 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
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 _
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
21def is_user_registered(member, event):
22 """Return if the user is registered for the specified event.
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
31 return event.registrations.filter(member=member, date_cancelled=None).exists()
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
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
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))
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
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
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
89 if registration.queue_position:
90 return status.STATUS_WAITINGLIST
91 if event.optional_registration_allowed:
92 return status.STATUS_OPTIONAL_REGISTERED
94 return status.STATUS_REGISTERED
95 if event.optional_registration_allowed:
96 return status.STATUS_OPTIONAL
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
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
108 raise ValueError("invalid/unexpected registration status")
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 ]
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
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]
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
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 )
147def user_registration_pending(member, event):
148 """Return if the user is in the queue, but not yet registered for, the specific event.
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
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
170def event_permissions(member, event: Event, name=None, registration_prefetch=False):
171 """Return a dictionary with the available event permissions of the user.
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
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
203 now = timezone.now()
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
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
274 if event:
275 return (
276 member.get_member_groups()
277 .filter(pk__in=event.organisers.values_list("pk"))
278 .exists()
279 )
281 return False
284def create_registration(member, event):
285 """Create a new user registration for an event.
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
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()
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."))
319def cancel_registration(member, event):
320 """Cancel a user registration for an event.
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
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 )
346 if event.send_cancel_email and event.after_cancel_deadline:
347 emails.notify_organiser(event, registration)
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."))
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.
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
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
388 permissions = event_permissions(actor, event, name)
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."))
395 for field_id, field_value in field_values:
396 field = RegistrationInformationField.objects.get(
397 id=field_id.replace("info_field_", "")
398 )
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 = ""
416 field.set_value_for(registration, field_value)
419def registration_fields(request, member=None, event=None, registration=None, name=None):
420 """Return information about the registration fields of a registration.
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
443 member = registration.member
444 event = registration.event
445 name = registration.name
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()
454 for information_field in information_fields:
455 field = information_field["field"]
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 }
465 return fields
466 raise RegistrationError(_("You are not allowed to update this registration."))
469def generate_category_statistics() -> dict:
470 """Generate statistics about events per category."""
471 current_year = datetime_to_lectureyear(timezone.now())
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 }
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)
486 data["datasets"][index]["data"].append(
487 Event.objects.filter(
488 category=key, start__gte=year_start, end__lte=year_end
489 ).count()
490 )
492 return data
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)
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()
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
513 if event_doc and member.has_perm("documents.change_document"):
514 return member.get_member_groups().filter(pk=event_doc.owner.pk).exists()
516 return False