mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-04 01:12:08 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			208 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			208 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# 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.urls import reverse_lazy
 | 
						|
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'),
 | 
						|
        default=0,
 | 
						|
    )
 | 
						|
 | 
						|
    rank = models.PositiveIntegerField(
 | 
						|
        verbose_name=_('rank'),
 | 
						|
    )
 | 
						|
 | 
						|
    display_image = models.ImageField(
 | 
						|
        verbose_name=_('display image'),
 | 
						|
        max_length=255,
 | 
						|
        blank=False,
 | 
						|
        null=False,
 | 
						|
        upload_to='pic/',
 | 
						|
        default='pic/default.png'
 | 
						|
    )
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = _('Family')
 | 
						|
        verbose_name_plural = _('Families')
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.name
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        return reverse_lazy('family:family_detail', args=(self.pk,))
 | 
						|
 | 
						|
    def update_score(self, *args, **kwargs):
 | 
						|
        challenge_set = Challenge.objects.select_for_update().filter(achievement__family=self, achievement__valid=True)
 | 
						|
        points_sum = challenge_set.aggregate(models.Sum("points"))
 | 
						|
        self.score = points_sum["points__sum"] if points_sum["points__sum"] else 0
 | 
						|
        self.save()
 | 
						|
        self.update_ranking()
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def update_ranking(*args, **kwargs):
 | 
						|
        """
 | 
						|
        Update ranking when adding or removing points
 | 
						|
        """
 | 
						|
        family_set = Family.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()
 | 
						|
 | 
						|
    def save(self, *args, **kwargs):
 | 
						|
        if self.rank is None:
 | 
						|
            last_family = Family.objects.order_by("rank").last()
 | 
						|
            if last_family is None or last_family.score > self.score:
 | 
						|
                self.rank = Family.objects.count() + 1
 | 
						|
            else:
 | 
						|
                self.rank = last_family.rank
 | 
						|
        super().save(*args, **kwargs)
 | 
						|
 | 
						|
 | 
						|
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=_('memberships'),
 | 
						|
        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 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'),
 | 
						|
    )
 | 
						|
 | 
						|
    @property
 | 
						|
    def obtained(self):
 | 
						|
        achievements = Achievement.objects.filter(challenge=self, valid=True)
 | 
						|
        return achievements.count()
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.name
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        return reverse_lazy('family:challenge_detail', args=(self.pk,))
 | 
						|
 | 
						|
    @transaction.atomic
 | 
						|
    def save(self, *args, **kwargs):
 | 
						|
        super().save(*args, **kwargs)
 | 
						|
        # Update families who already obtained this challenge
 | 
						|
        achievements = Achievement.objects.filter(challenge=self)
 | 
						|
        for achievement in achievements:
 | 
						|
            achievement.save()
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = _('challenge')
 | 
						|
        verbose_name_plural = _('challenges')
 | 
						|
 | 
						|
 | 
						|
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,
 | 
						|
    )
 | 
						|
 | 
						|
    valid = models.BooleanField(
 | 
						|
        verbose_name=_('valid'),
 | 
						|
        default=False,
 | 
						|
    )
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        unique_together = ('challenge', 'family',)
 | 
						|
        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, )
 | 
						|
 | 
						|
    @transaction.atomic
 | 
						|
    def save(self, *args, update_score=True, **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)
 | 
						|
 | 
						|
        super().save(*args, **kwargs)
 | 
						|
 | 
						|
        if update_score:
 | 
						|
            self.family.refresh_from_db()
 | 
						|
            self.family.update_score()
 | 
						|
 | 
						|
    @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)
 | 
						|
 | 
						|
        # Delete the achievement
 | 
						|
        super().delete(*args, **kwargs)
 | 
						|
 | 
						|
        # Remove points from the family
 | 
						|
        self.family.refresh_from_db()
 | 
						|
        self.family.update_score()
 |