mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			347 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			347 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
 | |
| # SPDX-License-Identifier: GPL-3.0-or-later
 | |
| 
 | |
| import os
 | |
| from datetime import timedelta
 | |
| from threading import Thread
 | |
| 
 | |
| from django.conf import settings
 | |
| from django.contrib.auth.models import User
 | |
| from django.db import models, transaction
 | |
| from django.db.models import Q
 | |
| from django.utils import timezone
 | |
| from django.utils.translation import gettext_lazy as _
 | |
| from note.models import NoteUser, Transaction, Note
 | |
| from rest_framework.exceptions import ValidationError
 | |
| 
 | |
| 
 | |
| class ActivityType(models.Model):
 | |
|     """
 | |
|     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.
 | |
|     """
 | |
|     name = models.CharField(
 | |
|         verbose_name=_('name'),
 | |
|         max_length=255,
 | |
|     )
 | |
| 
 | |
|     manage_entries = models.BooleanField(
 | |
|         verbose_name=_('manage entries'),
 | |
|         help_text=_('Enable the support of entries for this activity.'),
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     can_invite = models.BooleanField(
 | |
|         verbose_name=_('can invite'),
 | |
|         default=False,
 | |
|     )
 | |
| 
 | |
|     guest_entry_fee = models.PositiveIntegerField(
 | |
|         verbose_name=_('guest entry fee'),
 | |
|         default=0,
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("activity type")
 | |
|         verbose_name_plural = _("activity types")
 | |
| 
 | |
|     def __str__(self):
 | |
|         return self.name
 | |
| 
 | |
| 
 | |
| class Activity(models.Model):
 | |
|     """
 | |
|     An IRL event organized by a club for other club.
 | |
| 
 | |
|     By default the invited clubs should be the Club containing all the active accounts.
 | |
|     """
 | |
|     name = models.CharField(
 | |
|         verbose_name=_('name'),
 | |
|         max_length=255,
 | |
|     )
 | |
| 
 | |
|     description = models.TextField(
 | |
|         verbose_name=_('description'),
 | |
|         blank=True,
 | |
|         default="",
 | |
|     )
 | |
| 
 | |
|     location = models.CharField(
 | |
|         verbose_name=_('location'),
 | |
|         max_length=255,
 | |
|         blank=True,
 | |
|         default="",
 | |
|         help_text=_("Place where the activity is organized, eg. Kfet."),
 | |
|     )
 | |
| 
 | |
|     activity_type = models.ForeignKey(
 | |
|         ActivityType,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name='+',
 | |
|         verbose_name=_('type'),
 | |
|     )
 | |
| 
 | |
|     creater = models.ForeignKey(
 | |
|         User,
 | |
|         on_delete=models.PROTECT,
 | |
|         verbose_name=_("user"),
 | |
|     )
 | |
| 
 | |
|     organizer = models.ForeignKey(
 | |
|         'member.Club',
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name='+',
 | |
|         verbose_name=_('organizer'),
 | |
|         help_text=_("Club that organizes the activity. The entry fees will go to this club."),
 | |
|     )
 | |
| 
 | |
|     attendees_club = models.ForeignKey(
 | |
|         'member.Club',
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name='+',
 | |
|         verbose_name=_('attendees club'),
 | |
|         help_text=_("Club that is authorized to join the activity. Mostly the Kfet club."),
 | |
|     )
 | |
| 
 | |
|     date_start = models.DateTimeField(
 | |
|         verbose_name=_('start date'),
 | |
|     )
 | |
| 
 | |
|     date_end = models.DateTimeField(
 | |
|         verbose_name=_('end date'),
 | |
|     )
 | |
| 
 | |
|     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
 | |
| 
 | |
|     @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)
 | |
|         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
 | |
| 
 | |
| 
 | |
| class Entry(models.Model):
 | |
|     """
 | |
|     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.
 | |
|     """
 | |
|     activity = models.ForeignKey(
 | |
|         Activity,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name="entries",
 | |
|         verbose_name=_("activity"),
 | |
|     )
 | |
| 
 | |
|     time = models.DateTimeField(
 | |
|         default=timezone.now,
 | |
|         verbose_name=_("entry time"),
 | |
|     )
 | |
| 
 | |
|     note = models.ForeignKey(
 | |
|         NoteUser,
 | |
|         on_delete=models.PROTECT,
 | |
|         verbose_name=_("note"),
 | |
|     )
 | |
| 
 | |
|     guest = models.OneToOneField(
 | |
|         'activity.Guest',
 | |
|         on_delete=models.PROTECT,
 | |
|         null=True,
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         unique_together = (('activity', 'note', 'guest', ), )
 | |
|         verbose_name = _("entry")
 | |
|         verbose_name_plural = _("entries")
 | |
| 
 | |
|     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))
 | |
| 
 | |
|     @transaction.atomic
 | |
|     def save(self, *args, **kwargs):
 | |
|         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(timezone.localtime(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."))
 | |
| 
 | |
|         ret = super().save(*args, **kwargs)
 | |
| 
 | |
|         if insert and self.guest:
 | |
|             GuestTransaction.objects.create(
 | |
|                 source=self.note,
 | |
|                 destination=self.activity.organizer.note,
 | |
|                 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,
 | |
|             ).save()
 | |
| 
 | |
|         return ret
 | |
| 
 | |
| 
 | |
| class Guest(models.Model):
 | |
|     """
 | |
|     People who are not current members of any clubs, and are invited by someone who is a current member.
 | |
|     """
 | |
|     activity = models.ForeignKey(
 | |
|         Activity,
 | |
|         on_delete=models.CASCADE,
 | |
|         related_name='+',
 | |
|     )
 | |
| 
 | |
|     last_name = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("last name"),
 | |
|     )
 | |
| 
 | |
|     first_name = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("first name"),
 | |
|     )
 | |
| 
 | |
|     school = models.CharField(
 | |
|         max_length=255,
 | |
|         verbose_name=_("school"),
 | |
|     )
 | |
| 
 | |
|     inviter = models.ForeignKey(
 | |
|         NoteUser,
 | |
|         on_delete=models.PROTECT,
 | |
|         related_name='guests',
 | |
|         verbose_name=_("inviter"),
 | |
|     )
 | |
| 
 | |
|     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
 | |
| 
 | |
|     @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:
 | |
|             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(
 | |
|                 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)
 | |
|             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
 | |
| 
 | |
| 
 | |
| class GuestTransaction(Transaction):
 | |
|     entry = models.OneToOneField(
 | |
|         Entry,
 | |
|         on_delete=models.PROTECT,
 | |
|     )
 | |
| 
 | |
|     @property
 | |
|     def type(self):
 | |
|         return _('Invitation')
 | |
| 
 | |
| 
 | |
| class Opener(models.Model):
 | |
|     """
 | |
|     Allow the user to make activity entries without more rights
 | |
|     """
 | |
|     activity = models.ForeignKey(
 | |
|         Activity,
 | |
|         on_delete=models.CASCADE,
 | |
|         related_name='opener',
 | |
|         verbose_name=_('activity')
 | |
|     )
 | |
| 
 | |
|     opener = models.ForeignKey(
 | |
|         Note,
 | |
|         on_delete=models.CASCADE,
 | |
|         related_name='activity_responsible',
 | |
|         verbose_name=_('Opener')
 | |
|     )
 | |
| 
 | |
|     class Meta:
 | |
|         verbose_name = _("Opener")
 | |
|         verbose_name_plural = _("Openers")
 | |
|         unique_together = ("opener", "activity")
 | |
| 
 | |
|     def __str__(self):
 | |
|         return _("{opener} is opener of activity {acivity}").format(
 | |
|             opener=str(self.opener), acivity=str(self.activity))
 |