Implement a generic way to adding actions to rows in a list.

This commit is contained in:
Víðir Valberg Guðmundsson 2023-01-03 21:36:34 +01:00
parent cf99c3f40e
commit 704b196128
8 changed files with 143 additions and 13 deletions

View File

@ -1,3 +1,4 @@
from django.contrib.auth.models import User
from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateRangeField from django.contrib.postgres.fields import DateRangeField
from django.contrib.postgres.fields import RangeOperators from django.contrib.postgres.fields import RangeOperators
@ -8,6 +9,26 @@ from django.utils.translation import gettext as _
from utils.mixins import CreatedModifiedAbstract 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): class SubscriptionPeriod(CreatedModifiedAbstract):
""" """
Denotes a period for which members should pay their membership fee for. Denotes a period for which members should pay their membership fee for.

View File

@ -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 Membership
from membership.models import SubscriptionPeriod from membership.models import SubscriptionPeriod
@ -12,6 +16,13 @@ def get_subscription_periods() -> list[SubscriptionPeriod]:
return list(subscription_periods) 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( def get_memberships(
*, user: User | None = None, period: SubscriptionPeriod | None = None *, user: User | None = None, period: SubscriptionPeriod | None = None
) -> Membership.QuerySet: ) -> Membership.QuerySet:
@ -26,5 +37,9 @@ def get_memberships(
return memberships return memberships
def get_users(): def get_members():
return User.objects.all() return Member.objects.all().annotate_membership()
def get_member(*, member_id: int) -> Member:
return get_members().get(id=member_id)

View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{{ member.username }}
{% endblock %}

View File

@ -4,10 +4,12 @@ from django.utils.translation import gettext_lazy as _
from zen_queries import render from zen_queries import render
from .permissions import ADMINISTRATE_MEMBERS from .permissions import ADMINISTRATE_MEMBERS
from .selectors import get_member
from .selectors import get_members
from .selectors import get_memberships from .selectors import get_memberships
from .selectors import get_users
from utils.view_utils import base_view_context from utils.view_utils import base_view_context
from utils.view_utils import render_list from utils.view_utils import render_list
from utils.view_utils import RowAction
@login_required @login_required
@ -34,7 +36,7 @@ def membership_overview(request):
@login_required @login_required
@permission_required(ADMINISTRATE_MEMBERS.path) @permission_required(ADMINISTRATE_MEMBERS.path)
def members_admin(request): def members_admin(request):
users = get_users() users = get_members()
return render_list( return render_list(
request=request, request=request,
@ -44,5 +46,27 @@ def members_admin(request):
("first_name", _("First name")), ("first_name", _("First name")),
("last_name", _("Last name")), ("last_name", _("Last name")),
("email", _("Email")), ("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,
)

View File

@ -203,7 +203,7 @@
</div> </div>
</nav> </nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4 p-4">
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</main> </main>

View File

@ -8,6 +8,7 @@ from django.urls import path
from .views import index from .views import index
from .views import services_overview from .views import services_overview
from membership.views import members_admin from membership.views import members_admin
from membership.views import members_admin_detail
from membership.views import membership_overview from membership.views import membership_overview
urlpatterns = [ urlpatterns = [
@ -15,8 +16,13 @@ urlpatterns = [
path("services/", login_required(services_overview), name="services-overview"), path("services/", login_required(services_overview), name="services-overview"),
path("membership/", membership_overview, name="membership-overview"), path("membership/", membership_overview, name="membership-overview"),
path("admin/members/", members_admin, name="admin-members"), path("admin/members/", members_admin, name="admin-members"),
path(
"admin/members/<int:member_id>/",
members_admin_detail,
name="admin-members-detail",
),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("admin/", admin.site.urls), path("_admin/", admin.site.urls),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -11,11 +11,20 @@
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
{% for row in object_rows %} {% for row in rows %}
<tr> <tr>
{% for value in row %} {% for value in row.data.values %}
<td>{{ value }}</td> <td>{{ value }}</td>
{% endfor %} {% endfor %}
{% if row.actions %}
<td>
{% for action in row.actions %}
<a href="{{ action.url }}">
{{ action.label }}
</a>
{% endfor %}
</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View File

@ -1,7 +1,10 @@
from dataclasses import dataclass
from django.contrib.sites.shortcuts import get_current_site as django_get_current_site from django.contrib.sites.shortcuts import get_current_site as django_get_current_site
from django.db.models import Model from django.db.models import Model
from django.http import HttpRequest from django.http import HttpRequest
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse
from zen_queries import render from zen_queries import render
@ -10,21 +13,65 @@ def base_view_context(request):
return {"site": django_get_current_site(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( def render_list(
request: HttpRequest, request: HttpRequest,
objects: list["Model"], objects: list["Model"],
columns: list[tuple[str, str]], columns: list[tuple[str, str]],
row_actions: list[RowAction] = None,
list_actions: list[tuple[str, str]] = None,
) -> HttpResponse: ) -> 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] column_labels = [column[1] for column in columns]
context = base_view_context(request) | { context = base_view_context(request) | {
"object_rows": object_rows, "rows": rows,
"columns": column_labels, "columns": column_labels,
"row_actions": row_actions,
"list_actions": list_actions,
} }
return render( return render(