mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-23 17:26:46 +02:00
Compare commits
6 Commits
9e700fd3de
...
family
Author | SHA1 | Date | |
---|---|---|---|
adc925e4b1 | |||
c66cc14576 | |||
db4d0dd83a | |||
2af671d61a | |||
4c3b714b56 | |||
ea8fcad8b5 |
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
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.views import APIView
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -21,9 +21,9 @@ class FamilyViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Family.objects.order_by('id')
|
queryset = Family.objects.order_by('id')
|
||||||
serializer_class = FamilySerializer
|
serializer_class = FamilySerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', ]
|
filterset_fields = ['name', 'description', 'score', 'rank', ]
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$name', '$description', ]
|
||||||
|
|
||||||
|
|
||||||
class FamilyMembershipViewSet(ReadProtectedModelViewSet):
|
class FamilyMembershipViewSet(ReadProtectedModelViewSet):
|
||||||
@ -34,9 +34,11 @@ class FamilyMembershipViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = FamilyMembership.objects.order_by('id')
|
queryset = FamilyMembership.objects.order_by('id')
|
||||||
serializer_class = FamilyMembershipSerializer
|
serializer_class = FamilyMembershipSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', ]
|
filterset_fields = ['user__username', 'user__first_name', 'user__last_name', 'user__email', 'user__note__alias__name',
|
||||||
search_fields = ['$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):
|
class ChallengeViewSet(ReadProtectedModelViewSet):
|
||||||
@ -47,9 +49,9 @@ class ChallengeViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Challenge.objects.order_by('id')
|
queryset = Challenge.objects.order_by('id')
|
||||||
serializer_class = ChallengeSerializer
|
serializer_class = ChallengeSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', ]
|
filterset_fields = ['name', 'description', 'points', ]
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$name', '$description', '$points', ]
|
||||||
|
|
||||||
|
|
||||||
class AchievementViewSet(ReadProtectedModelViewSet):
|
class AchievementViewSet(ReadProtectedModelViewSet):
|
||||||
@ -60,22 +62,19 @@ class AchievementViewSet(ReadProtectedModelViewSet):
|
|||||||
"""
|
"""
|
||||||
queryset = Achievement.objects.order_by('id')
|
queryset = Achievement.objects.order_by('id')
|
||||||
serializer_class = AchievementSerializer
|
serializer_class = AchievementSerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', ]
|
filterset_fields = ['family__name', 'family__description', 'challenge__name', 'challenge__description', 'obtained_at', 'valid', ]
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$family__name', '$family__description', '$challenge__name', '$challenge__description', ]
|
||||||
|
|
||||||
|
|
||||||
class BatchAchievementsAPIView(APIView):
|
class BatchAchievementsAPIView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request, format=None):
|
def post(self, request, format=None):
|
||||||
print("POST de la view spéciale")
|
family_ids = request.data.get('families')
|
||||||
family_ids = request.data.get('families', [])
|
challenge_ids = request.data.get('challenges')
|
||||||
challenge_ids = request.data.get('challenges', [])
|
|
||||||
|
|
||||||
families = Family.objects.filter(id__in=family_ids)
|
families = Family.objects.filter(id__in=family_ids)
|
||||||
challenges = Challenge.objects.filter(id__in=challenge_ids)
|
challenges = Challenge.objects.filter(id__in=challenge_ids)
|
||||||
|
|
||||||
for family in families:
|
for family in families:
|
||||||
for challenge in challenges:
|
for challenge in challenges:
|
||||||
a = Achievement(family=family, challenge=challenge)
|
a = Achievement(family=family, challenge=challenge)
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
17
apps/family/migrations/0004_remove_challenge_obtained.py
Normal file
17
apps/family/migrations/0004_remove_challenge_obtained.py
Normal file
@ -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',
|
||||||
|
),
|
||||||
|
]
|
@ -4,6 +4,7 @@
|
|||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
@ -44,10 +45,13 @@ class Family(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse_lazy('family:family_detail', args=(self.pk,))
|
||||||
|
|
||||||
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 +90,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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -119,10 +123,16 @@ class Challenge(models.Model):
|
|||||||
verbose_name=_('points'),
|
verbose_name=_('points'),
|
||||||
)
|
)
|
||||||
|
|
||||||
obtained = models.PositiveIntegerField(
|
@property
|
||||||
verbose_name=_('obtained'),
|
def obtained(self):
|
||||||
default=0,
|
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
|
@transaction.atomic
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -136,9 +146,6 @@ class Challenge(models.Model):
|
|||||||
verbose_name = _('challenge')
|
verbose_name = _('challenge')
|
||||||
verbose_name_plural = _('challenges')
|
verbose_name_plural = _('challenges')
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Achievement(models.Model):
|
class Achievement(models.Model):
|
||||||
challenge = models.ForeignKey(
|
challenge = models.ForeignKey(
|
||||||
@ -157,6 +164,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')
|
||||||
@ -171,7 +183,6 @@ class Achievement(models.Model):
|
|||||||
"""
|
"""
|
||||||
self.family = Family.objects.select_for_update().get(pk=self.family_id)
|
self.family = Family.objects.select_for_update().get(pk=self.family_id)
|
||||||
self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id)
|
self.challenge = Challenge.objects.select_for_update().get(pk=self.challenge_id)
|
||||||
is_new = self.pk is None
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@ -179,13 +190,6 @@ class Achievement(models.Model):
|
|||||||
self.family.refresh_from_db()
|
self.family.refresh_from_db()
|
||||||
self.family.update_score()
|
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
|
@transaction.atomic
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -200,8 +204,3 @@ class Achievement(models.Model):
|
|||||||
# Remove points from the family
|
# Remove points from the family
|
||||||
self.family.refresh_from_db()
|
self.family.refresh_from_db()
|
||||||
self.family.update_score()
|
self.family.update_score()
|
||||||
|
|
||||||
self.challenge.refresh_from_db()
|
|
||||||
self.challenge.obtained -= 1
|
|
||||||
self.challenge._force_save = True
|
|
||||||
self.challenge.save()
|
|
||||||
|
BIN
apps/family/static/family/img/default_picture.png
Normal file
BIN
apps/family/static/family/img/default_picture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
@ -113,6 +113,7 @@ function reset () {
|
|||||||
* Apply all transactions: all notes in `notes` buy each item in `buttons`
|
* Apply all transactions: all notes in `notes` buy each item in `buttons`
|
||||||
*/
|
*/
|
||||||
function consumeAll () {
|
function consumeAll () {
|
||||||
|
console.log("test");
|
||||||
if (LOCK) { return }
|
if (LOCK) { return }
|
||||||
LOCK = true
|
LOCK = true
|
||||||
|
|
||||||
@ -130,11 +131,13 @@ function consumeAll () {
|
|||||||
LOCK = false
|
LOCK = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.log("couocu")
|
||||||
// Récupérer les IDs des familles et des challenges
|
// Récupérer les IDs des familles et des challenges
|
||||||
const family_ids = notes_display.map(fam => fam.id)
|
const family_ids = notes_display.map(fam => fam.id)
|
||||||
const challenge_ids = buttons.map(chal => chal.id)
|
const challenge_ids = buttons.map(chal => chal.id)
|
||||||
|
|
||||||
|
console.log(family_ids)
|
||||||
|
console.log(challenge_ids)
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/family/api/family/achievements/batch/',
|
url: '/family/api/family/achievements/batch/',
|
||||||
type: 'POST',
|
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 searchbar = document.getElementById("search-input")
|
||||||
var search_results = document.getElementById("search-results")
|
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) {
|
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)
|
const field = $('#' + field_id)
|
||||||
console.log("autoCompleteFamily commence")
|
|
||||||
// Configuration du tooltip
|
// Configuration du tooltip
|
||||||
field.tooltip({
|
field.tooltip({
|
||||||
html: true,
|
html: true,
|
||||||
|
@ -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,6 +104,20 @@ 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):
|
||||||
|
"""
|
||||||
|
Table des défis réalisés par une famille spécifique.
|
||||||
|
"""
|
||||||
|
class Meta:
|
||||||
|
model = Achievement
|
||||||
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
fields = ('challenge', 'challenge__points', 'obtained_at',)
|
||||||
|
attrs = {
|
||||||
|
'class': 'table table-condensed table-striped table-hover'
|
||||||
|
}
|
||||||
|
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 %}
|
@ -13,4 +13,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
{% render_table member_list %}
|
{% render_table member_list %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4"></div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header position-relative">
|
||||||
|
<i class="fa fa-trophy"></i> {% trans "Completed challenges" %}
|
||||||
|
</div>
|
||||||
|
{% render_table achievement_list %}
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -84,12 +84,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
{% if can_add_family %}
|
{% if can_add_family %}
|
||||||
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:add_family" %}">
|
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:family_create" %}">
|
||||||
{% trans "Add a family" %}
|
{% trans "Add a family" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_add_challenge %}
|
{% if can_add_challenge %}
|
||||||
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:add_challenge" %}">
|
<a class="btn btn-sm btn-primary mx-2" href="{% url "family:challenge_create" %}">
|
||||||
{% trans "Add a challenge" %}
|
{% trans "Add a challenge" %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -147,7 +147,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# transaction history #}
|
{# achievement history #}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header position-relative" id="historyListHeading">
|
<div class="card-header position-relative" id="historyListHeading">
|
||||||
<a class="stretched-link font-weight-bold"
|
<a class="stretched-link font-weight-bold"
|
||||||
@ -155,7 +155,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% trans "Recent achievements history" %}
|
{% trans "Recent achievements history" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="history_list">
|
<div id="history">
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
0
apps/family/tests/__init__.py
Normal file
0
apps/family/tests/__init__.py
Normal file
318
apps/family/tests/test_family.py
Normal file
318
apps/family/tests/test_family.py
Normal file
@ -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/')
|
@ -8,17 +8,18 @@ from . import views
|
|||||||
app_name = 'family'
|
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('create/', views.FamilyCreateView.as_view(), name="family_create"),
|
||||||
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('challenge/create/', views.ChallengeCreateView.as_view(), name="challenge_create"),
|
||||||
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.AchievementListView.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('api/family/', include('family.api.urls')),
|
path('achievement/<int:pk>/delete/', views.AchievementDeleteView.as_view(), name="achievement_delete"),
|
||||||
|
path('api/family/', include(('family.api.urls', 'family_api'), namespace='api')),
|
||||||
]
|
]
|
||||||
|
@ -4,19 +4,21 @@
|
|||||||
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, FormMixin
|
||||||
|
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
|
||||||
from member.views import PictureUpdateView
|
from member.forms import ImageForm
|
||||||
|
|
||||||
from .models import Family, Challenge, FamilyMembership, User, Achievement
|
from .models import Family, Challenge, FamilyMembership, User, Achievement
|
||||||
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable
|
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable, FamilyAchievementTable
|
||||||
from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm
|
from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm
|
||||||
|
|
||||||
|
|
||||||
@ -88,6 +90,12 @@ class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["can_add_members"] = PermissionBackend()\
|
context["can_add_members"] = PermissionBackend()\
|
||||||
.has_perm(self.request.user, "family.add_membership", empty_membership)
|
.has_perm(self.request.user, "family.add_membership", empty_membership)
|
||||||
|
|
||||||
|
# Défis réalisé par la famille
|
||||||
|
achievements = Achievement.objects.filter(family=family)
|
||||||
|
achievements_table = FamilyAchievementTable(data=achievements, prefix="achievement-")
|
||||||
|
achievements_table.paginate(per_page=5, page=self.request.GET.get('achievement-page', 1))
|
||||||
|
context["achievement_list"] = achievements_table
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -104,17 +112,28 @@ class FamilyUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
return reverse_lazy('family:family_detail', kwargs={'pk': self.object.pk})
|
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
|
Update profile picture of the family
|
||||||
"""
|
"""
|
||||||
model = Family
|
model = Family
|
||||||
extra_context = {"title": _("Update family picture")}
|
extra_context = {"title": _("Update family picture")}
|
||||||
template_name = 'family/picture_update.html'
|
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):
|
def get_success_url(self):
|
||||||
"""Redirect to family page after upload"""
|
"""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
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@ -133,6 +152,11 @@ class FamilyPictureUpdateView(PictureUpdateView):
|
|||||||
else:
|
else:
|
||||||
image.name = "{}_pic.png".format(self.object.pk)
|
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):
|
class FamilyAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
"""
|
"""
|
||||||
@ -274,30 +298,63 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
PermissionBackend.filter_queryset(self.request, Challenge, "view")
|
PermissionBackend.filter_queryset(self.request, Challenge, "view")
|
||||||
).order_by('name')
|
).order_by('name')
|
||||||
|
|
||||||
context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.add_family")
|
context["can_add_family"] = PermissionBackend.check_perm(self.request, "family.family_create")
|
||||||
context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.add_challenge")
|
context["can_add_challenge"] = PermissionBackend.check_perm(self.request, "family.challenge_create")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
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 AchievementListView(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):
|
||||||
|
achievement = Achievement.objects.get(pk=pk)
|
||||||
|
|
||||||
|
achievement.valid = True
|
||||||
|
achievement.save()
|
||||||
|
|
||||||
|
return redirect(reverse_lazy('family:achievement_list'))
|
||||||
|
|
||||||
|
|
||||||
class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||||
|
@ -7,6 +7,17 @@
|
|||||||
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.username }}</dd>
|
<dd class="col-xl-6">{{ user_object.username }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans 'family'|capfirst %}</dt>
|
||||||
|
<dd class="col-xl-6">
|
||||||
|
{% if families %}
|
||||||
|
{% for fam in families %}
|
||||||
|
<a href="{% url 'family:family_detail' fam.pk %}">{{ fam.name }}</a>{% if not forloop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Aucune</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
|
||||||
{% if user_object.pk == user.pk %}
|
{% if user_object.pk == user.pk %}
|
||||||
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">
|
<dd class="col-xl-6">
|
||||||
|
@ -26,6 +26,7 @@ from note_kfet.middlewares import _set_current_request
|
|||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
from family.models import Family
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
|
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
|
||||||
@ -207,6 +208,9 @@ 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(memberships__user=user).distinct()
|
||||||
|
context["families"] = families
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -4095,14 +4095,6 @@ msgstr "La note est indisponible pour le moment"
|
|||||||
msgid "Thank you for your understanding -- The Respos Info of BDE"
|
msgid "Thank you for your understanding -- The Respos Info of BDE"
|
||||||
msgstr "Merci de votre compréhension -- Les Respos Info du BDE"
|
msgstr "Merci de votre compréhension -- Les Respos Info du BDE"
|
||||||
|
|
||||||
#: note_kfet/templates/base_search.html:15
|
|
||||||
msgid "Search by attribute such as name..."
|
|
||||||
msgstr "Chercher par un attribut tel que le nom..."
|
|
||||||
|
|
||||||
#: note_kfet/templates/base_search.html:23
|
|
||||||
msgid "There is no results."
|
|
||||||
msgstr "Il n'y a pas de résultat."
|
|
||||||
|
|
||||||
#: note_kfet/templates/cas/logged.html:8
|
#: note_kfet/templates/cas/logged.html:8
|
||||||
msgid ""
|
msgid ""
|
||||||
"<h3>Log In Successful</h3>You have successfully logged into the Central "
|
"<h3>Log In Successful</h3>You have successfully logged into the Central "
|
||||||
|
Reference in New Issue
Block a user