forked from data.coop/membersystem
Add the start for administration of memberships.
This commit is contained in:
parent
01715a7704
commit
bc00b32c46
2
Makefile
2
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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
46
src/membership/permissions.py
Normal file
46
src/membership/permissions.py
Normal 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",
|
||||
)
|
26
src/membership/selectors.py
Normal file
26
src/membership/selectors.py
Normal 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
|
13
src/membership/templates/membership/membership_admin.html
Normal file
13
src/membership/templates/membership/membership_admin.html
Normal 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 %}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)}
|
|
@ -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",
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
6
src/utils/view_utils.py
Normal 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)}
|
Loading…
Reference in a new issue