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 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)
|
||||
|
@ -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.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,10 +45,13 @@ 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)
|
||||
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 +90,7 @@ class FamilyMembership(models.Model):
|
||||
family = models.ForeignKey(
|
||||
Family,
|
||||
on_delete=models.PROTECT,
|
||||
related_name=_('members'),
|
||||
related_name=_('memberships'),
|
||||
verbose_name=_('family'),
|
||||
)
|
||||
|
||||
@ -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(
|
||||
@ -157,6 +164,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')
|
||||
@ -171,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)
|
||||
|
||||
@ -179,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):
|
||||
"""
|
||||
@ -200,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()
|
||||
|
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`
|
||||
*/
|
||||
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,
|
||||
|
@ -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,6 +104,20 @@ 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.
|
||||
"""
|
||||
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">
|
||||
{% csrf_token %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</form>
|
||||
</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-header">
|
||||
<p class="card-text font-weight-bold">
|
||||
{% trans "Recent achievements history" %}
|
||||
{% trans "Invalid 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 table %}
|
||||
{% render_table invalid %}
|
||||
</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 %}
|
@ -13,4 +13,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
{% render_table member_list %}
|
||||
</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 %}
|
@ -84,12 +84,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</h3>
|
||||
<div class="card-body text-center">
|
||||
{% 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" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% 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" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -147,7 +147,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# transaction history #}
|
||||
{# achievement history #}
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="historyListHeading">
|
||||
<a class="stretched-link font-weight-bold"
|
||||
@ -155,7 +155,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% trans "Recent achievements history" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="history_list">
|
||||
<div id="history">
|
||||
{% render_table table %}
|
||||
</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'
|
||||
urlpatterns = [
|
||||
path('list/', views.FamilyListView.as_view(), name="family_list"),
|
||||
path('add-family/', views.FamilyCreateView.as_view(), name="add_family"),
|
||||
path('detail/<int:pk>/', views.FamilyDetailView.as_view(), name="family_detail"),
|
||||
path('update/<int:pk>/', views.FamilyUpdateView.as_view(), name="family_update"),
|
||||
path('update_pic/<int:pk>/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
|
||||
path('add_member/<int:family_pk>/', views.FamilyAddMemberView.as_view(), name="family_add_member"),
|
||||
path('create/', views.FamilyCreateView.as_view(), name="family_create"),
|
||||
path('<int:pk>/detail/', views.FamilyDetailView.as_view(), name="family_detail"),
|
||||
path('<int:pk>/update/', views.FamilyUpdateView.as_view(), name="family_update"),
|
||||
path('<int:pk>/update_pic/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
|
||||
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('add-challenge/', views.ChallengeCreateView.as_view(), name="add_challenge"),
|
||||
path('challenge/detail/<int:pk>/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
|
||||
path('challenge/update/<int:pk>/', views.ChallengeUpdateView.as_view(), name="challenge_update"),
|
||||
path('challenge/create/', views.ChallengeCreateView.as_view(), name="challenge_create"),
|
||||
path('challenge/<int:pk>/detail/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
|
||||
path('challenge/<int:pk>/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/<int:pk>/', views.AchievementDeleteView.as_view(), name="achievement_delete"),
|
||||
path('api/family/', include('family.api.urls')),
|
||||
path('achievement/list/', views.AchievementListView.as_view(), name="achievement_list"),
|
||||
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', 'family_api'), namespace='api')),
|
||||
]
|
||||
|
@ -4,19 +4,21 @@
|
||||
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.edit import DeleteView
|
||||
from django.views.generic import DetailView, UpdateView, ListView
|
||||
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
|
||||
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
|
||||
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable, FamilyAchievementTable
|
||||
from .forms import ChallengeForm, FamilyMembershipForm, FamilyForm
|
||||
|
||||
|
||||
@ -88,6 +90,12 @@ class FamilyDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
context["can_add_members"] = PermissionBackend()\
|
||||
.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
|
||||
|
||||
|
||||
@ -104,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):
|
||||
@ -133,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):
|
||||
"""
|
||||
@ -274,30 +298,63 @@ 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
|
||||
|
||||
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 AchievementListView(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):
|
||||
achievement = Achievement.objects.get(pk=pk)
|
||||
|
||||
achievement.valid = True
|
||||
achievement.save()
|
||||
|
||||
return redirect(reverse_lazy('family:achievement_list'))
|
||||
|
||||
|
||||
class AchievementDeleteView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView):
|
||||
|
@ -7,6 +7,17 @@
|
||||
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
|
||||
<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 %}
|
||||
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
|
@ -26,6 +26,7 @@ from note_kfet.middlewares import _set_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||
from family.models import Family
|
||||
from django import forms
|
||||
|
||||
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\
|
||||
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||
|
||||
families = Family.objects.filter(memberships__user=user).distinct()
|
||||
context["families"] = families
|
||||
|
||||
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"
|
||||
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
|
||||
msgid ""
|
||||
"<h3>Log In Successful</h3>You have successfully logged into the Central "
|
||||
|
Reference in New Issue
Block a user