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
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 %}
+
+
+
+
+ {% 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
- {% render_table table %}
+ {% render_table invalid %}
+
+
+ {% 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