"""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, date
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from members.models import Membership, Profile
from payments.models import Payable
from utils import countries
[docs]class Entry(models.Model, Payable):
"""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.DecimalField(
verbose_name=_("contribution"),
max_digits=5,
decimal_places=2,
validators=[MinValueValidator(settings.MEMBERSHIP_PRICES["year"])],
default=settings.MEMBERSHIP_PRICES["year"],
blank=False,
null=False,
)
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.SET_NULL,
blank=True,
null=True,
)
membership = models.OneToOneField(
"members.Membership", on_delete=models.SET_NULL, blank=True, null=True,
)
@property
def payment_amount(self):
return self.contribution
@property
def payment_payer(self):
if self.membership:
return self.membership.user
return None
@property
def payment_topic(self):
return "Registration entry"
@property
def payment_notes(self):
return f"{self.payment_topic}. Creation date: {date(self.created_at)}. Completion date: {date(self.updated_at)}"
[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.membership_type == Membership.BENEFACTOR:
self.length = self.MEMBERSHIP_YEAR
else:
self.contribution = settings.MEMBERSHIP_PRICES[self.length]
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,)
@property
def language(self):
"""@todo: Remove usage of this property."""
return "en"
# ---- 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
)
@property
def payment_topic(self):
return f"Membership registration {self.membership_type} ({self.length})"
[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,
)
@property
def payment_payer(self):
return self.member
@property
def payment_topic(self):
return f"Membership renewal {self.membership_type} ({self.length})"
[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")