From f6ad6197de62a231b0e95b8c0918fa0d467e2187 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Sat, 5 Jul 2025 19:47:35 +0200 Subject: [PATCH] ListViews et templates --- apps/family/admin.py | 3 - apps/family/migrations/0001_initial.py | 85 +++++++++++++++++++ apps/family/models.py | 37 ++++++++ apps/family/tables.py | 40 +++++++++ .../templates/family/challenge_list.html | 30 +++++++ apps/family/templates/family/family_list.html | 30 +++++++ apps/family/tests.py | 3 - apps/family/urls.py | 12 +++ apps/family/views.py | 64 +++++++++++++- note_kfet/templates/base.html | 7 ++ note_kfet/urls.py | 5 +- 11 files changed, 306 insertions(+), 10 deletions(-) delete mode 100644 apps/family/admin.py create mode 100644 apps/family/migrations/0001_initial.py create mode 100644 apps/family/tables.py create mode 100644 apps/family/templates/family/challenge_list.html create mode 100644 apps/family/templates/family/family_list.html delete mode 100644 apps/family/tests.py create mode 100644 apps/family/urls.py diff --git a/apps/family/admin.py b/apps/family/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/apps/family/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/family/migrations/0001_initial.py b/apps/family/migrations/0001_initial.py new file mode 100644 index 00000000..86c7c135 --- /dev/null +++ b/apps/family/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 4.2.21 on 2025-07-04 19:05 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ChallengeCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'challenge category', + 'verbose_name_plural': 'challenge categories', + }, + ), + migrations.CreateModel( + name='Family', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), + ('description', models.CharField(max_length=255, verbose_name='description')), + ('score', models.PositiveIntegerField(verbose_name='score')), + ('rank', models.PositiveIntegerField(verbose_name='rank')), + ], + options={ + 'verbose_name': 'Family', + 'verbose_name_plural': 'Families', + }, + ), + migrations.CreateModel( + name='Challenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('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')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challengecategory', verbose_name='category')), + ], + options={ + 'verbose_name': 'challenge', + 'verbose_name_plural': 'challenges', + }, + ), + migrations.CreateModel( + name='Achievement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('obtained_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='obtained at')), + ('challenge', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.challenge')), + ('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='family.family', verbose_name='family')), + ], + options={ + 'verbose_name': 'achievement', + 'verbose_name_plural': 'achievements', + }, + ), + migrations.CreateModel( + name='FamilyMembership', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.PositiveIntegerField(default=2025, verbose_name='year')), + ('family', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='members', to='family.family', verbose_name='family')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='family_memberships', to=settings.AUTH_USER_MODEL, verbose_name='user')), + ], + options={ + 'verbose_name': 'family membership', + 'verbose_name_plural': 'family memberships', + 'unique_together': {('user', 'year')}, + }, + ), + ] diff --git a/apps/family/models.py b/apps/family/models.py index b3cb9abd..2b10999b 100644 --- a/apps/family/models.py +++ b/apps/family/models.py @@ -1,3 +1,6 @@ +# 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 @@ -53,6 +56,7 @@ class FamilyMembership(models.Model): ) class Meta: + unique_together = ('user', 'year',) verbose_name = _('family membership') verbose_name_plural = _('family memberships') @@ -96,6 +100,10 @@ class Challenge(models.Model): on_delete=models.PROTECT ) + obtained = models.PositiveIntegerField( + verbose_name=_('obtained') + ) + class Meta: verbose_name = _('challenge') verbose_name_plural = _('challenges') @@ -128,12 +136,27 @@ class Achievement(models.Model): 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 @@ -146,6 +169,13 @@ class Achievement(models.Model): 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): """ @@ -163,3 +193,10 @@ class Achievement(models.Model): 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() diff --git a/apps/family/tables.py b/apps/family/tables.py new file mode 100644 index 00000000..0e3ffc47 --- /dev/null +++ b/apps/family/tables.py @@ -0,0 +1,40 @@ +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import django_tables2 as tables +from django_tables2 import A + +from .models import Family, Challenge + + +class FamilyTable(tables.Table): + """ + List all families + """ + name = tables.LinkColumn( + "family:family_detail", + args=[A("pk")], + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Family + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'score', 'rank',) + order_by = ('rank',) + + +class ChallengeTable(tables.Table): + """ + List all challenges + """ + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + order_by = ('id',) + model = Challenge + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'points', 'category',) diff --git a/apps/family/templates/family/challenge_list.html b/apps/family/templates/family/challenge_list.html new file mode 100644 index 00000000..f16f37a7 --- /dev/null +++ b/apps/family/templates/family/challenge_list.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% comment %} +Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+ +
+ +<
+

+ {{ title }} +

+ {% render_table table %} +
+{% endblock %} + diff --git a/apps/family/templates/family/family_list.html b/apps/family/templates/family/family_list.html new file mode 100644 index 00000000..38738fcf --- /dev/null +++ b/apps/family/templates/family/family_list.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% comment %} +Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block content %} +
+ +
+ +<
+

+ {{ title }} +

+ {% render_table table %} +
+{% endblock %} + diff --git a/apps/family/tests.py b/apps/family/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/apps/family/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/apps/family/urls.py b/apps/family/urls.py new file mode 100644 index 00000000..99b87d92 --- /dev/null +++ b/apps/family/urls.py @@ -0,0 +1,12 @@ +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from .views import FamilyListView, ChallengeListView + +app_name = 'family' +urlpatterns = [ + path('list/', FamilyListView.as_view(), name="family_list"), + path('challenge/list/', ChallengeListView.as_view(), name="challenge_list"), +] diff --git a/apps/family/views.py b/apps/family/views.py index 91ea44a2..8d41ccac 100644 --- a/apps/family/views.py +++ b/apps/family/views.py @@ -1,3 +1,63 @@ -from django.shortcuts import render +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later -# Create your views here. +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import DetailView, UpdateView +from django.utils.translation import gettext_lazy as _ +from django_tables2 import SingleTableView +from permission.views import ProtectQuerysetMixin, ProtectedCreateView + +from .models import Family, Challenge +from .tables import FamilyTable, ChallengeTable + + +class FamilyCreateView(ProtectQuerysetMixin, ProtectedCreateView): + """ + Create family + """ + model = Family + extra_context = {"title": _('Create family')} + + def get_sample_object(self): + return Family( + name="", + description="Sample family", + score=0, + rank=0, + ) + + +class FamilyListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + List existing Families + """ + model = Family + table_class = FamilyTable + extra_context = {"title": _('Families list')} + + +class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Display details of a family + """ + model = Family + context_object_name = "family" + extra_context = {"title": _('Family detail')} + + +class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update the information of a family. + """ + model = Family + context_object_name = "family" + extra_context = {"title": _('Update family')} + + +class ChallengeListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + List all challenges + """ + model = Challenge + table_class = ChallengeTable + extra_context = {"title": _('Challenges list')} diff --git a/note_kfet/templates/base.html b/note_kfet/templates/base.html index 1c601c50..9301ee36 100644 --- a/note_kfet/templates/base.html +++ b/note_kfet/templates/base.html @@ -78,6 +78,13 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Transfer' %} {% endif %} + {% if user.is_authenticated %} + + {% endif %} + {% if "auth.user"|model_list_length >= 2 %}