Coverage for website/events/models/event_registration.py: 85.98%
89 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 django.core import validators
2from django.core.exceptions import ValidationError
3from django.db import models
4from django.db.models import Case, Count, F, Q, When
5from django.db.models.functions import Coalesce, Greatest, NullIf
6from django.utils import timezone
7from django.utils.translation import gettext_lazy as _
9from queryable_properties.managers import QueryablePropertiesManager
10from queryable_properties.properties import AnnotationProperty, queryable_property
12from payments.models import PaymentAmountField
14from .event import Event
17def registration_member_choices_limit():
18 """Define queryset filters to only include current members."""
19 return Q(membership__until__isnull=True) | Q(
20 membership__until__gt=timezone.now().date()
21 )
24class EventRegistration(models.Model):
25 """Describes a registration for an Event."""
27 objects = QueryablePropertiesManager()
29 event = models.ForeignKey(Event, models.CASCADE)
31 member = models.ForeignKey(
32 "members.Member",
33 models.CASCADE,
34 blank=True,
35 null=True,
36 )
38 name = models.CharField(
39 _("name"),
40 max_length=50,
41 help_text=_("Use this for non-members"),
42 null=True,
43 blank=True,
44 )
46 alt_email = models.EmailField(
47 _("email"),
48 help_text=_("Email address for non-members"),
49 max_length=254,
50 null=True,
51 blank=True,
52 )
54 alt_phone_number = models.CharField(
55 max_length=20,
56 verbose_name=_("Phone number"),
57 help_text=_("Phone number for non-members"),
58 validators=[
59 validators.RegexValidator(
60 regex=r"^\+?\d+$",
61 message=_("Please enter a valid phone number"),
62 )
63 ],
64 null=True,
65 blank=True,
66 )
68 date = models.DateTimeField(_("registration date"), default=timezone.now)
69 date_cancelled = models.DateTimeField(_("cancellation date"), null=True, blank=True)
71 present = models.BooleanField(
72 _("present"),
73 default=False,
74 )
76 special_price = PaymentAmountField(
77 verbose_name=_("special price"),
78 blank=True,
79 null=True,
80 validators=[validators.MinValueValidator(0)],
81 )
83 remarks = models.TextField(_("remarks"), null=True, blank=True)
85 payment = models.OneToOneField(
86 "payments.Payment",
87 related_name="events_registration",
88 on_delete=models.SET_NULL,
89 blank=True,
90 null=True,
91 )
93 @property
94 def phone_number(self):
95 if self.member:
96 return self.member.profile.phone_number
97 return self.alt_phone_number
99 @property
100 def email(self):
101 if self.member: 101 ↛ 103line 101 didn't jump to line 103 because the condition on line 101 was always true
102 return self.member.email
103 return self.alt_email
105 @property
106 def information_fields(self):
107 fields = self.event.registrationinformationfield_set.all()
108 return [
109 {"field": field, "value": field.get_value_for(self)} for field in fields
110 ]
112 @property
113 def is_registered(self):
114 return self.date_cancelled is None
116 queue_position = AnnotationProperty(
117 Case(
118 # Get queue position by counting amount of registrations with lower date and in case of same date lower id
119 # Subsequently cast to None if this is 0 or lower, in which case it isn't in the queue
120 # If the current registration is cancelled, also force it to None.
121 When(
122 date_cancelled=None,
123 then=NullIf(
124 Greatest(
125 Count(
126 "event__eventregistration",
127 filter=Q(event__eventregistration__date_cancelled=None)
128 & (
129 Q(event__eventregistration__date__lt=F("date"))
130 | Q(event__eventregistration__id__lte=F("id"))
131 & Q(event__eventregistration__date__exact=F("date"))
132 ),
133 )
134 - F("event__max_participants"),
135 0,
136 ),
137 0,
138 ),
139 ),
140 default=None,
141 )
142 )
144 @property
145 def is_invited(self):
146 return self.is_registered and not self.queue_position
148 def is_external(self):
149 return bool(self.name)
151 def is_late_cancellation(self):
152 # First check whether or not the user cancelled
153 # If the user cancelled then check if this was after the deadline
154 # And if there is a max participants number:
155 # do a complex check to calculate if this user was on
156 # the waiting list at the time of cancellation, since
157 # you shouldn't need to pay the costs of something
158 # you weren't even able to go to.
159 return (
160 self.date_cancelled
161 and self.event.cancel_deadline
162 and self.date_cancelled > self.event.cancel_deadline
163 and (
164 self.event.max_participants is None
165 or self.event.eventregistration_set.filter(
166 (
167 Q(date_cancelled__gte=self.date_cancelled)
168 | Q(date_cancelled=None)
169 )
170 & Q(date__lte=self.date)
171 ).count()
172 < self.event.max_participants
173 )
174 )
176 def is_paid(self):
177 return self.payment
179 @queryable_property
180 def payment_amount(self):
181 return self.event.price if not self.special_price else self.special_price
183 @payment_amount.annotater
184 @classmethod
185 def payment_amount(cls):
186 return Coalesce("special_price", "event__price")
188 def would_cancel_after_deadline(self):
189 now = timezone.now()
190 if not self.event.registration_required:
191 return False
192 return not self.queue_position and now >= self.event.cancel_deadline
194 def clean(self):
195 errors = {}
196 if (self.member is None and not self.name) or (self.member and self.name):
197 errors.update(
198 {
199 "member": _("Either specify a member or a name"),
200 "name": _("Either specify a member or a name"),
201 }
202 )
203 if self.member and self.alt_email: 203 ↛ 204line 203 didn't jump to line 204 because the condition on line 203 was never true
204 errors.update(
205 {"alt_email": _("Email should only be specified for non-members")}
206 )
207 if self.member and self.alt_phone_number: 207 ↛ 208line 207 didn't jump to line 208 because the condition on line 207 was never true
208 errors.update(
209 {
210 "alt_phone_number": _(
211 "Phone number should only be specified for non-members"
212 )
213 }
214 )
215 if ( 215 ↛ 220line 215 didn't jump to line 220
216 self.payment
217 and self.special_price
218 and self.special_price != self.payment.amount
219 ):
220 errors.update(
221 {
222 "special_price": _(
223 "Cannot change price of already paid registration"
224 ),
225 }
226 )
228 if errors:
229 raise ValidationError(errors)
231 def save(self, **kwargs):
232 self.full_clean()
233 super().save(**kwargs)
235 def __str__(self):
236 if self.member:
237 return f"{self.member.get_full_name()}: {self.event}"
238 return f"{self.name}: {self.event}"
240 class Meta:
241 verbose_name = _("Registration")
242 verbose_name_plural = _("Registrations")
243 ordering = ("date",)
244 unique_together = (("member", "event"),)