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:
|
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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
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.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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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.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",
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
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