From bc00b32c4640b21e9ca6deb24aa172dab4a5ad01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 2 Jan 2023 23:06:00 +0100 Subject: [PATCH] Add the start for administration of memberships. --- Makefile | 2 +- requirements/base.in | 1 + requirements/base.txt | 9 ++-- requirements/dev.txt | 9 ++-- requirements/test.txt | 9 ++-- src/membership/apps.py | 6 +++ src/membership/models.py | 13 ++++-- src/membership/permissions.py | 46 +++++++++++++++++++ src/membership/selectors.py | 26 +++++++++++ .../membership/membership_admin.html | 13 ++++++ .../{ => membership}/membership_overview.html | 0 src/membership/views.py | 30 ++++++++++-- src/project/context_processors.py | 7 --- src/project/settings.py | 1 - src/project/templates/base.html | 6 +-- src/project/urls.py | 2 + src/utils/view_utils.py | 6 +++ 17 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 src/membership/permissions.py create mode 100644 src/membership/selectors.py create mode 100644 src/membership/templates/membership/membership_admin.html rename src/membership/templates/{ => membership}/membership_overview.html (100%) delete mode 100644 src/project/context_processors.py create mode 100644 src/utils/view_utils.py diff --git a/Makefile b/Makefile index 6ff9d94..bfb0bab 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ poetry_lock: poetry_command: ${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} poetry ${COMMAND} -build_docker_image: +build_dev_docker_image: compile_requirements_dev ${DOCKER_COMPOSE} build ${DOCKER_CONTAINER_NAME} compile_requirements: diff --git a/requirements/base.in b/requirements/base.in index 93244ec..a1667aa 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -5,3 +5,4 @@ psycopg2-binary==2.9.5 environs[django]==9.3 uvicorn==0.13 whitenoise==5.2 +django-zen-queries==2.1.0 diff --git a/requirements/base.txt b/requirements/base.txt index bd60189..052d79a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile --output-file=requirements/base.txt requirements/base.in # @@ -22,18 +22,21 @@ dj-database-url==1.0.0 # via environs dj-email-url==1.0.6 # via environs -django==4.1 +django==4.1.5 # via # -r requirements/base.in # dj-database-url # django-allauth # django-money + # django-zen-queries django-allauth==0.46 # via -r requirements/base.in django-cache-url==3.4.2 # via environs django-money==1.3 # via -r requirements/base.in +django-zen-queries==2.1.0 + # via -r requirements/base.in environs[django]==9.3 # via -r requirements/base.in h11==0.14.0 diff --git a/requirements/dev.txt b/requirements/dev.txt index 92d7f98..e3f707e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile --output-file=requirements/dev.txt requirements/dev.in # @@ -48,7 +48,7 @@ dj-email-url==1.0.6 # via # -r requirements/test.txt # environs -django==4.1 +django==4.1.5 # via # -r requirements/test.txt # dj-database-url @@ -59,6 +59,7 @@ django==4.1 # django-money # django-stubs # django-stubs-ext + # django-zen-queries django-allauth==0.46 # via -r requirements/test.txt django-browser-reload==1.6.0 @@ -77,6 +78,8 @@ django-stubs==1.12.0 # via -r requirements/dev.in django-stubs-ext==0.7.0 # via django-stubs +django-zen-queries==2.1.0 + # via -r requirements/test.txt environs[django]==9.3 # via -r requirements/test.txt executing==1.2.0 diff --git a/requirements/test.txt b/requirements/test.txt index 797fb9c..7a21b7b 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile --output-file=requirements/test.txt requirements/test.in # @@ -42,12 +42,13 @@ dj-email-url==1.0.6 # via # -r requirements/base.txt # environs -django==4.1 +django==4.1.5 # via # -r requirements/base.txt # dj-database-url # django-allauth # django-money + # django-zen-queries django-allauth==0.46 # via -r requirements/base.txt django-cache-url==3.4.2 @@ -56,6 +57,8 @@ django-cache-url==3.4.2 # environs django-money==1.3 # via -r requirements/base.txt +django-zen-queries==2.1.0 + # via -r requirements/base.txt environs[django]==9.3 # via -r requirements/base.txt h11==0.14.0 diff --git a/src/membership/apps.py b/src/membership/apps.py index d4f0399..5248df4 100644 --- a/src/membership/apps.py +++ b/src/membership/apps.py @@ -1,5 +1,11 @@ from django.apps import AppConfig +from django.db.models.signals import post_migrate class MembershipConfig(AppConfig): name = "membership" + + def ready(self): + from .permissions import persist_permissions + + post_migrate.connect(persist_permissions, sender=self) diff --git a/src/membership/models.py b/src/membership/models.py index eab7458..c58ef31 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -1,5 +1,3 @@ -from typing import Optional - from django.contrib.postgres.constraints import ExclusionConstraint from django.contrib.postgres.fields import DateRangeField from django.contrib.postgres.fields import RangeOperators @@ -27,6 +25,11 @@ class SubscriptionPeriod(CreatedModifiedAbstract): ), ] + def __str__(self): + return ( + f"{self.period.lower} - {self.period.upper or _('next general assembly')}" + ) + class Membership(CreatedModifiedAbstract): """ @@ -40,17 +43,17 @@ class Membership(CreatedModifiedAbstract): def _current(self): return self.filter(period__period__contains=timezone.now()) - def current(self) -> Optional["Membership"]: + def current(self) -> "Membership | None": try: return self._current().get() except self.model.DoesNotExist: return None - def previous(self): + def previous(self) -> list["Membership"]: # A naïve way to get previous by just excluding the current. This # means that there must be some protection against "future" # memberships. - return self.all().difference(self._current()) + return list(self.all().difference(self._current())) objects = QuerySet.as_manager() diff --git a/src/membership/permissions.py b/src/membership/permissions.py new file mode 100644 index 0000000..4582149 --- /dev/null +++ b/src/membership/permissions.py @@ -0,0 +1,46 @@ +from dataclasses import dataclass + +from django.contrib.auth.models import Permission as DjangoPermission +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext_lazy as _ + +PERMISSIONS = [] + + +def persist_permissions(sender, **kwargs): + for permission in PERMISSIONS: + permission.persist_permission() + + +@dataclass +class Permission: + name: str + codename: str + app_label: str + model: str + + def __post_init__(self, *args, **kwargs): + PERMISSIONS.append(self) + + @property + def path(self): + return f"{self.app_label}.{self.codename}" + + def persist_permission(self): + + content_type, _ = ContentType.objects.get_or_create( + app_label=self.app_label, model=self.model + ) + DjangoPermission.objects.get_or_create( + content_type=content_type, + codename=self.codename, + defaults={"name": self.name}, + ) + + +ADMINISTRATE_MEMBERSHIPS = Permission( + name=_("Can administrate memberships"), + codename="administrate_memberships", + app_label="membership", + model="membership", +) diff --git a/src/membership/selectors.py b/src/membership/selectors.py new file mode 100644 index 0000000..944fa99 --- /dev/null +++ b/src/membership/selectors.py @@ -0,0 +1,26 @@ +from django.contrib.auth.models import User + +from membership.models import Membership +from membership.models import SubscriptionPeriod + + +def get_subscription_periods() -> list[SubscriptionPeriod]: + subscription_periods = SubscriptionPeriod.objects.prefetch_related( + "membership_set", + "membership_set__user", + ).all() + return list(subscription_periods) + + +def get_memberships( + *, user: User | None = None, period: SubscriptionPeriod | None = None +) -> Membership.QuerySet: + memberships = Membership.objects.select_related("membership_type").all() + + if user: + memberships = memberships.for_user(user) + + if period: + memberships = memberships.filter(period=period) + + return memberships diff --git a/src/membership/templates/membership/membership_admin.html b/src/membership/templates/membership/membership_admin.html new file mode 100644 index 0000000..2a67013 --- /dev/null +++ b/src/membership/templates/membership/membership_admin.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + + {% for period in subscription_periods %} +

{{ period }}

+ {% for membership in period.membership_set.all %} +

{{ membership.user.username }}

+ {% endfor %} + {% endfor %} + +{% endblock %} diff --git a/src/membership/templates/membership_overview.html b/src/membership/templates/membership/membership_overview.html similarity index 100% rename from src/membership/templates/membership_overview.html rename to src/membership/templates/membership/membership_overview.html diff --git a/src/membership/views.py b/src/membership/views.py index a0ed1b9..7bcc74a 100644 --- a/src/membership/views.py +++ b/src/membership/views.py @@ -1,18 +1,22 @@ from django.contrib.auth.decorators import login_required -from django.shortcuts import render +from django.contrib.auth.decorators import permission_required +from zen_queries import render -from .models import Membership +from .permissions import ADMINISTRATE_MEMBERSHIPS +from .selectors import get_memberships +from .selectors import get_subscription_periods +from utils.view_utils import base_view_context @login_required def membership_overview(request): - memberships = Membership.objects.for_user(request.user) + memberships = get_memberships(user=request.user) current_membership = memberships.current() previous_memberships = memberships.previous() current_period = current_membership.period.period if current_membership else None - context = { + context = base_view_context(request) | { "current_membership": current_membership, "current_period": current_period, "previous_memberships": previous_memberships, @@ -20,6 +24,22 @@ def membership_overview(request): return render( request=request, - template_name="membership_overview.html", + template_name="membership/membership_overview.html", + context=context, + ) + + +@login_required +@permission_required(ADMINISTRATE_MEMBERSHIPS.path) +def membership_admin(request): + subscription_periods = get_subscription_periods() + + context = base_view_context(request) | { + "subscription_periods": subscription_periods, + } + + return render( + request=request, + template_name="membership/membership_admin.html", context=context, ) diff --git a/src/project/context_processors.py b/src/project/context_processors.py deleted file mode 100644 index 7e80076..0000000 --- a/src/project/context_processors.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Context processors for the membersystem app.""" -from django.contrib.sites.shortcuts import get_current_site - - -def current_site(request): - """Include the current site in the context.""" - return {"site": get_current_site(request)} diff --git a/src/project/settings.py b/src/project/settings.py index d6a0f96..a9f9763 100644 --- a/src/project/settings.py +++ b/src/project/settings.py @@ -68,7 +68,6 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", - "project.context_processors.current_site", ] }, } diff --git a/src/project/templates/base.html b/src/project/templates/base.html index ac030a6..88eb3ae 100644 --- a/src/project/templates/base.html +++ b/src/project/templates/base.html @@ -187,13 +187,13 @@ - {% if user.is_staff %} + {% if perms.membership.administrate_memberships %}