From c66cc14576a9e02ee3b228732dad2dde91f17abf Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Tue, 22 Jul 2025 01:30:47 +0200 Subject: [PATCH] Added valid field and logic for Achievement --- ...ent_valid_alter_familymembership_family.py | 24 +++++++++ apps/family/models.py | 11 ++-- apps/family/tables.py | 26 +++++++-- .../family/achievement_confirm_delete.html | 4 +- .../family/achievement_confirm_validate.html | 28 ++++++++++ .../templates/family/achievement_list.html | 15 +++++- apps/family/urls.py | 17 +++--- apps/family/views.py | 54 +++++++++++++++---- apps/member/views.py | 4 +- 9 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 apps/family/migrations/0003_achievement_valid_alter_familymembership_family.py create mode 100644 apps/family/templates/family/achievement_confirm_validate.html diff --git a/apps/family/migrations/0003_achievement_valid_alter_familymembership_family.py b/apps/family/migrations/0003_achievement_valid_alter_familymembership_family.py new file mode 100644 index 00000000..9121a1ff --- /dev/null +++ b/apps/family/migrations/0003_achievement_valid_alter_familymembership_family.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.4 on 2025-07-21 21:02 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('family', '0002_family_display_image'), + ] + + operations = [ + migrations.AddField( + model_name='achievement', + name='valid', + field=models.BooleanField(default=False, verbose_name='valid'), + ), + migrations.AlterField( + model_name='familymembership', + name='family', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='family.family', verbose_name='family'), + ), + ] diff --git a/apps/family/models.py b/apps/family/models.py index 1acc9ba8..708c3929 100644 --- a/apps/family/models.py +++ b/apps/family/models.py @@ -45,9 +45,9 @@ class Family(models.Model): return self.name def update_score(self, *args, **kwargs): - challenge_set = Challenge.objects.select_for_update().filter(achievement__family=self) + 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"] + self.score = points_sum["points__sum"] if points_sum["points__sum"] else 0 self.save() self.update_ranking() @@ -86,7 +86,7 @@ class FamilyMembership(models.Model): family = models.ForeignKey( Family, on_delete=models.PROTECT, - related_name=_('members'), + related_name=_('memberships'), verbose_name=_('family'), ) @@ -157,6 +157,11 @@ class Achievement(models.Model): default=timezone.now, ) + valid = models.BooleanField( + verbose_name=_('valid'), + default=False, + ) + class Meta: verbose_name = _('achievement') verbose_name_plural = _('achievements') diff --git a/apps/family/tables.py b/apps/family/tables.py index 759de96d..0a0b773a 100644 --- a/apps/family/tables.py +++ b/apps/family/tables.py @@ -65,6 +65,23 @@ class AchievementTable(tables.Table): """ List recent achievements. """ + validate = tables.LinkColumn( + 'family:achievement_validate', + args=[A('id')], + verbose_name=_("Validate"), + text=_("Validate"), + orderable=False, + attrs={ + 'th': { + 'id': 'validate-achievement-header' + }, + 'a': { + 'class': 'btn btn-success', + 'data-type': 'validate-achievement' + } + }, + ) + delete = tables.LinkColumn( 'family:achievement_delete', args=[A('id')], @@ -73,11 +90,11 @@ class AchievementTable(tables.Table): orderable=False, attrs={ 'th': { - 'id': 'delete-membership-header' + 'id': 'delete-achievement-header' }, 'a': { 'class': 'btn btn-danger', - 'data-type': 'delete-membership' + 'data-type': 'delete-achievement' } }, ) @@ -87,10 +104,11 @@ class AchievementTable(tables.Table): 'class': 'table table-condensed table-striped table-hover' } model = Achievement - fields = ('family', 'challenge', 'challenge__points', 'obtained_at', ) + fields = ('family', 'challenge', 'challenge__points', 'obtained_at', 'valid') template_name = 'django_tables2/bootstrap4.html' order_by = ('-obtained_at',) + class FamilyAchievementTable(tables.Table): """ Table des défis réalisés par une famille spécifique. @@ -102,4 +120,4 @@ class FamilyAchievementTable(tables.Table): attrs = { 'class': 'table table-condensed table-striped table-hover' } - order_by = ('-obtained_at',) \ No newline at end of file + order_by = ('-obtained_at',) diff --git a/apps/family/templates/family/achievement_confirm_delete.html b/apps/family/templates/family/achievement_confirm_delete.html index 893e0afb..3b378fa5 100644 --- a/apps/family/templates/family/achievement_confirm_delete.html +++ b/apps/family/templates/family/achievement_confirm_delete.html @@ -18,9 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% csrf_token %} {% trans "Return to achievements list" %} - {% if not object.locked %} - - {% endif %} +
diff --git a/apps/family/templates/family/achievement_confirm_validate.html b/apps/family/templates/family/achievement_confirm_validate.html new file mode 100644 index 00000000..e417480a --- /dev/null +++ b/apps/family/templates/family/achievement_confirm_validate.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +
+
+

{% trans "Validate achievement" %}

+
+
+
+ {% blocktrans %}Are you sure you want to validate this achievement? This action can't be undone.{% endblocktrans %} +
+
+ +
+{% endblock %} diff --git a/apps/family/templates/family/achievement_list.html b/apps/family/templates/family/achievement_list.html index fe1fd62f..63cd6255 100644 --- a/apps/family/templates/family/achievement_list.html +++ b/apps/family/templates/family/achievement_list.html @@ -10,13 +10,24 @@ SPDX-License-Identifier: GPL-3.0-or-later

- {% trans "Recent achievements history" %} + {% trans "Invalid achievements history" %}

{% trans "Return to management page" %}
- {% render_table table %} + {% render_table invalid %}
+
+
+

+ {% trans "Valid achievements history" %} +

+ + {% trans "Return to management page" %} + +
+ {% render_table valid %} +
{% endblock %} \ No newline at end of file diff --git a/apps/family/urls.py b/apps/family/urls.py index 072cbada..7d9e9c5b 100644 --- a/apps/family/urls.py +++ b/apps/family/urls.py @@ -9,16 +9,17 @@ app_name = 'family' urlpatterns = [ path('list/', views.FamilyListView.as_view(), name="family_list"), path('add-family/', views.FamilyCreateView.as_view(), name="add_family"), - path('detail//', views.FamilyDetailView.as_view(), name="family_detail"), - path('update//', views.FamilyUpdateView.as_view(), name="family_update"), - path('update_pic//', views.FamilyPictureUpdateView.as_view(), name="update_pic"), - path('add_member//', views.FamilyAddMemberView.as_view(), name="family_add_member"), + path('/detail/', views.FamilyDetailView.as_view(), name="family_detail"), + path('/update/', views.FamilyUpdateView.as_view(), name="family_update"), + path('/update_pic/', views.FamilyPictureUpdateView.as_view(), name="update_pic"), + path('/add_member/', views.FamilyAddMemberView.as_view(), name="family_add_member"), path('challenge/list/', views.ChallengeListView.as_view(), name="challenge_list"), path('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"), - path('challenge/detail//', views.ChallengeDetailView.as_view(), name="challenge_detail"), - path('challenge/update//', views.ChallengeUpdateView.as_view(), name="challenge_update"), + path('challenge//detail/', views.ChallengeDetailView.as_view(), name="challenge_detail"), + path('challenge//update/', views.ChallengeUpdateView.as_view(), name="challenge_update"), path('manage/', views.FamilyManageView.as_view(), name="manage"), - path('achievements/', views.AchievementsView.as_view(), name="achievement_list"), - path('achievement/delete//', views.AchievementDeleteView.as_view(), name="achievement_delete"), + path('achievement/list/', views.AchievementsView.as_view(), name="achievement_list"), + path('achievement//validate/', views.AchievementValidateView.as_view(), name="achievement_validate"), + path('achievement//delete/', views.AchievementDeleteView.as_view(), name="achievement_delete"), path('api/family/', include('family.api.urls')), ] diff --git a/apps/family/views.py b/apps/family/views.py index 33108f32..a6e886a8 100644 --- a/apps/family/views.py +++ b/apps/family/views.py @@ -4,12 +4,14 @@ from datetime import date from django.conf import settings +from django.shortcuts import redirect from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction -from django.views.generic import DetailView, UpdateView +from django.views.generic import DetailView, UpdateView, ListView from django.views.generic.edit import DeleteView +from django.views.generic.base import TemplateView from django.utils.translation import gettext_lazy as _ -from django_tables2 import SingleTableView +from django_tables2 import SingleTableView, MultiTableMixin from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin, ProtectedCreateView from django.urls import reverse_lazy @@ -287,23 +289,57 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView def get_table(self, **kwargs): table = super().get_table(**kwargs) - table.exclude = ('delete',) + table.exclude = ('delete', 'validate',) table.orderable = False return table -class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): +class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): """ List all achievements """ model = Achievement - table_class = AchievementTable + tables = [AchievementTable, AchievementTable, ] extra_context = {'title': _('Achievement list')} - def get_table(self, **kwargs): - table = super().get_table(**kwargs) - table.orderable = True - return table + def get_tables(self, **kwargs): + tables = super().get_tables(**kwargs) + + tables[0].prefix = 'invalid-' + tables[1].prefix = 'valid-' + tables[1].exclude = ('validate', 'delete',) + + return tables + + def get_tables_data(self): + table_valid = self.get_queryset().filter(valid=True) + table_invalid = self.get_queryset().filter(valid=False) + return [table_invalid, table_valid, ] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + tables = context['tables'] + + context['invalid'] = tables[0] + context['valid'] = tables[1] + return context + + +class AchievementValidateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView): + """ + Validate an achievement obtained by a family + """ + template_name = 'family/achievement_confirm_validate.html' + + def post(self, request, pk): + # On récupère l'objet à valider + achievement = Achievement.objects.get(pk=pk) + # On modifie le champ valid + achievement.valid = True + achievement.save() + # On redirige vers la page de détail ou la liste + return redirect(reverse_lazy('family:achievement_list')) class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): diff --git a/apps/member/views.py b/apps/member/views.py index 3c1ebef7..3cf3cd32 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -207,8 +207,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): modified_note.is_active = True context["can_unlock_note"] = not user.note.is_active and PermissionBackend\ .check_perm(self.request, "note.change_noteuser_is_active", modified_note) - - families = Family.objects.filter(members__user=user).distinct() + + families = Family.objects.filter(memberships__user=user).distinct() context["families"] = families return context