2024-07-14 22:19:37 +00:00
|
|
|
"""Models for the membership app."""
|
|
|
|
|
|
|
|
from typing import ClassVar
|
|
|
|
from typing import Self
|
|
|
|
|
2023-01-03 20:36:34 +00:00
|
|
|
from django.contrib.auth.models import User
|
2023-01-02 21:13:25 +00:00
|
|
|
from django.contrib.postgres.constraints import ExclusionConstraint
|
|
|
|
from django.contrib.postgres.fields import DateRangeField
|
|
|
|
from django.contrib.postgres.fields import RangeOperators
|
2018-06-23 19:08:56 +00:00
|
|
|
from django.db import models
|
2021-02-28 22:00:11 +00:00
|
|
|
from django.utils import timezone
|
2018-06-23 19:08:56 +00:00
|
|
|
from django.utils.translation import gettext as _
|
2023-01-02 21:13:25 +00:00
|
|
|
from utils.mixins import CreatedModifiedAbstract
|
2018-06-23 19:08:56 +00:00
|
|
|
|
|
|
|
|
2024-07-14 22:19:37 +00:00
|
|
|
class NoSubscriptionPeriodFoundError(Exception):
|
|
|
|
"""Raised when no subscription period is found."""
|
|
|
|
|
|
|
|
|
2023-01-03 20:36:34 +00:00
|
|
|
class Member(User):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""Proxy model for the User model to add some convenience methods."""
|
|
|
|
|
2023-01-03 20:36:34 +00:00
|
|
|
class QuerySet(models.QuerySet):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""QuerySet for the Member model."""
|
|
|
|
|
|
|
|
def annotate_membership(self) -> Self:
|
|
|
|
"""Annotate whether the user has an active membership."""
|
2023-09-18 18:58:30 +00:00
|
|
|
from .selectors import get_current_subscription_period
|
|
|
|
|
|
|
|
current_subscription_period = get_current_subscription_period()
|
|
|
|
|
|
|
|
if not current_subscription_period:
|
2024-07-14 22:19:37 +00:00
|
|
|
raise NoSubscriptionPeriodFoundError
|
2023-01-03 20:36:34 +00:00
|
|
|
|
|
|
|
return self.annotate(
|
|
|
|
active_membership=models.Exists(
|
|
|
|
Membership.objects.filter(
|
|
|
|
user=models.OuterRef("pk"),
|
2023-09-18 18:58:30 +00:00
|
|
|
period=current_subscription_period.id,
|
2023-01-11 20:55:58 +00:00
|
|
|
),
|
|
|
|
),
|
2023-01-03 20:36:34 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
objects = QuerySet.as_manager()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
proxy = True
|
|
|
|
|
|
|
|
|
2023-01-02 21:13:25 +00:00
|
|
|
class SubscriptionPeriod(CreatedModifiedAbstract):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""A subscription period.
|
|
|
|
|
|
|
|
Denotes a period for which members should pay their membership fee for.
|
|
|
|
"""
|
2023-01-02 21:13:25 +00:00
|
|
|
|
|
|
|
period = DateRangeField(verbose_name=_("period"))
|
2018-06-23 19:08:56 +00:00
|
|
|
|
|
|
|
class Meta:
|
2024-07-14 22:19:37 +00:00
|
|
|
constraints: ClassVar = [
|
2023-01-02 21:13:25 +00:00
|
|
|
ExclusionConstraint(
|
|
|
|
name="exclude_overlapping_periods",
|
|
|
|
expressions=[
|
|
|
|
("period", RangeOperators.OVERLAPS),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
]
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2024-02-29 20:30:11 +00:00
|
|
|
def __str__(self) -> str:
|
2024-02-29 20:25:59 +00:00
|
|
|
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
2023-01-02 22:06:00 +00:00
|
|
|
|
2018-06-23 19:08:56 +00:00
|
|
|
|
|
|
|
class Membership(CreatedModifiedAbstract):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""A membership.
|
|
|
|
|
|
|
|
Tracks that a user has membership of a given type for a given period.
|
|
|
|
"""
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2021-02-28 22:00:11 +00:00
|
|
|
class QuerySet(models.QuerySet):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""QuerySet for the Membership model."""
|
|
|
|
|
|
|
|
def for_member(self, member: Member) -> Self:
|
|
|
|
"""Filter memberships for a given member."""
|
2023-01-14 22:33:58 +00:00
|
|
|
return self.filter(user=member)
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2024-07-14 22:19:37 +00:00
|
|
|
def _current(self) -> Self:
|
|
|
|
"""Filter memberships for the current period."""
|
2023-01-02 21:13:25 +00:00
|
|
|
return self.filter(period__period__contains=timezone.now())
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2023-01-02 22:06:00 +00:00
|
|
|
def current(self) -> "Membership | None":
|
2024-07-14 22:19:37 +00:00
|
|
|
"""Get the current membership."""
|
2021-02-28 22:00:11 +00:00
|
|
|
try:
|
|
|
|
return self._current().get()
|
|
|
|
except self.model.DoesNotExist:
|
|
|
|
return None
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2023-01-02 22:06:00 +00:00
|
|
|
def previous(self) -> list["Membership"]:
|
2024-07-14 22:19:37 +00:00
|
|
|
"""Get previous memberships."""
|
2021-02-28 22:00:11 +00:00
|
|
|
# A naïve way to get previous by just excluding the current. This
|
|
|
|
# means that there must be some protection against "future"
|
|
|
|
# memberships.
|
2023-01-02 22:06:00 +00:00
|
|
|
return list(self.all().difference(self._current()))
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2021-02-28 22:00:11 +00:00
|
|
|
objects = QuerySet.as_manager()
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2021-02-28 22:00:11 +00:00
|
|
|
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2021-02-28 22:00:11 +00:00
|
|
|
membership_type = models.ForeignKey(
|
|
|
|
"membership.MembershipType",
|
2019-08-31 22:27:36 +00:00
|
|
|
related_name="memberships",
|
2018-06-23 19:08:56 +00:00
|
|
|
verbose_name=_("subscription type"),
|
|
|
|
on_delete=models.PROTECT,
|
|
|
|
)
|
|
|
|
|
2023-01-02 21:13:25 +00:00
|
|
|
period = models.ForeignKey(
|
2023-01-11 20:55:58 +00:00
|
|
|
"membership.SubscriptionPeriod",
|
|
|
|
on_delete=models.PROTECT,
|
2023-01-02 21:13:25 +00:00
|
|
|
)
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2024-07-14 22:19:37 +00:00
|
|
|
class Meta:
|
|
|
|
verbose_name = _("membership")
|
|
|
|
verbose_name_plural = _("memberships")
|
|
|
|
|
2024-02-29 20:30:11 +00:00
|
|
|
def __str__(self) -> str:
|
2021-02-28 22:00:11 +00:00
|
|
|
return f"{self.user} - {self.period}"
|
|
|
|
|
|
|
|
|
|
|
|
class MembershipType(CreatedModifiedAbstract):
|
2024-07-14 22:19:37 +00:00
|
|
|
"""A membership type.
|
|
|
|
|
|
|
|
Models membership types. Currently only a name, but will in the future
|
2021-02-28 22:00:11 +00:00
|
|
|
possibly contain more information like fees.
|
|
|
|
"""
|
2018-06-23 19:08:56 +00:00
|
|
|
|
2024-07-14 22:19:37 +00:00
|
|
|
name = models.CharField(verbose_name=_("name"), max_length=64)
|
|
|
|
|
2024-07-21 11:41:50 +00:00
|
|
|
product = models.ForeignKey("accounting.Product", on_delete=models.PROTECT)
|
|
|
|
|
|
|
|
current = models.BooleanField(default=False)
|
|
|
|
|
2018-06-23 19:08:56 +00:00
|
|
|
class Meta:
|
2021-02-28 22:00:11 +00:00
|
|
|
verbose_name = _("membership type")
|
|
|
|
verbose_name_plural = _("membership types")
|
|
|
|
|
2024-02-29 20:30:11 +00:00
|
|
|
def __str__(self) -> str:
|
2021-02-28 22:00:11 +00:00
|
|
|
return self.name
|
2024-07-21 11:41:50 +00:00
|
|
|
|
|
|
|
def create_membership(self, user: User) -> Membership:
|
|
|
|
"""Create a current membership for this type."""
|
|
|
|
from .selectors import get_current_subscription_period
|
|
|
|
|
|
|
|
return Membership.objects.create(
|
|
|
|
membership_type=self,
|
|
|
|
user=user,
|
|
|
|
period=get_current_subscription_period(),
|
|
|
|
)
|