mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 07:49:57 +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()
 |