Source code for activemembers.models

"""The models defined by the activemembers package"""
import datetime
import logging

from django.conf import settings
from django.contrib.auth.models import Permission
from django.core.exceptions import (
    NON_FIELD_ERRORS,
    ValidationError,
)

from django.core.validators import MinValueValidator
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from tinymce import HTMLField

from utils.snippets import overlaps
from utils.translation import ModelTranslateMeta, MultilingualField, localize_attr_name

logger = logging.getLogger(__name__)


[docs]class ActiveMemberGroupManager(models.Manager): """Returns active objects only sorted by the localized name"""
[docs] def get_queryset(self): return ( super() .get_queryset() .exclude(active=False) .order_by(localize_attr_name("name")) )
[docs]class MemberGroup(models.Model, metaclass=ModelTranslateMeta): """Describes a groups of members""" objects = models.Manager() active_objects = ActiveMemberGroupManager() name = MultilingualField( models.CharField, max_length=40, verbose_name=_("Name"), unique=True, ) description = MultilingualField(HTMLField, verbose_name=_("Description"),) photo = models.ImageField( verbose_name=_("Image"), upload_to="public/committeephotos/", null=True, blank=True, ) members = models.ManyToManyField( "members.Member", through="activemembers.MemberGroupMembership" ) permissions = models.ManyToManyField( Permission, verbose_name=_("permissions"), blank=True, ) since = models.DateField(_("founded in"), null=True, blank=True,) until = models.DateField(_("existed until"), null=True, blank=True,) active = models.BooleanField( default=False, help_text=_( "This should only be unchecked if the committee has been " "dissolved. The websites assumes that any committees on it" " existed at some point." ), ) contact_email = models.EmailField( _("contact email address"), blank=True, null=True, ) contact_mailinglist = models.OneToOneField( "mailinglists.MailingList", verbose_name=_("contact mailing list"), null=True, blank=True, on_delete=models.SET_NULL, ) display_members = models.BooleanField(default=False,) @property def contact_address(self): if self.contact_mailinglist: return f"{self.contact_mailinglist.name}@{settings.SITE_DOMAIN}" return self.contact_email
[docs] def clean(self): if ( self.contact_email is not None and self.contact_mailinglist is not None ) or (self.contact_email is None and self.contact_mailinglist is None): raise ValidationError( { "contact_email": _( "Please use either the mailing list or email address option." ), "contact_mailinglist": _( "Please use either the mailing list or email address option." ), } )
def __str__(self): return self.name
[docs] def get_absolute_url(self): try: return self.board.get_absolute_url() except self.DoesNotExist: try: return self.committee.get_absolute_url() except self.DoesNotExist: try: return self.society.get_absolute_url() except self.DoesNotExist: pass
class Meta: verbose_name = _("member group") verbose_name_plural = _("member groups")
# ordering is done in the manager, to sort on a translated field
[docs]class Committee(MemberGroup): """Describes a committee, which is a type of MemberGroup""" objects = models.Manager() active_objects = ActiveMemberGroupManager()
[docs] def get_absolute_url(self): return reverse("activemembers:committee", args=[str(self.pk)])
class Meta: verbose_name = _("committee") verbose_name_plural = _("committees")
# ordering is done in the manager, to sort on a translated field
[docs]class Society(MemberGroup): """Describes a society, which is a type of MemberGroup""" objects = models.Manager() active_objects = ActiveMemberGroupManager()
[docs] def get_absolute_url(self): return reverse("activemembers:society", args=[str(self.pk)])
class Meta: verbose_name = _("society") verbose_name_plural = _("societies")
# ordering is done in the manager, to sort on a translated field
[docs]class Board(MemberGroup): """Describes a board, which is a type of MemberGroup""" class Meta: verbose_name = _("board") verbose_name_plural = _("boards") ordering = ["-since"]
[docs] def save(self, *args, **kwargs): self.active = True super().save(*args, **kwargs)
[docs] def get_absolute_url(self): return reverse( "activemembers:board", args=[str(self.since.year), str(self.until.year)] )
[docs] def validate_unique(self, *args, **kwargs): super().validate_unique(*args, **kwargs) boards = Board.objects.all() if self.since is not None: if overlaps(self, boards, can_equal=False): raise ValidationError( { "since": _("A board already exists for those years"), "until": _("A board already exists for those years"), } )
[docs]class ActiveMembershipManager(models.Manager): """ Custom manager that gets the currently active membergroup memberships """
[docs] def get_queryset(self): return super().get_queryset().exclude(until__lt=timezone.now().date())
[docs]class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta): """Describes a group membership""" objects = models.Manager() active_objects = ActiveMembershipManager() member = models.ForeignKey( "members.Member", on_delete=models.CASCADE, verbose_name=_("Member"), ) group = models.ForeignKey( MemberGroup, on_delete=models.CASCADE, verbose_name=_("Group"), ) since = models.DateField( verbose_name=_("Member since"), help_text=_("The date this member joined in this role"), default=datetime.date.today, ) until = models.DateField( verbose_name=_("Member until"), help_text=_("A member until this time (can't be in the future)."), blank=True, null=True, ) chair = models.BooleanField( verbose_name=_("Chair of the group"), help_text=_("There can only be one chair at a time!"), default=False, ) role = MultilingualField( models.CharField, _("role"), help_text=_("The role of this member"), max_length=255, blank=True, null=True, ) @property def initial_connected_membership(self): """Find the oldest membership directly connected to the current one""" qs = MemberGroupMembership.objects.filter( group=self.group, member=self.member, until__lte=self.since, until__gte=self.since - datetime.timedelta(days=1), ) if qs.count() >= 1: # should only be one; should be unique return qs.first().initial_connected_membership else: return self @property def latest_connected_membership(self): """ Find the newest membership directly connected to the current one (thus the membership that started at the moment the current one ended) """ if self.until: qs = MemberGroupMembership.objects.filter( group=self.group, member=self.member, since__lte=self.until, since__gte=self.until + datetime.timedelta(days=1), ) if qs.count() >= 1: # should only be one; should be unique return qs.last().latest_connected_membership return self @property def is_active(self): """Is this membership currently active""" return self.until is None or self.until > timezone.now().date()
[docs] def clean(self): try: if self.until and (not self.since or self.until < self.since): raise ValidationError( {"until": _("End date can't be before start date")} ) if self.until and self.until > timezone.now().date(): raise ValidationError({"until": _("End date can't be in the future")}) if self.since and self.group.since and self.since < self.group.since: raise ValidationError( {"since": _("Start date can't be before group start date")} ) if self.since and self.group.until and self.since > self.group.until: raise ValidationError( {"since": _("Start date can't be after group end date")} ) except MemberGroupMembership.group.RelatedObjectDoesNotExist: return False
[docs] def validate_unique(self, *args, **kwargs): try: super().validate_unique(*args, **kwargs) # Check if a group has more than one chair if self.chair: chairs = MemberGroupMembership.objects.filter( group=self.group, chair=True ) if overlaps(self, chairs): raise ValidationError( { NON_FIELD_ERRORS: _( "There already is a chair for this time period" ) } ) # check if this member is already in the group in this period memberships = MemberGroupMembership.objects.filter( group=self.group, member=self.member ) if overlaps(self, memberships): raise ValidationError( {"member": _("This member is already in the group for this period")} ) except ( MemberGroupMembership.member.RelatedObjectDoesNotExist, MemberGroupMembership.group.RelatedObjectDoesNotExist, ): return False
[docs] def save(self, *args, **kwargs): super().save(*args, **kwargs) self.member.is_staff = ( self.member.membergroupmembership_set.exclude( until__lte=timezone.now().date() ).count() ) >= 1 self.member.save()
def __str__(self): return _("{member} membership of {group} since {since}, until {until}").format( member=self.member, group=self.group, since=self.since, until=self.until ) class Meta: verbose_name = _("group membership") verbose_name_plural = _("group memberships")
[docs]class Mentorship(models.Model): """Describe a mentorship during the orientation""" member = models.ForeignKey( "members.Member", on_delete=models.CASCADE, verbose_name=_("Member"), ) year = models.IntegerField(validators=[MinValueValidator(1990)]) def __str__(self): return _("{name} mentor in {year}").format(name=self.member, year=self.year) class Meta: unique_together = ("member", "year")