Merge branch 'tests' into 'master'

Tests

See merge request animath/si/plateforme-corres2math!3
This commit is contained in:
Yohann D'ANELLO 2020-11-03 20:15:23 +00:00
commit cba3e56fb8
27 changed files with 1132 additions and 264 deletions

View File

@ -6,7 +6,7 @@ py38:
stage: test stage: test
image: python:3.8-alpine image: python:3.8-alpine
before_script: 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 - pip install tox --no-cache-dir
script: tox -e py38 script: tox -e py38
@ -14,7 +14,7 @@ py39:
stage: test stage: test
image: python:3.9-alpine image: python:3.9-alpine
before_script: 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 - pip install tox --no-cache-dir
script: tox -e py39 script: tox -e py39

View File

@ -11,7 +11,7 @@ RUN apk add --no-cache bash
RUN mkdir /code RUN mkdir /code
WORKDIR /code WORKDIR /code
COPY requirements.txt /code/requirements.txt 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/ COPY . /code/

View File

@ -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',)

View File

@ -2,7 +2,7 @@ import re
from bootstrap_datepicker_plus import DateTimePickerInput from bootstrap_datepicker_plus import DateTimePickerInput
from django import forms 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.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -95,7 +95,7 @@ class UploadVideoForm(forms.ModelForm):
fields = ('link',) fields = ('link',)
def clean(self): 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.")) self.add_error("link", _("You can't upload your video after the deadline."))
return super().clean() return super().clean()
@ -126,16 +126,20 @@ class SendParticipationForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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( self.fields["sent_participation"].queryset = Participation.objects.filter(
~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True) ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
) )
def clean(self, commit=True): def clean(self, commit=True):
cleaned_data = super().clean() cleaned_data = super().clean()
participation = cleaned_data["sent_participation"] if "sent_participation" in cleaned_data:
participation.received_participation = self.instance participation = cleaned_data["sent_participation"]
self.instance = participation participation.received_participation = self.instance
self.instance = participation
return cleaned_data return cleaned_data
class Meta: class Meta:
@ -151,6 +155,11 @@ class QuestionForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")}) 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: class Meta:
model = Question model = Question
fields = ('question',) fields = ('question',)
@ -171,10 +180,12 @@ class PhaseForm(forms.ModelForm):
def clean(self): def clean(self):
# Ensure that dates are in a right order # Ensure that dates are in a right order
cleaned_data = super().clean() 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.")) 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.")) 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.")) self.add_error("end", _("This phase must end after the next phases."))
return cleaned_data return cleaned_data

View File

@ -1,9 +1,8 @@
import os import os
from asgiref.sync import async_to_sync 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 django.core.management import BaseCommand
from nio import RoomPreset
from registration.models import AdminRegistration, Registration from registration.models import AdminRegistration, Registration
@ -11,17 +10,21 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
Matrix.set_display_name("Bot des Correspondances") Matrix.set_display_name("Bot des Correspondances")
if not os.path.isfile(".matrix_avatar"): if not os.getenv("SYNAPSE_PASSWORD"):
stat_file = os.stat("corres2math/static/logo.png") avatar_uri = "plop"
with open("corres2math/static/logo.png", "rb") as f: else: # pragma: no cover
resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size) if not os.path.isfile(".matrix_avatar"):
if isinstance(resp, UploadError): stat_file = os.stat("corres2math/static/logo.png")
raise Exception(resp) with open("corres2math/static/logo.png", "rb") as f:
avatar_uri = resp.content_uri resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png",
with open(".matrix_avatar", "w") as f: filesize=stat_file.st_size)
f.write(avatar_uri) if not hasattr(resp, "content_uri"):
Matrix.set_avatar(avatar_uri) raise Exception(resp)
else: 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: with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n") avatar_uri = f.read().rstrip(" \t\r\n")

View File

@ -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. Drop all phases in order to unapply this migration.
""" """

View File

@ -2,7 +2,7 @@ import os
import re import re
from corres2math.lists import get_sympa_client 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.exceptions import ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import models 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.crypto import get_random_string
from django.utils.text import format_lazy from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from nio import RoomPreset, RoomVisibility
class Team(models.Model): class Team(models.Model):

View File

@ -72,19 +72,17 @@ class ParticipationTable(tables.Table):
class VideoTable(tables.Table): class VideoTable(tables.Table):
participationname = tables.LinkColumn( participation_name = tables.LinkColumn(
'participation:participation_detail', 'participation:participation_detail',
args=[tables.A("participation__pk")], args=[tables.A("participation__pk")],
verbose_name=lambda: _("name").capitalize(), verbose_name=lambda: _("name").capitalize(),
accessor=tables.A("participation__team__name"),
) )
def render_participationname(self, record):
return record.participation.team.name
class Meta: class Meta:
attrs = { attrs = {
'class': 'table table condensed table-striped', 'class': 'table table condensed table-striped',
} }
model = Team model = Team
fields = ('participationname', 'link',) fields = ('participation_name', 'link',)
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'

View File

@ -1,13 +1,25 @@
from datetime import timedelta
from django.contrib.auth.models import User 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.test import TestCase
from django.urls import reverse 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): class TestStudentParticipation(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.superuser = User.objects.create_superuser(
username="admin",
email="admin@example.com",
password="toto1234",
)
self.user = User.objects.create( self.user = User.objects.create(
first_name="Toto", first_name="Toto",
last_name="Toto", last_name="Toto",
@ -27,11 +39,94 @@ class TestStudentParticipation(TestCase):
access_code="azerty", access_code="azerty",
grant_animath_access_videos=True, 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) self.client.force_login(self.user)
# TODO Remove these lines self.second_user = User.objects.create(
str(self.team) first_name="Lalala",
str(self.team.participation) 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): def test_create_team(self):
""" """
@ -62,6 +157,7 @@ class TestStudentParticipation(TestCase):
trigram="TET", trigram="TET",
grant_animath_access_videos=False, grant_animath_access_videos=False,
)) ))
self.assertEqual(response.status_code, 403)
def test_join_team(self): def test_join_team(self):
""" """
@ -109,6 +205,161 @@ class TestStudentParticipation(TestCase):
response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
self.assertEqual(response.status_code, 200) 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): def test_update_team(self):
""" """
Try to update team information. Try to update team information.
@ -116,6 +367,9 @@ class TestStudentParticipation(TestCase):
self.user.registration.team = self.team self.user.registration.team = self.team
self.user.registration.save() 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,))) response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
self.assertEqual(response.status_code, 200) 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.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists()) 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): def test_no_myparticipation_redirect_nomyparticipation(self):
""" """
Ensure a permission denied when we search my team participation when we are in no team. 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.team = self.team
self.user.registration.save() self.user.registration.save()
# Can't see the participation if it is not valid
response = self.client.get(reverse("participation:my_participation_detail")) response = self.client.get(reverse("participation:my_participation_detail"))
self.assertRedirects(response, self.assertRedirects(response,
reverse("participation:participation_detail", args=(self.team.participation.pk,)), 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,))) response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
self.assertEqual(response.status_code, 200) 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): def test_upload_video(self):
""" """
Try to send a solution video link. 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,))) response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
self.assertEqual(response.status_code, 200) 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: def setUp(self) -> None:
self.user = User.objects.create_superuser( self.user = User.objects.create_superuser(
username="admin@example.com", username="admin@example.com",
@ -201,6 +715,96 @@ class TestAdminForbidden(TestCase):
) )
self.client.force_login(self.user) 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): def test_create_team_forbidden(self):
""" """
Ensure that an admin can't create a team. Ensure that an admin can't create a team.
@ -223,9 +827,23 @@ class TestAdminForbidden(TestCase):
)) ))
self.assertTrue(response.status_code, 403) 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): def test_my_team_forbidden(self):
""" """
Ensure that an admin can't access to "My team". Ensure that an admin can't access to "My team".
""" """
response = self.client.get(reverse("participation:my_team_detail")) response = self.client.get(reverse("participation:my_team_detail"))
self.assertEqual(response.status_code, 403) 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)

View File

@ -144,7 +144,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
user = request.user user = request.user
self.object = self.get_object() self.object = self.get_object()
# Ensure that the user is an admin or a member of the team # 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) return super().get(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -171,54 +172,69 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
return RequestValidationForm return RequestValidationForm
elif self.request.POST["_form_type"] == "ValidateParticipationForm": elif self.request.POST["_form_type"] == "ValidateParticipationForm":
return ValidateParticipationForm return ValidateParticipationForm
return None
def form_valid(self, form): def form_valid(self, form):
self.object = self.get_object() self.object = self.get_object()
if isinstance(form, RequestValidationForm): if isinstance(form, RequestValidationForm):
if not self.request.user.registration.participates: return self.handle_request_validation(form)
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)
elif isinstance(form, ValidateParticipationForm): elif isinstance(form, ValidateParticipationForm):
if not self.request.user.registration.is_admin: return self.handle_validate_participation(form)
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: def handle_request_validation(self, form):
self.object.participation.valid = True """
self.object.participation.save() A team requests to be validated
mail_context = dict(team=self.object, message=form.cleaned_data["message"]) """
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context) if not self.request.user.registration.participates:
mail_html = render_to_string("participation/mails/team_validated.html", mail_context) form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html) return self.form_invalid(form)
elif "invalidate" in self.request.POST: if self.object.participation.valid is not None:
self.object.participation.valid = None form.add_error(None, _("The validation of the team is already done or pending."))
self.object.participation.save() return self.form_invalid(form)
mail_context = dict(team=self.object, message=form.cleaned_data["message"]) if not self.get_context_data()["can_validate"]:
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context) form.add_error(None, _("The team can't be validated: missing email address confirmations, "
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context) "photo authorizations, people or the chosen problem is not set."))
send_mail("[Corres2math] Équipe non validée", mail_plain, None, [self.object.email], return self.form_invalid(form)
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_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): def get_success_url(self):
return self.request.path return self.request.path
@ -234,7 +250,9 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
user = request.user 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) return super().dispatch(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -298,7 +316,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return self.handle_no_permission() 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.")) raise PermissionDenied(_("You are not in a team."))
if request.user.registration.team.participation.valid is not None: if request.user.registration.team.participation.valid is not None:
raise PermissionDenied(_("The team is already validated or the validation is pending.")) 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: if not self.get_object().valid:
raise PermissionDenied(_("The team is not validated yet.")) raise PermissionDenied(_("The team is not validated yet."))
if user.registration.is_admin or user.registration.participates \ if user.registration.is_admin or user.registration.participates \
and user.registration.team.participation \
and user.registration.team.participation.pk == kwargs["pk"]: and user.registration.team.participation.pk == kwargs["pk"]:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -369,7 +388,7 @@ class SetParticipationReceiveParticipationView(AdminMixin, UpdateView):
template_name = "participation/receive_participation_form.html" template_name = "participation/receive_participation_form.html"
def get_success_url(self): 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): class SetParticipationSendParticipationView(AdminMixin, UpdateView):
@ -381,7 +400,7 @@ class SetParticipationSendParticipationView(AdminMixin, UpdateView):
template_name = "participation/send_participation_form.html" template_name = "participation/send_participation_form.html"
def get_success_url(self): 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): class CreateQuestionView(LoginRequiredMixin, CreateView):
@ -399,6 +418,7 @@ class CreateQuestionView(LoginRequiredMixin, CreateView):
self.participation = Participation.objects.get(pk=kwargs["pk"]) self.participation = Participation.objects.get(pk=kwargs["pk"])
if request.user.registration.is_admin or \ if request.user.registration.is_admin or \
request.user.registration.participates and \ request.user.registration.participates and \
self.participation.valid and \
request.user.registration.team.pk == self.participation.team_id: request.user.registration.team.pk == self.participation.team_id:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -424,6 +444,7 @@ class UpdateQuestionView(LoginRequiredMixin, UpdateView):
return self.handle_no_permission() return self.handle_no_permission()
if request.user.registration.is_admin or \ if request.user.registration.is_admin or \
request.user.registration.participates and \ request.user.registration.participates and \
self.object.participation.valid and \
request.user.registration.team.pk == self.object.participation.team_id: request.user.registration.team.pk == self.object.participation.team_id:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied
@ -445,6 +466,7 @@ class DeleteQuestionView(LoginRequiredMixin, DeleteView):
return self.handle_no_permission() return self.handle_no_permission()
if request.user.registration.is_admin or \ if request.user.registration.is_admin or \
request.user.registration.participates and \ request.user.registration.participates and \
self.object.participation.valid and \
request.user.registration.team.pk == self.object.participation.team_id: request.user.registration.team.pk == self.object.participation.team_id:
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
raise PermissionDenied raise PermissionDenied

View File

@ -15,3 +15,6 @@ class RegistrationConfig(AppConfig):
pre_save.connect(send_email_link, "auth.User") pre_save.connect(send_email_link, "auth.User")
post_save.connect(create_admin_registration, "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.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")

View File

@ -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. Override Django Auth User model to define a custom Matrix username.
""" """

View File

@ -58,11 +58,11 @@ class Registration(PolymorphicModel):
self.user.email_user(subject, message, html_message=html) self.user.email_user(subject, message, html_message=html)
@property @property
def type(self): def type(self): # pragma: no cover
raise NotImplementedError raise NotImplementedError
@property @property
def form_class(self): def form_class(self): # pragma: no cover
raise NotImplementedError raise NotImplementedError
@property @property

View File

@ -41,11 +41,11 @@ def create_admin_registration(instance, **_):
AdminRegistration.objects.get_or_create(user=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. 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("#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("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr", Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr",

View File

@ -9,6 +9,7 @@ from ..tables import RegistrationTable
def search_table(results): def search_table(results):
model_class = results[0].object.__class__ model_class = results[0].object.__class__
table_class = Table
if issubclass(model_class, Registration): if issubclass(model_class, Registration):
table_class = RegistrationTable table_class = RegistrationTable
elif issubclass(model_class, Team): elif issubclass(model_class, Team):
@ -17,8 +18,6 @@ def search_table(results):
table_class = ParticipationTable table_class = ParticipationTable
elif issubclass(model_class, Video): elif issubclass(model_class, Video):
table_class = VideoTable table_class = VideoTable
else:
table_class = Table
return table_class([result.object for result in results], prefix=model_class._meta.model_name) return table_class([result.object for result in results], prefix=model_class._meta.model_name)

View File

@ -1,11 +1,16 @@
import os
from corres2math.tokens import email_validation_token from corres2math.tokens import email_validation_token
from django.contrib.auth.models import User 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.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode from django.utils.http import urlsafe_base64_encode
from .models import CoachRegistration, Registration, StudentRegistration from .models import AdminRegistration, CoachRegistration, StudentRegistration
class TestIndexPage(TestCase): class TestIndexPage(TestCase):
@ -16,6 +21,13 @@ class TestIndexPage(TestCase):
response = self.client.get(reverse("index")) response = self.client.get(reverse("index"))
self.assertEqual(response.status_code, 200) 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): class TestRegistration(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
@ -41,14 +53,29 @@ class TestRegistration(TestCase):
response = self.client.get(reverse("admin:index") response = self.client.get(reverse("admin:index")
+ f"registration/registration/{self.user.registration.pk}/change/") + f"registration/registration/{self.user.registration.pk}/change/")
self.assertEqual(response.status_code, 200) 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") response = self.client.get(reverse("admin:index")
+ f"registration/registration/{self.student.registration.pk}/change/") + f"registration/registration/{self.student.registration.pk}/change/")
self.assertEqual(response.status_code, 200) 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") response = self.client.get(reverse("admin:index")
+ f"registration/registration/{self.coach.registration.pk}/change/") + f"registration/registration/{self.coach.registration.pk}/change/")
self.assertEqual(response.status_code, 200) 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): def test_registration(self):
""" """
@ -152,6 +179,14 @@ class TestRegistration(TestCase):
""" """
Update the user information, for each type of user. 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")), for user, data in [(self.user, dict(role="Bot")),
(self.student, dict(student_class=11, school="Sky")), (self.student, dict(student_class=11, school="Sky")),
(self.coach, dict(professional_activity="God"))]: (self.coach, dict(professional_activity="God"))]:
@ -215,9 +250,77 @@ class TestRegistration(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertEqual(response["content-type"], "application/zip") 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() self.student.registration.photo_authorization.delete()
def test_string_render(self): def test_user_detail_forbidden(self):
# TODO These string field tests will be removed when used in a template """
self.assertRaises(NotImplementedError, lambda: Registration().type) Create a new user and ensure that it can't see the detail of another user.
self.assertRaises(NotImplementedError, lambda: Registration().form_class) """
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"])

View File

@ -256,7 +256,6 @@ class UserImpersonateView(LoginRequiredMixin, RedirectView):
session = request.session session = request.session
session["admin"] = request.user.pk session["admin"] = request.user.pk
session["_fake_user_id"] = kwargs["pk"] session["_fake_user_id"] = kwargs["pk"]
return redirect(request.path)
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
@ -274,4 +273,4 @@ class ResetAdminView(LoginRequiredMixin, View):
return self.handle_no_permission() return self.handle_no_permission()
if "_fake_user_id" in request.session: if "_fake_user_id" in request.session:
del request.session["_fake_user_id"] del request.session["_fake_user_id"]
return redirect(request.GET.get("path", "/")) return redirect(request.GET.get("path", reverse_lazy("index")))

View File

@ -6,7 +6,7 @@ _client = None
def get_sympa_client(): def get_sympa_client():
global _client global _client
if _client is None: 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 from sympasoap import Client
_client = Client("https://" + os.getenv("SYMPA_URL")) _client = Client("https://" + os.getenv("SYMPA_URL"))
_client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) _client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD"))

View File

@ -1,8 +1,7 @@
from enum import Enum
import os import os
from typing import Tuple
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from nio import *
class Matrix: class Matrix:
@ -14,11 +13,11 @@ class Matrix:
Tasks are normally asynchronous, but for compatibility we make Tasks are normally asynchronous, but for compatibility we make
them synchronous. them synchronous.
""" """
_token: str = None _token = None
_device_id: str = None _device_id = None
@classmethod @classmethod
async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: async def _get_client(cls): # pragma: no cover
""" """
Retrieve the bot account. Retrieve the bot account.
If not logged, log in and store access token. If not logged, log in and store access token.
@ -26,6 +25,7 @@ class Matrix:
if not os.getenv("SYNAPSE_PASSWORD"): if not os.getenv("SYNAPSE_PASSWORD"):
return FakeMatrixClient() return FakeMatrixClient()
from nio import AsyncClient
client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr") client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr")
client.user_id = "@corres2mathbot:correspondances-maths.fr" client.user_id = "@corres2mathbot:correspondances-maths.fr"
@ -49,7 +49,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @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. Set the display name of the bot account.
""" """
@ -58,7 +58,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @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. Set the display avatar of the bot account.
""" """
@ -69,13 +69,13 @@ class Matrix:
@async_to_sync @async_to_sync
async def upload( async def upload(
cls, cls,
data_provider: DataProvider, data_provider,
content_type: str = "application/octet-stream", content_type: str = "application/octet-stream",
filename: Optional[str] = None, filename: str = None,
encrypt: bool = False, encrypt: bool = False,
monitor: Optional[TransferMonitor] = None, monitor=None,
filesize: Optional[int] = None, filesize: int = None,
) -> Tuple[Union[UploadResponse, UploadError], Optional[Dict[str, Any]]]: ): # pragma: no cover
""" """
Upload a file to the content repository. Upload a file to the content repository.
@ -129,24 +129,25 @@ class Matrix:
If left as ``None``, some servers might refuse the upload. If left as ``None``, some servers might refuse the upload.
""" """
client = await cls._get_client() 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 @classmethod
@async_to_sync @async_to_sync
async def create_room( async def create_room(
cls, cls,
visibility: RoomVisibility = RoomVisibility.private, visibility=None,
alias: Optional[str] = None, alias=None,
name: Optional[str] = None, name=None,
topic: Optional[str] = None, topic=None,
room_version: Optional[str] = None, room_version=None,
federate: bool = True, federate=True,
is_direct: bool = False, is_direct=False,
preset: Optional[RoomPreset] = None, preset=None,
invite=(), invite=(),
initial_state=(), initial_state=(),
power_level_override: Optional[Dict[str, Any]] = None, power_level_override=None,
) -> Union[RoomCreateResponse, RoomCreateError]: ):
""" """
Create a new room. Create a new room.
@ -208,20 +209,18 @@ class Matrix:
power_level_override) power_level_override)
@classmethod @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. Resolve a room alias to a room ID.
Return None if the alias does not exist. Return None if the alias does not exist.
""" """
client = await cls._get_client() client = await cls._get_client()
resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias) resp = await client.room_resolve_alias(room_alias)
if isinstance(resp, RoomResolveAliasResponse): return resp.room_id if resp else None
return resp.room_id
return None
@classmethod @classmethod
@async_to_sync @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. Invite a user to a room.
@ -263,13 +262,13 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @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. Kick a user from a room, or withdraw their invitation.
Kicking a user adjusts their membership to "leave" with an optional Kicking a user adjusts their membership to "leave" with an optional
reason. reason.
²
Returns either a `RoomKickResponse` if the request was successful or Returns either a `RoomKickResponse` if the request was successful or
a `RoomKickError` if there was an error with the request. a `RoomKickError` if there was an error with the request.
@ -286,8 +285,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\ async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
-> Union[RoomPutStateResponse, RoomPutStateError]:
""" """
Put a given power level to a user in a certain room. 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. power_level (int): The target power level to give.
""" """
client = await cls._get_client() client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"): if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id) room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels") resp = await client.room_get_state_event(room_id, "m.room.power_levels")
@ -311,8 +312,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\ async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
-> Union[RoomPutStateResponse, RoomPutStateError]:
""" """
Define the minimal power level to have to send a certain event type Define the minimal power level to have to send a certain event type
in a given room. in a given room.
@ -328,6 +328,9 @@ class Matrix:
power_level (int): The target power level to give. power_level (int): The target power level to give.
""" """
client = await cls._get_client() client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"): if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id) room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels") resp = await client.room_get_state_event(room_id, "m.room.power_levels")
@ -340,8 +343,7 @@ class Matrix:
@classmethod @classmethod
@async_to_sync @async_to_sync
async def set_room_avatar(cls, room_id: str, avatar_uri: str)\ async def set_room_avatar(cls, room_id: str, avatar_uri: str):
-> Union[RoomPutStateResponse, RoomPutStateError]:
""" """
Define the avatar of a room. Define the avatar of a room.
@ -361,6 +363,22 @@ class Matrix:
}, state_key="") }, 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: class FakeMatrixClient:
""" """
Simulate a Matrix client to run tests, if no Matrix homeserver is connected. Simulate a Matrix client to run tests, if no Matrix homeserver is connected.
@ -370,4 +388,3 @@ class FakeMatrixClient:
async def func(*_, **_2): async def func(*_, **_2):
return None return None
return func return func

View File

@ -1,9 +1,7 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User
from threading import local 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') USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') 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) 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: def get_current_ip() -> str:
return getattr(_thread_locals, IP_ATTR_NAME, None) return getattr(_thread_locals, IP_ATTR_NAME, None)
def get_current_authenticated_user(): def get_current_authenticated_user():
current_user = get_current_user() current_user = get_current_user()
if isinstance(current_user, AnonymousUser): return None if isinstance(current_user, AnonymousUser) else current_user
return None
return current_user
class SessionMiddleware(object): class SessionMiddleware(object):
@ -50,10 +42,7 @@ class SessionMiddleware(object):
request.user = User.objects.get(pk=request.session["_fake_user_id"]) request.user = User.objects.get(pk=request.session["_fake_user_id"])
user = request.user user = request.user
if 'HTTP_X_REAL_IP' in request.META: ip = request.META.get('HTTP_X_REAL_IP' if 'HTTP_X_REAL_IP' in request.META else 'REMOTE_ADDR')
ip = request.META.get('HTTP_X_REAL_IP')
else:
ip = request.META.get('REMOTE_ADDR')
_set_current_user_and_ip(user, request.session, ip) _set_current_user_and_ip(user, request.session, ip)
response = self.get_response(request) response = self.get_response(request)
@ -62,7 +51,7 @@ class SessionMiddleware(object):
return response return response
class TurbolinksMiddleware(object): class TurbolinksMiddleware(object): # pragma: no cover
""" """
Send the `Turbolinks-Location` header in response to a visit that was redirected, Send the `Turbolinks-Location` header in response to a visit that was redirected,
and Turbolinks will replace the browser's topmost history entry. and Turbolinks will replace the browser's topmost history entry.

View File

@ -52,15 +52,12 @@ INSTALLED_APPS = [
'bootstrap_datepicker_plus', 'bootstrap_datepicker_plus',
'crispy_forms', 'crispy_forms',
'django_extensions',
'django_tables2', 'django_tables2',
'haystack', 'haystack',
'logs', 'logs',
'mailer',
'polymorphic', 'polymorphic',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'cas_server',
'api', 'api',
'eastereggs', 'eastereggs',
@ -68,6 +65,13 @@ INSTALLED_APPS = [
'participation', 'participation',
] ]
if "test" not in sys.argv: # pragma: no cover
INSTALLED_APPS += [
'cas_server',
'django_extensions',
'mailer',
]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@ -89,8 +93,7 @@ LOGIN_REDIRECT_URL = "index"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', '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, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
@ -196,7 +199,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
_db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower() _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 = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2', '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": if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": # pragma: no cover
from .settings_prod import * from .settings_prod import * # noqa: F401,F403
else: else:
from .settings_dev import * from .settings_dev import * # noqa: F401,F403

View File

@ -6,7 +6,6 @@
<h2>{% trans "Search" %}</h2> <h2>{% trans "Search" %}</h2>
<form> <form>
{% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<button type="submit" class="btn btn-primary">{% trans "Search" %}</button> <button type="submit" class="btn btn-primary">{% trans "Search" %}</button>
</form> </form>

24
corres2math/tests.py Normal file
View 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)

View File

@ -13,9 +13,10 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import include, path
from django.views.defaults import bad_request, permission_denied, page_not_found, server_error from django.views.defaults import bad_request, page_not_found, permission_denied, server_error
from django.views.generic import TemplateView from django.views.generic import TemplateView
from registration.views import PhotoAuthorizationView from registration.views import PhotoAuthorizationView
@ -35,11 +36,14 @@ urlpatterns = [
path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), name='photo_authorization'), 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')), 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 handler400 = bad_request
handler403 = permission_denied handler403 = permission_denied
handler404 = page_not_found handler404 = page_not_found

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Corres2math\n" "Project-Id-Version: Corres2math\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n" "Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n"
"Language-Team: LANGUAGE <LL@li.org>\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." msgstr "Cette tâche a échoué avec succès."
#: apps/eastereggs/templates/eastereggs/xp_modal.html:16 #: apps/eastereggs/templates/eastereggs/xp_modal.html:16
#: templates/base_modal.html:19 #: corres2math/templates/base_modal.html:19
msgid "Close" msgid "Close"
msgstr "Fermer" msgstr "Fermer"
@ -99,6 +99,16 @@ msgstr "changelogs"
msgid "Changelog of type \"{action}\" for model {model} at {timestamp}" msgid "Changelog of type \"{action}\" for model {model} at {timestamp}"
msgstr "Changelog de type \"{action}\" pour le modèle {model} le {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 #: apps/participation/forms.py:20 apps/participation/models.py:33
msgid "The trigram must be composed of three uppercase letters." msgid "The trigram must be composed of three uppercase letters."
msgstr "Le trigramme doit être composé de trois lettres majuscules." 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" msgid "Send to team"
msgstr "Envoyer à l'équipe" msgstr "Envoyer à l'équipe"
#: apps/participation/forms.py:152 #: apps/participation/forms.py:156
msgid "How did you get the idea to ...?" msgid "How did you get the idea to ...?"
msgstr "Comment avez-vous eu l'idée de ... ?" 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." msgid "Start date must be before the end date."
msgstr "La date de début doit être avant la date de fin." 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." msgid "This phase must start after the previous phases."
msgstr "Cette phase doit commencer après les phases précédentes." 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." msgid "This phase must end after the next phases."
msgstr "Cette phase doit finir avant les phases suivantes." msgstr "Cette phase doit finir avant les phases suivantes."
@ -187,15 +203,6 @@ msgstr "équipes"
msgid "Problem #{problem:d}" msgid "Problem #{problem:d}"
msgstr "Problème n°{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 #: apps/participation/models.py:129 apps/participation/models.py:183
msgid "The video got the validation of the administrators." msgid "The video got the validation of the administrators."
msgstr "La vidéo a été validée par les administrateurs." msgstr "La vidéo a été validée par les administrateurs."
@ -283,12 +290,12 @@ msgid "phases"
msgstr "phases" msgstr "phases"
#: apps/participation/templates/participation/create_team.html:11 #: apps/participation/templates/participation/create_team.html:11
#: templates/base.html:231 #: corres2math/templates/base.html:231
msgid "Create" msgid "Create"
msgstr "Créer" msgstr "Créer"
#: apps/participation/templates/participation/join_team.html:11 #: apps/participation/templates/participation/join_team.html:11
#: templates/base.html:227 #: corres2math/templates/base.html:227
msgid "Join" msgid "Join"
msgstr "Rejoindre" 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:181
#: apps/participation/templates/participation/participation_detail.html:233 #: apps/participation/templates/participation/participation_detail.html:233
#: apps/participation/views.py:463 #: apps/participation/views.py:482
msgid "Upload video" msgid "Upload video"
msgstr "Envoyer la vidéo" msgstr "Envoyer la vidéo"
@ -497,7 +504,7 @@ msgid "Update question"
msgstr "Modifier la question" msgstr "Modifier la question"
#: apps/participation/templates/participation/participation_detail.html:217 #: apps/participation/templates/participation/participation_detail.html:217
#: apps/participation/views.py:440 #: apps/participation/views.py:459
msgid "Delete question" msgid "Delete question"
msgstr "Supprimer la question" msgstr "Supprimer la question"
@ -507,8 +514,8 @@ msgid "Display synthesis"
msgstr "Afficher la synthèse" msgstr "Afficher la synthèse"
#: apps/participation/templates/participation/phase_list.html:10 #: apps/participation/templates/participation/phase_list.html:10
#: apps/participation/views.py:482 templates/base.html:68 #: apps/participation/views.py:501 corres2math/templates/base.html:68
#: templates/base.html:70 templates/base.html:217 #: corres2math/templates/base.html:70 corres2math/templates/base.html:217
msgid "Calendar" msgid "Calendar"
msgstr "Calendrier" msgstr "Calendrier"
@ -620,7 +627,7 @@ msgid "Update team"
msgstr "Modifier l'équipe" msgstr "Modifier l'équipe"
#: apps/participation/templates/participation/team_detail.html:127 #: apps/participation/templates/participation/team_detail.html:127
#: apps/participation/views.py:296 #: apps/participation/views.py:314
msgid "Leave team" msgid "Leave team"
msgstr "Quitter l'équipe" msgstr "Quitter l'équipe"
@ -628,8 +635,8 @@ msgstr "Quitter l'équipe"
msgid "Are you sure that you want to leave this team?" msgid "Are you sure that you want to leave this team?"
msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?" msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?"
#: apps/participation/views.py:36 templates/base.html:77 #: apps/participation/views.py:36 corres2math/templates/base.html:77
#: templates/base.html:230 #: corres2math/templates/base.html:230
msgid "Create team" msgid "Create team"
msgstr "Créer une équipe" 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." msgid "You are already in a team."
msgstr "Vous êtes déjà dans une équipe." msgstr "Vous êtes déjà dans une équipe."
#: apps/participation/views.py:82 templates/base.html:82 #: apps/participation/views.py:82 corres2math/templates/base.html:82
#: templates/base.html:226 #: corres2math/templates/base.html:226
msgid "Join team" msgid "Join team"
msgstr "Rejoindre une équipe" msgstr "Rejoindre une équipe"
#: apps/participation/views.py:133 apps/participation/views.py:302 #: apps/participation/views.py:133 apps/participation/views.py:320
#: apps/participation/views.py:335 #: apps/participation/views.py:353
msgid "You are not in a team." msgid "You are not in a team."
msgstr "Vous n'êtes pas dans une équipe." 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." msgid "You don't participate, so you don't have any team."
msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." 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 #, python-brace-format
msgid "Detail of team {trigram}" msgid "Detail of team {trigram}"
msgstr "Détails de l'équipe {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." msgid "You don't participate, so you can't request the validation of the team."
msgstr "" msgstr ""
"Vous ne participez pas, vous ne pouvez pas demander la validation de " "Vous ne participez pas, vous ne pouvez pas demander la validation de "
"l'équipe." "l'équipe."
#: apps/participation/views.py:183 #: apps/participation/views.py:191
msgid "The validation of the team is already done or pending." msgid "The validation of the team is already done or pending."
msgstr "La validation de l'équipe est déjà faite ou en cours." 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." msgid "You are not an administrator."
msgstr "Vous n'êtes pas administrateur." msgstr "Vous n'êtes pas administrateur."
#: apps/participation/views.py:199 #: apps/participation/views.py:216
msgid "This team has no pending validation." msgid "This team has no pending validation."
msgstr "L'équipe n'a pas de validation en attente." 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." msgid "You must specify if you validate the registration or not."
msgstr "Vous devez spécifier si vous validez l'inscription ou non." 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 #, python-brace-format
msgid "Update team {trigram}" msgid "Update team {trigram}"
msgstr "Mise à jour de l'équipe {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 #, python-brace-format
msgid "Photo authorization of {student}.{ext}" msgid "Photo authorization of {student}.{ext}"
msgstr "Autorisation de droit à l'image de {student}.{ext}" msgstr "Autorisation de droit à l'image de {student}.{ext}"
#: apps/participation/views.py:286 #: apps/participation/views.py:304
#, python-brace-format #, python-brace-format
msgid "Photo authorizations of team {trigram}.zip" msgid "Photo authorizations of team {trigram}.zip"
msgstr "Autorisations de droit à l'image de l'équipe {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." msgid "The team is already validated or the validation is pending."
msgstr "La validation de l'équipe est déjà faite ou en cours." 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." msgid "The team is not validated yet."
msgstr "L'équipe n'est pas encore validée." msgstr "L'équipe n'est pas encore validée."
#: apps/participation/views.py:357 #: apps/participation/views.py:376
#, python-brace-format #, python-brace-format
msgid "Participation of team {trigram}" msgid "Participation of team {trigram}"
msgstr "Participation de l'équipe {trigram}" msgstr "Participation de l'équipe {trigram}"
#: apps/participation/views.py:394 #: apps/participation/views.py:413
msgid "Create question" msgid "Create question"
msgstr "Créer une question" msgstr "Créer une question"
#: apps/participation/views.py:491 #: apps/participation/views.py:510
msgid "Calendar update" msgid "Calendar update"
msgstr "Mise à jour du calendrier" 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." msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter."
#: apps/registration/templates/registration/password_reset_complete.html:10 #: apps/registration/templates/registration/password_reset_complete.html:10
#: templates/base.html:130 templates/base.html:221 templates/base.html:222 #: corres2math/templates/base.html:130 corres2math/templates/base.html:221
#: templates/registration/login.html:7 templates/registration/login.html:8 #: corres2math/templates/base.html:222
#: templates/registration/login.html:25 #: corres2math/templates/registration/login.html:7
#: corres2math/templates/registration/login.html:8
#: corres2math/templates/registration/login.html:25
msgid "Log in" msgid "Log in"
msgstr "Connexion" msgstr "Connexion"
@ -1082,11 +1100,11 @@ msgstr "Anglais"
msgid "French" msgid "French"
msgstr "Français" msgstr "Français"
#: templates/400.html:6 #: corres2math/templates/400.html:6
msgid "Bad request" msgid "Bad request"
msgstr "Requête invalide" msgstr "Requête invalide"
#: templates/400.html:7 #: corres2math/templates/400.html:7
msgid "" msgid ""
"Sorry, your request was bad. Don't know what could be wrong. An email has " "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 " "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 " "email a été envoyé aux administrateurs avec les détails de l'erreur. Vous "
"pouvez désormais retourner voir des vidéos." "pouvez désormais retourner voir des vidéos."
#: templates/403.html:6 #: corres2math/templates/403.html:6
msgid "Permission denied" msgid "Permission denied"
msgstr "Permission refusée" msgstr "Permission refusée"
#: templates/403.html:7 #: corres2math/templates/403.html:7
msgid "You don't have the right to perform this request." msgid "You don't have the right to perform this request."
msgstr "Vous n'avez pas le droit d'effectuer cette requête." 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:" msgid "Exception message:"
msgstr "Message d'erreur :" msgstr "Message d'erreur :"
#: templates/404.html:6 #: corres2math/templates/404.html:6
msgid "Page not found" msgid "Page not found"
msgstr "Page non trouvée" msgstr "Page non trouvée"
#: templates/404.html:7 #: corres2math/templates/404.html:7
#, python-format #, python-format
msgid "" msgid ""
"The requested path <code>%(request_path)s</code> was not found on the server." "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 " "Le chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le "
"serveur." "serveur."
#: templates/500.html:6 #: corres2math/templates/500.html:6
msgid "Server error" msgid "Server error"
msgstr "Erreur du serveur" msgstr "Erreur du serveur"
#: templates/500.html:7 #: corres2math/templates/500.html:7
msgid "" msgid ""
"Sorry, an error occurred when processing your request. An email has been " "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 " "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 " "avec les détails de l'erreur. Vous pouvez désormais retourner voir des "
"vidéos." "vidéos."
#: templates/base.html:64 #: corres2math/templates/base.html:64
msgid "Home" msgid "Home"
msgstr "Accueil" msgstr "Accueil"
#: templates/base.html:88 #: corres2math/templates/base.html:88
msgid "My team" msgid "My team"
msgstr "Mon équipe" msgstr "Mon équipe"
#: templates/base.html:93 #: corres2math/templates/base.html:93
msgid "My participation" msgid "My participation"
msgstr "Ma participation" msgstr "Ma participation"
#: templates/base.html:100 #: corres2math/templates/base.html:100
msgid "Chat" msgid "Chat"
msgstr "Chat" msgstr "Chat"
#: templates/base.html:104 #: corres2math/templates/base.html:104
msgid "Administration" msgid "Administration"
msgstr "Administration" msgstr "Administration"
#: templates/base.html:112 #: corres2math/templates/base.html:112
msgid "Search..." msgid "Search..."
msgstr "Chercher ..." msgstr "Chercher ..."
#: templates/base.html:121 #: corres2math/templates/base.html:121
msgid "Return to admin view" msgid "Return to admin view"
msgstr "Retourner à l'interface administrateur" msgstr "Retourner à l'interface administrateur"
#: templates/base.html:126 #: corres2math/templates/base.html:126
msgid "Register" msgid "Register"
msgstr "S'inscrire" msgstr "S'inscrire"
#: templates/base.html:142 #: corres2math/templates/base.html:142
msgid "My account" msgid "My account"
msgstr "Mon compte" msgstr "Mon compte"
#: templates/base.html:145 #: corres2math/templates/base.html:145
msgid "Log out" msgid "Log out"
msgstr "Déconnexion" msgstr "Déconnexion"
#: templates/base.html:162 #: corres2math/templates/base.html:162
#, python-format #, python-format
msgid "" msgid ""
"Your email address is not validated. Please click on the link you received " "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=" "avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a href="
"\"%(send_email_url)s\">ce lien</a>." "\"%(send_email_url)s\">ce lien</a>."
#: templates/base.html:186 #: corres2math/templates/base.html:186
msgid "Contact us" msgid "Contact us"
msgstr "Nous contacter" msgstr "Nous contacter"
#: templates/base.html:219 #: corres2math/templates/base.html:219
msgid "Search results" msgid "Search results"
msgstr "Résultats de la recherche" 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." msgid "Thanks for spending some quality time with the Web site today."
msgstr "Merci d'avoir utilisé la plateforme des Correspondances." 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" msgid "Log in again"
msgstr "Se reconnecter" msgstr "Se reconnecter"
#: templates/registration/login.html:13 #: corres2math/templates/registration/login.html:13
#, python-format #, python-format
msgid "" msgid ""
"You are authenticated as %(user)s, but are not authorized to access this " "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 à " "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 ?" "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?" msgid "Forgotten your password or username?"
msgstr "Mot de passe oublié ?" 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" msgid "Search"
msgstr "Chercher" msgstr "Chercher"
#: templates/search/search.html:16 #: corres2math/templates/search/search.html:15
msgid "Results" msgid "Results"
msgstr "Résultats" msgstr "Résultats"
#: templates/search/search.html:26 #: corres2math/templates/search/search.html:25
msgid "No results found." msgid "No results found."
msgstr "Aucun résultat." msgstr "Aucun résultat."

View File

@ -1,17 +1,19 @@
Django~=3.0 Django~=3.1
django-bootstrap-datepicker-plus django-bootstrap-datepicker-plus~=3.0
django-cas-server django-cas-server~=1.2
django-crispy-forms django-crispy-forms~=1.9
django-extensions django-extensions~=3.0
django-filter~=2.3.0 django-filter~=2.3
django-haystack~=3.0 django-haystack~=3.0
django-mailer django-mailer~=2.0
django-polymorphic django-polymorphic~=3.0
django-tables2 django-tables2~=2.3
djangorestframework~=3.11.1 djangorestframework~=3.12
django-rest-polymorphic django-rest-polymorphic~=0.1
matrix-nio gunicorn~=20.0
ptpython matrix-nio~=0.15
python-magic~=0.4.18 psycopg2-binary~=2.8
gunicorn ptpython~=3.0
whoosh python-magic~=0.4
sympasoap~=1.0
whoosh~=2.7

18
tox.ini
View File

@ -7,12 +7,22 @@ envlist =
skipsdist = True skipsdist = True
[testenv] [testenv]
sitepackages = True sitepackages = False
deps = deps =
-r{toxinidir}/requirements.txt
coverage 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 = 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 coverage report -m
[testenv:linters] [testenv:linters]
@ -25,7 +35,7 @@ deps =
pep8-naming pep8-naming
pyflakes pyflakes
commands = commands =
flake8 apps/ flake8 apps/ corres2math/
[flake8] [flake8]
exclude = exclude =