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

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 _ 

8 

9from queryable_properties.managers import QueryablePropertiesManager 

10from queryable_properties.properties import AnnotationProperty, queryable_property 

11 

12from payments.models import PaymentAmountField 

13 

14from .event import Event 

15 

16 

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 ) 

22 

23 

24class EventRegistration(models.Model): 

25 """Describes a registration for an Event.""" 

26 

27 objects = QueryablePropertiesManager() 

28 

29 event = models.ForeignKey(Event, models.CASCADE) 

30 

31 member = models.ForeignKey( 

32 "members.Member", 

33 models.CASCADE, 

34 blank=True, 

35 null=True, 

36 ) 

37 

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 ) 

45 

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 ) 

53 

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 ) 

67 

68 date = models.DateTimeField(_("registration date"), default=timezone.now) 

69 date_cancelled = models.DateTimeField(_("cancellation date"), null=True, blank=True) 

70 

71 present = models.BooleanField( 

72 _("present"), 

73 default=False, 

74 ) 

75 

76 special_price = PaymentAmountField( 

77 verbose_name=_("special price"), 

78 blank=True, 

79 null=True, 

80 validators=[validators.MinValueValidator(0)], 

81 ) 

82 

83 remarks = models.TextField(_("remarks"), null=True, blank=True) 

84 

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 ) 

92 

93 @property 

94 def phone_number(self): 

95 if self.member: 

96 return self.member.profile.phone_number 

97 return self.alt_phone_number 

98 

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 

104 

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 ] 

111 

112 @property 

113 def is_registered(self): 

114 return self.date_cancelled is None 

115 

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 ) 

143 

144 @property 

145 def is_invited(self): 

146 return self.is_registered and not self.queue_position 

147 

148 def is_external(self): 

149 return bool(self.name) 

150 

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 ) 

175 

176 def is_paid(self): 

177 return self.payment 

178 

179 @queryable_property 

180 def payment_amount(self): 

181 return self.event.price if not self.special_price else self.special_price 

182 

183 @payment_amount.annotater 

184 @classmethod 

185 def payment_amount(cls): 

186 return Coalesce("special_price", "event__price") 

187 

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 

193 

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 ) 

227 

228 if errors: 

229 raise ValidationError(errors) 

230 

231 def save(self, **kwargs): 

232 self.full_clean() 

233 super().save(**kwargs) 

234 

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}" 

239 

240 class Meta: 

241 verbose_name = _("Registration") 

242 verbose_name_plural = _("Registrations") 

243 ordering = ("date",) 

244 unique_together = (("member", "event"),)