mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-17 23:00:23 +02:00
207 lines
5.7 KiB
Python
207 lines
5.7 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.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 update_score(self, *args, **kwargs):
|
|
challenge_set = Challenge.objects.select_for_update().filter(achievement__family=self)
|
|
points_sum = challenge_set.aggregate(models.Sum("points"))
|
|
self.score = points_sum["points__sum"]
|
|
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=_('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 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'),
|
|
)
|
|
|
|
obtained = models.PositiveIntegerField(
|
|
verbose_name=_('obtained'),
|
|
default=0,
|
|
)
|
|
|
|
@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')
|
|
|
|
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, )
|
|
|
|
@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)
|
|
is_new = self.pk is None
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
self.family.refresh_from_db()
|
|
self.family.update_score()
|
|
|
|
# Count only when getting a new achievement
|
|
if is_new:
|
|
self.challenge.refresh_from_db()
|
|
self.challenge.obtained += 1
|
|
self.challenge._force_save = True
|
|
self.challenge.save()
|
|
|
|
@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()
|
|
|
|
self.challenge.refresh_from_db()
|
|
self.challenge.obtained -= 1
|
|
self.challenge._force_save = True
|
|
self.challenge.save()
|