membersystem/src/membership/models.py

114 lines
3.5 KiB
Python
Raw Normal View History

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
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
2023-01-02 21:13:25 +00:00
from utils.mixins import CreatedModifiedAbstract
class Member(User):
class QuerySet(models.QuerySet):
def annotate_membership(self):
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:
raise ValueError("No current subscription period found")
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,
),
),
)
objects = QuerySet.as_manager()
class Meta:
proxy = True
2023-01-02 21:13:25 +00:00
class SubscriptionPeriod(CreatedModifiedAbstract):
2024-02-29 20:25:59 +00:00
"""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"))
class Meta:
2023-01-02 21:13:25 +00:00
constraints = [
ExclusionConstraint(
name="exclude_overlapping_periods",
expressions=[
("period", RangeOperators.OVERLAPS),
],
),
]
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')}"
class Membership(CreatedModifiedAbstract):
2024-02-29 20:25:59 +00:00
"""Tracks that a user has membership of a given type for a given period."""
class QuerySet(models.QuerySet):
def for_member(self, member: Member):
return self.filter(user=member)
def _current(self):
2023-01-02 21:13:25 +00:00
return self.filter(period__period__contains=timezone.now())
def current(self) -> "Membership | None":
try:
return self._current().get()
except self.model.DoesNotExist:
return None
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 list(self.all().difference(self._current()))
objects = QuerySet.as_manager()
class Meta:
verbose_name = _("membership")
verbose_name_plural = _("memberships")
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
membership_type = models.ForeignKey(
"membership.MembershipType",
2019-08-31 22:27:36 +00:00
related_name="memberships",
verbose_name=_("subscription type"),
on_delete=models.PROTECT,
)
2023-01-02 21:13:25 +00:00
period = models.ForeignKey(
"membership.SubscriptionPeriod",
on_delete=models.PROTECT,
2023-01-02 21:13:25 +00:00
)
2024-02-29 20:30:11 +00:00
def __str__(self) -> str:
return f"{self.user} - {self.period}"
class MembershipType(CreatedModifiedAbstract):
2024-02-29 20:25:59 +00:00
"""Models membership types. Currently only a name, but will in the future
possibly contain more information like fees.
"""
class Meta:
verbose_name = _("membership type")
verbose_name_plural = _("membership types")
name = models.CharField(verbose_name=_("name"), max_length=64)
2024-02-29 20:30:11 +00:00
def __str__(self) -> str:
return self.name