Add the start for administration of memberships.

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:
${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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.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,
)

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.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"project.context_processors.current_site",
]
},
}

View file

@ -187,13 +187,13 @@
</li>
</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">
<span>{% trans "Admin" %}</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="#">
<li class="nav-item {% active_path "membership-admin" "active" %}">
<a class="nav-link" href="{% url "membership-admin" %}">
<span data-feather="file-text"></span>
{% trans "Members" %}
</a>

View file

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