forked from data.coop/membersystem
Implement a generic way to adding actions to rows in a list.
This commit is contained in:
parent
cf99c3f40e
commit
704b196128
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{{ member.username }}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue