1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-07-23 09:16:48 +02:00

Added valid field and logic for Achievement

This commit is contained in:
Ehouarn
2025-07-22 01:30:47 +02:00
parent db4d0dd83a
commit c66cc14576
9 changed files with 152 additions and 31 deletions

View File

@ -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'),
),
]

View File

@ -45,9 +45,9 @@ class Family(models.Model):
return self.name return self.name
def update_score(self, *args, **kwargs): 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")) 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.save()
self.update_ranking() self.update_ranking()
@ -86,7 +86,7 @@ class FamilyMembership(models.Model):
family = models.ForeignKey( family = models.ForeignKey(
Family, Family,
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name=_('members'), related_name=_('memberships'),
verbose_name=_('family'), verbose_name=_('family'),
) )
@ -157,6 +157,11 @@ class Achievement(models.Model):
default=timezone.now, default=timezone.now,
) )
valid = models.BooleanField(
verbose_name=_('valid'),
default=False,
)
class Meta: class Meta:
verbose_name = _('achievement') verbose_name = _('achievement')
verbose_name_plural = _('achievements') verbose_name_plural = _('achievements')

View File

@ -65,6 +65,23 @@ class AchievementTable(tables.Table):
""" """
List recent achievements. 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( delete = tables.LinkColumn(
'family:achievement_delete', 'family:achievement_delete',
args=[A('id')], args=[A('id')],
@ -73,11 +90,11 @@ class AchievementTable(tables.Table):
orderable=False, orderable=False,
attrs={ attrs={
'th': { 'th': {
'id': 'delete-membership-header' 'id': 'delete-achievement-header'
}, },
'a': { 'a': {
'class': 'btn btn-danger', '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' 'class': 'table table-condensed table-striped table-hover'
} }
model = Achievement model = Achievement
fields = ('family', 'challenge', 'challenge__points', 'obtained_at', ) fields = ('family', 'challenge', 'challenge__points', 'obtained_at', 'valid')
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
order_by = ('-obtained_at',) order_by = ('-obtained_at',)
class FamilyAchievementTable(tables.Table): class FamilyAchievementTable(tables.Table):
""" """
Table des défis réalisés par une famille spécifique. Table des défis réalisés par une famille spécifique.

View File

@ -18,9 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<a class="btn btn-primary" href="{% url 'family:achievement_list' %}">{% trans "Return to achievements list" %}</a> <a class="btn btn-primary" href="{% url 'family:achievement_list' %}">{% trans "Return to achievements list" %}</a>
{% if not object.locked %}
<button class="btn btn-danger" type="submit">{% trans "Delete" %}</button> <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
{% endif %}
</form> </form>
</div> </div>
</div> </div>

View File

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-light">
<div class="card-header text-center">
<h4>{% trans "Validate achievement" %}</h4>
</div>
<div class="card-body">
<div class="alert alert-warning">
{% blocktrans %}Are you sure you want to validate this achievement? This action can't be undone.{% endblocktrans %}
</div>
</div>
<div class="card-footer text-center">
<form method="post">
{% csrf_token %}
<a class="btn btn-primary" href="{% url 'family:achievement_list' %}">{% trans "Return to achievements list" %}</a>
<form method="post" action="{% url 'family:achievement_validate' pk %}">
{% csrf_token %}
<button type="submit" class="btn btn-success">{% trans "Validate" %}</button>
</form>
</form>
</div>
</div>
{% endblock %}

View File

@ -10,13 +10,24 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card mb-4" id="history"> <div class="card mb-4" id="history">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
{% trans "Recent achievements history" %} {% trans "Invalid achievements history" %}
</p> </p>
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:manage" %}"> <a class="btn btn-sm btn-primary mx-2" href="{% url "family:manage" %}">
{% trans "Return to management page" %} {% trans "Return to management page" %}
</a> </a>
</div> </div>
{% render_table table %} {% render_table invalid %}
</div> </div>
<div class="card mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Valid achievements history" %}
</p>
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:manage" %}">
{% trans "Return to management page" %}
</a>
</div>
{% render_table valid %}
</div>
{% endblock %} {% endblock %}

View File

@ -9,16 +9,17 @@ app_name = 'family'
urlpatterns = [ urlpatterns = [
path('list/', views.FamilyListView.as_view(), name="family_list"), path('list/', views.FamilyListView.as_view(), name="family_list"),
path('add-family/', views.FamilyCreateView.as_view(), name="add_family"), path('add-family/', views.FamilyCreateView.as_view(), name="add_family"),
path('detail/<int:pk>/', views.FamilyDetailView.as_view(), name="family_detail"), path('<int:pk>/detail/', views.FamilyDetailView.as_view(), name="family_detail"),
path('update/<int:pk>/', views.FamilyUpdateView.as_view(), name="family_update"), path('<int:pk>/update/', views.FamilyUpdateView.as_view(), name="family_update"),
path('update_pic/<int:pk>/', views.FamilyPictureUpdateView.as_view(), name="update_pic"), path('<int:pk>/update_pic/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
path('add_member/<int:family_pk>/', views.FamilyAddMemberView.as_view(), name="family_add_member"), path('<int:family_pk>/add_member/', views.FamilyAddMemberView.as_view(), name="family_add_member"),
path('challenge/list/', views.ChallengeListView.as_view(), name="challenge_list"), path('challenge/list/', views.ChallengeListView.as_view(), name="challenge_list"),
path('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"), path('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"),
path('challenge/detail/<int:pk>/', views.ChallengeDetailView.as_view(), name="challenge_detail"), path('challenge/<int:pk>/detail/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
path('challenge/update/<int:pk>/', views.ChallengeUpdateView.as_view(), name="challenge_update"), path('challenge/<int:pk>/update/', views.ChallengeUpdateView.as_view(), name="challenge_update"),
path('manage/', views.FamilyManageView.as_view(), name="manage"), path('manage/', views.FamilyManageView.as_view(), name="manage"),
path('achievements/', views.AchievementsView.as_view(), name="achievement_list"), path('achievement/list/', views.AchievementsView.as_view(), name="achievement_list"),
path('achievement/delete/<int:pk>/', views.AchievementDeleteView.as_view(), name="achievement_delete"), path('achievement/<int:pk>/validate/', views.AchievementValidateView.as_view(), name="achievement_validate"),
path('achievement/<int:pk>/delete/', views.AchievementDeleteView.as_view(), name="achievement_delete"),
path('api/family/', include('family.api.urls')), path('api/family/', include('family.api.urls')),
] ]

View File

@ -4,12 +4,14 @@
from datetime import date from datetime import date
from django.conf import settings from django.conf import settings
from django.shortcuts import redirect
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction 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.edit import DeleteView
from django.views.generic.base import TemplateView
from django.utils.translation import gettext_lazy as _ 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.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -287,23 +289,57 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
def get_table(self, **kwargs): def get_table(self, **kwargs):
table = super().get_table(**kwargs) table = super().get_table(**kwargs)
table.exclude = ('delete',) table.exclude = ('delete', 'validate',)
table.orderable = False table.orderable = False
return table return table
class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
""" """
List all achievements List all achievements
""" """
model = Achievement model = Achievement
table_class = AchievementTable tables = [AchievementTable, AchievementTable, ]
extra_context = {'title': _('Achievement list')} extra_context = {'title': _('Achievement list')}
def get_table(self, **kwargs): def get_tables(self, **kwargs):
table = super().get_table(**kwargs) tables = super().get_tables(**kwargs)
table.orderable = True
return table 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): class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):

View File

@ -208,7 +208,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\ context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .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 context["families"] = families
return context return context