From d792efc0840ece782c17e6b17c783d4e131f2f41 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Wed, 7 Aug 2024 12:27:32 -0400 Subject: [PATCH] reservations: Make ReservationAdmin more useful for subclasses --- membershipworks/admin.py | 7 ++++ membershipworks/models.py | 3 ++ reservations/admin.py | 84 ++++++++++++++++++++++++++++++++++++--- reservations/models.py | 9 +++++ 4 files changed, 97 insertions(+), 6 deletions(-) diff --git a/membershipworks/admin.py b/membershipworks/admin.py index ba12a22..f6fed07 100644 --- a/membershipworks/admin.py +++ b/membershipworks/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin from django.contrib.humanize.templatetags.humanize import naturaltime +from django.http import HttpRequest from django.utils.html import format_html from django_object_actions import ( @@ -223,3 +224,9 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin): def has_delete_permission(self, request, obj=None): return False + + +@admin.register(EventMeetingTime) +class EventMeetingTimeAdmin(admin.ModelAdmin): + def has_module_permission(self, request: HttpRequest) -> bool: + return False diff --git a/membershipworks/models.py b/membershipworks/models.py index aca367f..2e84aab 100644 --- a/membershipworks/models.py +++ b/membershipworks/models.py @@ -626,6 +626,9 @@ class EventMeetingTime(Reservation): EventExt, on_delete=models.CASCADE, related_name="meeting_times" ) + def get_title(self) -> str: + return self.event.unescaped_title + # TODO: should probably do some validation in python to enforce # - uniqueness and non-overlapping (per event) # - min/max start/end time == event start end diff --git a/reservations/admin.py b/reservations/admin.py index 3640aab..16eff76 100644 --- a/reservations/admin.py +++ b/reservations/admin.py @@ -1,6 +1,13 @@ -from django.contrib import admin +from typing import Any -from .models import Reservation, Resource, UserReservation +from django.contrib import admin +from django.contrib.admin.views.main import ChangeList +from django.db import models +from django.db.models import QuerySet +from django.http import HttpRequest +from django.urls import reverse + +from .models import ExternalReservation, Reservation, Resource, UserReservation @admin.register(Resource) @@ -11,18 +18,83 @@ class ResourceAdmin(admin.ModelAdmin): show_facets = admin.ShowFacets.ALWAYS +class ReservationTypeFilter(admin.SimpleListFilter): + """Filter by subclass""" + + title = "Reservation Type" + parameter_name = "type" + + # TODO: this could be automatic + _subtypes = { + "eventmeetingtime": "Event Meeting Time", + "userreservation": "User Reservation", + "externalreservation": "External Reservation", + } + + def lookups( + self, request: HttpRequest, model_admin: admin.ModelAdmin + ) -> list[tuple[str, str]]: + return [ + ("generic", "Generic Reservation"), + ] + list(self._subtypes.items()) + + def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet: + if self.value() in self._subtypes: + return queryset.filter(**{f"{self.value()}__isnull": False}) + elif self.value() == "generic": + return queryset.filter( + **{f"{subtype}__isnull": True for subtype in self._subtypes} + ) + else: + return queryset + + +class SubclassChangeList(ChangeList): + def url_for_result(self, result: models.Model) -> str: + opts = type(result)._meta + return reverse( + f"admin:{opts.app_label}_{opts.model_name}_change", + args=(result.pk,), + current_app=self.model_admin.admin_site.name, + ) + + @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): - list_display = ["_resources", "start", "end"] + list_display = ["_title", "_resources", "start", "end"] readonly_fields = ["google_calendar_event_id"] - list_filter = ["resources"] + list_filter = ["resources", ReservationTypeFilter] show_facets = admin.ShowFacets.ALWAYS + date_hierarchy = "start" + + def get_queryset(self, request): + return ( + super() + .get_queryset(request) + .select_related("eventmeetingtime__event") + .prefetch_related("resources") + .select_subclasses() + ) + + @admin.display() + def _title(self, obj: Reservation): + return obj.get_title() @admin.display() def _resources(self, obj: Reservation): return list(obj.resources.all()) or None + def get_changelist(self, request: HttpRequest, **kwargs: Any) -> type[ChangeList]: + return SubclassChangeList + @admin.register(UserReservation) -class UserReservationAdmin(ReservationAdmin): - list_display = ["_resources", "user", "start", "end"] +class UserReservationAdmin(admin.ModelAdmin): + def has_module_permission(self, request: HttpRequest) -> bool: + return False + + +@admin.register(ExternalReservation) +class ExternalReservationAdmin(admin.ModelAdmin): + def has_module_permission(self, request: HttpRequest) -> bool: + return False diff --git a/reservations/models.py b/reservations/models.py index 40d58d7..5dd972c 100644 --- a/reservations/models.py +++ b/reservations/models.py @@ -98,6 +98,9 @@ class Reservation(models.Model): resources = ", ".join(str(resource) for resource in self.resources.all()) return f"{resources}: {self.start} - {self.end}" + def get_title(self) -> str: + return "Unknown Reservation" + def make_google_calendar_event(self): event = { "summary": "CMSManage Reservation", @@ -128,6 +131,9 @@ class UserReservation(Reservation): def __str__(self) -> str: return f"{self.user} | {super().__str__()}" + def get_title(self) -> str: + return str(self.user) + def make_google_calendar_event(self): return super().make_google_calendar_event() | { "summary": str(self.user), @@ -142,6 +148,9 @@ class ExternalReservation(Reservation): def __str__(self) -> str: return f'External "{self.title}" | {super().__str__()}' + def get_title(self) -> str: + return str(self.title) + def make_google_calendar_event(self): """This should never be called, as these are reservations from Google Calendar and shouldn't be synced back""" raise AttributeError(