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
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1"""Registers admin interfaces for the event model."""
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 _
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
26@admin.register(models.Event)
27class EventAdmin(DoNextModelAdmin):
28 """Manage the events."""
30 form = EventAdminForm
32 inlines = (
33 RegistrationInformationFieldInline,
34 PizzaEventInline,
35 PromotionRequestInline,
36 )
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 }
57 filter_horizontal = ("documents", "organisers")
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 )
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
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
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 )
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)
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)
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")
181 event_date.short_description = _("Event Date")
182 event_date.admin_order_field = "start"
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
190 return _date(start_date, "l d b Y, G:i")
192 registration_date.short_description = _("Registration Start")
193 registration_date.admin_order_field = "registration_start"
195 def edit_link(self, obj):
196 return _("Edit")
198 edit_link.short_description = ""
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}"
207 num_participants.short_description = _("Number of participants")
209 def get_organisers(self, obj):
210 return ", ".join(str(o) for o in obj.organisers.all())
212 get_organisers.short_description = _("Organisers")
214 def make_published(self, request, queryset):
215 """Change the status of the event to published."""
216 self._change_published(request, queryset, True)
218 make_published.short_description = _("Publish selected events")
220 def make_unpublished(self, request, queryset):
221 """Change the status of the event to unpublished."""
222 self._change_published(request, queryset, False)
224 make_unpublished.short_description = _("Unpublish selected events")
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)
234 def save_formset(self, request, form, formset, change):
235 """Save formsets with their order."""
236 formset.save()
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()
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
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)
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
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
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