mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-07-23 17:26:46 +02:00
Tests
This commit is contained in:
@ -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)
|
||||||
|
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,6 +45,9 @@ 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, achievement__valid=True)
|
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"))
|
||||||
@ -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(
|
||||||
@ -176,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)
|
||||||
|
|
||||||
@ -184,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):
|
||||||
"""
|
"""
|
||||||
@ -205,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,
|
||||||
|
@ -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,18 +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('<int:pk>/detail/', views.FamilyDetailView.as_view(), name="family_detail"),
|
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/', views.FamilyUpdateView.as_view(), name="family_update"),
|
||||||
path('<int:pk>/update_pic/', views.FamilyPictureUpdateView.as_view(), name="update_pic"),
|
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('<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/<int:pk>/detail/', views.ChallengeDetailView.as_view(), name="challenge_detail"),
|
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('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('achievement/list/', views.AchievementsView.as_view(), name="achievement_list"),
|
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>/validate/', views.AchievementValidateView.as_view(), name="achievement_validate"),
|
||||||
path('achievement/<int:pk>/delete/', views.AchievementDeleteView.as_view(), name="achievement_delete"),
|
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', 'family_api'), namespace='api')),
|
||||||
]
|
]
|
||||||
|
@ -8,14 +8,14 @@ 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, ListView
|
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.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, MultiTableMixin
|
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, FamilyAchievementTable
|
from .tables import FamilyTable, ChallengeTable, FamilyMembershipTable, AchievementTable, FamilyAchievementTable
|
||||||
@ -112,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):
|
||||||
@ -141,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):
|
||||||
"""
|
"""
|
||||||
@ -282,8 +298,8 @@ 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
|
||||||
|
|
||||||
@ -294,7 +310,7 @@ class FamilyManageView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
class AchievementsView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
class AchievementListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||||
"""
|
"""
|
||||||
List all achievements
|
List all achievements
|
||||||
"""
|
"""
|
||||||
@ -333,12 +349,11 @@ class AchievementValidateView(ProtectQuerysetMixin, LoginRequiredMixin, Template
|
|||||||
template_name = 'family/achievement_confirm_validate.html'
|
template_name = 'family/achievement_confirm_validate.html'
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
# On récupère l'objet à valider
|
|
||||||
achievement = Achievement.objects.get(pk=pk)
|
achievement = Achievement.objects.get(pk=pk)
|
||||||
# On modifie le champ valid
|
|
||||||
achievement.valid = True
|
achievement.valid = True
|
||||||
achievement.save()
|
achievement.save()
|
||||||
# On redirige vers la page de détail ou la liste
|
|
||||||
return redirect(reverse_lazy('family:achievement_list'))
|
return redirect(reverse_lazy('family:achievement_list'))
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user