mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-23 01:06:47 +02:00
Added valid field and logic for Achievement
This commit is contained in:
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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')
|
||||||
|
@ -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.
|
||||||
@ -102,4 +120,4 @@ class FamilyAchievementTable(tables.Table):
|
|||||||
attrs = {
|
attrs = {
|
||||||
'class': 'table table-condensed table-striped table-hover'
|
'class': 'table table-condensed table-striped table-hover'
|
||||||
}
|
}
|
||||||
order_by = ('-obtained_at',)
|
order_by = ('-obtained_at',)
|
||||||
|
@ -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>
|
||||||
|
@ -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 %}
|
@ -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 %}
|
@ -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')),
|
||||||
]
|
]
|
||||||
|
@ -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):
|
||||||
|
@ -207,8 +207,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
modified_note.is_active = True
|
modified_note.is_active = True
|
||||||
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
|
||||||
|
Reference in New Issue
Block a user