From 704b196128b79c7ef6dc20a006be23541a58d441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Tue, 3 Jan 2023 21:36:34 +0100 Subject: [PATCH] Implement a generic way to adding actions to rows in a list. --- src/membership/models.py | 21 +++++++ src/membership/selectors.py | 21 ++++++- .../membership/members_admin_detail.html | 8 +++ src/membership/views.py | 28 +++++++++- src/project/templates/base.html | 2 +- src/project/urls.py | 8 ++- src/utils/templates/utils/list.html | 13 ++++- src/utils/view_utils.py | 55 +++++++++++++++++-- 8 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 src/membership/templates/membership/members_admin_detail.html diff --git a/src/membership/models.py b/src/membership/models.py index c58ef31..212cd61 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import User from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.fields import DateRangeField from django.contrib.postgres.fields import RangeOperators @@ -8,6 +9,26 @@ from django.utils.translation import gettext as _ from utils.mixins import CreatedModifiedAbstract +class Member(User): + class QuerySet(models.QuerySet): + def annotate_membership(self): + from membership.selectors import get_current_subscription_period + + return self.annotate( + active_membership=models.Exists( + Membership.objects.filter( + user=models.OuterRef("pk"), + period=get_current_subscription_period().id, + ) + ) + ) + + objects = QuerySet.as_manager() + + class Meta: + proxy = True + + class SubscriptionPeriod(CreatedModifiedAbstract): """ Denotes a period for which members should pay their membership fee for. diff --git a/src/membership/selectors.py b/src/membership/selectors.py index d1eb97d..c1caec2 100644 --- a/src/membership/selectors.py +++ b/src/membership/selectors.py @@ -1,5 +1,9 @@ -from django.contrib.auth.models import User +import contextlib +from django.contrib.auth.models import User +from django.utils import timezone + +from membership.models import Member from membership.models import Membership from membership.models import SubscriptionPeriod @@ -12,6 +16,13 @@ def get_subscription_periods() -> list[SubscriptionPeriod]: return list(subscription_periods) +def get_current_subscription_period() -> SubscriptionPeriod | None: + with contextlib.suppress(SubscriptionPeriod.DoesNotExist): + return SubscriptionPeriod.objects.prefetch_related( + "membership_set", "membership_set__user" + ).get(period__contains=timezone.now()) + + def get_memberships( *, user: User | None = None, period: SubscriptionPeriod | None = None ) -> Membership.QuerySet: @@ -26,5 +37,9 @@ def get_memberships( return memberships -def get_users(): - return User.objects.all() +def get_members(): + return Member.objects.all().annotate_membership() + + +def get_member(*, member_id: int) -> Member: + return get_members().get(id=member_id) diff --git a/src/membership/templates/membership/members_admin_detail.html b/src/membership/templates/membership/members_admin_detail.html new file mode 100644 index 0000000..2844d5f --- /dev/null +++ b/src/membership/templates/membership/members_admin_detail.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + + {{ member.username }} + +{% endblock %} diff --git a/src/membership/views.py b/src/membership/views.py index 83a193b..2af8d1f 100644 --- a/src/membership/views.py +++ b/src/membership/views.py @@ -4,10 +4,12 @@ from django.utils.translation import gettext_lazy as _ from zen_queries import render from .permissions import ADMINISTRATE_MEMBERS +from .selectors import get_member +from .selectors import get_members from .selectors import get_memberships -from .selectors import get_users from utils.view_utils import base_view_context from utils.view_utils import render_list +from utils.view_utils import RowAction @login_required @@ -34,7 +36,7 @@ def membership_overview(request): @login_required @permission_required(ADMINISTRATE_MEMBERS.path) def members_admin(request): - users = get_users() + users = get_members() return render_list( request=request, @@ -44,5 +46,27 @@ def members_admin(request): ("first_name", _("First name")), ("last_name", _("Last name")), ("email", _("Email")), + ("active_membership", _("Active membership")), + ], + row_actions=[ + RowAction( + label=_("View"), + url_name="admin-members-detail", + url_kwargs={"member_id": "id"}, + ) ], ) + + +@login_required +@permission_required(ADMINISTRATE_MEMBERS.path) +def members_admin_detail(request, member_id): + member = get_member(member_id=member_id) + + context = base_view_context(request) | {"member": member} + + return render( + request=request, + template_name="membership/members_admin_detail.html", + context=context, + ) diff --git a/src/project/templates/base.html b/src/project/templates/base.html index 5c478ee..9225f6d 100644 --- a/src/project/templates/base.html +++ b/src/project/templates/base.html @@ -203,7 +203,7 @@ -
+
{% block content %} {% endblock %}
diff --git a/src/project/urls.py b/src/project/urls.py index 1dcd368..42f2114 100644 --- a/src/project/urls.py +++ b/src/project/urls.py @@ -8,6 +8,7 @@ from django.urls import path from .views import index from .views import services_overview from membership.views import members_admin +from membership.views import members_admin_detail from membership.views import membership_overview urlpatterns = [ @@ -15,8 +16,13 @@ urlpatterns = [ path("services/", login_required(services_overview), name="services-overview"), path("membership/", membership_overview, name="membership-overview"), path("admin/members/", members_admin, name="admin-members"), + path( + "admin/members//", + members_admin_detail, + name="admin-members-detail", + ), path("accounts/", include("allauth.urls")), - path("admin/", admin.site.urls), + path("_admin/", admin.site.urls), ] if settings.DEBUG: diff --git a/src/utils/templates/utils/list.html b/src/utils/templates/utils/list.html index be87d7f..445c4cb 100644 --- a/src/utils/templates/utils/list.html +++ b/src/utils/templates/utils/list.html @@ -11,11 +11,20 @@ {% endfor %} - {% for row in object_rows %} + {% for row in rows %} - {% for value in row %} + {% for value in row.data.values %} {{ value }} {% endfor %} + {% if row.actions %} + + {% for action in row.actions %} + + {{ action.label }} + + {% endfor %} + + {% endif %} {% endfor %} diff --git a/src/utils/view_utils.py b/src/utils/view_utils.py index 56ba8f8..c88849c 100644 --- a/src/utils/view_utils.py +++ b/src/utils/view_utils.py @@ -1,7 +1,10 @@ +from dataclasses import dataclass + from django.contrib.sites.shortcuts import get_current_site as django_get_current_site from django.db.models import Model from django.http import HttpRequest from django.http import HttpResponse +from django.urls import reverse from zen_queries import render @@ -10,21 +13,65 @@ def base_view_context(request): return {"site": django_get_current_site(request)} +@dataclass +class Row: + """ + A row in a table. + """ + + data: dict[str, str] + actions: list[dict[str, str]] + + +@dataclass +class RowAction: + """ + An action that can be performed on a row in a table. + """ + + label: str + url_name: str + url_kwargs: dict[str, str] + + def render(self, obj) -> dict[str, str]: + """ + Render the action as a dictionary for the given object. + """ + url = reverse( + self.url_name, + kwargs={key: getattr(obj, value) for key, value in self.url_kwargs.items()}, + ) + return {"label": self.label, "url": url} + + def render_list( request: HttpRequest, objects: list["Model"], columns: list[tuple[str, str]], + row_actions: list[RowAction] = None, + list_actions: list[tuple[str, str]] = None, ) -> HttpResponse: - # TODO: Actions per object - # TODO: Listwide actions + """ + Render a list of objects with a table. + """ - object_rows = [[getattr(obj, column[0]) for column in columns] for obj in objects] + # TODO: List actions + + rows = [] + for obj in objects: + row = Row( + data={column: getattr(obj, column[0]) for column in columns}, + actions=[action.render(obj) for action in row_actions], + ) + rows.append(row) column_labels = [column[1] for column in columns] context = base_view_context(request) | { - "object_rows": object_rows, + "rows": rows, "columns": column_labels, + "row_actions": row_actions, + "list_actions": list_actions, } return render(