Skip to content
Snippets Groups Projects
models.py 12.1 KiB
Newer Older
Dorian Lesbre's avatar
Dorian Lesbre committed
from django.db import models
Dorian Lesbre's avatar
Dorian Lesbre committed
from django.forms import ValidationError
Dorian Lesbre's avatar
Dorian Lesbre committed
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
Dorian Lesbre's avatar
Dorian Lesbre committed

from accounts.models import EmailUser
Dorian Lesbre's avatar
Dorian Lesbre committed
from site_settings.models import Colors, SiteSettings
Dorian Lesbre's avatar
Dorian Lesbre committed
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):
Dorian Lesbre's avatar
Dorian Lesbre committed
	"""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")

Dorian Lesbre's avatar
Dorian Lesbre committed
	class ActivityTypes(models.TextChoices):
		"""quantité d'activité"""
		GAME = "1 partie", _("Une partie")
		GAMES = "2+ parties", _("Quelques parties")
		TOURNAMENT = "Tournoi", _("Tournoi")
		FREEPLAY = "freeplay", _("Freeplay")
Dorian Lesbre's avatar
Dorian Lesbre committed
		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")

Dorian Lesbre's avatar
Dorian Lesbre committed
	class Availability(models.TextChoices):
		"""Diponibilité à un moment donné"""
		IDEAL = "0", _("Idéal")
		POSSIBLE = "1", _("Acceptable")
		UNAVAILABLE = "2", _("Indisponible")
Dorian Lesbre's avatar
Dorian Lesbre committed
	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"
	)

Dorian Lesbre's avatar
Dorian Lesbre committed
	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'
Dorian Lesbre's avatar
Dorian Lesbre committed
	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."
Dorian Lesbre's avatar
Dorian Lesbre committed
	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é"
	)
Dorian Lesbre's avatar
Dorian Lesbre committed
	host_info = models.TextField(
		"Autre orgas/contacts", max_length=1000, blank=True, null=True
Dorian Lesbre's avatar
Dorian Lesbre committed

	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
Dorian Lesbre's avatar
Dorian Lesbre committed
	## 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
	)

bleizi's avatar
bleizi committed
	status = models.CharField(
		"Présentiel/distanciel", choices=Status.choices, max_length=1,
		default=Status.PRESENT, blank=True
bleizi's avatar
bleizi committed
	)
Dorian Lesbre's avatar
Dorian Lesbre committed
	needs = models.TextField(
		"Besoin particuliers", max_length=2000, blank=True, null=True
	)

	comments = models.TextField(
		"Commentaires", max_length=2000, blank=True, null=True
	)
Dorian Lesbre's avatar
Dorian Lesbre committed

	@property
	def nb_participants(self) -> str:
		if self.max_participants == 0:
Dorian Lesbre's avatar
Dorian Lesbre committed
			ret = "Illimités"
		elif self.max_participants == self.min_participants:
			ret = "{}".format(self.min_participants)
Dorian Lesbre's avatar
Dorian Lesbre committed
		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)
	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
Dorian Lesbre's avatar
Dorian Lesbre committed
	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"
	)
Dorian Lesbre's avatar
Dorian Lesbre committed
	color = models.CharField(
		"Couleur", choices=Colors.choices, max_length=1, default=Colors.RED,
Dorian Lesbre's avatar
Dorian Lesbre committed
		help_text="La légende des couleurs est modifiable dans les paramètres"
Dorian Lesbre's avatar
Dorian Lesbre committed
	@property
	def participants(self):
		return ActivityChoicesModel.objects.filter(slot=self, accepted=True)
Dorian Lesbre's avatar
Dorian Lesbre committed

	@property
	def end(self):
Dorian Lesbre's avatar
Dorian Lesbre committed
		"""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)
Dorian Lesbre's avatar
Dorian Lesbre committed
			return timezone.datetime.combine(
				settings.date_start + offset,
Dorian Lesbre's avatar
Dorian Lesbre committed
				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)
Dorian Lesbre's avatar
Dorian Lesbre committed
		verbose_name = "créneau"
		verbose_name_plural = "créneaux"
class ParticipantModel(models.Model):
Dorian Lesbre's avatar
Dorian Lesbre committed
	"""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")
		EXTERIEUR = "E", _("Extérieur")
Dorian Lesbre's avatar
Dorian Lesbre committed

	user = models.OneToOneField(EmailUser, on_delete=models.CASCADE, related_name="Utilisateur")
	school = models.CharField("ENS Paris-Saclay ou extérieur", choices=ENS.choices, max_length=1)
Dorian Lesbre's avatar
Dorian Lesbre committed

Dorian Lesbre's avatar
Dorian Lesbre committed
	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)

Dorian Lesbre's avatar
Dorian Lesbre committed
	# mug = models.BooleanField("commander une tasse", default=False)
	def __str__(self) -> str:
		school = self.ENS(self.school).label if self.school else ""
Dorian Lesbre's avatar
Dorian Lesbre committed
		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):
Dorian Lesbre's avatar
Dorian Lesbre committed
	"""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",
Dorian Lesbre's avatar
Dorian Lesbre committed
	accepted = models.BooleanField("Obtenue", default=False)
Dorian Lesbre's avatar
Dorian Lesbre committed

	class Meta:
Dorian Lesbre's avatar
Dorian Lesbre committed
		# 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])