From adc925e4b14bcf911eba217b3f6f492e1f432d73 Mon Sep 17 00:00:00 2001 From: Ehouarn Date: Tue, 22 Jul 2025 18:31:55 +0200 Subject: [PATCH] Tests --- apps/family/api/views.py | 35 +- .../0004_remove_challenge_obtained.py | 17 + apps/family/models.py | 34 +- .../static/family/img/default_picture.png | Bin 0 -> 4054 bytes apps/family/static/family/js/achievements.js | 34 +- apps/family/templates/family/manage.html | 8 +- apps/family/tests/__init__.py | 0 apps/family/tests/test_family.py | 318 ++++++++++++++++++ apps/family/urls.py | 8 +- apps/family/views.py | 35 +- 10 files changed, 403 insertions(+), 86 deletions(-) create mode 100644 apps/family/migrations/0004_remove_challenge_obtained.py create mode 100644 apps/family/static/family/img/default_picture.png create mode 100644 apps/family/tests/__init__.py create mode 100644 apps/family/tests/test_family.py diff --git a/apps/family/api/views.py b/apps/family/api/views.py index 50ac0496..79a719d1 100644 --- a/apps/family/api/views.py +++ b/apps/family/api/views.py @@ -3,7 +3,7 @@ from api.viewsets import ReadProtectedModelViewSet from django_filters.rest_framework import DjangoFilterBackend -from rest_framework.filters import SearchFilter +from api.filters import RegexSafeSearchFilter from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -21,9 +21,9 @@ class FamilyViewSet(ReadProtectedModelViewSet): """ queryset = Family.objects.order_by('id') serializer_class = FamilySerializer - filter_backends = [DjangoFilterBackend, SearchFilter] - filterset_fields = ['name', ] - search_fields = ['$name', ] + filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] + filterset_fields = ['name', 'description', 'score', 'rank', ] + search_fields = ['$name', '$description', ] class FamilyMembershipViewSet(ReadProtectedModelViewSet): @@ -34,9 +34,11 @@ class FamilyMembershipViewSet(ReadProtectedModelViewSet): """ queryset = FamilyMembership.objects.order_by('id') serializer_class = FamilyMembershipSerializer - filter_backends = [DjangoFilterBackend, SearchFilter] - filterset_fields = ['name', ] - search_fields = ['$name', ] + filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] + filterset_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__email', 'user__note__alias__name', + 'user__note__alias__normalized_name', 'family__name', 'family__description', 'year', ] + search_fields = ['$user__username', '$user__first_name', '$user__last_name', '$user__email', '$user__note__alias__name', + '$user__note__alias__normalized_name', '$family__name', '$family__description', '$year', ] class ChallengeViewSet(ReadProtectedModelViewSet): @@ -47,9 +49,9 @@ class ChallengeViewSet(ReadProtectedModelViewSet): """ queryset = Challenge.objects.order_by('id') serializer_class = ChallengeSerializer - filter_backends = [DjangoFilterBackend, SearchFilter] - filterset_fields = ['name', ] - search_fields = ['$name', ] + filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] + filterset_fields = ['name', 'description', 'points', ] + search_fields = ['$name', '$description', '$points', ] class AchievementViewSet(ReadProtectedModelViewSet): @@ -60,22 +62,19 @@ class AchievementViewSet(ReadProtectedModelViewSet): """ queryset = Achievement.objects.order_by('id') serializer_class = AchievementSerializer - filter_backends = [DjangoFilterBackend, SearchFilter] - filterset_fields = ['name', ] - search_fields = ['$name', ] + filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] + filterset_fields = ['family__name', 'family__description', 'challenge__name', 'challenge__description', 'obtained_at', 'valid', ] + search_fields = ['$family__name', '$family__description', '$challenge__name', '$challenge__description', ] class BatchAchievementsAPIView(APIView): permission_classes = [IsAuthenticated] def post(self, request, format=None): - print("POST de la view spéciale") - family_ids = request.data.get('families', []) - challenge_ids = request.data.get('challenges', []) - + family_ids = request.data.get('families') + challenge_ids = request.data.get('challenges') families = Family.objects.filter(id__in=family_ids) challenges = Challenge.objects.filter(id__in=challenge_ids) - for family in families: for challenge in challenges: a = Achievement(family=family, challenge=challenge) diff --git a/apps/family/migrations/0004_remove_challenge_obtained.py b/apps/family/migrations/0004_remove_challenge_obtained.py new file mode 100644 index 00000000..2831bac1 --- /dev/null +++ b/apps/family/migrations/0004_remove_challenge_obtained.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.4 on 2025-07-22 14:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('family', '0003_achievement_valid_alter_familymembership_family'), + ] + + operations = [ + migrations.RemoveField( + model_name='challenge', + name='obtained', + ), + ] diff --git a/apps/family/models.py b/apps/family/models.py index 708c3929..71ccfa08 100644 --- a/apps/family/models.py +++ b/apps/family/models.py @@ -4,6 +4,7 @@ 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 _ @@ -44,6 +45,9 @@ class Family(models.Model): 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")) @@ -119,10 +123,16 @@ class Challenge(models.Model): verbose_name=_('points'), ) - obtained = models.PositiveIntegerField( - verbose_name=_('obtained'), - default=0, - ) + @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): @@ -136,9 +146,6 @@ class Challenge(models.Model): verbose_name = _('challenge') verbose_name_plural = _('challenges') - def __str__(self): - return self.name - class Achievement(models.Model): challenge = models.ForeignKey( @@ -176,7 +183,6 @@ class Achievement(models.Model): """ 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) @@ -184,13 +190,6 @@ class Achievement(models.Model): 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): """ @@ -205,8 +204,3 @@ class Achievement(models.Model): # 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() diff --git a/apps/family/static/family/img/default_picture.png b/apps/family/static/family/img/default_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..41a31a1cf5eddf31e98a7e6cd06b4c45f2faee92 GIT binary patch literal 4054 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 IM6N<$f;wYbJOBUy literal 0 HcmV?d00001 diff --git a/apps/family/static/family/js/achievements.js b/apps/family/static/family/js/achievements.js index db29923d..ebec8689 100644 --- a/apps/family/static/family/js/achievements.js +++ b/apps/family/static/family/js/achievements.js @@ -113,6 +113,7 @@ function reset () { * Apply all transactions: all notes in `notes` buy each item in `buttons` */ function consumeAll () { + console.log("test"); if (LOCK) { return } LOCK = true @@ -130,11 +131,13 @@ function consumeAll () { LOCK = false return } - + console.log("couocu") // Récupérer les IDs des familles et des challenges const family_ids = notes_display.map(fam => fam.id) const challenge_ids = buttons.map(chal => chal.id) + console.log(family_ids) + console.log(challenge_ids) $.ajax({ url: '/family/api/family/achievements/batch/', type: 'POST', @@ -157,34 +160,6 @@ function consumeAll () { }) } -/** - * Create a new achievement through the API. - * @param family The selected family - * @param challenge The selected challenge - */ -function grantAchievement (family, challenge) { - console.log("grant lancée",family,challenge) - $.post('/api/family/achievement/', - { - csrfmiddlewaretoken: CSRF_TOKEN, - family: family.id, - challenge: challenge.id, - }) - .done(function () { - reset() - addMsg("Défi validé pour la famille !", 'success', 5000) - }) - .fail(function (e) { - reset() - if (e.responseJSON) { - errMsg(e.responseJSON) - } else if (e.responseText) { - errMsg(e.responseText) - } else { - errMsg("Erreur inconnue lors de la création de l'achievement.") - } - }) -} var searchbar = document.getElementById("search-input") var search_results = document.getElementById("search-results") @@ -264,7 +239,6 @@ function li (id, text, extra_css) { */ function autoCompleteFamily(field_id, family_list_id, families, families_display, family_prefix = 'family', user_family_field = null, profile_pic_field = null, family_click = null) { const field = $('#' + field_id) - console.log("autoCompleteFamily commence") // Configuration du tooltip field.tooltip({ html: true, diff --git a/apps/family/templates/family/manage.html b/apps/family/templates/family/manage.html index 0ecac60a..22a4ed90 100644 --- a/apps/family/templates/family/manage.html +++ b/apps/family/templates/family/manage.html @@ -84,12 +84,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if can_add_family %} - + {% trans "Add a family" %} {% endif %} {% if can_add_challenge %} - + {% trans "Add a challenge" %} {% endif %} @@ -147,7 +147,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
-{# transaction history #} +{# achievement history #}
-
+
{% render_table table %}
diff --git a/apps/family/tests/__init__.py b/apps/family/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/family/tests/test_family.py b/apps/family/tests/test_family.py new file mode 100644 index 00000000..f2b31784 --- /dev/null +++ b/apps/family/tests/test_family.py @@ -0,0 +1,318 @@ +# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import os + +from api.tests import TestAPI +from django.contrib.auth.models import User +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from rest_framework.test import APITestCase +from django.urls import reverse +from django.utils import timezone + +from ..api.views import FamilyViewSet, FamilyMembershipViewSet, ChallengeViewSet, AchievementViewSet +from ..models import Family, FamilyMembership, Challenge, Achievement + + +class TestFamily(TestCase): + """ + Test family + """ + + def setUp(self): + self.user = User.objects.create_superuser( + username='admintoto', + password='toto1234', + email='toto@example.com', + ) + self.client.force_login(self.user) + + sess = self.client.session + sess['permission_mask'] = 42 + sess.save() + + self.family = Family.objects.create( + name='Test family', + description='', + ) + + self.challenge = Challenge.objects.create( + name='Test challenge', + description='', + points=100, + ) + + self.achievement = Achievement.objects.create( + family=self.family, + challenge=self.challenge, + valid=False, + ) + + def test_family_list(self): + """ + Test display family list + """ + response = self.client.get(reverse("family:family_list")) + self.assertEqual(response.status_code, 200) + + def test_family_create(self): + """ + Test create a family + """ + response = self.client.get(reverse("family:family_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("family:family_create"), data={ + "name": "Family toto", + "description": "A test family", + }) + self.assertTrue(Family.objects.filter(name="Family toto").exists()) + self.assertRedirects(response, reverse("family:manage"), 302, 200) + + def test_family_detail(self): + """ + Test display the detail of a family + """ + response = self.client.get(reverse("family:family_detail", args=(self.family.pk,))) + self.assertEqual(response.status_code, 200) + + def test_family_update(self): + """ + Test update a family + """ + response = self.client.get(reverse("family:family_update", args=(self.family.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("family:family_update", args=(self.family.pk,)), data=dict( + name="Toto family updated", + description="A larger description for the test family" + )) + self.assertRedirects(response, self.family.get_absolute_url(), 302, 200) + self.assertTrue(Family.objects.filter(name="Toto family updated").exists()) + + def test_family_update_picture(self): + """ + Test update the picture of a family + """ + response = self.client.get(reverse("family:update_pic", args=(self.family.pk,))) + self.assertEqual(response.status_code, 200) + + old_pic = self.family.display_image + + with open("apps/family/static/family/img/default_picture.png", "rb") as f: + image = SimpleUploadedFile("image.png", f.read(), "image/png") + response = self.client.post(reverse("family:update_pic", args=(self.family.pk,)), dict( + image=image, + x=0, + y=0, + width=200, + height=200, + )) + self.assertRedirects(response, self.family.get_absolute_url(), 302, 200) + + self.family.refresh_from_db() + self.assertTrue(os.path.exists(self.family.display_image.path)) + os.remove(self.family.display_image.path) + + self.family.display_image = old_pic + self.family.save() + + def test_family_add_member(self): + """ + Test add memberships to a family + """ + response = self.client.get(reverse("family:family_add_member", args=(self.family.pk,))) + self.assertEqual(response.status_code, 200) + + user = User.objects.create(username="totototo") + user.profile.registration_valid = True + user.profile.email_confirmed = True + user.profile.save() + user.save() + + response = self.client.post(reverse("family:family_add_member", args=(self.family.pk,)), data=dict( + user=user.pk, + )) + self.assertRedirects(response, self.family.get_absolute_url(), 302, 200) + + self.assertTrue(FamilyMembership.objects.filter(user=user, family=self.family, year=timezone.now().year).exists()) + + def test_challenge_list(self): + """ + Test display challenge list + """ + response = self.client.get(reverse('family:challenge_list')) + self.assertEqual(response.status_code, 200) + + def test_challenge_create(self): + """ + Test create a challenge + """ + response = self.client.get(reverse("family:challenge_create")) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("family:challenge_create"), data={ + "name": "Challenge for toto", + "description": "A test challenge", + "points": 50, + }) + self.assertTrue(Challenge.objects.filter(name="Challenge for toto").exists()) + self.assertRedirects(response, reverse("family:manage"), 302, 200) + + def test_challenge_detail(self): + """ + Test display the detail of a challenge + """ + response = self.client.get(reverse("family:challenge_detail", args=(self.challenge.pk,))) + self.assertEqual(response.status_code, 200) + + def test_challenge_update(self): + """ + Test update a challenge + """ + response = self.client.get(reverse("family:challenge_update", args=(self.challenge.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("family:challenge_update", args=(self.challenge.pk,)), data=dict( + name="Challenge updated", + description="Another description", + points=10, + )) + self.assertRedirects(response, self.challenge.get_absolute_url(), 302, 200) + self.assertTrue(Challenge.objects.filter(name="Challenge updated").exists()) + + def test_render_manage_page(self): + """ + Test render manage page + """ + response = self.client.get(reverse("family:manage")) + self.assertEqual(response.status_code, 200) + + def test_validate_achievement(self): + """ + Test validate an achievement + """ + old_family_score = self.family.score + + response = self.client.get(reverse("family:achievement_validate", args=(self.achievement.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("family:achievement_validate", args=(self.achievement.pk,))) + self.assertRedirects(response, reverse("family:achievement_list"), 302, 200) + + self.achievement.refresh_from_db() + self.assertIs(self.achievement.valid, True) + + self.family.refresh_from_db() + self.assertEqual(self.family.score, old_family_score + self.achievement.challenge.points) + + def test_delete_achievement(self): + """ + Test delete an achievement + """ + response = self.client.get(reverse("family:achievement_delete", args=(self.achievement.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.delete(reverse("family:achievement_delete", args=(self.achievement.pk,))) + self.assertRedirects(response, reverse("family:achievement_list"), 302, 200) + self.assertFalse(Achievement.objects.filter(pk=self.achievement.pk).exists()) + + +class TestBatchAchievements(APITestCase): + def setUp(self): + self.user = User.objects.create_superuser( + username='admintoto', + password='toto1234', + email='toto@example.com', + ) + self.client.force_login(self.user) + + sess = self.client.session + sess['permission_mask'] = 42 + sess.save() + + self.families = [ + Family.objects.create(name=f'Famille {i}', description='') for i in range(2) + ] + self.challenges = [ + Challenge.objects.create(name=f'Challenge {i}', description='', points=50) for i in range(3) + ] + + self.url = reverse("family:api:batch_achievements") + + def test_batch_achievement_creation(self): + family_ids = [f.id for f in self.families] + challenge_ids = [c.id for c in self.challenges] + response = self.client.post( + self.url, + data={ + 'families': family_ids, + 'challenges': challenge_ids + }, + format='json' + ) + + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data['status'], 'ok') + + expected_count = len(family_ids) * len(challenge_ids) + self.assertEqual(Achievement.objects.count(), expected_count) + + # Check that correct couples family/challenge exist + for f in self.families: + for c in self.challenges: + self.assertTrue( + Achievement.objects.filter(family=f, challenge=c).exists() + ) + + +class TestFamilyAPI(TestAPI): + def setUp(self): + super().setUp() + + self.family = Family.objects.create( + name='Test family', + description='', + ) + + self.familymembership = FamilyMembership.objects.create( + user=self.user, + family=self.family, + ) + + self.challenge = Challenge.objects.create( + name='Test challenge', + description='', + points=100, + ) + + self.achievement = Achievement.objects.create( + family=self.family, + challenge=self.challenge, + valid=False, + ) + + def test_family_api(self): + """ + Load Family API page and test all filters and permissions + """ + self.check_viewset(FamilyViewSet, '/api/family/family/') + + def test_familymembership_api(self): + """ + Load FamilyMembership API page and test all filters and permissions + """ + self.check_viewset(FamilyMembershipViewSet, '/api/family/familymembership/') + + def test_challenge_api(self): + """ + Load Challenge API page and test all filters and permissions + """ + self.check_viewset(ChallengeViewSet, '/api/family/challenge/') + + def test_achievement_api(self): + """ + Load Achievement API page and test all filters and permissions + """ + self.check_viewset(AchievementViewSet, '/api/family/achievement/') diff --git a/apps/family/urls.py b/apps/family/urls.py index 7d9e9c5b..edb0d18a 100644 --- a/apps/family/urls.py +++ b/apps/family/urls.py @@ -8,18 +8,18 @@ from . import views app_name = 'family' urlpatterns = [ path('list/', views.FamilyListView.as_view(), name="family_list"), - path('add-family/', views.FamilyCreateView.as_view(), name="add_family"), + path('create/', views.FamilyCreateView.as_view(), name="family_create"), 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/create/', views.ChallengeCreateView.as_view(), name="challenge_create"), 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('achievement/list/', views.AchievementsView.as_view(), name="achievement_list"), + path('achievement/list/', views.AchievementListView.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')), + path('api/family/', include(('family.api.urls', 'family_api'), namespace='api')), ] diff --git a/apps/family/views.py b/apps/family/views.py index a6e886a8..aee6f276 100644 --- a/apps/family/views.py +++ b/apps/family/views.py @@ -8,14 +8,14 @@ from django.shortcuts import redirect from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.views.generic import DetailView, UpdateView, ListView -from django.views.generic.edit import DeleteView +from django.views.generic.edit import DeleteView, FormMixin from django.views.generic.base import TemplateView from django.utils.translation import gettext_lazy as _ from django_tables2 import SingleTableView, MultiTableMixin from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin, ProtectedCreateView from django.urls import reverse_lazy -from member.views import PictureUpdateView +from member.forms import ImageForm from .models import Family, Challenge, FamilyMembership, User, Achievement from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable, FamilyAchievementTable @@ -112,17 +112,28 @@ class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): return reverse_lazy('family:family_detail', kwargs={'pk': self.object.pk}) -class FamilyPictureUpdateView(PictureUpdateView): +class FamilyPictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): """ Update profile picture of the family """ model = Family extra_context = {"title": _("Update family picture")} template_name = 'family/picture_update.html' + form_class = ImageForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = self.form_class(self.request.POST, self.request.FILES) + return context def get_success_url(self): """Redirect to family page after upload""" - return reverse_lazy('family:family_detail', kwargs={'pk': self.object.id}) + return reverse_lazy('family:family_detail', kwargs={'pk': self.object.pk}) + + def post(self, request, *args, **kwargs): + form = self.get_form() + self.object = self.get_object() + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) @transaction.atomic def form_valid(self, form): @@ -141,6 +152,11 @@ class FamilyPictureUpdateView(PictureUpdateView): else: image.name = "{}_pic.png".format(self.object.pk) + # Save + self.object.display_image = image + self.object.save() + return super().form_valid(form) + class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView): """ @@ -282,8 +298,8 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView PermissionBackend.filter_queryset(self.request, Challenge, "view") ).order_by('name') - context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.add_family") - context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.add_challenge") + context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.family_create") + context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.challenge_create") return context @@ -294,7 +310,7 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView return table -class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): +class AchievementListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): """ List all achievements """ @@ -333,12 +349,11 @@ class AchievementValidateView(ProtectQuerysetMixin, LoginRequiredMixin, Template 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'))