import datetime from django.db import models from django.forms import ValidationError from django.utils.translation import gettext_lazy as _ from django.utils import timezone from accounts.models import EmailUser from site_settings.models import Colors, SiteSettings def validate_nonzero(value): """Make a positive integer field non-zero""" if value == 0: raise ValidationError( _('Cette valeur doit-être non-nulle'), ) class ActivityModel(models.Model): """une activité des interludes (i.e. JDR, murder)...""" class Status(models.TextChoices): """en presentiel ou non""" PRESENT = "P", _("En présentiel uniquement") DISTANT = "D", _("En distanciel uniquement") BOTH = "2", _("Les deux") class ActivityTypes(models.TextChoices): """quantité d'activité""" GAME = "1 partie", _("Une partie") GAMES = "2+ parties", _("Quelques parties") TOURNAMENT = "Tournoi", _("Tournoi") FREEPLAY = "freeplay", _("Freeplay") OTHER = "other", _("Autre") class GameTypes(models.TextChoices): """types de jeu""" CARD_GAME = "jeu cartes", _("Jeu de cartes") BOARD_GAME = "jeu plateau", _("Jeu de société") TABLETOP_RPG = "table RPG", _("Jeu de rôle sur table") LARGE_RPG = "large RPG", _("Jeu de rôle grandeur nature") VIDEOGAME = "videogame", _("Jeu vidéo") PARTYGAME = "partygame", _("Party game") PUZZLE = "puzzle", _("Puzzle ou analogue") SECRET_ROLES = "secret roles", _("Jeu à rôles secrets") COOP = "coop", _("Jeu coopératif") OTHER = "other", _("Autre") class Availability(models.TextChoices): """Diponibilité à un moment donné""" IDEAL = "0", _("Idéal") POSSIBLE = "1", _("Acceptable") UNAVAILABLE = "2", _("Indisponible") display = models.BooleanField("afficher dans la liste", default=False, help_text="Si vrai, s'affiche sur la page activités" ) show_email = models.BooleanField("afficher l'email de l'orga", default=True, help_text="Si l'affichage d'email global et cette case sont vrai, affiche l'email de l'orga" ) title = models.CharField("Titre", max_length=200) act_type = models.CharField("Type d'activité", choices=ActivityTypes.choices, max_length=12) game_type = models.CharField("Type de jeu", choices=GameTypes.choices, max_length=12) description = models.TextField( "description", max_length=10000, help_text='Texte ou html selon la valeur de "Description HTML".\n' ) desc_as_html = models.BooleanField("Description au format HTML", default=False, help_text="Assurer vous que le texte est bien formaté, cette option peut casser la page activités." ) host = models.ForeignKey( EmailUser, on_delete=models.SET_NULL, verbose_name="Organisateur", blank=True, null=True ) host_name = models.CharField( "nom de l'organisateur", max_length=50, null=True, blank=True, help_text="Peut-être laissé vide pour des simples activités sans orga" ) host_email = models.EmailField( "email de l'organisateur", help_text="Utilisé pour communiquer la liste des participants si demandé" ) host_info = models.TextField( "Autre orgas/contacts", max_length=1000, blank=True, null=True ) must_subscribe = models.BooleanField("sur inscription", default=False, help_text="Informatif, il faut utiliser les créneaux pour ajouter dans la liste d'inscription" ) communicate_participants = models.BooleanField("communiquer la liste des participants à l'orga avant l'événement") max_participants = models.PositiveIntegerField( "Nombre maximum de participants", help_text="0 pour illimité", default=0 ) min_participants = models.PositiveIntegerField( "Nombre minimum de participants", default=0 ) ## Information fournies par le respo duration = models.DurationField("Durée", help_text="format hh:mm:ss") desired_slot_nb = models.PositiveIntegerField( "Nombre de créneaux souhaités", default=1, validators=[validate_nonzero] ) available_friday_evening = models.CharField( "Crénau vendredi soir", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_friday_night = models.CharField( "Crénau vendredi nuit", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_saturday_morning = models.CharField( "Crénau samedi matin", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_saturday_afternoon = models.CharField( "Crénau samedi après-midi", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_saturday_evening = models.CharField( "Crénau samedi soir", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_saturday_night = models.CharField( "Crénau samedi nuit", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_sunday_morning = models.CharField( "Crénau dimanche matin", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) available_sunday_afternoon = models.CharField( "Crénau dimanche après-midi", choices=Availability.choices, max_length=1, default=Availability.POSSIBLE, ) constraints = models.TextField( "Contraintes particulières", max_length=2000, blank=True, null=True ) status = models.CharField("Présentiel/distanciel", choices=Status.choices, max_length=1) needs = models.TextField( "Besoin particuliers", max_length=2000, blank=True, null=True ) comments = models.TextField( "Commentaires", max_length=2000, blank=True, null=True ) @property def nb_participants(self) -> str: if self.max_participants == 0: ret = "Illimités" elif self.max_participants == self.min_participants: ret = "{}".format(self.min_participants) else: ret = "{} - {}".format(self.min_participants, self.max_participants) if self.must_subscribe: ret += " (sur inscription)" return ret @property def pretty_duration(self) -> str: hours, rem = divmod(self.duration.seconds, 3600) minutes = "{:02}".format(rem // 60) if rem // 60 else "" return "{}h{}".format(hours, minutes) @property def pretty_type(self) -> str: type = self.ActivityTypes(self.act_type).label game = self.GameTypes(self.game_type).label return "{}, {}".format(game, type.lower()) @property def slug(self) -> str: """Returns the planning/display slug for this activity""" return "act-{}".format(self.id) @property def slots(self): """Returns a list of slots related to self""" return SlotModel.objects.filter(activity=self, on_activity=True).order_by("start") def __str__(self): return self.title class Meta: verbose_name = "activité" class SlotModel(models.Model): """Crénaux indiquant ou une activité se place dans le planning Dans une table à part car un activité peut avoir plusieurs créneaux. Les inscriptions se font à des créneaux et non des activités""" TITLE_SPECIFIER = "{act_title}" activity = models.ForeignKey(ActivityModel, on_delete=models.CASCADE, verbose_name="Activité") title = models.CharField( "Titre", max_length=200, default=TITLE_SPECIFIER, help_text="Utilisez '{}' pour insérer le titre de l'activité correspondante".format( TITLE_SPECIFIER), ) start = models.DateTimeField("début") duration = models.DurationField( "durée", blank=True, null=True, help_text="Format 00:00:00. Laisser vide pour prendre la durée de l'activité correspondante" ) room = models.CharField("salle", max_length=100, null=True, blank=True) on_planning = models.BooleanField( "afficher sur le planning", default=True, ) on_activity = models.BooleanField( "afficher dans la description de l'activité", default=True, ) subscribing_open = models.BooleanField("ouvert aux inscriptions", default=False, help_text="Si vrai, apparaît dans la liste du formulaire d'inscription" ) color = models.CharField( "Couleur", choices=Colors.choices, max_length=1, default=Colors.RED, help_text="La légende des couleurs est modifiable dans les paramètres" ) @property def participants(self): return ActivityChoicesModel.objects.filter(slot=self, accepted=True) @property def end(self): """Heure de fin du créneau""" if self.duration: return self.start + self.duration return self.start + self.activity.duration def conflicts(self, other: "SlotModel") -> bool: """Check whether these slots overlap""" if self.start <= other.start: return other.start <= self.end return self.start <= other.end @staticmethod def relative_day(date: datetime.datetime) -> int: """Relative day to start. - friday 04:00 -> 03:59 = day 0 - saturday 04:00 -> 03:59 = day 1 - sunday 04:00 -> 03:59 = day 2 returns 0 if no date_start is defined in settings""" settings = SiteSettings.load() if settings.date_start: return (date - timezone.datetime.combine( settings.date_start, datetime.time(hour=4), timezone.get_current_timezone() )).days else: return 0 @staticmethod def fake_date(date: datetime.datetime): """Fake day for display on the (single day planning)""" settings = SiteSettings.load() if settings.date_start: time = date.timetz() offset = datetime.timedelta(0) if time.hour < 4: offset = datetime.timedelta(days=1) return timezone.datetime.combine( settings.date_start + offset, date.timetz() ) return None @property def start_day(self) -> int: """returns a day (0-2)""" return self.relative_day(self.start) @property def end_day(self) -> int: """returns a day (0-2)""" return self.relative_day(self.end) @property def planning_start(self) -> int: return self.fake_date(self.start) @property def planning_end(self) -> int: return self.fake_date(self.end) def __str__(self) -> str: return self.title.replace(self.TITLE_SPECIFIER, self.activity.title) class Meta: verbose_name = "créneau" verbose_name_plural = "créneaux" class ParticipantModel(models.Model): """un participant aux interludes""" class ENS(models.TextChoices): """enum representant les ENS""" ENS_ULM = "U", _("ENS Ulm") ENS_LYON = "L", _("ENS Lyon") ENS_RENNES = "R", _("ENS Rennes") ENS_CACHAN = "C", _("ENS Paris Saclay") user = models.OneToOneField(EmailUser, on_delete=models.CASCADE, related_name="Utilisateur") school = models.CharField("ENS de rattachement", choices=ENS.choices, max_length=1) is_registered = models.BooleanField("est inscrit", default=False) meal_friday_evening = models.BooleanField("repas de vendredi soir", default=False) meal_saturday_morning = models.BooleanField("repas de samedi matin", default=False) meal_saturday_midday = models.BooleanField("repas de samedi midi", default=False) meal_saturday_evening = models.BooleanField("repas de samedi soir", default=False) meal_sunday_morning = models.BooleanField("repas de dimanche matin", default=False) meal_sunday_midday = models.BooleanField("repas de dimanche soir", default=False) sleeps = models.BooleanField("dormir sur place", default=False) # mug = models.BooleanField("commander une tasse", default=False) def __str__(self) -> str: school = self.ENS(self.school).label.replace("ENS ", "") if self.school else "" return "{} {} ({})".format(self.user.first_name, self.user.last_name, school) @property def nb_meals(self) -> int: return ( self.meal_friday_evening + self.meal_saturday_evening + self.meal_saturday_midday + self.meal_saturday_morning + self.meal_sunday_midday + self.meal_sunday_morning ) class Meta: verbose_name = "participant" class ActivityChoicesModel(models.Model): """liste d'activités souhaitée de chaque participant, avec un order de priorité""" priority = models.PositiveIntegerField("priorité") participant = models.ForeignKey( ParticipantModel, on_delete=models.CASCADE, verbose_name="participant", ) slot = models.ForeignKey( SlotModel, on_delete=models.CASCADE, verbose_name="créneau", ) accepted = models.BooleanField("Obtenue", default=False) class Meta: # couples uniques unique_together = (("priority", "participant"), ("participant", "slot")) ordering = ("participant", "priority") verbose_name = "choix d'activités" verbose_name_plural = "choix d'activités" EmailUser.profile = property(lambda user: ParticipantModel.objects.get_or_create(user=user)[0])