Coverage for website/events/admin/event.py: 66.23%

121 statements  

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

1"""Registers admin interfaces for the event model.""" 

2 

3from django.contrib import admin, messages 

4from django.template.defaultfilters import date as _date 

5from django.urls import path, reverse 

6from django.utils import timezone 

7from django.utils.html import format_html 

8from django.utils.translation import gettext_lazy as _ 

9 

10from events import emails, models, services 

11from events.admin.filters import LectureYearFilter 

12from events.admin.forms import EventAdminForm, RegistrationInformationFieldForm 

13from events.admin.inlines import ( 

14 PizzaEventInline, 

15 PromotionRequestInline, 

16 RegistrationInformationFieldInline, 

17) 

18from events.admin.views import ( 

19 EventAdminDetails, 

20 EventMarkPresentQR, 

21 EventRegistrationsExport, 

22) 

23from utils.admin import DoNextModelAdmin 

24 

25 

26@admin.register(models.Event) 

27class EventAdmin(DoNextModelAdmin): 

28 """Manage the events.""" 

29 

30 form = EventAdminForm 

31 

32 inlines = ( 

33 RegistrationInformationFieldInline, 

34 PizzaEventInline, 

35 PromotionRequestInline, 

36 ) 

37 

38 list_display = ( 

39 "overview_link", 

40 "event_date", 

41 "registration_date", 

42 "num_participants", 

43 "get_organisers", 

44 "category", 

45 "published", 

46 "edit_link", 

47 ) 

48 list_display_links = ("edit_link",) 

49 list_filter = (LectureYearFilter, "start", "published", "category") 

50 actions = ("make_published", "make_unpublished") 

51 date_hierarchy = "start" 

52 search_fields = ("title", "description") 

53 prepopulated_fields = { 

54 "map_location": ("location",), 

55 } 

56 

57 filter_horizontal = ("documents", "organisers") 

58 

59 fieldsets = ( 

60 ( 

61 _("General"), 

62 { 

63 "fields": ( 

64 "title", 

65 "slug", 

66 "published", 

67 "organisers", 

68 ) 

69 }, 

70 ), 

71 ( 

72 _("Detail"), 

73 { 

74 "fields": ( 

75 "category", 

76 "start", 

77 "end", 

78 "description", 

79 "caption", 

80 "location", 

81 "map_location", 

82 "show_map_location", 

83 ), 

84 "classes": ("collapse", "start-open"), 

85 }, 

86 ), 

87 ( 

88 _("Registrations"), 

89 { 

90 "fields": ( 

91 "price", 

92 "fine", 

93 "tpay_allowed", 

94 "max_participants", 

95 "registration_without_membership", 

96 "registration_start", 

97 "registration_end", 

98 "update_deadline", 

99 "cancel_deadline", 

100 "send_cancel_email", 

101 "optional_registrations", 

102 ), 

103 "classes": ("collapse",), 

104 }, 

105 ), 

106 ( 

107 _("Registration status messages"), 

108 { 

109 "fields": ( 

110 "no_registration_message", 

111 "registration_msg_optional", 

112 "registration_msg_optional_registered", 

113 "registration_msg_registered", 

114 "registration_msg_open", 

115 "registration_msg_full", 

116 "registration_msg_waitinglist", 

117 "registration_msg_will_open", 

118 "registration_msg_expired", 

119 "registration_msg_cancelled", 

120 "registration_msg_cancelled_late", 

121 "registration_msg_cancelled_final", 

122 ), 

123 "classes": ("collapse",), 

124 }, 

125 ), 

126 ( 

127 _("Extra"), 

128 {"fields": ("documents", "shift"), "classes": ("collapse",)}, 

129 ), 

130 ) 

131 

132 def get_queryset(self, request): 

133 queryset = ( 

134 super() 

135 .get_queryset(request) 

136 .select_properties("participant_count") 

137 .prefetch_related("organisers") 

138 ) 

139 if not ( 

140 request.user.has_perm("events.override_organiser") 

141 or request.user.has_perm("events.view_unpublished") 

142 ): 

143 queryset_published = queryset.filter(published=True) 

144 queryset_unpublished = queryset.filter( 

145 published=False, 

146 organisers__in=list( 

147 request.member.get_member_groups().values_list("id", flat=True) 

148 ), 

149 ) 

150 queryset = queryset_published | queryset_unpublished 

151 return queryset 

152 

153 def get_form(self, request, obj=None, change=False, **kwargs): 

154 form = super().get_form(request, obj, change, **kwargs) 

155 form.request = request 

156 return form 

157 

158 def overview_link(self, obj): 

159 return format_html( 

160 '<a href="{link}">{title}</a>', 

161 link=reverse("admin:events_event_details", kwargs={"pk": obj.pk}), 

162 title=obj.title, 

163 ) 

164 

165 def has_delete_permission(self, request, obj=None): 

166 """Only allow deleting an event if the user is an organiser.""" 

167 if obj is not None and not services.is_organiser(request.member, obj): 

168 return False 

169 return super().has_delete_permission(request, obj) 

170 

171 def has_change_permission(self, request, obj=None): 

172 """Only allow access to the change form if the user is an organiser.""" 

173 if obj is not None and not services.is_organiser(request.member, obj): 

174 return False 

175 return super().has_change_permission(request, obj) 

176 

177 def event_date(self, obj): 

178 event_date = timezone.make_naive(obj.start) 

179 return _date(event_date, "l d b Y, G:i") 

180 

181 event_date.short_description = _("Event Date") 

182 event_date.admin_order_field = "start" 

183 

184 def registration_date(self, obj): 

185 if obj.registration_start is not None: 

186 start_date = timezone.make_naive(obj.registration_start) 

187 else: 

188 start_date = obj.registration_start 

189 

190 return _date(start_date, "l d b Y, G:i") 

191 

192 registration_date.short_description = _("Registration Start") 

193 registration_date.admin_order_field = "registration_start" 

194 

195 def edit_link(self, obj): 

196 return _("Edit") 

197 

198 edit_link.short_description = "" 

199 

200 def num_participants(self, obj): 

201 """Pretty-print the number of participants.""" 

202 num = obj.participant_count # prefetched aggregateproperty 

203 if not obj.max_participants: 

204 return f"{num}/∞" 

205 return f"{num}/{obj.max_participants}" 

206 

207 num_participants.short_description = _("Number of participants") 

208 

209 def get_organisers(self, obj): 

210 return ", ".join(str(o) for o in obj.organisers.all()) 

211 

212 get_organisers.short_description = _("Organisers") 

213 

214 def make_published(self, request, queryset): 

215 """Change the status of the event to published.""" 

216 self._change_published(request, queryset, True) 

217 

218 make_published.short_description = _("Publish selected events") 

219 

220 def make_unpublished(self, request, queryset): 

221 """Change the status of the event to unpublished.""" 

222 self._change_published(request, queryset, False) 

223 

224 make_unpublished.short_description = _("Unpublish selected events") 

225 

226 @staticmethod 

227 def _change_published(request, queryset, published): 

228 if not request.user.is_superuser: 

229 queryset = queryset.filter( 

230 organisers__in=request.member.get_member_groups() 

231 ) 

232 queryset.update(published=published) 

233 

234 def save_formset(self, request, form, formset, change): 

235 """Save formsets with their order.""" 

236 formset.save() 

237 

238 informationfield_forms = ( 

239 x 

240 for x in formset.forms 

241 if isinstance(x, RegistrationInformationFieldForm) 

242 and "DELETE" not in x.changed_data 

243 ) 

244 form.instance.set_registrationinformationfield_order( 

245 [ 

246 f.instance.pk 

247 for f in sorted( 

248 informationfield_forms, 

249 key=lambda x: (x.cleaned_data["order"], x.instance.pk), 

250 ) 

251 ] 

252 ) 

253 form.instance.save() 

254 

255 def save_model(self, request, obj, form, change): 

256 if change and "max_participants" in form.changed_data: 

257 prev = self.model.objects.get(id=obj.id) 

258 prev_limit = prev.max_participants 

259 self_limit = obj.max_participants 

260 if prev_limit is None: 

261 prev_limit = prev.participant_count 

262 if self_limit is None: 

263 self_limit = obj.participant_count 

264 

265 if prev_limit < self_limit and prev_limit < obj.participant_count: 

266 diff = self_limit - prev_limit 

267 joiners = prev.queue[:diff] 

268 for registration in joiners: 

269 emails.notify_waiting(obj, registration) 

270 messages.info( 

271 request, 

272 "The maximum number of participants was increased. Any members that moved from the waiting list to the participants list have been notified.", 

273 ) 

274 elif self_limit < prev_limit and self_limit < obj.participant_count: 

275 diff = self_limit - prev_limit 

276 leavers = prev.registrations[self_limit:] 

277 address = map(lambda r: r.email, leavers) 

278 link = "mailto:?bcc=" + ",".join(address) 

279 messages.warning( 

280 request, 

281 format_html( 

282 "The maximum number of participants was decreased and some members moved to the waiting list. <a href='{}' style='text-decoration: underline;'>Use this link to send them an email.</a>", 

283 link, 

284 ), 

285 ) 

286 super().save_model(request, obj, form, change) 

287 

288 def get_actions(self, request): 

289 actions = super().get_actions(request) 

290 if "delete_selected" in actions: 

291 del actions["delete_selected"] 

292 return actions 

293 

294 def get_formsets_with_inlines(self, request, obj=None): 

295 for inline in self.get_inline_instances(request, obj): 

296 if self.has_change_permission(request, obj) or obj is None: 

297 yield inline.get_formset(request, obj), inline 

298 

299 def get_urls(self): 

300 urls = super().get_urls() 

301 custom_urls = [ 

302 path( 

303 "<int:pk>/details/", 

304 self.admin_site.admin_view(EventAdminDetails.as_view()), 

305 name="events_event_details", 

306 ), 

307 path( 

308 "<int:pk>/export/", 

309 self.admin_site.admin_view(EventRegistrationsExport.as_view()), 

310 name="events_event_export", 

311 ), 

312 path( 

313 "<int:pk>/mark-present-qr/", 

314 self.admin_site.admin_view(EventMarkPresentQR.as_view()), 

315 name="events_event_mark_present_qr", 

316 ), 

317 ] 

318 return custom_urls + urls