# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models, transaction from django.utils import timezone from django.contrib.auth.models import User from django.utils.translation import gettext_lazy as _ class Family(models.Model): name = models.CharField( max_length=255, verbose_name=_('name'), unique=True ) description = models.CharField( max_length=255, verbose_name=_('description') ) score = models.PositiveIntegerField( verbose_name=_('score') ) rank = models.PositiveIntegerField( verbose_name=_('rank'), ) class Meta: verbose_name = _('Family') verbose_name_plural = _('Families') def __str__(self): return self.name class FamilyMembership(models.Model): user = models.OneToOneField( User, on_delete=models.PROTECT, related_name=_('family_memberships'), verbose_name=_('user'), ) family = models.ForeignKey( Family, on_delete=models.PROTECT, related_name=_('members'), verbose_name=_('family'), ) year = models.PositiveIntegerField( verbose_name=_('year'), default=timezone.now().year, ) class Meta: unique_together = ('user', 'year',) verbose_name = _('family membership') verbose_name_plural = _('family memberships') def __str__(self): return _('Family membership of {user} to {family}').format(user=self.user.username, family=self.family.name, ) class ChallengeCategory(models.Model): name = models.CharField( max_length=255, verbose_name=_('name'), unique=True, ) class Meta: verbose_name = _('challenge category') verbose_name_plural = _('challenge categories') def __str__(self): return self.name class Challenge(models.Model): name = models.CharField( max_length=255, verbose_name=_('name'), ) description = models.CharField( max_length=255, verbose_name=_('description'), ) points = models.PositiveIntegerField( verbose_name=_('points'), ) category = models.ForeignKey( ChallengeCategory, verbose_name=_('category'), on_delete=models.PROTECT ) obtained = models.PositiveIntegerField( verbose_name=_('obtained') ) class Meta: verbose_name = _('challenge') verbose_name_plural = _('challenges') def __str__(self): return self.name class Achievement(models.Model): challenge = models.ForeignKey( Challenge, on_delete=models.PROTECT, ) family = models.ForeignKey( Family, on_delete=models.PROTECT, verbose_name=_('family'), ) obtained_at = models.DateTimeField( verbose_name=_('obtained at'), default=timezone.now, ) class Meta: verbose_name = _('achievement') verbose_name_plural = _('achievements') def __str__(self): return _('Challenge {challenge} carried out by Family {family}').format(challenge=self.challenge.name, family=self.family.name, ) @classmethod def update_ranking(cls, *args, **kwargs): """ Update ranking when adding or removing points """ family_set = cls.objects.select_for_update().all().order_by("-score") for i in range(family_set.count()): if i == 0 or family_set[i].score != family_set[i - 1].score: new_rank = i + 1 family = family_set[i] family.rank = new_rank family._force_save = True family.save() @transaction.atomic def save(self, *args, **kwargs): """ When saving, also grants points to the family """ self.family = Family.objects.select_for_update().get(pk=self.family_id) self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id) challenge_points = self.challenge.points is_new = self.pk is None super.save(*args, **kwargs) # Only grant points when getting a new achievement if is_new: self.family.refresh_from_db() self.family.score += challenge_points self.family._force_save = True self.family.save() self.challenge.refresh_from_db() self.challenge.obtained += 1 self.challenge._force_save = True self.challenge.save() self.__class__.update_ranking() @transaction.atomic def delete(self, *args, **kwargs): """ When deleting, also removes points from the family """ # Get the family and challenge before deletion self.family = Family.objects.select_for_update().get(pk=self.family_id) challenge_points = self.challenge.points # Delete the achievement super().delete(*args, **kwargs) # Remove points from the family self.family.refresh_from_db() self.family.score -= challenge_points self.family._force_save = True self.family.save() self.challenge.refresh_from_db() self.challenge.obtained -= 1 self.challenge._force_save = True self.challenge.save() self.__class__.update_ranking()