notes-ker-lann/apps/member/models.py
2022-07-18 20:16:12 +02:00

468 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
import os
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models import Q
from django.template import loader
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from permission.models import Role
from registration.tokens import email_validation_token
from note.models import MembershipTransaction
class Profile(models.Model):
"""
An user profile
We do not want to patch the Django Contrib :model:`auth.User`model;
so this model add an user profile with additional information.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
phone_number = PhoneNumberField(
verbose_name=_('phone number'),
max_length=50,
blank=True,
null=True,
)
section = models.CharField(
verbose_name=_('section'),
help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'),
max_length=255,
blank=True,
default="",
)
department = models.CharField(
max_length=8,
verbose_name=_("department"),
choices=[
('INFO', _("Informatique")),
('MATH', _("Mathématiques")),
('DEM', _("Droit - économie - management")),
('MECATRO', _("Mécatronique")),
('2SEP', _("Sciences du sport et éducation physique")),
('SE', _("Sciences pour lEnvironnement")),
('EXT', _("Externe")),
]
)
promotion = models.PositiveSmallIntegerField(
null=True,
default=datetime.date.today().year if datetime.date.today().month >= 8 else datetime.date.today().year - 1,
verbose_name=_("promotion"),
help_text=_("Year of entry to the school (None if not ENS student)"),
)
address = models.CharField(
verbose_name=_('address'),
max_length=255,
blank=True,
default="",
)
paid = models.BooleanField(
verbose_name=_("paid"),
help_text=_("Tells if the user receive a salary."),
default=False,
)
ml_events_registration = models.CharField(
blank=True,
default='',
max_length=2,
choices=[
('', _("No")),
('fr', _("Yes")),
],
verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"),
)
ml_sport_registration = models.BooleanField(
default=False,
verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"),
)
ml_art_registration = models.BooleanField(
default=False,
verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"),
)
report_frequency = models.PositiveSmallIntegerField(
verbose_name=_("report frequency (in days)"),
default=0,
)
last_report = models.DateTimeField(
verbose_name=_("last report date"),
default=timezone.now,
)
email_confirmed = models.BooleanField(
verbose_name=_("email confirmed"),
default=False,
)
registration_valid = models.BooleanField(
verbose_name=_("registration valid"),
default=False,
)
@property
def ens_year(self):
"""
Number of years since the 1st august of the entry year, rounded up.
"""
if self.promotion is None:
return 0
today = datetime.date.today()
years = today.year - self.promotion
if today.month >= 8:
years += 1
return years
@property
def section_generated(self):
return str(self.ens_year) + self.department
class Meta:
verbose_name = _('user profile')
verbose_name_plural = _('user profile')
indexes = [models.Index(fields=['user'])]
def get_absolute_url(self):
return reverse('member:user_detail', args=(self.user_id,))
def __str__(self):
return str(self.user)
def send_email_validation_link(self):
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
token = email_validation_token.make_token(self.user)
uid = urlsafe_base64_encode(force_bytes(self.user_id))
message = loader.render_to_string('registration/mails/email_validation_email.txt',
{
'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"),
'token': token,
'uid': uid,
})
html = loader.render_to_string('registration/mails/email_validation_email.html',
{
'user': self.user,
'domain': os.getenv("NOTE_URL", "note.example.com"),
'token': token,
'uid': uid,
})
self.user.email_user(subject, message, html_message=html)
class Club(models.Model):
"""
A club is a group of people, whose membership is handle by their
:model:`member.Membership`, and gives access to right defined by a :model:`member.Role`.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
unique=True,
)
email = models.EmailField(
verbose_name=_('email'),
)
parent_club = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.PROTECT,
verbose_name=_('parent club'),
)
# Memberships
# When set to False, the membership system won't be used.
# Useful to create notes for activities or departments.
require_memberships = models.BooleanField(
default=True,
verbose_name=_("require memberships"),
help_text=_("Uncheck if this club don't require memberships."),
)
membership_fee_paid = models.PositiveIntegerField(
default=0,
verbose_name=_('membership fee (paid students)'),
)
membership_fee_unpaid = models.PositiveIntegerField(
default=0,
verbose_name=_('membership fee (unpaid students)'),
)
membership_duration = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name=_('membership duration'),
help_text=_('The longest time (in days) a membership can last '
'(NULL = infinite).'),
)
membership_start = models.DateField(
blank=True,
null=True,
verbose_name=_('membership start'),
help_text=_('Date from which the members can renew their membership.'),
)
membership_end = models.DateField(
blank=True,
null=True,
verbose_name=_('membership end'),
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
def update_membership_dates(self):
"""
This function is called each time the club detail view is displayed.
Update the year of the membership dates.
"""
if not self.membership_start or not self.membership_end:
return
today = datetime.date.today()
if (today - self.membership_start).days >= 365:
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
if self.membership_end:
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self._force_save = True
self.save(force_update=True)
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if not self.require_memberships:
self.membership_fee_paid = 0
self.membership_fee_unpaid = 0
self.membership_duration = None
self.membership_start = None
self.membership_end = None
super().save(force_insert, force_update, update_fields)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,))
class Membership(models.Model):
"""
Register the membership of a user to a club, including roles and membership duration.
"""
user = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name="memberships",
verbose_name=_("user"),
)
club = models.ForeignKey(
Club,
on_delete=models.PROTECT,
verbose_name=_("club"),
)
roles = models.ManyToManyField(
"permission.Role",
related_name="memberships",
verbose_name=_("roles"),
)
date_start = models.DateField(
default=datetime.date.today,
verbose_name=_('membership starts on'),
)
date_end = models.DateField(
verbose_name=_('membership ends on'),
null=True,
)
fee = models.PositiveIntegerField(
verbose_name=_('fee'),
)
@property
def valid(self):
"""
A membership is valid if today is between the start and the end date.
"""
if self.date_end is not None:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal()
else:
return self.date_start.toordinal() <= datetime.datetime.now().toordinal()
def renew(self):
"""
If the current membership comes to expiration, create a new membership that starts immediately after this one.
"""
if not Membership.objects.filter(
user=self.user,
club=self.club,
date_start__gte=self.club.membership_start,
).exists():
# Membership is not renewed yet
new_membership = Membership(
user=self.user,
club=self.club,
date_start=max(self.date_end + datetime.timedelta(days=1), self.club.membership_start),
)
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
new_membership._force_renew_parent = True
if hasattr(self, '_force_save') and self._force_save:
new_membership._force_save = True
new_membership.save()
new_membership.roles.set(self.roles.all())
new_membership.save()
def renew_parent(self):
"""
Ensure that the parent membership is renewed, and renew/create it if needed.
"""
parent_membership = Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
).order_by("-date_start")
if parent_membership.exists():
# Renew the previous membership of the parent club
parent_membership = parent_membership.first()
parent_membership._force_renew_parent = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.renew()
else:
# Create a new membership in the parent club
parent_membership = Membership(
user=self.user,
club=self.club.parent_club,
date_start=self.date_start,
)
parent_membership._force_renew_parent = True
if hasattr(self, '_force_save'):
parent_membership._force_save = True
parent_membership.save()
parent_membership.refresh_from_db()
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()
@transaction.atomic
def save(self, *args, **kwargs):
"""
Calculate fee and end date before saving the membership and creating the transaction if needed.
"""
# Ensure that club membership dates are valid
old_membership_start = self.club.membership_start
self.club.update_membership_dates()
if self.club.membership_start != old_membership_start:
self.club.save()
created = not self.pk
if not created:
for role in self.roles.all():
club = role.for_club
if club is not None:
if club.pk != self.club_id:
raise ValidationError(_('The role {role} does not apply to the club {club}.')
.format(role=role.name, club=club.name))
else:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.club.parent_club is not None:
# Check that the user is already a member of the parent club if the membership is created
if not Membership.objects.filter(
user=self.user,
club=self.club.parent_club,
date_start__gte=self.club.parent_club.membership_start,
).exists():
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
self.renew_parent()
else:
raise ValidationError(_('User is not a member of the parent club')
+ ' ' + self.club.parent_club.name)
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
def make_transaction(self):
"""
Create Membership transaction associated to this membership.
"""
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
return
if self.fee:
transaction = MembershipTransaction(
membership=self,
source=self.user.note,
destination=self.club.note,
quantity=1,
amount=self.fee,
reason="Adhésion " + self.club.name,
)
transaction._force_save = True
transaction.save(force_insert=True)
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
indexes = [models.Index(fields=['user'])]