Add the start for administration of memberships.
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Víðir Valberg Guðmundsson 2023-01-02 23:06:00 +01:00
parent 01715a7704
commit bc00b32c46
17 changed files with 155 additions and 31 deletions

View File

@ -45,7 +45,7 @@ poetry_lock:
poetry_command: poetry_command:
${DOCKER_RUN} ${DOCKER_CONTAINER_NAME} 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} ${DOCKER_COMPOSE} build ${DOCKER_CONTAINER_NAME}
compile_requirements: compile_requirements:

View File

@ -5,3 +5,4 @@ psycopg2-binary==2.9.5
environs[django]==9.3 environs[django]==9.3
uvicorn==0.13 uvicorn==0.13
whitenoise==5.2 whitenoise==5.2
django-zen-queries==2.1.0

View File

@ -1,6 +1,6 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.10
# To update, run: # by the following command:
# #
# pip-compile --output-file=requirements/base.txt requirements/base.in # pip-compile --output-file=requirements/base.txt requirements/base.in
# #
@ -22,18 +22,21 @@ dj-database-url==1.0.0
# via environs # via environs
dj-email-url==1.0.6 dj-email-url==1.0.6
# via environs # via environs
django==4.1 django==4.1.5
# via # via
# -r requirements/base.in # -r requirements/base.in
# dj-database-url # dj-database-url
# django-allauth # django-allauth
# django-money # django-money
# django-zen-queries
django-allauth==0.46 django-allauth==0.46
# via -r requirements/base.in # via -r requirements/base.in
django-cache-url==3.4.2 django-cache-url==3.4.2
# via environs # via environs
django-money==1.3 django-money==1.3
# via -r requirements/base.in # via -r requirements/base.in
django-zen-queries==2.1.0
# via -r requirements/base.in
environs[django]==9.3 environs[django]==9.3
# via -r requirements/base.in # via -r requirements/base.in
h11==0.14.0 h11==0.14.0

View File

@ -1,6 +1,6 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.10
# To update, run: # by the following command:
# #
# pip-compile --output-file=requirements/dev.txt requirements/dev.in # pip-compile --output-file=requirements/dev.txt requirements/dev.in
# #
@ -48,7 +48,7 @@ dj-email-url==1.0.6
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# environs # environs
django==4.1 django==4.1.5
# via # via
# -r requirements/test.txt # -r requirements/test.txt
# dj-database-url # dj-database-url
@ -59,6 +59,7 @@ django==4.1
# django-money # django-money
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# django-zen-queries
django-allauth==0.46 django-allauth==0.46
# via -r requirements/test.txt # via -r requirements/test.txt
django-browser-reload==1.6.0 django-browser-reload==1.6.0
@ -77,6 +78,8 @@ django-stubs==1.12.0
# via -r requirements/dev.in # via -r requirements/dev.in
django-stubs-ext==0.7.0 django-stubs-ext==0.7.0
# via django-stubs # via django-stubs
django-zen-queries==2.1.0
# via -r requirements/test.txt
environs[django]==9.3 environs[django]==9.3
# via -r requirements/test.txt # via -r requirements/test.txt
executing==1.2.0 executing==1.2.0

View File

@ -1,6 +1,6 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.10
# To update, run: # by the following command:
# #
# pip-compile --output-file=requirements/test.txt requirements/test.in # pip-compile --output-file=requirements/test.txt requirements/test.in
# #
@ -42,12 +42,13 @@ dj-email-url==1.0.6
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# environs # environs
django==4.1 django==4.1.5
# via # via
# -r requirements/base.txt # -r requirements/base.txt
# dj-database-url # dj-database-url
# django-allauth # django-allauth
# django-money # django-money
# django-zen-queries
django-allauth==0.46 django-allauth==0.46
# via -r requirements/base.txt # via -r requirements/base.txt
django-cache-url==3.4.2 django-cache-url==3.4.2
@ -56,6 +57,8 @@ django-cache-url==3.4.2
# environs # environs
django-money==1.3 django-money==1.3
# via -r requirements/base.txt # via -r requirements/base.txt
django-zen-queries==2.1.0
# via -r requirements/base.txt
environs[django]==9.3 environs[django]==9.3
# via -r requirements/base.txt # via -r requirements/base.txt
h11==0.14.0 h11==0.14.0

View File

@ -1,5 +1,11 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.db.models.signals import post_migrate
class MembershipConfig(AppConfig): class MembershipConfig(AppConfig):
name = "membership" name = "membership"
def ready(self):
from .permissions import persist_permissions
post_migrate.connect(persist_permissions, sender=self)

View File

@ -1,5 +1,3 @@
from typing import Optional
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
@ -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): class Membership(CreatedModifiedAbstract):
""" """
@ -40,17 +43,17 @@ class Membership(CreatedModifiedAbstract):
def _current(self): def _current(self):
return self.filter(period__period__contains=timezone.now()) return self.filter(period__period__contains=timezone.now())
def current(self) -> Optional["Membership"]: def current(self) -> "Membership | None":
try: try:
return self._current().get() return self._current().get()
except self.model.DoesNotExist: except self.model.DoesNotExist:
return None return None
def previous(self): def previous(self) -> list["Membership"]:
# A naïve way to get previous by just excluding the current. This # A naïve way to get previous by just excluding the current. This
# means that there must be some protection against "future" # means that there must be some protection against "future"
# memberships. # memberships.
return self.all().difference(self._current()) return list(self.all().difference(self._current()))
objects = QuerySet.as_manager() objects = QuerySet.as_manager()

View File

@ -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",
)

View File

@ -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

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% for period in subscription_periods %}
<h3>{{ period }}</h3>
{% for membership in period.membership_set.all %}
<p>{{ membership.user.username }}</p>
{% endfor %}
{% endfor %}
{% endblock %}

View File

@ -1,18 +1,22 @@
from django.contrib.auth.decorators import login_required 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 @login_required
def membership_overview(request): def membership_overview(request):
memberships = Membership.objects.for_user(request.user) memberships = get_memberships(user=request.user)
current_membership = memberships.current() current_membership = memberships.current()
previous_memberships = memberships.previous() previous_memberships = memberships.previous()
current_period = current_membership.period.period if current_membership else None current_period = current_membership.period.period if current_membership else None
context = { context = base_view_context(request) | {
"current_membership": current_membership, "current_membership": current_membership,
"current_period": current_period, "current_period": current_period,
"previous_memberships": previous_memberships, "previous_memberships": previous_memberships,
@ -20,6 +24,22 @@ def membership_overview(request):
return render( return render(
request=request, 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, context=context,
) )

View File

@ -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)}

View File

@ -68,7 +68,6 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"project.context_processors.current_site",
] ]
}, },
} }

View File

@ -187,13 +187,13 @@
</li> </li>
</ul> </ul>
{% if user.is_staff %} {% if perms.membership.administrate_memberships %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted"> <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Admin" %}</span> <span>{% trans "Admin" %}</span>
</h6> </h6>
<ul class="nav flex-column mb-2"> <ul class="nav flex-column mb-2">
<li class="nav-item"> <li class="nav-item {% active_path "membership-admin" "active" %}">
<a class="nav-link" href="#"> <a class="nav-link" href="{% url "membership-admin" %}">
<span data-feather="file-text"></span> <span data-feather="file-text"></span>
{% trans "Members" %} {% trans "Members" %}
</a> </a>

View File

@ -7,12 +7,14 @@ 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 membership_admin
from membership.views import membership_overview from membership.views import membership_overview
urlpatterns = [ urlpatterns = [
path("", login_required(index), name="index"), path("", login_required(index), name="index"),
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("membership/admin/", membership_admin, name="membership-admin"),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] ]

6
src/utils/view_utils.py Normal file
View File

@ -0,0 +1,6 @@
from django.contrib.sites.shortcuts import get_current_site as django_get_current_site
def base_view_context(request):
"""Include the current site in the context."""
return {"site": django_get_current_site(request)}