"""The models defined by the registrations package"""
import uuid
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core import validators
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, RegexValidator
from django.db import models
from django.template.defaultfilters import floatformat
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from members.models import Membership, Profile
from utils import countries
[docs]class Entry(models.Model):
"""Describes a registration entry"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(_("created at"), default=timezone.now)
updated_at = models.DateTimeField(_("updated at"), default=timezone.now)
STATUS_CONFIRM = "confirm"
STATUS_REVIEW = "review"
STATUS_REJECTED = "rejected"
STATUS_ACCEPTED = "accepted"
STATUS_COMPLETED = "completed"
STATUS_TYPE = (
(STATUS_CONFIRM, _("Awaiting email confirmation")),
(STATUS_REVIEW, _("Ready for review")),
(STATUS_REJECTED, _("Rejected")),
(STATUS_ACCEPTED, _("Accepted")),
(STATUS_COMPLETED, _("Completed")),
)
status = models.CharField(
verbose_name=_("status"), choices=STATUS_TYPE, max_length=20, default="confirm",
)
MEMBERSHIP_YEAR = "year"
MEMBERSHIP_STUDY = "study"
MEMBERSHIP_LENGTHS = (
(
MEMBERSHIP_YEAR,
_("One year")
+ f" -- €{floatformat(settings.MEMBERSHIP_PRICES['year'], 2)}",
),
(
MEMBERSHIP_STUDY,
_("Until graduation")
+ f" -- €{floatformat(settings.MEMBERSHIP_PRICES['study'], 2)}",
),
)
length = models.CharField(
verbose_name=_("membership length"), choices=MEMBERSHIP_LENGTHS, max_length=20,
)
MEMBERSHIP_TYPES = [
m for m in Membership.MEMBERSHIP_TYPES if m[0] != Membership.HONORARY
]
contribution = models.FloatField(
verbose_name=_("contribution"),
validators=[MinValueValidator(settings.MEMBERSHIP_PRICES["year"])],
default=settings.MEMBERSHIP_PRICES["year"],
blank=True,
null=True,
)
no_references = models.BooleanField(
verbose_name=_("no references required"), default=False
)
membership_type = models.CharField(
verbose_name=_("membership type"),
choices=MEMBERSHIP_TYPES,
max_length=40,
default=Membership.MEMBER,
)
remarks = models.TextField(_("remarks"), blank=True, null=True,)
payment = models.OneToOneField(
"payments.Payment",
related_name="registrations_entry",
on_delete=models.PROTECT,
blank=True,
null=True,
)
membership = models.OneToOneField(
"members.Membership", on_delete=models.SET_NULL, blank=True, null=True,
)
[docs] def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if self.status != self.STATUS_ACCEPTED and self.status != self.STATUS_REJECTED:
self.updated_at = timezone.now()
if (
self.contribution is not None
and self.membership_type != Membership.BENEFACTOR
):
self.contribution = None
elif self.membership_type == Membership.BENEFACTOR:
self.length = self.MEMBERSHIP_YEAR
super().save(force_insert, force_update, using, update_fields)
[docs] def clean(self):
super().clean()
errors = {}
if self.contribution is None and self.membership_type == Membership.BENEFACTOR:
errors.update(
{"contribution": _("This field is required for benefactors.")}
)
if errors:
raise ValidationError(errors)
def __str__(self):
try:
return self.registration.__str__()
except Registration.DoesNotExist:
return self.renewal.__str__()
class Meta:
verbose_name = _("entry")
verbose_name_plural = _("entries")
permissions = (
("review_entries", _("Review registration and renewal entries")),
)
[docs]class Registration(Entry):
"""Describes a new registration for the association"""
# ---- Personal information -----
username = models.CharField(
_("Username"),
max_length=64, # This length is lower than Django because of G Suite
blank=True,
null=True,
help_text=_(
"Enter value to override the auto-generated username "
"(e.g. if it is not unique)"
),
validators=[
RegexValidator(
regex="^[a-zA-Z0-9]{1,64}$",
message=_(
"Please use 64 characters or fewer. Letters and digits only."
),
)
],
)
first_name = models.CharField(_("First name"), max_length=30, blank=False,)
last_name = models.CharField(_("Last name"), max_length=200, blank=False,)
birthday = models.DateField(verbose_name=_("birthday"), blank=False,)
language = models.CharField(
verbose_name=_("language"),
max_length=5,
choices=settings.LANGUAGES,
default="nl",
)
# ---- Contact information -----
email = models.EmailField(_("Email address"), blank=False,)
phone_number = models.CharField(
max_length=20,
verbose_name=_("phone number"),
validators=[
validators.RegexValidator(
regex=r"^\+?\d+$", message=_("please enter a valid phone number"),
)
],
blank=True,
null=True,
)
# ---- University information -----
student_number = models.CharField(
verbose_name=_("student number"),
max_length=8,
validators=[
validators.RegexValidator(
regex=r"([Ss]\d{7}|[EZUezu]\d{6,7})",
message=_("enter a valid student- or e/z/u-number."),
)
],
help_text=_("With prefix. For example: 's5603249'."),
blank=True,
null=True,
)
programme = models.CharField(
max_length=20,
choices=Profile.PROGRAMME_CHOICES,
verbose_name=_("study programme"),
blank=True,
null=True,
)
starting_year = models.IntegerField(
verbose_name=_("starting year"), blank=True, null=True,
)
# ---- Address information -----
address_street = models.CharField(
max_length=100,
validators=[
validators.RegexValidator(
regex=r"^.+ \d+.*",
message=_("please use the format <street> <number>"),
)
],
verbose_name=_("street and house number"),
blank=False,
)
address_street2 = models.CharField(
max_length=100, verbose_name=_("second address line"), blank=True, null=True,
)
address_postal_code = models.CharField(
max_length=10, verbose_name=_("postal code"), blank=False,
)
address_city = models.CharField(max_length=40, verbose_name=_("city"), blank=False,)
address_country = models.CharField(
max_length=2, choices=countries.EUROPE, verbose_name=_("Country"), null=True,
)
# ---- Opt-ins -----
optin_mailinglist = models.BooleanField(
verbose_name=_("mailinglist opt-in"), default=False
)
optin_birthday = models.BooleanField(
verbose_name=_("birthday calendar opt-in"), default=False
)
[docs] def get_full_name(self):
full_name = "{} {}".format(self.first_name, self.last_name)
return full_name.strip()
[docs] def clean(self):
super().clean()
errors = {}
if (
get_user_model().objects.filter(email=self.email).exists()
or Registration.objects.filter(email=self.email)
.exclude(pk=self.pk)
.exists()
):
errors.update(
{
"email": _(
"A user with that email address already exists. "
"Login using the existing account and renew the "
"membership by visiting the account settings."
)
}
)
if self.student_number is not None:
self.student_number = self.student_number.lower()
if (
Profile.objects.filter(student_number=self.student_number).exists()
or Registration.objects.filter(student_number=self.student_number)
.exclude(pk=self.pk)
.exists()
):
errors.update(
{
"student_number": _(
"A user with that student number already exists. "
"Login using the existing account and renew the "
"membership by visiting the account settings."
)
}
)
elif (
self.student_number is None
and self.membership_type != Membership.BENEFACTOR
):
errors.update({"student_number": _("This field is required.")})
if (
self.username is not None
and get_user_model().objects.filter(username=self.username).exists()
):
errors.update({"username": _("A user with that username already exists.")})
if self.starting_year is None and self.membership_type != Membership.BENEFACTOR:
errors.update({"starting_year": _("This field is required.")})
if self.programme is None and self.membership_type != Membership.BENEFACTOR:
errors.update({"programme": _("This field is required.")})
if errors:
raise ValidationError(errors)
def __str__(self):
return "{} {} ({})".format(self.first_name, self.last_name, self.email)
class Meta:
verbose_name = _("registration")
verbose_name_plural = _("registrations")
[docs]class Renewal(Entry):
"""Describes a renewal for the association membership"""
member = models.ForeignKey(
"members.Member",
on_delete=models.CASCADE,
verbose_name=_("member"),
blank=False,
null=False,
)
[docs] def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
if self.pk is None:
self.status = Entry.STATUS_REVIEW
super().save(force_insert, force_update, using, update_fields)
[docs] def clean(self):
super().clean()
errors = {}
if (
Renewal.objects.filter(member=self.member, status=Entry.STATUS_REVIEW)
.exclude(pk=self.pk)
.exists()
):
raise ValidationError(
_("You already have a renewal request queued for review.")
)
# Invalid form for study and honorary members
current_membership = self.member.current_membership
if current_membership is not None and current_membership.until is None:
errors.update(
{
"length": _("You currently have an active membership."),
"membership_type": _("You currently have an active membership."),
}
)
latest_membership = self.member.latest_membership
hide_year_choice = not (
latest_membership is not None
and latest_membership.until is not None
and (latest_membership.until - timezone.now().date()).days <= 31
)
if self.length == Entry.MEMBERSHIP_YEAR and hide_year_choice:
errors.update(
{"length": _("You cannot renew your membership at this moment.")}
)
if (
self.membership_type == Membership.BENEFACTOR
and self.length == Entry.MEMBERSHIP_STUDY
):
errors.update(
{
"length": _(
"Benefactors cannot have a membership "
"that lasts their entire study duration."
)
}
)
if errors:
raise ValidationError(errors)
def __str__(self):
return "{} {} ({})".format(
self.member.first_name, self.member.last_name, self.member.email
)
class Meta:
verbose_name = _("renewal")
verbose_name_plural = _("renewals")
[docs]class Reference(models.Model):
"""Describes a reference of a member for a potential member"""
member = models.ForeignKey(
"members.Member",
on_delete=models.CASCADE,
verbose_name=_("member"),
blank=False,
null=False,
)
entry = models.ForeignKey(
"registrations.Entry",
on_delete=models.CASCADE,
verbose_name=_("entry"),
blank=False,
null=False,
)
def __str__(self):
return f"Reference from {self.member} for {self.entry}"
class Meta:
unique_together = ("member", "entry")