diff --git a/pyproject.toml b/pyproject.toml index 6a52861..a6dc9c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "django-registries==0.0.3", "django-view-decorator==0.0.4", "django-oauth-toolkit==2.4.0", + "django_stubs_ext", ] version = "0.0.1" @@ -104,10 +105,10 @@ show_error_codes = true strict = true warn_unreachable = true follow_imports = "normal" -#plugins = ["mypy_django_plugin.main"] +plugins = ["mypy_django_plugin.main"] [tool.django-stubs] -#django_settings_module = "tests.settings" +django_settings_module = "project.settings" [[tool.mypy.overrides]] module = "tests.*" diff --git a/src/membership/admin.py b/src/membership/admin.py index 8224ff6..2d391a5 100644 --- a/src/membership/admin.py +++ b/src/membership/admin.py @@ -1,8 +1,14 @@ """Admin configuration for membership app.""" +from collections.abc import Callable + from django.contrib import admin +from django.contrib.admin import ModelAdmin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User +from django.db.models import QuerySet +from django.http import HttpRequest +from django.http import HttpResponse from . import models @@ -31,11 +37,39 @@ class MembershipInlineAdmin(admin.TabularInline): model = models.Membership +def decorate_ensure_membership_type_exists(membership_type: models.MembershipType, label: str) -> Callable: + """Generate an admin action for given membership type and label.""" + + @admin.action(description=label) + def admin_action(modeladmin: ModelAdmin, request: HttpRequest, queryset: QuerySet) -> HttpResponse: # noqa: ARG001 + return ensure_membership_type_exists(request, queryset, membership_type) + + return admin_action + + +def ensure_membership_type_exists( + request: HttpRequest, # noqa: ARG001 + queryset: QuerySet, # noqa: ARG001 + membership_type: models.MembershipType, # noqa: ARG001 +) -> HttpResponse: + """Inner function that ensures that a membership exists for a given queryset of Member objects.""" + + @admin.register(models.Member) class MemberAdmin(UserAdmin): """Member admin is actually an admin for User objects.""" inlines = (MembershipInlineAdmin,) + actions: list[Callable] = [] # noqa: RUF012 + + def get_actions(self, request: HttpRequest) -> dict: + """Populate actions with dynamic data (MembershipType).""" + current_period = models.SubscriptionPeriod.objects.current() + if current_period: + for mtype in models.MembershipType.objects.filter(active=True): + action_label = f"Ensure membership {mtype.name}, {current_period.period}, {mtype.total_including_vat}" + self.actions.append(decorate_ensure_membership_type_exists(mtype, action_label)) + return super().get_actions(request) @admin.register(models.WaitingListEntry) diff --git a/src/membership/models.py b/src/membership/models.py index e8cce22..23ac4fc 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -10,6 +10,7 @@ from django.contrib.postgres.fields import RangeOperators from django.db import models from django.utils import timezone from django.utils.translation import gettext as _ +from djmoney.money import Money from utils.mixins import CreatedModifiedAbstract @@ -53,6 +54,22 @@ class SubscriptionPeriod(CreatedModifiedAbstract): Denotes a period for which members should pay their membership fee for. """ + class QuerySet(models.QuerySet): + """QuerySet for the Membership model.""" + + def _current(self) -> Self: + """Filter memberships for the current period.""" + return self.filter(period__contains=timezone.now()) + + def current(self) -> "Membership | None": + """Get the current membership.""" + try: + return self._current().get() + except self.model.DoesNotExist: + return None + + objects = QuerySet.as_manager() + period = DateRangeField(verbose_name=_("period")) class Meta: @@ -179,6 +196,11 @@ class MembershipType(CreatedModifiedAbstract): period=get_current_subscription_period(), ) + @property + def total_including_vat(self) -> Money: + """Calculate the total price of this membership (including VAT).""" + return sum(product.price + product.vat for product in self.products.all()) + class WaitingListEntry(CreatedModifiedAbstract): """People who for some reason could want to be added to a waiting list and invited to join later.""" diff --git a/src/project/settings.py b/src/project/settings.py index 9cf846d..21018c0 100644 --- a/src/project/settings.py +++ b/src/project/settings.py @@ -2,9 +2,12 @@ from pathlib import Path +import django_stubs_ext from django.utils.translation import gettext_lazy as _ from environs import Env +django_stubs_ext.monkeypatch() + env = Env() env.read_env()