from django.contrib.auth.models import User 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 _ from utils.mixins import CreatedModifiedAbstract class Member(User): class QuerySet(models.QuerySet): def annotate_membership(self): 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"), period=current_subscription_period.id, ), ), ) objects = QuerySet.as_manager() class Meta: proxy = True class SubscriptionPeriod(CreatedModifiedAbstract): """Denotes a period for which members should pay their membership fee for.""" period = DateRangeField(verbose_name=_("period")) class Meta: constraints = [ ExclusionConstraint( name="exclude_overlapping_periods", expressions=[ ("period", RangeOperators.OVERLAPS), ], ), ] def __str__(self): return f"{self.period.lower} - {self.period.upper or _('next general assembly')}" class Membership(CreatedModifiedAbstract): """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): 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", related_name="memberships", verbose_name=_("subscription type"), on_delete=models.PROTECT, ) period = models.ForeignKey( "membership.SubscriptionPeriod", on_delete=models.PROTECT, ) def __str__(self): return f"{self.user} - {self.period}" class MembershipType(CreatedModifiedAbstract): """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) def __str__(self): return self.name