nk20/apps/activity/models.py

313 lines
9.5 KiB
Python
Raw Permalink Normal View History

# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
2019-07-16 10:43:23 +00:00
# SPDX-License-Identifier: GPL-3.0-or-later
2020-08-06 10:50:24 +00:00
import os
2020-08-06 12:11:55 +00:00
from datetime import timedelta
from threading import Thread
2019-07-16 10:43:23 +00:00
from django.conf import settings
from django.contrib.auth.models import User
2020-09-11 20:52:16 +00:00
from django.db import models, transaction
2020-03-28 12:38:31 +00:00
from django.db.models import Q
from django.utils import timezone
2019-07-16 10:43:23 +00:00
from django.utils.translation import gettext_lazy as _
2020-03-27 21:48:20 +00:00
from note.models import NoteUser, Transaction
from rest_framework.exceptions import ValidationError
2019-07-16 10:43:23 +00:00
class ActivityType(models.Model):
2019-08-11 15:52:41 +00:00
"""
Type of Activity, (e.g "Pot", "Soirée Club") and associated properties.
Activity Type are used as a search field for Activity, and determine how
some rules about the activity:
- Can people be invited
- What is the entrance fee.
2019-08-11 15:52:41 +00:00
"""
2019-07-16 10:43:23 +00:00
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
2020-08-06 15:41:30 +00:00
manage_entries = models.BooleanField(
verbose_name=_('manage entries'),
help_text=_('Enable the support of entries for this activity.'),
default=False,
)
2019-07-16 10:43:23 +00:00
can_invite = models.BooleanField(
verbose_name=_('can invite'),
2020-08-06 15:41:30 +00:00
default=False,
2019-07-16 10:43:23 +00:00
)
2020-08-06 15:41:30 +00:00
2019-07-16 10:43:23 +00:00
guest_entry_fee = models.PositiveIntegerField(
verbose_name=_('guest entry fee'),
2020-08-06 15:41:30 +00:00
default=0,
2019-07-16 10:43:23 +00:00
)
2019-07-16 11:50:05 +00:00
class Meta:
verbose_name = _("activity type")
verbose_name_plural = _("activity types")
2019-07-16 13:42:31 +00:00
def __str__(self):
return self.name
2019-07-16 10:43:23 +00:00
class Activity(models.Model):
2019-08-11 15:52:41 +00:00
"""
An IRL event organized by a club for other club.
By default the invited clubs should be the Club containing all the active accounts.
2019-08-11 15:52:41 +00:00
"""
2019-07-16 10:43:23 +00:00
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
2020-03-27 21:48:20 +00:00
2019-07-16 10:43:23 +00:00
description = models.TextField(
verbose_name=_('description'),
2024-03-23 13:32:31 +00:00
blank=True,
default="",
2019-07-16 10:43:23 +00:00
)
2020-03-27 21:48:20 +00:00
location = models.CharField(
verbose_name=_('location'),
max_length=255,
blank=True,
default="",
2020-08-30 22:13:38 +00:00
help_text=_("Place where the activity is organized, eg. Kfet."),
)
2019-07-16 10:43:23 +00:00
activity_type = models.ForeignKey(
ActivityType,
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('type'),
)
2020-03-27 21:48:20 +00:00
creater = models.ForeignKey(
User,
on_delete=models.PROTECT,
verbose_name=_("user"),
)
2019-07-16 10:43:23 +00:00
organizer = models.ForeignKey(
'member.Club',
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('organizer'),
2020-08-30 22:13:38 +00:00
help_text=_("Club that organizes the activity. The entry fees will go to this club."),
2019-07-16 10:43:23 +00:00
)
2020-03-27 21:48:20 +00:00
2019-07-16 10:43:23 +00:00
attendees_club = models.ForeignKey(
'member.Club',
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('attendees club'),
2020-08-30 22:13:38 +00:00
help_text=_("Club that is authorized to join the activity. Mostly the Kfet club."),
2019-07-16 10:43:23 +00:00
)
2020-03-27 21:48:20 +00:00
2019-07-16 10:43:23 +00:00
date_start = models.DateTimeField(
verbose_name=_('start date'),
)
2020-03-27 21:48:20 +00:00
2019-07-16 10:43:23 +00:00
date_end = models.DateTimeField(
verbose_name=_('end date'),
)
2020-03-27 21:48:20 +00:00
valid = models.BooleanField(
default=False,
verbose_name=_('valid'),
)
open = models.BooleanField(
default=False,
verbose_name=_('open'),
)
class Meta:
verbose_name = _("activity")
verbose_name_plural = _("activities")
unique_together = ("name", "date_start", "date_end",)
def __str__(self):
return self.name
2020-09-11 20:52:16 +00:00
@transaction.atomic
def save(self, *args, **kwargs):
"""
Update the activity wiki page each time the activity is updated (validation, change description, ...)
"""
if self.date_end < self.date_start:
raise ValidationError(_("The end date must be after the start date."))
ret = super().save(*args, **kwargs)
2020-09-04 19:46:40 +00:00
if not settings.DEBUG and self.pk and "scripts" in settings.INSTALLED_APPS:
def refresh_activities():
from scripts.management.commands.refresh_activities import Command as RefreshActivitiesCommand
# Consider that we can update the wiki iff the WIKI_PASSWORD env var is not empty
RefreshActivitiesCommand.refresh_human_readable_wiki_page("Modification de l'activité " + self.name,
False, os.getenv("WIKI_PASSWORD"))
RefreshActivitiesCommand.refresh_raw_wiki_page("Modification de l'activité " + self.name,
False, os.getenv("WIKI_PASSWORD"))
Thread(daemon=True, target=refresh_activities).start()\
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
return ret
2019-07-16 10:43:23 +00:00
2020-03-27 17:02:22 +00:00
class Entry(models.Model):
2020-04-06 10:32:44 +00:00
"""
Register the entry of someone:
- a member with a :model:`note.NoteUser`
- or a :model:`activity.Guest`
In the case of a Guest Entry, the inviter note is also save.
"""
2020-03-28 12:38:31 +00:00
activity = models.ForeignKey(
Activity,
on_delete=models.PROTECT,
related_name="entries",
2020-03-28 12:38:31 +00:00
verbose_name=_("activity"),
)
2020-03-27 17:02:22 +00:00
time = models.DateTimeField(
default=timezone.now,
2020-03-27 17:02:22 +00:00
verbose_name=_("entry time"),
)
note = models.ForeignKey(
NoteUser,
on_delete=models.PROTECT,
verbose_name=_("note"),
)
2020-03-28 12:38:31 +00:00
guest = models.OneToOneField(
'activity.Guest',
on_delete=models.PROTECT,
null=True,
)
class Meta:
unique_together = (('activity', 'note', 'guest', ), )
2020-04-06 08:58:16 +00:00
verbose_name = _("entry")
verbose_name_plural = _("entries")
2020-03-28 12:38:31 +00:00
def __str__(self):
return _("Entry for {guest}, invited by {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity)) if self.guest \
else _("Entry for {note} to the activity {activity}").format(
guest=str(self.guest), note=str(self.note), activity=str(self.activity))
2020-09-11 20:52:16 +00:00
@transaction.atomic
2020-04-11 01:37:06 +00:00
def save(self, *args, **kwargs):
2020-03-28 12:38:31 +00:00
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
if self.guest:
self.note = self.guest.inviter
insert = not self.pk
if insert:
if self.note.balance < 0:
raise ValidationError(_("The balance is negative."))
2020-04-11 01:37:06 +00:00
ret = super().save(*args, **kwargs)
2020-03-28 12:38:31 +00:00
if insert and self.guest:
GuestTransaction.objects.create(
source=self.note,
2020-04-02 13:43:41 +00:00
destination=self.activity.organizer.note,
2020-03-28 12:38:31 +00:00
quantity=1,
amount=self.activity.activity_type.guest_entry_fee,
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
valid=True,
entry=self,
2020-03-28 12:38:31 +00:00
).save()
return ret
2020-03-27 17:02:22 +00:00
2019-07-16 10:43:23 +00:00
class Guest(models.Model):
2019-08-11 15:52:41 +00:00
"""
People who are not current members of any clubs, and are invited by someone who is a current member.
2019-08-11 15:52:41 +00:00
"""
2019-07-16 10:43:23 +00:00
activity = models.ForeignKey(
Activity,
on_delete=models.PROTECT,
related_name='+',
)
2020-03-27 17:02:22 +00:00
last_name = models.CharField(
2019-07-16 10:43:23 +00:00
max_length=255,
2020-03-27 17:02:22 +00:00
verbose_name=_("last name"),
2019-07-16 10:43:23 +00:00
)
2020-03-27 17:02:22 +00:00
first_name = models.CharField(
max_length=255,
verbose_name=_("first name"),
)
2019-07-16 10:43:23 +00:00
inviter = models.ForeignKey(
2020-03-27 17:02:22 +00:00
NoteUser,
2019-07-16 10:43:23 +00:00
on_delete=models.PROTECT,
related_name='guests',
2020-03-27 17:02:22 +00:00
verbose_name=_("inviter"),
2019-07-16 10:43:23 +00:00
)
2020-03-27 17:02:22 +00:00
class Meta:
verbose_name = _("guest")
verbose_name_plural = _("guests")
unique_together = ("activity", "last_name", "first_name", )
def __str__(self):
return self.first_name + " " + self.last_name
2020-03-27 17:02:22 +00:00
2020-09-11 20:52:16 +00:00
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
one_year = timedelta(days=365)
if not force_insert:
2020-08-06 15:41:30 +00:00
if timezone.now() > timezone.localtime(self.activity.date_start):
raise ValidationError(_("You can't invite someone once the activity is started."))
if not self.activity.valid:
raise ValidationError(_("This activity is not validated yet."))
qs = Guest.objects.filter(
2020-08-06 15:41:30 +00:00
first_name__iexact=self.first_name,
last_name__iexact=self.last_name,
activity__date_start__gte=self.activity.date_start - one_year,
)
if qs.filter(entry__isnull=False).count() >= 5:
raise ValidationError(_("This person has been already invited 5 times this year."))
qs = qs.filter(activity=self.activity)
if qs.exists():
raise ValidationError(_("This person is already invited."))
qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity)
2020-08-06 15:41:30 +00:00
if qs.count() >= 3:
raise ValidationError(_("You can't invite more than 3 people to this activity."))
return super().save(force_insert, force_update, using, update_fields)
@property
def has_entry(self):
try:
if self.entry:
return True
return False
except AttributeError:
return False
2020-03-27 21:48:20 +00:00
class GuestTransaction(Transaction):
entry = models.OneToOneField(
Entry,
2020-03-27 21:48:20 +00:00
on_delete=models.PROTECT,
)
@property
def type(self):
return _('Invitation')