mirror of
https://gitlab.com/animath/si/plateforme-corres2math.git
synced 2024-12-05 02:06:52 +00:00
Merge branch 'tests' into 'master'
Tests See merge request animath/si/plateforme-corres2math!3
This commit is contained in:
commit
cba3e56fb8
@ -6,7 +6,7 @@ py38:
|
||||
stage: test
|
||||
image: python:3.8-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev
|
||||
- apk add --no-cache libmagic
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py38
|
||||
|
||||
@ -14,7 +14,7 @@ py39:
|
||||
stage: test
|
||||
image: python:3.9-alpine
|
||||
before_script:
|
||||
- apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev
|
||||
- apk add --no-cache gcc libmagic
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py39
|
||||
|
||||
|
@ -11,7 +11,7 @@ RUN apk add --no-cache bash
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
COPY requirements.txt /code/requirements.txt
|
||||
RUN pip install -r requirements.txt psycopg2-binary sympasoap --no-cache-dir
|
||||
RUN pip install -r requirements.txt --no-cache-dir
|
||||
|
||||
COPY . /code/
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Participation, Phase, Question, Team, Video
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'trigram', 'problem', 'valid',)
|
||||
search_fields = ('name', 'trigram',)
|
||||
list_filter = ('participation__problem', 'participation__valid',)
|
||||
|
||||
def problem(self, team):
|
||||
return team.participation.get_problem_display()
|
||||
|
||||
problem.short_description = _('problem number')
|
||||
|
||||
def valid(self, team):
|
||||
return team.participation.valid
|
||||
|
||||
valid.short_description = _('valid')
|
||||
|
||||
|
||||
@admin.register(Participation)
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'problem', 'valid',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('problem', 'valid',)
|
||||
|
||||
|
||||
@admin.register(Video)
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'link',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'link',)
|
||||
|
||||
|
||||
@admin.register(Question)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'question',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'question',)
|
||||
|
||||
|
||||
@admin.register(Phase)
|
||||
class PhaseAdmin(admin.ModelAdmin):
|
||||
list_display = ('phase_number', 'start', 'end',)
|
||||
ordering = ('phase_number', 'start',)
|
@ -2,7 +2,7 @@ import re
|
||||
|
||||
from bootstrap_datepicker_plus import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -95,7 +95,7 @@ class UploadVideoForm(forms.ModelForm):
|
||||
fields = ('link',)
|
||||
|
||||
def clean(self):
|
||||
if Phase.current_phase().phase_number != 1 and self.instance.link:
|
||||
if Phase.current_phase().phase_number != 1 and Phase.current_phase().phase_number != 4 and self.instance.link:
|
||||
self.add_error("link", _("You can't upload your video after the deadline."))
|
||||
return super().clean()
|
||||
|
||||
@ -126,16 +126,20 @@ class SendParticipationForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["sent_participation"].initial = self.instance.sent_participation
|
||||
try:
|
||||
self.fields["sent_participation"].initial = self.instance.sent_participation
|
||||
except ObjectDoesNotExist: # No sent participation
|
||||
pass
|
||||
self.fields["sent_participation"].queryset = Participation.objects.filter(
|
||||
~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
|
||||
)
|
||||
|
||||
def clean(self, commit=True):
|
||||
cleaned_data = super().clean()
|
||||
participation = cleaned_data["sent_participation"]
|
||||
participation.received_participation = self.instance
|
||||
self.instance = participation
|
||||
if "sent_participation" in cleaned_data:
|
||||
participation = cleaned_data["sent_participation"]
|
||||
participation.received_participation = self.instance
|
||||
self.instance = participation
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
@ -151,6 +155,11 @@ class QuestionForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")})
|
||||
|
||||
def clean(self):
|
||||
if Phase.current_phase().phase_number != 2:
|
||||
self.add_error(None, _("You can only create or update a question during the second phase."))
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('question',)
|
||||
@ -171,10 +180,12 @@ class PhaseForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
# Ensure that dates are in a right order
|
||||
cleaned_data = super().clean()
|
||||
if cleaned_data["end"] <= cleaned_data["start"]:
|
||||
start = cleaned_data["start"]
|
||||
end = cleaned_data["end"]
|
||||
if end <= start:
|
||||
self.add_error("end", _("Start date must be before the end date."))
|
||||
if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=cleaned_data["start"]).exists():
|
||||
if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=start).exists():
|
||||
self.add_error("start", _("This phase must start after the previous phases."))
|
||||
if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=cleaned_data["end"]).exists():
|
||||
if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=end).exists():
|
||||
self.add_error("end", _("This phase must end after the next phases."))
|
||||
return cleaned_data
|
||||
|
@ -1,9 +1,8 @@
|
||||
import os
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from corres2math.matrix import Matrix, RoomVisibility, UploadError
|
||||
from corres2math.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
from django.core.management import BaseCommand
|
||||
from nio import RoomPreset
|
||||
from registration.models import AdminRegistration, Registration
|
||||
|
||||
|
||||
@ -11,17 +10,21 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
Matrix.set_display_name("Bot des Correspondances")
|
||||
|
||||
if not os.path.isfile(".matrix_avatar"):
|
||||
stat_file = os.stat("corres2math/static/logo.png")
|
||||
with open("corres2math/static/logo.png", "rb") as f:
|
||||
resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size)
|
||||
if isinstance(resp, UploadError):
|
||||
raise Exception(resp)
|
||||
avatar_uri = resp.content_uri
|
||||
with open(".matrix_avatar", "w") as f:
|
||||
f.write(avatar_uri)
|
||||
Matrix.set_avatar(avatar_uri)
|
||||
else:
|
||||
if not os.getenv("SYNAPSE_PASSWORD"):
|
||||
avatar_uri = "plop"
|
||||
else: # pragma: no cover
|
||||
if not os.path.isfile(".matrix_avatar"):
|
||||
stat_file = os.stat("corres2math/static/logo.png")
|
||||
with open("corres2math/static/logo.png", "rb") as f:
|
||||
resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png",
|
||||
filesize=stat_file.st_size)
|
||||
if not hasattr(resp, "content_uri"):
|
||||
raise Exception(resp)
|
||||
avatar_uri = resp.content_uri
|
||||
with open(".matrix_avatar", "w") as f:
|
||||
f.write(avatar_uri)
|
||||
Matrix.set_avatar(avatar_uri)
|
||||
|
||||
with open(".matrix_avatar", "r") as f:
|
||||
avatar_uri = f.read().rstrip(" \t\r\n")
|
||||
|
||||
|
@ -27,7 +27,7 @@ def register_phases(apps, schema_editor):
|
||||
)
|
||||
|
||||
|
||||
def reverse_phase_registering(apps, schema_editor):
|
||||
def reverse_phase_registering(apps, _): # pragma: no cover
|
||||
"""
|
||||
Drop all phases in order to unapply this migration.
|
||||
"""
|
||||
|
@ -2,7 +2,7 @@ import os
|
||||
import re
|
||||
|
||||
from corres2math.lists import get_sympa_client
|
||||
from corres2math.matrix import Matrix
|
||||
from corres2math.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
@ -13,7 +13,6 @@ from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from nio import RoomPreset, RoomVisibility
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
|
@ -72,19 +72,17 @@ class ParticipationTable(tables.Table):
|
||||
|
||||
|
||||
class VideoTable(tables.Table):
|
||||
participationname = tables.LinkColumn(
|
||||
participation_name = tables.LinkColumn(
|
||||
'participation:participation_detail',
|
||||
args=[tables.A("participation__pk")],
|
||||
verbose_name=lambda: _("name").capitalize(),
|
||||
accessor=tables.A("participation__team__name"),
|
||||
)
|
||||
|
||||
def render_participationname(self, record):
|
||||
return record.participation.team.name
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('participationname', 'link',)
|
||||
fields = ('participation_name', 'link',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
@ -1,13 +1,25 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from registration.models import StudentRegistration
|
||||
from django.utils import timezone
|
||||
from registration.models import CoachRegistration, StudentRegistration
|
||||
|
||||
from .models import Team
|
||||
from .models import Participation, Phase, Question, Team
|
||||
|
||||
|
||||
class TestStudentParticipation(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="toto1234",
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
first_name="Toto",
|
||||
last_name="Toto",
|
||||
@ -27,11 +39,94 @@ class TestStudentParticipation(TestCase):
|
||||
access_code="azerty",
|
||||
grant_animath_access_videos=True,
|
||||
)
|
||||
self.question = Question.objects.create(participation=self.team.participation,
|
||||
question="Pourquoi l'existence précède l'essence ?")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# TODO Remove these lines
|
||||
str(self.team)
|
||||
str(self.team.participation)
|
||||
self.second_user = User.objects.create(
|
||||
first_name="Lalala",
|
||||
last_name="Lalala",
|
||||
email="lalala@example.com",
|
||||
password="lalala",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.second_user,
|
||||
student_class=11,
|
||||
school="Moon",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.second_team = Team.objects.create(
|
||||
name="Poor team",
|
||||
trigram="FFF",
|
||||
access_code="qwerty",
|
||||
grant_animath_access_videos=True,
|
||||
)
|
||||
|
||||
self.coach = User.objects.create(
|
||||
first_name="Coach",
|
||||
last_name="Coach",
|
||||
email="coach@example.com",
|
||||
password="coach",
|
||||
)
|
||||
CoachRegistration.objects.create(user=self.coach)
|
||||
|
||||
def test_admin_pages(self):
|
||||
"""
|
||||
Load Django-admin pages.
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Test team pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/team/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/team/{self.team.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Team).id}/"
|
||||
f"{self.team.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.get_absolute_url()), 302, 200)
|
||||
|
||||
# Test participation pages
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.get(reverse("admin:index") + "participation/participation/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/participation/{self.team.participation.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Participation).id}/"
|
||||
f"{self.team.participation.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.participation.get_absolute_url()), 302, 200)
|
||||
|
||||
# Test video pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/video/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/video/{self.team.participation.solution.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test question pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/question/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/question/{self.question.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test phase pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/phase/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "participation/phase/1/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_team(self):
|
||||
"""
|
||||
@ -62,6 +157,7 @@ class TestStudentParticipation(TestCase):
|
||||
trigram="TET",
|
||||
grant_animath_access_videos=False,
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_join_team(self):
|
||||
"""
|
||||
@ -109,6 +205,161 @@ class TestStudentParticipation(TestCase):
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other teams
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_request_validate_team(self):
|
||||
"""
|
||||
The team ask for validation.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
second_user = User.objects.create(
|
||||
first_name="Blublu",
|
||||
last_name="Blublu",
|
||||
email="blublu@example.com",
|
||||
password="blublu",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=second_user,
|
||||
student_class=12,
|
||||
school="Jupiter",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/mai-linh",
|
||||
)
|
||||
|
||||
third_user = User.objects.create(
|
||||
first_name="Zupzup",
|
||||
last_name="Zupzup",
|
||||
email="zupzup@example.com",
|
||||
password="zupzup",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=third_user,
|
||||
student_class=10,
|
||||
school="Sun",
|
||||
give_contact_to_animath=False,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/yohann",
|
||||
)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
# Admin users can't ask for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["can_validate"])
|
||||
# Can't validate
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.user.registration.photo_authorization = "authorization/photo/ananas"
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["can_validate"])
|
||||
|
||||
self.team.participation.problem = 2
|
||||
self.team.participation.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.context["can_validate"])
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertFalse(self.team.participation.valid)
|
||||
self.assertIsNotNone(self.team.participation.valid)
|
||||
|
||||
# Team already asked for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_validate_team(self):
|
||||
"""
|
||||
A team asked for validation. Try to validate it.
|
||||
"""
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# No right to do that
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="J'ai 4 ans",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Woops I didn't said anything",
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Test invalidate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Wsh nope",
|
||||
invalidate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
# Team did not ask validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# Test validate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertTrue(self.team.participation.valid)
|
||||
|
||||
def test_update_team(self):
|
||||
"""
|
||||
Try to update team information.
|
||||
@ -116,6 +367,9 @@ class TestStudentParticipation(TestCase):
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
self.coach.registration.team = self.team
|
||||
self.coach.registration.save()
|
||||
|
||||
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ -137,6 +391,45 @@ class TestStudentParticipation(TestCase):
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
|
||||
|
||||
def test_leave_team(self):
|
||||
"""
|
||||
A user is in a team, and leaves it.
|
||||
"""
|
||||
# User is not in a team
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Team is pending validation
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Team is valid
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Unauthenticated users are redirected to login page
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team.participation.valid = None
|
||||
self.team.participation.save()
|
||||
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("index"), 302, 200)
|
||||
self.user.registration.refresh_from_db()
|
||||
self.assertIsNone(self.user.registration.team)
|
||||
self.assertFalse(Team.objects.filter(pk=self.team.pk).exists())
|
||||
|
||||
def test_no_myparticipation_redirect_nomyparticipation(self):
|
||||
"""
|
||||
Ensure a permission denied when we search my team participation when we are in no team.
|
||||
@ -151,6 +444,7 @@ class TestStudentParticipation(TestCase):
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Can't see the participation if it is not valid
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
|
||||
@ -166,6 +460,13 @@ class TestStudentParticipation(TestCase):
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other participations
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_upload_video(self):
|
||||
"""
|
||||
Try to send a solution video link.
|
||||
@ -191,8 +492,221 @@ class TestStudentParticipation(TestCase):
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Set the second phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
|
||||
end=timezone.now() + timedelta(days=i - 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 2)
|
||||
|
||||
class TestAdminForbidden(TestCase):
|
||||
# Can't update the link during the second phase
|
||||
response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)),
|
||||
data=dict(link="https://youtube.com/watch?v=73nsrixx7eI"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_questions(self):
|
||||
"""
|
||||
Ensure that creating/updating/deleting a question is working.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
|
||||
response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# We are not in second phase
|
||||
response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)),
|
||||
data=dict(question="I got censored!"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Set the second phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
|
||||
end=timezone.now() + timedelta(days=i - 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 2)
|
||||
|
||||
# Create a question
|
||||
response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)),
|
||||
data=dict(question="I asked a question!"))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team.participation.pk,)), 302, 200)
|
||||
qs = Question.objects.filter(participation=self.team.participation, question="I asked a question!")
|
||||
self.assertTrue(qs.exists())
|
||||
question = qs.get()
|
||||
|
||||
# Update a question
|
||||
response = self.client.get(reverse("participation:update_question", args=(question.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(reverse("participation:update_question", args=(question.pk,)), data=dict(
|
||||
question="The question changed!",
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team.participation.pk,)), 302, 200)
|
||||
question.refresh_from_db()
|
||||
self.assertEqual(question.question, "The question changed!")
|
||||
|
||||
# Delete the question
|
||||
response = self.client.get(reverse("participation:delete_question", args=(question.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(reverse("participation:delete_question", args=(question.pk,)))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team.participation.pk,)), 302, 200)
|
||||
self.assertFalse(Question.objects.filter(pk=question.pk).exists())
|
||||
|
||||
# Non-authenticated users are redirected to login page
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,)))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" +
|
||||
reverse("participation:add_question", args=(self.team.participation.pk,)), 302, 200)
|
||||
response = self.client.get(reverse("participation:update_question", args=(self.question.pk,)))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" +
|
||||
reverse("participation:update_question", args=(self.question.pk,)), 302, 200)
|
||||
response = self.client.get(reverse("participation:delete_question", args=(self.question.pk,)))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" +
|
||||
reverse("participation:delete_question", args=(self.question.pk,)), 302, 200)
|
||||
|
||||
def test_current_phase(self):
|
||||
"""
|
||||
Ensure that the current phase is the good one.
|
||||
"""
|
||||
# We are before the beginning
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=2 * i),
|
||||
end=timezone.now() + timedelta(days=2 * i + 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 1)
|
||||
|
||||
# We are after the end
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() - timedelta(days=2 * i),
|
||||
end=timezone.now() - timedelta(days=2 * i + 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, Phase.objects.count())
|
||||
|
||||
# First phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 1),
|
||||
end=timezone.now() + timedelta(days=i))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 1)
|
||||
|
||||
# Second phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
|
||||
end=timezone.now() + timedelta(days=i - 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 2)
|
||||
|
||||
# Third phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 3),
|
||||
end=timezone.now() + timedelta(days=i - 2))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 3)
|
||||
|
||||
# Fourth phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 4),
|
||||
end=timezone.now() + timedelta(days=i - 3))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 4)
|
||||
|
||||
response = self.client.get(reverse("participation:calendar"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:update_phase", args=(4,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||
start=timezone.now(),
|
||||
end=timezone.now() + timedelta(days=3),
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
response = self.client.get(reverse("participation:update_phase", args=(4,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||
start=timezone.now(),
|
||||
end=timezone.now() + timedelta(days=3),
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:calendar"), 302, 200)
|
||||
fourth_phase = Phase.objects.get(phase_number=4)
|
||||
self.assertEqual((fourth_phase.end - fourth_phase.start).days, 3)
|
||||
|
||||
# First phase must be before the other phases
|
||||
response = self.client.post(reverse("participation:update_phase", args=(1,)), data=dict(
|
||||
start=timezone.now() + timedelta(days=8),
|
||||
end=timezone.now() + timedelta(days=9),
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Fourth phase must be after the other phases
|
||||
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||
start=timezone.now() - timedelta(days=9),
|
||||
end=timezone.now() - timedelta(days=8),
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# End must be after start
|
||||
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||
start=timezone.now() + timedelta(days=3),
|
||||
end=timezone.now(),
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Unauthenticated user can't update the calendar
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse("participation:calendar"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("participation:update_phase", args=(2,)))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" +
|
||||
reverse("participation:update_phase", args=(2,)), 302, 200)
|
||||
|
||||
def test_forbidden_access(self):
|
||||
"""
|
||||
Load personnal pages and ensure that these are protected.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:update_team", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:team_authorizations", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:upload_video",
|
||||
args=(self.second_team.participation.solution.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:upload_video",
|
||||
args=(self.second_team.participation.synthesis.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:add_question", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
question = Question.objects.create(participation=self.second_team.participation,
|
||||
question=self.question.question)
|
||||
resp = self.client.get(reverse("participation:update_question", args=(question.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:delete_question", args=(question.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_cover_matrix(self):
|
||||
"""
|
||||
Load matrix scripts, to cover them and ensure that they can run.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.received_participation = self.second_team.participation
|
||||
self.team.participation.save()
|
||||
|
||||
call_command('fix_matrix_channels')
|
||||
call_command('setup_third_phase')
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_superuser(
|
||||
username="admin@example.com",
|
||||
@ -201,6 +715,96 @@ class TestAdminForbidden(TestCase):
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team1 = Team.objects.create(
|
||||
name="Toto",
|
||||
trigram="TOT",
|
||||
)
|
||||
self.team1.participation.valid = True
|
||||
self.team1.participation.problem = 1
|
||||
self.team1.participation.save()
|
||||
|
||||
self.team2 = Team.objects.create(
|
||||
name="Bliblu",
|
||||
trigram="BIU",
|
||||
)
|
||||
self.team2.participation.valid = True
|
||||
self.team2.participation.problem = 1
|
||||
self.team2.participation.save()
|
||||
|
||||
self.team3 = Team.objects.create(
|
||||
name="Zouplop",
|
||||
trigram="ZPL",
|
||||
)
|
||||
self.team3.participation.valid = True
|
||||
self.team3.participation.problem = 1
|
||||
self.team3.participation.save()
|
||||
|
||||
self.other_team = Team.objects.create(
|
||||
name="I am different",
|
||||
trigram="IAD",
|
||||
)
|
||||
self.other_team.participation.valid = True
|
||||
self.other_team.participation.problem = 2
|
||||
self.other_team.participation.save()
|
||||
|
||||
def test_research(self):
|
||||
"""
|
||||
Try to search some things.
|
||||
"""
|
||||
call_command("rebuild_index", "--noinput", "--verbosity", 0)
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team1.name)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
def test_set_received_video(self):
|
||||
"""
|
||||
Try to define the received video of a participation.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(received_participation=self.team2.participation.pk))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team1.participation.pk,)), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_send_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(sent_participation=self.team3.participation.pk))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team1.participation.pk,)), 302, 200)
|
||||
|
||||
self.team1.participation.refresh_from_db()
|
||||
self.team2.participation.refresh_from_db()
|
||||
self.team3.participation.refresh_from_db()
|
||||
|
||||
self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk)
|
||||
self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk)
|
||||
self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk)
|
||||
self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk)
|
||||
|
||||
# The other team didn't work on the same problem
|
||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(received_participation=self.other_team.participation.pk))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_send_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(sent_participation=self.other_team.participation.pk))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't create a team.
|
||||
@ -223,9 +827,23 @@ class TestAdminForbidden(TestCase):
|
||||
))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_leave_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't leave a team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_my_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My team".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_my_participation_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My participation".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -144,7 +144,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
user = request.user
|
||||
self.object = self.get_object()
|
||||
# Ensure that the user is an admin or a member of the team
|
||||
if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"]:
|
||||
if user.registration.is_admin or user.registration.participates and \
|
||||
user.registration.team and user.registration.team.pk == kwargs["pk"]:
|
||||
return super().get(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
@ -171,54 +172,69 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||
return RequestValidationForm
|
||||
elif self.request.POST["_form_type"] == "ValidateParticipationForm":
|
||||
return ValidateParticipationForm
|
||||
return None
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = self.get_object()
|
||||
if isinstance(form, RequestValidationForm):
|
||||
if not self.request.user.registration.participates:
|
||||
form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
|
||||
return self.form_invalid(form)
|
||||
if self.object.participation.valid is not None:
|
||||
form.add_error(None, _("The validation of the team is already done or pending."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
self.object.participation.valid = False
|
||||
self.object.participation.save()
|
||||
|
||||
for admin in AdminRegistration.objects.all():
|
||||
mail_context = dict(user=admin.user, team=self.object)
|
||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||
admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html)
|
||||
return self.handle_request_validation(form)
|
||||
elif isinstance(form, ValidateParticipationForm):
|
||||
if not self.request.user.registration.is_admin:
|
||||
form.add_error(None, _("You are not an administrator."))
|
||||
return self.form_invalid(form)
|
||||
elif self.object.participation.valid is not False:
|
||||
form.add_error(None, _("This team has no pending validation."))
|
||||
return self.form_invalid(form)
|
||||
return self.handle_validate_participation(form)
|
||||
|
||||
if "validate" in self.request.POST:
|
||||
self.object.participation.valid = True
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
||||
elif "invalidate" in self.request.POST:
|
||||
self.object.participation.valid = None
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe non validée", mail_plain, None, [self.object.email],
|
||||
html_message=mail_html)
|
||||
else:
|
||||
form.add_error(None, _("You must specify if you validate the registration or not."))
|
||||
return self.form_invalid(form)
|
||||
def handle_request_validation(self, form):
|
||||
"""
|
||||
A team requests to be validated
|
||||
"""
|
||||
if not self.request.user.registration.participates:
|
||||
form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
|
||||
return self.form_invalid(form)
|
||||
if self.object.participation.valid is not None:
|
||||
form.add_error(None, _("The validation of the team is already done or pending."))
|
||||
return self.form_invalid(form)
|
||||
if not self.get_context_data()["can_validate"]:
|
||||
form.add_error(None, _("The team can't be validated: missing email address confirmations, "
|
||||
"photo authorizations, people or the chosen problem is not set."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
return super().form_invalid(form)
|
||||
self.object.participation.valid = False
|
||||
self.object.participation.save()
|
||||
|
||||
for admin in AdminRegistration.objects.all():
|
||||
mail_context = dict(user=admin.user, team=self.object)
|
||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||
admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html)
|
||||
return super().form_valid(form)
|
||||
|
||||
def handle_validate_participation(self, form):
|
||||
"""
|
||||
An admin validates the team (or not)
|
||||
"""
|
||||
if not self.request.user.registration.is_admin:
|
||||
form.add_error(None, _("You are not an administrator."))
|
||||
return self.form_invalid(form)
|
||||
elif self.object.participation.valid is not False:
|
||||
form.add_error(None, _("This team has no pending validation."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if "validate" in self.request.POST:
|
||||
self.object.participation.valid = True
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
||||
elif "invalidate" in self.request.POST:
|
||||
self.object.participation.valid = None
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe non validée", mail_plain, None, [self.object.email],
|
||||
html_message=mail_html)
|
||||
else:
|
||||
form.add_error(None, _("You must specify if you validate the registration or not."))
|
||||
return self.form_invalid(form)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
@ -234,7 +250,9 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"]:
|
||||
if user.registration.is_admin or user.registration.participates and \
|
||||
user.registration.team and \
|
||||
user.registration.team.pk == kwargs["pk"]:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
@ -298,7 +316,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
if not request.user.registration.team:
|
||||
if not request.user.registration.participates or not request.user.registration.team:
|
||||
raise PermissionDenied(_("You are not in a team."))
|
||||
if request.user.registration.team.participation.valid is not None:
|
||||
raise PermissionDenied(_("The team is already validated or the validation is pending."))
|
||||
@ -347,6 +365,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
||||
if not self.get_object().valid:
|
||||
raise PermissionDenied(_("The team is not validated yet."))
|
||||
if user.registration.is_admin or user.registration.participates \
|
||||
and user.registration.team.participation \
|
||||
and user.registration.team.participation.pk == kwargs["pk"]:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
@ -369,7 +388,7 @@ class SetParticipationReceiveParticipationView(AdminMixin, UpdateView):
|
||||
template_name = "participation/receive_participation_form.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.object.pk,))
|
||||
return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
|
||||
|
||||
|
||||
class SetParticipationSendParticipationView(AdminMixin, UpdateView):
|
||||
@ -381,7 +400,7 @@ class SetParticipationSendParticipationView(AdminMixin, UpdateView):
|
||||
template_name = "participation/send_participation_form.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.object.pk,))
|
||||
return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
|
||||
|
||||
|
||||
class CreateQuestionView(LoginRequiredMixin, CreateView):
|
||||
@ -399,6 +418,7 @@ class CreateQuestionView(LoginRequiredMixin, CreateView):
|
||||
self.participation = Participation.objects.get(pk=kwargs["pk"])
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.participation.valid and \
|
||||
request.user.registration.team.pk == self.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
@ -424,6 +444,7 @@ class UpdateQuestionView(LoginRequiredMixin, UpdateView):
|
||||
return self.handle_no_permission()
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.object.participation.valid and \
|
||||
request.user.registration.team.pk == self.object.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
@ -445,6 +466,7 @@ class DeleteQuestionView(LoginRequiredMixin, DeleteView):
|
||||
return self.handle_no_permission()
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.object.participation.valid and \
|
||||
request.user.registration.team.pk == self.object.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
@ -15,3 +15,6 @@ class RegistrationConfig(AppConfig):
|
||||
pre_save.connect(send_email_link, "auth.User")
|
||||
post_save.connect(create_admin_registration, "auth.User")
|
||||
post_save.connect(invite_to_public_rooms, "registration.Registration")
|
||||
post_save.connect(invite_to_public_rooms, "registration.StudentRegistration")
|
||||
post_save.connect(invite_to_public_rooms, "registration.CoachRegistration")
|
||||
post_save.connect(invite_to_public_rooms, "registration.AdminRegistration")
|
||||
|
@ -1,7 +1,7 @@
|
||||
from cas_server.auth import DjangoAuthUser
|
||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||
|
||||
|
||||
class CustomAuthUser(DjangoAuthUser):
|
||||
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
|
||||
"""
|
||||
Override Django Auth User model to define a custom Matrix username.
|
||||
"""
|
||||
|
@ -58,11 +58,11 @@ class Registration(PolymorphicModel):
|
||||
self.user.email_user(subject, message, html_message=html)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
def type(self): # pragma: no cover
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def form_class(self):
|
||||
def form_class(self): # pragma: no cover
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
|
@ -41,11 +41,11 @@ def create_admin_registration(instance, **_):
|
||||
AdminRegistration.objects.get_or_create(user=instance)
|
||||
|
||||
|
||||
def invite_to_public_rooms(instance: Registration, **_):
|
||||
def invite_to_public_rooms(instance: Registration, created: bool, **_):
|
||||
"""
|
||||
When a user got registered, automatically invite the Matrix user into public rooms.
|
||||
"""
|
||||
if not instance.pk:
|
||||
if not created:
|
||||
Matrix.invite("#annonces:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
||||
Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
||||
Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr",
|
||||
|
@ -9,6 +9,7 @@ from ..tables import RegistrationTable
|
||||
|
||||
def search_table(results):
|
||||
model_class = results[0].object.__class__
|
||||
table_class = Table
|
||||
if issubclass(model_class, Registration):
|
||||
table_class = RegistrationTable
|
||||
elif issubclass(model_class, Team):
|
||||
@ -17,8 +18,6 @@ def search_table(results):
|
||||
table_class = ParticipationTable
|
||||
elif issubclass(model_class, Video):
|
||||
table_class = VideoTable
|
||||
else:
|
||||
table_class = Table
|
||||
return table_class([result.object for result in results], prefix=model_class._meta.model_name)
|
||||
|
||||
|
||||
|
@ -1,11 +1,16 @@
|
||||
import os
|
||||
|
||||
from corres2math.tokens import email_validation_token
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.http import urlsafe_base64_encode
|
||||
|
||||
from .models import CoachRegistration, Registration, StudentRegistration
|
||||
from .models import AdminRegistration, CoachRegistration, StudentRegistration
|
||||
|
||||
|
||||
class TestIndexPage(TestCase):
|
||||
@ -16,6 +21,13 @@ class TestIndexPage(TestCase):
|
||||
response = self.client.get(reverse("index"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_not_authenticated(self):
|
||||
"""
|
||||
Try to load some pages without being authenticated.
|
||||
"""
|
||||
response = self.client.get(reverse("registration:reset_admin"))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" + reverse("registration:reset_admin"), 302, 200)
|
||||
|
||||
|
||||
class TestRegistration(TestCase):
|
||||
def setUp(self) -> None:
|
||||
@ -41,14 +53,29 @@ class TestRegistration(TestCase):
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"registration/registration/{self.user.registration.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(AdminRegistration).id}/"
|
||||
f"{self.user.registration.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.user.registration.get_absolute_url()), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"registration/registration/{self.student.registration.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(StudentRegistration).id}/"
|
||||
f"{self.student.registration.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.student.registration.get_absolute_url()), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"registration/registration/{self.coach.registration.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(CoachRegistration).id}/"
|
||||
f"{self.coach.registration.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.coach.registration.get_absolute_url()), 302, 200)
|
||||
|
||||
def test_registration(self):
|
||||
"""
|
||||
@ -152,6 +179,14 @@ class TestRegistration(TestCase):
|
||||
"""
|
||||
Update the user information, for each type of user.
|
||||
"""
|
||||
# To test the modification of mailing lists
|
||||
from participation.models import Team
|
||||
self.student.registration.team = Team.objects.create(
|
||||
name="toto",
|
||||
trigram="TOT",
|
||||
)
|
||||
self.student.registration.save()
|
||||
|
||||
for user, data in [(self.user, dict(role="Bot")),
|
||||
(self.student, dict(student_class=11, school="Sky")),
|
||||
(self.coach, dict(professional_activity="God"))]:
|
||||
@ -215,9 +250,77 @@ class TestRegistration(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response["content-type"], "application/zip")
|
||||
|
||||
# Do it twice, ensure that the previous authorization got deleted
|
||||
old_authoratization = self.student.registration.photo_authorization.path
|
||||
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
||||
args=(self.student.registration.pk,)), data=dict(
|
||||
photo_authorization=open("corres2math/static/Autorisation de droit à l'image - majeur.pdf", "rb"),
|
||||
))
|
||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||
self.assertFalse(os.path.isfile(old_authoratization))
|
||||
|
||||
self.student.registration.refresh_from_db()
|
||||
self.student.registration.photo_authorization.delete()
|
||||
|
||||
def test_string_render(self):
|
||||
# TODO These string field tests will be removed when used in a template
|
||||
self.assertRaises(NotImplementedError, lambda: Registration().type)
|
||||
self.assertRaises(NotImplementedError, lambda: Registration().form_class)
|
||||
def test_user_detail_forbidden(self):
|
||||
"""
|
||||
Create a new user and ensure that it can't see the detail of another user.
|
||||
"""
|
||||
self.client.force_login(self.coach)
|
||||
|
||||
response = self.client.get(reverse("registration:user_detail", args=(self.user.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.get(reverse("registration:update_user", args=(self.user.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.get(reverse("registration:upload_user_photo_authorization", args=(self.user.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
response = self.client.get(reverse("photo_authorization", args=("inexisting-authorization",)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
with open("media/authorization/photo/example", "w") as f:
|
||||
f.write("I lost the game.")
|
||||
self.student.registration.photo_authorization = "authorization/photo/example"
|
||||
self.student.registration.save()
|
||||
response = self.client.get(reverse("photo_authorization", args=("example",)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
os.remove("media/authorization/photo/example")
|
||||
|
||||
def test_impersonate(self):
|
||||
"""
|
||||
Admin can impersonate other people to act as them.
|
||||
"""
|
||||
response = self.client.get(reverse("registration:user_impersonate", args=(0x7ffff42ff,)))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Impersonate student account
|
||||
response = self.client.get(reverse("registration:user_impersonate", args=(self.student.pk,)))
|
||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||
self.assertEqual(self.client.session["_fake_user_id"], self.student.id)
|
||||
|
||||
# Reset admin view
|
||||
response = self.client.get(reverse("registration:reset_admin"))
|
||||
self.assertRedirects(response, reverse("index"), 302, 200)
|
||||
self.assertFalse("_fake_user_id" in self.client.session)
|
||||
|
||||
def test_research(self):
|
||||
"""
|
||||
Try to search some things.
|
||||
"""
|
||||
call_command("rebuild_index", "--noinput", "-v", 0)
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.user.email)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" +
|
||||
str(self.coach.registration.professional_activity))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" +
|
||||
self.student.registration.get_student_class_display())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
@ -256,7 +256,6 @@ class UserImpersonateView(LoginRequiredMixin, RedirectView):
|
||||
session = request.session
|
||||
session["admin"] = request.user.pk
|
||||
session["_fake_user_id"] = kwargs["pk"]
|
||||
return redirect(request.path)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
@ -274,4 +273,4 @@ class ResetAdminView(LoginRequiredMixin, View):
|
||||
return self.handle_no_permission()
|
||||
if "_fake_user_id" in request.session:
|
||||
del request.session["_fake_user_id"]
|
||||
return redirect(request.GET.get("path", "/"))
|
||||
return redirect(request.GET.get("path", reverse_lazy("index")))
|
||||
|
@ -6,7 +6,7 @@ _client = None
|
||||
def get_sympa_client():
|
||||
global _client
|
||||
if _client is None:
|
||||
if os.getenv("SYMPA_PASSWORD", None) is not None:
|
||||
if os.getenv("SYMPA_PASSWORD", None) is not None: # pragma: no cover
|
||||
from sympasoap import Client
|
||||
_client = Client("https://" + os.getenv("SYMPA_URL"))
|
||||
_client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD"))
|
||||
|
@ -1,8 +1,7 @@
|
||||
from enum import Enum
|
||||
import os
|
||||
from typing import Tuple
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from nio import *
|
||||
|
||||
|
||||
class Matrix:
|
||||
@ -14,11 +13,11 @@ class Matrix:
|
||||
Tasks are normally asynchronous, but for compatibility we make
|
||||
them synchronous.
|
||||
"""
|
||||
_token: str = None
|
||||
_device_id: str = None
|
||||
_token = None
|
||||
_device_id = None
|
||||
|
||||
@classmethod
|
||||
async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]:
|
||||
async def _get_client(cls): # pragma: no cover
|
||||
"""
|
||||
Retrieve the bot account.
|
||||
If not logged, log in and store access token.
|
||||
@ -26,6 +25,7 @@ class Matrix:
|
||||
if not os.getenv("SYNAPSE_PASSWORD"):
|
||||
return FakeMatrixClient()
|
||||
|
||||
from nio import AsyncClient
|
||||
client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr")
|
||||
client.user_id = "@corres2mathbot:correspondances-maths.fr"
|
||||
|
||||
@ -49,7 +49,7 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def set_display_name(cls, name: str) -> Union[ProfileSetDisplayNameResponse, ProfileSetDisplayNameError]:
|
||||
async def set_display_name(cls, name: str):
|
||||
"""
|
||||
Set the display name of the bot account.
|
||||
"""
|
||||
@ -58,7 +58,7 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def set_avatar(cls, avatar_url: str) -> Union[ProfileSetAvatarResponse, ProfileSetAvatarError]:
|
||||
async def set_avatar(cls, avatar_url: str): # pragma: no cover
|
||||
"""
|
||||
Set the display avatar of the bot account.
|
||||
"""
|
||||
@ -69,13 +69,13 @@ class Matrix:
|
||||
@async_to_sync
|
||||
async def upload(
|
||||
cls,
|
||||
data_provider: DataProvider,
|
||||
data_provider,
|
||||
content_type: str = "application/octet-stream",
|
||||
filename: Optional[str] = None,
|
||||
filename: str = None,
|
||||
encrypt: bool = False,
|
||||
monitor: Optional[TransferMonitor] = None,
|
||||
filesize: Optional[int] = None,
|
||||
) -> Tuple[Union[UploadResponse, UploadError], Optional[Dict[str, Any]]]:
|
||||
monitor=None,
|
||||
filesize: int = None,
|
||||
): # pragma: no cover
|
||||
"""
|
||||
Upload a file to the content repository.
|
||||
|
||||
@ -129,24 +129,25 @@ class Matrix:
|
||||
If left as ``None``, some servers might refuse the upload.
|
||||
"""
|
||||
client = await cls._get_client()
|
||||
return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize)
|
||||
return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \
|
||||
if not isinstance(client, FakeMatrixClient) else None, None
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def create_room(
|
||||
cls,
|
||||
visibility: RoomVisibility = RoomVisibility.private,
|
||||
alias: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
topic: Optional[str] = None,
|
||||
room_version: Optional[str] = None,
|
||||
federate: bool = True,
|
||||
is_direct: bool = False,
|
||||
preset: Optional[RoomPreset] = None,
|
||||
visibility=None,
|
||||
alias=None,
|
||||
name=None,
|
||||
topic=None,
|
||||
room_version=None,
|
||||
federate=True,
|
||||
is_direct=False,
|
||||
preset=None,
|
||||
invite=(),
|
||||
initial_state=(),
|
||||
power_level_override: Optional[Dict[str, Any]] = None,
|
||||
) -> Union[RoomCreateResponse, RoomCreateError]:
|
||||
power_level_override=None,
|
||||
):
|
||||
"""
|
||||
Create a new room.
|
||||
|
||||
@ -208,20 +209,18 @@ class Matrix:
|
||||
power_level_override)
|
||||
|
||||
@classmethod
|
||||
async def resolve_room_alias(cls, room_alias: str) -> Optional[str]:
|
||||
async def resolve_room_alias(cls, room_alias: str):
|
||||
"""
|
||||
Resolve a room alias to a room ID.
|
||||
Return None if the alias does not exist.
|
||||
"""
|
||||
client = await cls._get_client()
|
||||
resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias)
|
||||
if isinstance(resp, RoomResolveAliasResponse):
|
||||
return resp.room_id
|
||||
return None
|
||||
resp = await client.room_resolve_alias(room_alias)
|
||||
return resp.room_id if resp else None
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def invite(cls, room_id: str, user_id: str) -> Union[RoomInviteResponse, RoomInviteError]:
|
||||
async def invite(cls, room_id: str, user_id: str):
|
||||
"""
|
||||
Invite a user to a room.
|
||||
|
||||
@ -263,13 +262,13 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomInviteError]:
|
||||
async def kick(cls, room_id: str, user_id: str, reason: str = None):
|
||||
"""
|
||||
Kick a user from a room, or withdraw their invitation.
|
||||
|
||||
Kicking a user adjusts their membership to "leave" with an optional
|
||||
reason.
|
||||
|
||||
²
|
||||
Returns either a `RoomKickResponse` if the request was successful or
|
||||
a `RoomKickError` if there was an error with the request.
|
||||
|
||||
@ -286,8 +285,7 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\
|
||||
-> Union[RoomPutStateResponse, RoomPutStateError]:
|
||||
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
|
||||
"""
|
||||
Put a given power level to a user in a certain room.
|
||||
|
||||
@ -302,6 +300,9 @@ class Matrix:
|
||||
power_level (int): The target power level to give.
|
||||
"""
|
||||
client = await cls._get_client()
|
||||
if isinstance(client, FakeMatrixClient):
|
||||
return None
|
||||
|
||||
if room_id.startswith("#"):
|
||||
room_id = await cls.resolve_room_alias(room_id)
|
||||
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
|
||||
@ -311,8 +312,7 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\
|
||||
-> Union[RoomPutStateResponse, RoomPutStateError]:
|
||||
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
|
||||
"""
|
||||
Define the minimal power level to have to send a certain event type
|
||||
in a given room.
|
||||
@ -328,6 +328,9 @@ class Matrix:
|
||||
power_level (int): The target power level to give.
|
||||
"""
|
||||
client = await cls._get_client()
|
||||
if isinstance(client, FakeMatrixClient):
|
||||
return None
|
||||
|
||||
if room_id.startswith("#"):
|
||||
room_id = await cls.resolve_room_alias(room_id)
|
||||
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
|
||||
@ -340,8 +343,7 @@ class Matrix:
|
||||
|
||||
@classmethod
|
||||
@async_to_sync
|
||||
async def set_room_avatar(cls, room_id: str, avatar_uri: str)\
|
||||
-> Union[RoomPutStateResponse, RoomPutStateError]:
|
||||
async def set_room_avatar(cls, room_id: str, avatar_uri: str):
|
||||
"""
|
||||
Define the avatar of a room.
|
||||
|
||||
@ -361,6 +363,22 @@ class Matrix:
|
||||
}, state_key="")
|
||||
|
||||
|
||||
if os.getenv("SYNAPSE_PASSWORD"): # pragma: no cover
|
||||
from nio import RoomVisibility, RoomPreset
|
||||
RoomVisibility = RoomVisibility
|
||||
RoomPreset = RoomPreset
|
||||
else:
|
||||
# When running tests, faking matrix-nio classes to don't include the module
|
||||
class RoomVisibility(Enum):
|
||||
private = 'private'
|
||||
public = 'public'
|
||||
|
||||
class RoomPreset(Enum):
|
||||
private_chat = "private_chat"
|
||||
trusted_private_chat = "trusted_private_chat"
|
||||
public_chat = "public_chat"
|
||||
|
||||
|
||||
class FakeMatrixClient:
|
||||
"""
|
||||
Simulate a Matrix client to run tests, if no Matrix homeserver is connected.
|
||||
@ -370,4 +388,3 @@ class FakeMatrixClient:
|
||||
async def func(*_, **_2):
|
||||
return None
|
||||
return func
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
|
||||
from threading import local
|
||||
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
|
||||
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
|
||||
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
|
||||
@ -22,19 +20,13 @@ def get_current_user() -> User:
|
||||
return getattr(_thread_locals, USER_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_session() -> SessionStore:
|
||||
return getattr(_thread_locals, SESSION_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_ip() -> str:
|
||||
return getattr(_thread_locals, IP_ATTR_NAME, None)
|
||||
|
||||
|
||||
def get_current_authenticated_user():
|
||||
current_user = get_current_user()
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
return None
|
||||
return current_user
|
||||
return None if isinstance(current_user, AnonymousUser) else current_user
|
||||
|
||||
|
||||
class SessionMiddleware(object):
|
||||
@ -50,10 +42,7 @@ class SessionMiddleware(object):
|
||||
request.user = User.objects.get(pk=request.session["_fake_user_id"])
|
||||
|
||||
user = request.user
|
||||
if 'HTTP_X_REAL_IP' in request.META:
|
||||
ip = request.META.get('HTTP_X_REAL_IP')
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
ip = request.META.get('HTTP_X_REAL_IP' if 'HTTP_X_REAL_IP' in request.META else 'REMOTE_ADDR')
|
||||
|
||||
_set_current_user_and_ip(user, request.session, ip)
|
||||
response = self.get_response(request)
|
||||
@ -62,7 +51,7 @@ class SessionMiddleware(object):
|
||||
return response
|
||||
|
||||
|
||||
class TurbolinksMiddleware(object):
|
||||
class TurbolinksMiddleware(object): # pragma: no cover
|
||||
"""
|
||||
Send the `Turbolinks-Location` header in response to a visit that was redirected,
|
||||
and Turbolinks will replace the browser's topmost history entry.
|
||||
|
@ -52,15 +52,12 @@ INSTALLED_APPS = [
|
||||
|
||||
'bootstrap_datepicker_plus',
|
||||
'crispy_forms',
|
||||
'django_extensions',
|
||||
'django_tables2',
|
||||
'haystack',
|
||||
'logs',
|
||||
'mailer',
|
||||
'polymorphic',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'cas_server',
|
||||
|
||||
'api',
|
||||
'eastereggs',
|
||||
@ -68,6 +65,13 @@ INSTALLED_APPS = [
|
||||
'participation',
|
||||
]
|
||||
|
||||
if "test" not in sys.argv: # pragma: no cover
|
||||
INSTALLED_APPS += [
|
||||
'cas_server',
|
||||
'django_extensions',
|
||||
'mailer',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
@ -89,8 +93,7 @@ LOGIN_REDIRECT_URL = "index"
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')]
|
||||
,
|
||||
'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -196,7 +199,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
|
||||
|
||||
_db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower()
|
||||
|
||||
if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql':
|
||||
if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': # pragma: no cover
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2',
|
||||
@ -215,7 +218,7 @@ else:
|
||||
}
|
||||
}
|
||||
|
||||
if os.getenv("CORRES2MATH_STAGE", "dev") == "prod":
|
||||
from .settings_prod import *
|
||||
if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": # pragma: no cover
|
||||
from .settings_prod import * # noqa: F401,F403
|
||||
else:
|
||||
from .settings_dev import *
|
||||
from .settings_dev import * # noqa: F401,F403
|
||||
|
@ -6,7 +6,6 @@
|
||||
<h2>{% trans "Search" %}</h2>
|
||||
|
||||
<form>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
|
||||
</form>
|
||||
|
24
corres2math/tests.py
Normal file
24
corres2math/tests.py
Normal file
@ -0,0 +1,24 @@
|
||||
import os
|
||||
|
||||
from django.core.handlers.asgi import ASGIHandler
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestLoadModules(TestCase):
|
||||
"""
|
||||
Load modules that are not used in development mode in order to increase coverage.
|
||||
"""
|
||||
def test_asgi(self):
|
||||
from corres2math import asgi
|
||||
self.assertTrue(isinstance(asgi.application, ASGIHandler))
|
||||
|
||||
def test_wsgi(self):
|
||||
from corres2math import wsgi
|
||||
self.assertTrue(isinstance(wsgi.application, WSGIHandler))
|
||||
|
||||
def test_load_production_settings(self):
|
||||
os.putenv("CORRES2MATH_STAGE", "prod")
|
||||
os.putenv("DJANGO_DB_TYPE", "postgres")
|
||||
from corres2math import settings_prod
|
||||
self.assertFalse(settings_prod.DEBUG)
|
@ -13,9 +13,10 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error
|
||||
from django.urls import include, path
|
||||
from django.views.defaults import bad_request, page_not_found, permission_denied, server_error
|
||||
from django.views.generic import TemplateView
|
||||
from registration.views import PhotoAuthorizationView
|
||||
|
||||
@ -35,11 +36,14 @@ urlpatterns = [
|
||||
|
||||
path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), name='photo_authorization'),
|
||||
|
||||
path('cas/', include('cas_server.urls', namespace="cas_server")),
|
||||
|
||||
path('', include('eastereggs.urls')),
|
||||
]
|
||||
|
||||
if 'cas_server' in settings.INSTALLED_APPS: # pragma: no cover
|
||||
urlpatterns += [
|
||||
path('cas/', include('cas_server.urls', namespace="cas_server")),
|
||||
]
|
||||
|
||||
handler400 = bad_request
|
||||
handler403 = permission_denied
|
||||
handler404 = page_not_found
|
||||
|
@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Corres2math\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-11-02 10:56+0100\n"
|
||||
"POT-Creation-Date: 2020-11-03 17:32+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -30,7 +30,7 @@ msgid "This task failed successfully."
|
||||
msgstr "Cette tâche a échoué avec succès."
|
||||
|
||||
#: apps/eastereggs/templates/eastereggs/xp_modal.html:16
|
||||
#: templates/base_modal.html:19
|
||||
#: corres2math/templates/base_modal.html:19
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
|
||||
@ -99,6 +99,16 @@ msgstr "changelogs"
|
||||
msgid "Changelog of type \"{action}\" for model {model} at {timestamp}"
|
||||
msgstr "Changelog de type \"{action}\" pour le modèle {model} le {timestamp}"
|
||||
|
||||
#: apps/participation/admin.py:16 apps/participation/models.py:122
|
||||
#: apps/participation/tables.py:35 apps/participation/tables.py:62
|
||||
msgid "problem number"
|
||||
msgstr "numéro de problème"
|
||||
|
||||
#: apps/participation/admin.py:21 apps/participation/models.py:128
|
||||
#: apps/participation/models.py:182
|
||||
msgid "valid"
|
||||
msgstr "valide"
|
||||
|
||||
#: apps/participation/forms.py:20 apps/participation/models.py:33
|
||||
msgid "The trigram must be composed of three uppercase letters."
|
||||
msgstr "Le trigramme doit être composé de trois lettres majuscules."
|
||||
@ -123,19 +133,25 @@ msgstr "Vous ne pouvez pas envoyer de vidéo après la date limite."
|
||||
msgid "Send to team"
|
||||
msgstr "Envoyer à l'équipe"
|
||||
|
||||
#: apps/participation/forms.py:152
|
||||
#: apps/participation/forms.py:156
|
||||
msgid "How did you get the idea to ...?"
|
||||
msgstr "Comment avez-vous eu l'idée de ... ?"
|
||||
|
||||
#: apps/participation/forms.py:175
|
||||
#: apps/participation/forms.py:160
|
||||
msgid "You can only create or update a question during the second phase."
|
||||
msgstr ""
|
||||
"Vous pouvez créer ou modifier une question seulement pendant la seconde "
|
||||
"phase."
|
||||
|
||||
#: apps/participation/forms.py:186
|
||||
msgid "Start date must be before the end date."
|
||||
msgstr "La date de début doit être avant la date de fin."
|
||||
|
||||
#: apps/participation/forms.py:177
|
||||
#: apps/participation/forms.py:188
|
||||
msgid "This phase must start after the previous phases."
|
||||
msgstr "Cette phase doit commencer après les phases précédentes."
|
||||
|
||||
#: apps/participation/forms.py:179
|
||||
#: apps/participation/forms.py:190
|
||||
msgid "This phase must end after the next phases."
|
||||
msgstr "Cette phase doit finir avant les phases suivantes."
|
||||
|
||||
@ -187,15 +203,6 @@ msgstr "équipes"
|
||||
msgid "Problem #{problem:d}"
|
||||
msgstr "Problème n°{problem:d}"
|
||||
|
||||
#: apps/participation/models.py:122 apps/participation/tables.py:35
|
||||
#: apps/participation/tables.py:62
|
||||
msgid "problem number"
|
||||
msgstr "numéro de problème"
|
||||
|
||||
#: apps/participation/models.py:128 apps/participation/models.py:182
|
||||
msgid "valid"
|
||||
msgstr "valide"
|
||||
|
||||
#: apps/participation/models.py:129 apps/participation/models.py:183
|
||||
msgid "The video got the validation of the administrators."
|
||||
msgstr "La vidéo a été validée par les administrateurs."
|
||||
@ -283,12 +290,12 @@ msgid "phases"
|
||||
msgstr "phases"
|
||||
|
||||
#: apps/participation/templates/participation/create_team.html:11
|
||||
#: templates/base.html:231
|
||||
#: corres2math/templates/base.html:231
|
||||
msgid "Create"
|
||||
msgstr "Créer"
|
||||
|
||||
#: apps/participation/templates/participation/join_team.html:11
|
||||
#: templates/base.html:227
|
||||
#: corres2math/templates/base.html:227
|
||||
msgid "Join"
|
||||
msgstr "Rejoindre"
|
||||
|
||||
@ -462,7 +469,7 @@ msgstr "Définir l'équipe qui recevra votre vidéo"
|
||||
|
||||
#: apps/participation/templates/participation/participation_detail.html:181
|
||||
#: apps/participation/templates/participation/participation_detail.html:233
|
||||
#: apps/participation/views.py:463
|
||||
#: apps/participation/views.py:482
|
||||
msgid "Upload video"
|
||||
msgstr "Envoyer la vidéo"
|
||||
|
||||
@ -497,7 +504,7 @@ msgid "Update question"
|
||||
msgstr "Modifier la question"
|
||||
|
||||
#: apps/participation/templates/participation/participation_detail.html:217
|
||||
#: apps/participation/views.py:440
|
||||
#: apps/participation/views.py:459
|
||||
msgid "Delete question"
|
||||
msgstr "Supprimer la question"
|
||||
|
||||
@ -507,8 +514,8 @@ msgid "Display synthesis"
|
||||
msgstr "Afficher la synthèse"
|
||||
|
||||
#: apps/participation/templates/participation/phase_list.html:10
|
||||
#: apps/participation/views.py:482 templates/base.html:68
|
||||
#: templates/base.html:70 templates/base.html:217
|
||||
#: apps/participation/views.py:501 corres2math/templates/base.html:68
|
||||
#: corres2math/templates/base.html:70 corres2math/templates/base.html:217
|
||||
msgid "Calendar"
|
||||
msgstr "Calendrier"
|
||||
|
||||
@ -620,7 +627,7 @@ msgid "Update team"
|
||||
msgstr "Modifier l'équipe"
|
||||
|
||||
#: apps/participation/templates/participation/team_detail.html:127
|
||||
#: apps/participation/views.py:296
|
||||
#: apps/participation/views.py:314
|
||||
msgid "Leave team"
|
||||
msgstr "Quitter l'équipe"
|
||||
|
||||
@ -628,8 +635,8 @@ msgstr "Quitter l'équipe"
|
||||
msgid "Are you sure that you want to leave this team?"
|
||||
msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?"
|
||||
|
||||
#: apps/participation/views.py:36 templates/base.html:77
|
||||
#: templates/base.html:230
|
||||
#: apps/participation/views.py:36 corres2math/templates/base.html:77
|
||||
#: corres2math/templates/base.html:230
|
||||
msgid "Create team"
|
||||
msgstr "Créer une équipe"
|
||||
|
||||
@ -641,80 +648,89 @@ msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe."
|
||||
msgid "You are already in a team."
|
||||
msgstr "Vous êtes déjà dans une équipe."
|
||||
|
||||
#: apps/participation/views.py:82 templates/base.html:82
|
||||
#: templates/base.html:226
|
||||
#: apps/participation/views.py:82 corres2math/templates/base.html:82
|
||||
#: corres2math/templates/base.html:226
|
||||
msgid "Join team"
|
||||
msgstr "Rejoindre une équipe"
|
||||
|
||||
#: apps/participation/views.py:133 apps/participation/views.py:302
|
||||
#: apps/participation/views.py:335
|
||||
#: apps/participation/views.py:133 apps/participation/views.py:320
|
||||
#: apps/participation/views.py:353
|
||||
msgid "You are not in a team."
|
||||
msgstr "Vous n'êtes pas dans une équipe."
|
||||
|
||||
#: apps/participation/views.py:134 apps/participation/views.py:336
|
||||
#: apps/participation/views.py:134 apps/participation/views.py:354
|
||||
msgid "You don't participate, so you don't have any team."
|
||||
msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe."
|
||||
|
||||
#: apps/participation/views.py:155
|
||||
#: apps/participation/views.py:156
|
||||
#, python-brace-format
|
||||
msgid "Detail of team {trigram}"
|
||||
msgstr "Détails de l'équipe {trigram}"
|
||||
|
||||
#: apps/participation/views.py:180
|
||||
#: apps/participation/views.py:188
|
||||
msgid "You don't participate, so you can't request the validation of the team."
|
||||
msgstr ""
|
||||
"Vous ne participez pas, vous ne pouvez pas demander la validation de "
|
||||
"l'équipe."
|
||||
|
||||
#: apps/participation/views.py:183
|
||||
#: apps/participation/views.py:191
|
||||
msgid "The validation of the team is already done or pending."
|
||||
msgstr "La validation de l'équipe est déjà faite ou en cours."
|
||||
|
||||
#: apps/participation/views.py:196
|
||||
#: apps/participation/views.py:194
|
||||
msgid ""
|
||||
"The team can't be validated: missing email address confirmations, photo "
|
||||
"authorizations, people or the chosen problem is not set."
|
||||
msgstr ""
|
||||
"L'équipe ne peut pas être validée : il manque soit les confirmations "
|
||||
"d'adresse e-mail, soit une autorisation parentale, soit des personnes soit "
|
||||
"le problème n'a pas été choisi."
|
||||
|
||||
#: apps/participation/views.py:213
|
||||
msgid "You are not an administrator."
|
||||
msgstr "Vous n'êtes pas administrateur."
|
||||
|
||||
#: apps/participation/views.py:199
|
||||
#: apps/participation/views.py:216
|
||||
msgid "This team has no pending validation."
|
||||
msgstr "L'équipe n'a pas de validation en attente."
|
||||
|
||||
#: apps/participation/views.py:218
|
||||
#: apps/participation/views.py:235
|
||||
msgid "You must specify if you validate the registration or not."
|
||||
msgstr "Vous devez spécifier si vous validez l'inscription ou non."
|
||||
|
||||
#: apps/participation/views.py:245
|
||||
#: apps/participation/views.py:263
|
||||
#, python-brace-format
|
||||
msgid "Update team {trigram}"
|
||||
msgstr "Mise à jour de l'équipe {trigram}"
|
||||
|
||||
#: apps/participation/views.py:282 apps/registration/views.py:243
|
||||
#: apps/participation/views.py:300 apps/registration/views.py:243
|
||||
#, python-brace-format
|
||||
msgid "Photo authorization of {student}.{ext}"
|
||||
msgstr "Autorisation de droit à l'image de {student}.{ext}"
|
||||
|
||||
#: apps/participation/views.py:286
|
||||
#: apps/participation/views.py:304
|
||||
#, python-brace-format
|
||||
msgid "Photo authorizations of team {trigram}.zip"
|
||||
msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip"
|
||||
|
||||
#: apps/participation/views.py:304
|
||||
#: apps/participation/views.py:322
|
||||
msgid "The team is already validated or the validation is pending."
|
||||
msgstr "La validation de l'équipe est déjà faite ou en cours."
|
||||
|
||||
#: apps/participation/views.py:348
|
||||
#: apps/participation/views.py:366
|
||||
msgid "The team is not validated yet."
|
||||
msgstr "L'équipe n'est pas encore validée."
|
||||
|
||||
#: apps/participation/views.py:357
|
||||
#: apps/participation/views.py:376
|
||||
#, python-brace-format
|
||||
msgid "Participation of team {trigram}"
|
||||
msgstr "Participation de l'équipe {trigram}"
|
||||
|
||||
#: apps/participation/views.py:394
|
||||
#: apps/participation/views.py:413
|
||||
msgid "Create question"
|
||||
msgstr "Créer une question"
|
||||
|
||||
#: apps/participation/views.py:491
|
||||
#: apps/participation/views.py:510
|
||||
msgid "Calendar update"
|
||||
msgstr "Mise à jour du calendrier"
|
||||
|
||||
@ -907,9 +923,11 @@ msgid "Your password has been set. You may go ahead and log in now."
|
||||
msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter."
|
||||
|
||||
#: apps/registration/templates/registration/password_reset_complete.html:10
|
||||
#: templates/base.html:130 templates/base.html:221 templates/base.html:222
|
||||
#: templates/registration/login.html:7 templates/registration/login.html:8
|
||||
#: templates/registration/login.html:25
|
||||
#: corres2math/templates/base.html:130 corres2math/templates/base.html:221
|
||||
#: corres2math/templates/base.html:222
|
||||
#: corres2math/templates/registration/login.html:7
|
||||
#: corres2math/templates/registration/login.html:8
|
||||
#: corres2math/templates/registration/login.html:25
|
||||
msgid "Log in"
|
||||
msgstr "Connexion"
|
||||
|
||||
@ -1082,11 +1100,11 @@ msgstr "Anglais"
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
#: templates/400.html:6
|
||||
#: corres2math/templates/400.html:6
|
||||
msgid "Bad request"
|
||||
msgstr "Requête invalide"
|
||||
|
||||
#: templates/400.html:7
|
||||
#: corres2math/templates/400.html:7
|
||||
msgid ""
|
||||
"Sorry, your request was bad. Don't know what could be wrong. An email has "
|
||||
"been sent to webmasters with the details of the error. You can now watch "
|
||||
@ -1096,23 +1114,23 @@ msgstr ""
|
||||
"email a été envoyé aux administrateurs avec les détails de l'erreur. Vous "
|
||||
"pouvez désormais retourner voir des vidéos."
|
||||
|
||||
#: templates/403.html:6
|
||||
#: corres2math/templates/403.html:6
|
||||
msgid "Permission denied"
|
||||
msgstr "Permission refusée"
|
||||
|
||||
#: templates/403.html:7
|
||||
#: corres2math/templates/403.html:7
|
||||
msgid "You don't have the right to perform this request."
|
||||
msgstr "Vous n'avez pas le droit d'effectuer cette requête."
|
||||
|
||||
#: templates/403.html:10 templates/404.html:10
|
||||
#: corres2math/templates/403.html:10 corres2math/templates/404.html:10
|
||||
msgid "Exception message:"
|
||||
msgstr "Message d'erreur :"
|
||||
|
||||
#: templates/404.html:6
|
||||
#: corres2math/templates/404.html:6
|
||||
msgid "Page not found"
|
||||
msgstr "Page non trouvée"
|
||||
|
||||
#: templates/404.html:7
|
||||
#: corres2math/templates/404.html:7
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The requested path <code>%(request_path)s</code> was not found on the server."
|
||||
@ -1120,11 +1138,11 @@ msgstr ""
|
||||
"Le chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le "
|
||||
"serveur."
|
||||
|
||||
#: templates/500.html:6
|
||||
#: corres2math/templates/500.html:6
|
||||
msgid "Server error"
|
||||
msgstr "Erreur du serveur"
|
||||
|
||||
#: templates/500.html:7
|
||||
#: corres2math/templates/500.html:7
|
||||
msgid ""
|
||||
"Sorry, an error occurred when processing your request. An email has been "
|
||||
"sent to webmasters with the detail of the error, and this will be fixed "
|
||||
@ -1135,47 +1153,47 @@ msgstr ""
|
||||
"avec les détails de l'erreur. Vous pouvez désormais retourner voir des "
|
||||
"vidéos."
|
||||
|
||||
#: templates/base.html:64
|
||||
#: corres2math/templates/base.html:64
|
||||
msgid "Home"
|
||||
msgstr "Accueil"
|
||||
|
||||
#: templates/base.html:88
|
||||
#: corres2math/templates/base.html:88
|
||||
msgid "My team"
|
||||
msgstr "Mon équipe"
|
||||
|
||||
#: templates/base.html:93
|
||||
#: corres2math/templates/base.html:93
|
||||
msgid "My participation"
|
||||
msgstr "Ma participation"
|
||||
|
||||
#: templates/base.html:100
|
||||
#: corres2math/templates/base.html:100
|
||||
msgid "Chat"
|
||||
msgstr "Chat"
|
||||
|
||||
#: templates/base.html:104
|
||||
#: corres2math/templates/base.html:104
|
||||
msgid "Administration"
|
||||
msgstr "Administration"
|
||||
|
||||
#: templates/base.html:112
|
||||
#: corres2math/templates/base.html:112
|
||||
msgid "Search..."
|
||||
msgstr "Chercher ..."
|
||||
|
||||
#: templates/base.html:121
|
||||
#: corres2math/templates/base.html:121
|
||||
msgid "Return to admin view"
|
||||
msgstr "Retourner à l'interface administrateur"
|
||||
|
||||
#: templates/base.html:126
|
||||
#: corres2math/templates/base.html:126
|
||||
msgid "Register"
|
||||
msgstr "S'inscrire"
|
||||
|
||||
#: templates/base.html:142
|
||||
#: corres2math/templates/base.html:142
|
||||
msgid "My account"
|
||||
msgstr "Mon compte"
|
||||
|
||||
#: templates/base.html:145
|
||||
#: corres2math/templates/base.html:145
|
||||
msgid "Log out"
|
||||
msgstr "Déconnexion"
|
||||
|
||||
#: templates/base.html:162
|
||||
#: corres2math/templates/base.html:162
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Your email address is not validated. Please click on the link you received "
|
||||
@ -1186,23 +1204,23 @@ msgstr ""
|
||||
"avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a href="
|
||||
"\"%(send_email_url)s\">ce lien</a>."
|
||||
|
||||
#: templates/base.html:186
|
||||
#: corres2math/templates/base.html:186
|
||||
msgid "Contact us"
|
||||
msgstr "Nous contacter"
|
||||
|
||||
#: templates/base.html:219
|
||||
#: corres2math/templates/base.html:219
|
||||
msgid "Search results"
|
||||
msgstr "Résultats de la recherche"
|
||||
|
||||
#: templates/registration/logged_out.html:8
|
||||
#: corres2math/templates/registration/logged_out.html:8
|
||||
msgid "Thanks for spending some quality time with the Web site today."
|
||||
msgstr "Merci d'avoir utilisé la plateforme des Correspondances."
|
||||
|
||||
#: templates/registration/logged_out.html:9
|
||||
#: corres2math/templates/registration/logged_out.html:9
|
||||
msgid "Log in again"
|
||||
msgstr "Se reconnecter"
|
||||
|
||||
#: templates/registration/login.html:13
|
||||
#: corres2math/templates/registration/login.html:13
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You are authenticated as %(user)s, but are not authorized to access this "
|
||||
@ -1211,18 +1229,19 @@ msgstr ""
|
||||
"Vous êtes connectés en tant que %(user)s, mais n'êtes pas autorisés à "
|
||||
"accéder à cette page. Voulez-vous vous reconnecter avec un autre compte ?"
|
||||
|
||||
#: templates/registration/login.html:23
|
||||
#: corres2math/templates/registration/login.html:23
|
||||
msgid "Forgotten your password or username?"
|
||||
msgstr "Mot de passe oublié ?"
|
||||
|
||||
#: templates/search/search.html:6 templates/search/search.html:11
|
||||
#: corres2math/templates/search/search.html:6
|
||||
#: corres2math/templates/search/search.html:10
|
||||
msgid "Search"
|
||||
msgstr "Chercher"
|
||||
|
||||
#: templates/search/search.html:16
|
||||
#: corres2math/templates/search/search.html:15
|
||||
msgid "Results"
|
||||
msgstr "Résultats"
|
||||
|
||||
#: templates/search/search.html:26
|
||||
#: corres2math/templates/search/search.html:25
|
||||
msgid "No results found."
|
||||
msgstr "Aucun résultat."
|
||||
|
@ -1,17 +1,19 @@
|
||||
Django~=3.0
|
||||
django-bootstrap-datepicker-plus
|
||||
django-cas-server
|
||||
django-crispy-forms
|
||||
django-extensions
|
||||
django-filter~=2.3.0
|
||||
Django~=3.1
|
||||
django-bootstrap-datepicker-plus~=3.0
|
||||
django-cas-server~=1.2
|
||||
django-crispy-forms~=1.9
|
||||
django-extensions~=3.0
|
||||
django-filter~=2.3
|
||||
django-haystack~=3.0
|
||||
django-mailer
|
||||
django-polymorphic
|
||||
django-tables2
|
||||
djangorestframework~=3.11.1
|
||||
django-rest-polymorphic
|
||||
matrix-nio
|
||||
ptpython
|
||||
python-magic~=0.4.18
|
||||
gunicorn
|
||||
whoosh
|
||||
django-mailer~=2.0
|
||||
django-polymorphic~=3.0
|
||||
django-tables2~=2.3
|
||||
djangorestframework~=3.12
|
||||
django-rest-polymorphic~=0.1
|
||||
gunicorn~=20.0
|
||||
matrix-nio~=0.15
|
||||
psycopg2-binary~=2.8
|
||||
ptpython~=3.0
|
||||
python-magic~=0.4
|
||||
sympasoap~=1.0
|
||||
whoosh~=2.7
|
18
tox.ini
18
tox.ini
@ -7,12 +7,22 @@ envlist =
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
sitepackages = True
|
||||
sitepackages = False
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
coverage
|
||||
Django~=3.1
|
||||
django-bootstrap-datepicker-plus~=3.0
|
||||
django-crispy-forms~=1.9
|
||||
django-filter~=2.3
|
||||
django-haystack~=3.0
|
||||
django-polymorphic~=3.0
|
||||
django-tables2~=2.3
|
||||
djangorestframework~=3.12
|
||||
django-rest-polymorphic~=0.1
|
||||
python-magic~=0.4
|
||||
whoosh~=2.7
|
||||
commands =
|
||||
coverage run --omit='*migrations*,apps/scripts*' --source=apps ./manage.py test apps/
|
||||
coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/
|
||||
coverage report -m
|
||||
|
||||
[testenv:linters]
|
||||
@ -25,7 +35,7 @@ deps =
|
||||
pep8-naming
|
||||
pyflakes
|
||||
commands =
|
||||
flake8 apps/
|
||||
flake8 apps/ corres2math/
|
||||
|
||||
[flake8]
|
||||
exclude =
|
||||
|
Loading…
Reference in New Issue
Block a user