From 6c7d86185af363ec7f4694713579e1de9b6ae555 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Thu, 3 Jul 2025 14:34:04 +0200 Subject: [PATCH] Models --- apps/family/__init__.py | 0 apps/family/admin.py | 3 + apps/family/apps.py | 11 ++ apps/family/migrations/__init__.py | 0 apps/family/models.py | 165 +++++++++++++++++++++++++++++ apps/family/tests.py | 3 + apps/family/views.py | 3 + note_kfet/settings/base.py | 3 +- 8 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 apps/family/__init__.py create mode 100644 apps/family/admin.py create mode 100644 apps/family/apps.py create mode 100644 apps/family/migrations/__init__.py create mode 100644 apps/family/models.py create mode 100644 apps/family/tests.py create mode 100644 apps/family/views.py diff --git a/apps/family/__init__.py b/apps/family/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/family/admin.py b/apps/family/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/family/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/family/apps.py b/apps/family/apps.py new file mode 100644 index 00000000..47007531 --- /dev/null +++ b/apps/family/apps.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + + +from django.utils.translation import gettext_lazy as _ +from django.apps import AppConfig + + +class FamilyConfig(AppConfig): + name = 'family' + verbose_name = _('family') diff --git a/apps/family/migrations/__init__.py b/apps/family/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/family/models.py b/apps/family/models.py new file mode 100644 index 00000000..b3cb9abd --- /dev/null +++ b/apps/family/models.py @@ -0,0 +1,165 @@ +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: + 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 + ) + + 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) + 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() + + @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() diff --git a/apps/family/tests.py b/apps/family/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/family/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/family/views.py b/apps/family/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/apps/family/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 0f3a757c..4b31e359 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -70,6 +70,7 @@ INSTALLED_APPS = [ # Note apps 'api', 'activity', + 'family', 'food', 'logs', 'member', @@ -270,7 +271,7 @@ OAUTH2_PROVIDER = { 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'OIDC_ENABLED': True, 'OIDC_RSA_PRIVATE_KEY': - os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'), + os.getenv('OIDC_RSA_PRIVATE_KEY', 'CHANGE_ME_IN_ENV_SETTINGS').replace('\\n', '\n'), # for multilines 'SCOPES': { 'openid': "OpenID Connect scope" }, }