From 5fc46e74d227c62add716ca34194eb7a9e7f6c16 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 11:44:53 +0100 Subject: [PATCH 01/27] Ensure that a user can't see what he can't see --- apps/registration/tests.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 18e5ff4..c364802 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,3 +1,5 @@ +import os + from corres2math.tokens import email_validation_token from django.contrib.auth.models import User from django.test import TestCase @@ -215,8 +217,44 @@ class TestRegistration(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/zip") + # Do it twice, ensure that the previous authorization got deleted + old_authoratization = self.student.registration.photo_authorization.path + response = self.client.post(reverse("registration:upload_user_photo_authorization", + args=(self.student.registration.pk,)), data=dict( + photo_authorization=open("corres2math/static/Autorisation de droit à l'image - majeur.pdf", "rb"), + )) + self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200) + self.assertFalse(os.path.isfile(old_authoratization)) + + self.student.registration.refresh_from_db() self.student.registration.photo_authorization.delete() + def test_user_detail_forbidden(self): + """ + Create a new user and ensure that it can't see the detail of another user. + """ + self.client.force_login(self.coach) + + response = self.client.get(reverse("registration:user_detail", args=(self.user.pk,))) + self.assertEqual(response.status_code, 403) + + response = self.client.get(reverse("registration:update_user", args=(self.user.pk,))) + self.assertEqual(response.status_code, 403) + + response = self.client.get(reverse("registration:upload_user_photo_authorization", args=(self.user.pk,))) + self.assertEqual(response.status_code, 403) + + response = self.client.get(reverse("photo_authorization", args=("inexisting-authorization",))) + self.assertEqual(response.status_code, 404) + + with open("media/authorization/photo/example", "w") as f: + f.write("I lost the game.") + self.student.registration.photo_authorization = "authorization/photo/example" + self.student.registration.save() + response = self.client.get(reverse("photo_authorization", args=("example",))) + self.assertEqual(response.status_code, 403) + os.remove("media/authorization/photo/example") + def test_string_render(self): # TODO These string field tests will be removed when used in a template self.assertRaises(NotImplementedError, lambda: Registration().type) From 62b883467cb52cf0aa597469a071a2e9a6767104 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 12:08:01 +0100 Subject: [PATCH 02/27] Test user impersonification --- apps/registration/tests.py | 24 ++++++++++++++++++++++++ apps/registration/views.py | 3 +-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index c364802..ebf70dc 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -18,6 +18,13 @@ class TestIndexPage(TestCase): response = self.client.get(reverse("index")) self.assertEqual(response.status_code, 200) + def test_not_authenticated(self): + """ + Try to load some pages without being authenticated. + """ + response = self.client.get(reverse("registration:reset_admin")) + self.assertRedirects(response, reverse("login") + "?next=" + reverse("registration:reset_admin"), 302, 200) + class TestRegistration(TestCase): def setUp(self) -> None: @@ -255,6 +262,23 @@ class TestRegistration(TestCase): 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_string_render(self): # TODO These string field tests will be removed when used in a template self.assertRaises(NotImplementedError, lambda: Registration().type) diff --git a/apps/registration/views.py b/apps/registration/views.py index 9195b86..91d13b6 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -256,7 +256,6 @@ class UserImpersonateView(LoginRequiredMixin, RedirectView): session = request.session session["admin"] = request.user.pk session["_fake_user_id"] = kwargs["pk"] - return redirect(request.path) return super().dispatch(request, *args, **kwargs) def get_redirect_url(self, *args, **kwargs): @@ -274,4 +273,4 @@ class ResetAdminView(LoginRequiredMixin, View): return self.handle_no_permission() if "_fake_user_id" in request.session: del request.session["_fake_user_id"] - return redirect(request.GET.get("path", "/")) + return redirect(request.GET.get("path", reverse_lazy("index"))) From c80355f2bc004a0ce3db058993c7261fc5bd70d4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 12:34:27 +0100 Subject: [PATCH 03/27] Test registration search --- apps/registration/tests.py | 22 ++++++++++++++++++++++ corres2math/templates/search/search.html | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index ebf70dc..dcef4a8 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,5 +1,7 @@ import os +from django.core.management import call_command + from corres2math.tokens import email_validation_token from django.contrib.auth.models import User from django.test import TestCase @@ -279,6 +281,26 @@ class TestRegistration(TestCase): 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"]) + def test_string_render(self): # TODO These string field tests will be removed when used in a template self.assertRaises(NotImplementedError, lambda: Registration().type) diff --git a/corres2math/templates/search/search.html b/corres2math/templates/search/search.html index e510d01..d3f41f1 100644 --- a/corres2math/templates/search/search.html +++ b/corres2math/templates/search/search.html @@ -6,7 +6,6 @@

{% trans "Search" %}

- {% csrf_token %} {{ form|crispy }}
From be17d1158190cfeec011395826bfecb8cdcbc149 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 15:46:31 +0100 Subject: [PATCH 04/27] Fix auto-invitation in Matrix public rooms --- apps/registration/apps.py | 3 +++ apps/registration/signals.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/registration/apps.py b/apps/registration/apps.py index 4c681a5..a6d9ede 100644 --- a/apps/registration/apps.py +++ b/apps/registration/apps.py @@ -15,3 +15,6 @@ class RegistrationConfig(AppConfig): pre_save.connect(send_email_link, "auth.User") post_save.connect(create_admin_registration, "auth.User") post_save.connect(invite_to_public_rooms, "registration.Registration") + post_save.connect(invite_to_public_rooms, "registration.StudentRegistration") + post_save.connect(invite_to_public_rooms, "registration.CoachRegistration") + post_save.connect(invite_to_public_rooms, "registration.AdminRegistration") diff --git a/apps/registration/signals.py b/apps/registration/signals.py index ff89f65..71b761e 100644 --- a/apps/registration/signals.py +++ b/apps/registration/signals.py @@ -41,11 +41,11 @@ def create_admin_registration(instance, **_): AdminRegistration.objects.get_or_create(user=instance) -def invite_to_public_rooms(instance: Registration, **_): +def invite_to_public_rooms(instance: Registration, created: bool, **_): """ When a user got registered, automatically invite the Matrix user into public rooms. """ - if not instance.pk: + if not created: Matrix.invite("#annonces:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr") Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr") Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr", From dee215261698ed0b1327f55269cc94ce257b3782 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 16:00:08 +0100 Subject: [PATCH 05/27] Test get absolute urls --- apps/registration/tests.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index dcef4a8..c25a09b 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,15 +1,17 @@ import os -from django.core.management import call_command +from django.contrib.sites.models import Site from corres2math.tokens import email_validation_token +from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User +from django.core.management import call_command from django.test import TestCase from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -from .models import CoachRegistration, Registration, StudentRegistration +from .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration class TestIndexPage(TestCase): @@ -52,14 +54,29 @@ class TestRegistration(TestCase): response = self.client.get(reverse("admin:index") + f"registration/registration/{self.user.registration.pk}/change/") self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + + f"r/{ContentType.objects.get_for_model(AdminRegistration).id}/" + f"{self.user.registration.pk}/") + self.assertRedirects(response, "http://" + Site.objects.get().domain + + str(self.user.registration.get_absolute_url()), 302, 200) response = self.client.get(reverse("admin:index") + f"registration/registration/{self.student.registration.pk}/change/") self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + + f"r/{ContentType.objects.get_for_model(StudentRegistration).id}/" + f"{self.student.registration.pk}/") + self.assertRedirects(response, "http://" + Site.objects.get().domain + + str(self.student.registration.get_absolute_url()), 302, 200) response = self.client.get(reverse("admin:index") + f"registration/registration/{self.coach.registration.pk}/change/") self.assertEqual(response.status_code, 200) + response = self.client.get(reverse("admin:index") + + f"r/{ContentType.objects.get_for_model(CoachRegistration).id}/" + f"{self.coach.registration.pk}/") + self.assertRedirects(response, "http://" + Site.objects.get().domain + + str(self.coach.registration.get_absolute_url()), 302, 200) def test_registration(self): """ From 4043d048268832e427b1259331522782d23f96d8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 16:17:07 +0100 Subject: [PATCH 06/27] Add admin pages for Participation app --- apps/participation/admin.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/apps/participation/admin.py b/apps/participation/admin.py index e69de29..e4b3953 100644 --- a/apps/participation/admin.py +++ b/apps/participation/admin.py @@ -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',) From 682ef05110720e3634d4c4a046d31e98dda850f9 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 16:19:39 +0100 Subject: [PATCH 07/27] Rename a test --- apps/registration/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index c25a09b..d239349 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -318,7 +318,7 @@ class TestRegistration(TestCase): self.assertEqual(response.status_code, 200) self.assertTrue(response.context["object_list"]) - def test_string_render(self): - # TODO These string field tests will be removed when used in a template + def test_not_implemented_error(self): + # Only for coverage self.assertRaises(NotImplementedError, lambda: Registration().type) self.assertRaises(NotImplementedError, lambda: Registration().form_class) From 7ae2b152c62ea180d4668b5aaf26a11914a2c963 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 16:57:38 +0100 Subject: [PATCH 08/27] Test participation admin pages --- apps/participation/tests.py | 72 ++++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index abd8dd7..df4a7d9 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -1,9 +1,11 @@ from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site from django.test import TestCase from django.urls import reverse from registration.models import StudentRegistration -from .models import Team +from .models import Participation, Question, Team, Video class TestStudentParticipation(TestCase): @@ -27,11 +29,72 @@ class TestStudentParticipation(TestCase): access_code="azerty", grant_animath_access_videos=True, ) + self.question = Question.objects.create(participation=self.team.participation, + question="Pourquoi l'existence précède l'essence ?") self.client.force_login(self.user) - # TODO Remove these lines - str(self.team) - str(self.team.participation) + def test_admin_pages(self): + """ + Load Django-admin pages. + """ + superuser = User.objects.create_superuser( + username="admin", + email="admin@example.com", + password="toto1234", + ) + self.client.force_login(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") + + f"participation/phase/1/change/") + self.assertEqual(response.status_code, 200) def test_create_team(self): """ @@ -62,6 +125,7 @@ class TestStudentParticipation(TestCase): trigram="TET", grant_animath_access_videos=False, )) + self.assertEqual(response.status_code, 403) def test_join_team(self): """ From 4c25ae29283c67c58feccc7f70ac15f6c704e23f Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 18:09:24 +0100 Subject: [PATCH 09/27] Test team validation --- apps/participation/tests.py | 198 ++++++++++++++++++++++++++++++++++-- apps/participation/views.py | 14 ++- 2 files changed, 202 insertions(+), 10 deletions(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index df4a7d9..1836e2a 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -5,11 +5,17 @@ from django.test import TestCase from django.urls import reverse from registration.models import StudentRegistration -from .models import Participation, Question, Team, Video +from .models import Participation, Question, Team class TestStudentParticipation(TestCase): def setUp(self) -> None: + self.superuser = User.objects.create_superuser( + username="admin", + email="admin@example.com", + password="toto1234", + ) + self.user = User.objects.create( first_name="Toto", last_name="Toto", @@ -33,16 +39,31 @@ class TestStudentParticipation(TestCase): question="Pourquoi l'existence précède l'essence ?") self.client.force_login(self.user) + self.second_user = User.objects.create( + first_name="Lalala", + last_name="Lalala", + email="lalala@example.com", + password="lalala", + ) + StudentRegistration.objects.create( + user=self.second_user, + student_class=11, + school="Moon", + give_contact_to_animath=True, + email_confirmed=True, + ) + self.second_team = Team.objects.create( + name="Poor team", + trigram="FFF", + access_code="qwerty", + grant_animath_access_videos=True, + ) + def test_admin_pages(self): """ Load Django-admin pages. """ - superuser = User.objects.create_superuser( - username="admin", - email="admin@example.com", - password="toto1234", - ) - self.client.force_login(superuser) + self.client.force_login(self.superuser) # Test team pages response = self.client.get(reverse("admin:index") + "participation/team/") @@ -173,6 +194,161 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) self.assertEqual(response.status_code, 200) + # Can't see other teams + self.second_user.registration.team = self.second_team + self.second_user.registration.save() + self.client.force_login(self.second_user) + response = self.client.get(reverse("participation:team_detail", args=(self.team.participation.pk,))) + self.assertEqual(response.status_code, 403) + + def test_request_validate_team(self): + """ + The team ask for validation. + """ + self.user.registration.team = self.team + self.user.registration.save() + + second_user = User.objects.create( + first_name="Blublu", + last_name="Blublu", + email="blublu@example.com", + password="blublu", + ) + StudentRegistration.objects.create( + user=second_user, + student_class=12, + school="Jupiter", + give_contact_to_animath=True, + email_confirmed=True, + team=self.team, + photo_authorization="authorization/photo/mai-linh", + ) + + third_user = User.objects.create( + first_name="Zupzup", + last_name="Zupzup", + email="zupzup@example.com", + password="zupzup", + ) + StudentRegistration.objects.create( + user=third_user, + student_class=10, + school="Sun", + give_contact_to_animath=False, + email_confirmed=True, + team=self.team, + photo_authorization="authorization/photo/yohann", + ) + + self.client.force_login(self.superuser) + # Admin users can't ask for validation + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="RequestValidationForm", + engagement=True, + )) + self.assertEqual(resp.status_code, 200) + + self.client.force_login(self.user) + + self.assertIsNone(self.team.participation.valid) + + resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) + self.assertEqual(resp.status_code, 200) + self.assertFalse(resp.context["can_validate"]) + # Can't validate + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="RequestValidationForm", + engagement=True, + )) + self.assertEqual(resp.status_code, 200) + + self.user.registration.photo_authorization = "authorization/photo/ananas" + self.user.registration.save() + + resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) + self.assertEqual(resp.status_code, 200) + self.assertFalse(resp.context["can_validate"]) + + self.team.participation.problem = 2 + self.team.participation.save() + + resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) + self.assertEqual(resp.status_code, 200) + self.assertTrue(resp.context["can_validate"]) + + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="RequestValidationForm", + engagement=True, + )) + self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200) + self.team.participation.refresh_from_db() + self.assertFalse(self.team.participation.valid) + self.assertIsNotNone(self.team.participation.valid) + + # Team already asked for validation + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="RequestValidationForm", + engagement=True, + )) + self.assertEqual(resp.status_code, 200) + + def test_validate_team(self): + """ + A team asked for validation. Try to validate it. + """ + self.team.participation.valid = False + self.team.participation.save() + + # No right to do that + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="ValidateParticipationForm", + message="J'ai 4 ans", + validate=True, + )) + self.assertEqual(resp.status_code, 200) + + self.client.force_login(self.superuser) + + resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,))) + self.assertEqual(resp.status_code, 200) + + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="ValidateParticipationForm", + message="Woops I didn't said anything", + )) + self.assertEqual(resp.status_code, 200) + + # Test invalidate team + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="ValidateParticipationForm", + message="Wsh nope", + invalidate=True, + )) + self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200) + self.team.participation.refresh_from_db() + self.assertIsNone(self.team.participation.valid) + + # Team did not ask validation + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="ValidateParticipationForm", + message="Bienvenue ça va être trop cool", + validate=True, + )) + self.assertEqual(resp.status_code, 200) + + self.team.participation.valid = False + self.team.participation.save() + + # Test validate team + resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict( + _form_type="ValidateParticipationForm", + message="Bienvenue ça va être trop cool", + validate=True, + )) + self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200) + self.team.participation.refresh_from_db() + self.assertTrue(self.team.participation.valid) + def test_update_team(self): """ Try to update team information. @@ -215,6 +391,7 @@ class TestStudentParticipation(TestCase): self.user.registration.team = self.team self.user.registration.save() + # Can't see the participation if it is not valid response = self.client.get(reverse("participation:my_participation_detail")) self.assertRedirects(response, reverse("participation:participation_detail", args=(self.team.participation.pk,)), @@ -230,6 +407,13 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) self.assertEqual(response.status_code, 200) + # Can't see other participations + self.second_user.registration.team = self.second_team + self.second_user.registration.save() + self.client.force_login(self.second_user) + response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) + self.assertEqual(response.status_code, 403) + def test_upload_video(self): """ Try to send a solution video link. diff --git a/apps/participation/views.py b/apps/participation/views.py index 95b0a75..064c25a 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -144,7 +144,8 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) user = request.user self.object = self.get_object() # Ensure that the user is an admin or a member of the team - if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"]: + if user.registration.is_admin or user.registration.participates and \ + user.registration.team and user.registration.team.pk == kwargs["pk"]: return super().get(request, *args, **kwargs) raise PermissionDenied @@ -171,7 +172,6 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) return RequestValidationForm elif self.request.POST["_form_type"] == "ValidateParticipationForm": return ValidateParticipationForm - return None def form_valid(self, form): self.object = self.get_object() @@ -182,6 +182,13 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) if self.object.participation.valid is not None: form.add_error(None, _("The validation of the team is already done or pending.")) return self.form_invalid(form) + if not (self.object.students.count() >= 3 and + all(r.email_confirmed for r in self.object.students.all()) and + all(r.photo_authorization for r in self.object.students.all()) and + self.object.participation.problem): + form.add_error(None, _("The team can't be validated: missing email address confirmations, " + "photo authorizations, people or the chosen problem is not set.")) + return self.form_invalid(form) self.object.participation.valid = False self.object.participation.save() @@ -218,7 +225,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) form.add_error(None, _("You must specify if you validate the registration or not.")) return self.form_invalid(form) - return super().form_invalid(form) + return super().form_valid(form) def get_success_url(self): return self.request.path @@ -347,6 +354,7 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView): if not self.get_object().valid: raise PermissionDenied(_("The team is not validated yet.")) if user.registration.is_admin or user.registration.participates \ + and user.registration.team.participation \ and user.registration.team.participation.pk == kwargs["pk"]: return super().dispatch(request, *args, **kwargs) raise PermissionDenied From 25756fb2ef293abb87df455fd69a231565d46c13 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 18:19:53 +0100 Subject: [PATCH 10/27] Test forbidden accesses --- apps/participation/tests.py | 30 ++++++++++++++++++++++++++++++ apps/participation/views.py | 4 +++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 1836e2a..9cdecfb 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -439,6 +439,36 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) self.assertEqual(response.status_code, 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) + class TestAdminForbidden(TestCase): def setUp(self) -> None: diff --git a/apps/participation/views.py b/apps/participation/views.py index 064c25a..3098a06 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -241,7 +241,9 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView): def dispatch(self, request, *args, **kwargs): user = request.user - if user.registration.is_admin or user.registration.participates and user.registration.team.pk == kwargs["pk"]: + if user.registration.is_admin or user.registration.participates and \ + user.registration.team and \ + user.registration.team.pk == kwargs["pk"]: return super().dispatch(request, *args, **kwargs) raise PermissionDenied From 41817421332f435deac926f35e95cd08746575a0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Mon, 2 Nov 2020 18:25:32 +0100 Subject: [PATCH 11/27] Linting --- apps/participation/tests.py | 3 +- apps/participation/views.py | 102 ++++++++++++++++++++---------------- apps/registration/tests.py | 5 +- 3 files changed, 59 insertions(+), 51 deletions(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 9cdecfb..f695b67 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -113,8 +113,7 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("admin:index") + "participation/phase/") self.assertEqual(response.status_code, 200) - response = self.client.get(reverse("admin:index") - + f"participation/phase/1/change/") + response = self.client.get(reverse("admin:index") + "participation/phase/1/change/") self.assertEqual(response.status_code, 200) def test_create_team(self): diff --git a/apps/participation/views.py b/apps/participation/views.py index 3098a06..ee43067 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -176,55 +176,65 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) def form_valid(self, form): self.object = self.get_object() if isinstance(form, RequestValidationForm): - if not self.request.user.registration.participates: - form.add_error(None, _("You don't participate, so you can't request the validation of the team.")) - return self.form_invalid(form) - if self.object.participation.valid is not None: - form.add_error(None, _("The validation of the team is already done or pending.")) - return self.form_invalid(form) - if not (self.object.students.count() >= 3 and - all(r.email_confirmed for r in self.object.students.all()) and - all(r.photo_authorization for r in self.object.students.all()) and - self.object.participation.problem): - form.add_error(None, _("The team can't be validated: missing email address confirmations, " - "photo authorizations, people or the chosen problem is not set.")) - return self.form_invalid(form) - - self.object.participation.valid = False - self.object.participation.save() - - for admin in AdminRegistration.objects.all(): - mail_context = dict(user=admin.user, team=self.object) - mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) - mail_html = render_to_string("participation/mails/request_validation.html", mail_context) - admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html) + return self.handle_request_validation(form) elif isinstance(form, ValidateParticipationForm): - if not self.request.user.registration.is_admin: - form.add_error(None, _("You are not an administrator.")) - return self.form_invalid(form) - elif self.object.participation.valid is not False: - form.add_error(None, _("This team has no pending validation.")) - return self.form_invalid(form) + return self.handle_validate_participation(form) + 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) + def handle_request_validation(self, form): + """ + A team requests to be validated + """ + if not self.request.user.registration.participates: + form.add_error(None, _("You don't participate, so you can't request the validation of the team.")) + return self.form_invalid(form) + if self.object.participation.valid is not None: + form.add_error(None, _("The validation of the team is already done or pending.")) + return self.form_invalid(form) + if not self.get_context_data()["can_validate"]: + form.add_error(None, _("The team can't be validated: missing email address confirmations, " + "photo authorizations, people or the chosen problem is not set.")) + return self.form_invalid(form) + self.object.participation.valid = False + self.object.participation.save() + + for admin in AdminRegistration.objects.all(): + mail_context = dict(user=admin.user, team=self.object) + mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) + mail_html = render_to_string("participation/mails/request_validation.html", mail_context) + admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html) + return self.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): diff --git a/apps/registration/tests.py b/apps/registration/tests.py index d239349..5991cdb 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,10 +1,9 @@ import os -from django.contrib.sites.models import Site - from corres2math.tokens import email_validation_token -from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site from django.core.management import call_command from django.test import TestCase from django.urls import reverse From c62aa3ebd18dba20a9277784075d696b87435998 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 14:26:55 +0100 Subject: [PATCH 12/27] Avoid infinite recursion --- apps/participation/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/participation/views.py b/apps/participation/views.py index ee43067..18a50af 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -204,7 +204,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) mail_html = render_to_string("participation/mails/request_validation.html", mail_context) admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html) - return self.form_valid(form) + return super().form_valid(form) def handle_validate_participation(self, form): """ From bf32c34d4ca0fddf28c2a75f0ebbbdf27c68b5b0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 14:31:11 +0100 Subject: [PATCH 13/27] Users should be able to send a synthesis during the fourth phase --- apps/participation/forms.py | 2 +- apps/participation/views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/participation/forms.py b/apps/participation/forms.py index fe083e8..aff330b 100644 --- a/apps/participation/forms.py +++ b/apps/participation/forms.py @@ -95,7 +95,7 @@ class UploadVideoForm(forms.ModelForm): fields = ('link',) def clean(self): - if Phase.current_phase().phase_number != 1 and self.instance.link: + if Phase.current_phase().phase_number != 1 and Phase.current_phase().phase_number != 4 and self.instance.link: self.add_error("link", _("You can't upload your video after the deadline.")) return super().clean() diff --git a/apps/participation/views.py b/apps/participation/views.py index 18a50af..6b98a67 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -179,7 +179,6 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) return self.handle_request_validation(form) elif isinstance(form, ValidateParticipationForm): return self.handle_validate_participation(form) - return self.form_invalid(form) def handle_request_validation(self, form): """ From 0a3fffe21edb6b3442b017c5617485a449e2d2d4 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 14:43:51 +0100 Subject: [PATCH 14/27] Test leave team --- apps/participation/tests.py | 46 +++++++++++++++++++++++++++++++++++++ apps/participation/views.py | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index f695b67..2459276 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -376,6 +376,45 @@ class TestStudentParticipation(TestCase): self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200) self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists()) + def test_leave_team(self): + """ + A user is in a team, and leaves it. + """ + # User is not in a team + response = self.client.post(reverse("participation:team_leave")) + self.assertEqual(response.status_code, 403) + + self.user.registration.team = self.team + self.user.registration.save() + + # Team is pending validation + self.team.participation.valid = False + self.team.participation.save() + response = self.client.post(reverse("participation:team_leave")) + self.assertEqual(response.status_code, 403) + + # Team is valid + self.team.participation.valid = True + self.team.participation.save() + response = self.client.post(reverse("participation:team_leave")) + self.assertEqual(response.status_code, 403) + + # Unauthenticated users are redirected to login page + self.client.logout() + response = self.client.get(reverse("participation:team_leave")) + self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200) + + self.client.force_login(self.user) + + self.team.participation.valid = None + self.team.participation.save() + + response = self.client.post(reverse("participation:team_leave")) + self.assertRedirects(response, reverse("index"), 302, 200) + self.user.registration.refresh_from_db() + self.assertIsNone(self.user.registration.team) + self.assertFalse(Team.objects.filter(pk=self.team.pk).exists()) + def test_no_myparticipation_redirect_nomyparticipation(self): """ Ensure a permission denied when we search my team participation when we are in no team. @@ -500,6 +539,13 @@ class TestAdminForbidden(TestCase): )) self.assertTrue(response.status_code, 403) + def test_leave_team_forbidden(self): + """ + Ensure that an admin can't leave a team. + """ + response = self.client.get(reverse("participation:team_leave")) + self.assertTrue(response.status_code, 403) + def test_my_team_forbidden(self): """ Ensure that an admin can't access to "My team". diff --git a/apps/participation/views.py b/apps/participation/views.py index 6b98a67..4923a5d 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -316,7 +316,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView): def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return self.handle_no_permission() - if not request.user.registration.team: + if not request.user.registration.participates or not request.user.registration.team: raise PermissionDenied(_("You are not in a team.")) if request.user.registration.team.participation.valid is not None: raise PermissionDenied(_("The team is already validated or the validation is pending.")) From 7353ecfd5fba13df8f2744b0c5e0e76f6f86fe63 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 14:46:00 +0100 Subject: [PATCH 15/27] Admins don't have any participation --- apps/participation/tests.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 2459276..eee362f 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -552,3 +552,10 @@ class TestAdminForbidden(TestCase): """ response = self.client.get(reverse("participation:my_team_detail")) self.assertEqual(response.status_code, 403) + + def test_my_participation_forbidden(self): + """ + Ensure that an admin can't access to "My participation". + """ + response = self.client.get(reverse("participation:my_participation_detail")) + self.assertEqual(response.status_code, 403) From e98540a2a81327966a61010438af4dc6cfd4a28b Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 15:12:33 +0100 Subject: [PATCH 16/27] Test search participation objects --- apps/participation/tests.py | 22 ++++++++++++++++++- .../templatetags/search_results_tables.py | 3 +-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index eee362f..f242211 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -1,6 +1,7 @@ from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site +from django.core.management import call_command from django.test import TestCase from django.urls import reverse from registration.models import StudentRegistration @@ -508,7 +509,7 @@ class TestStudentParticipation(TestCase): self.assertEqual(resp.status_code, 403) -class TestAdminForbidden(TestCase): +class TestAdmin(TestCase): def setUp(self) -> None: self.user = User.objects.create_superuser( username="admin@example.com", @@ -517,6 +518,25 @@ class TestAdminForbidden(TestCase): ) self.client.force_login(self.user) + def test_research(self): + """ + Try to search some things. + """ + team = Team.objects.create( + name="Best team ever", + trigram="BTE", + ) + + call_command("rebuild_index", "--noinput", "--verbosity", 0) + + response = self.client.get(reverse("haystack_search") + "?q=" + team.name) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["object_list"]) + + response = self.client.get(reverse("haystack_search") + "?q=" + team.trigram) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context["object_list"]) + def test_create_team_forbidden(self): """ Ensure that an admin can't create a team. diff --git a/apps/registration/templatetags/search_results_tables.py b/apps/registration/templatetags/search_results_tables.py index 840ebac..18825e8 100644 --- a/apps/registration/templatetags/search_results_tables.py +++ b/apps/registration/templatetags/search_results_tables.py @@ -9,6 +9,7 @@ from ..tables import RegistrationTable def search_table(results): model_class = results[0].object.__class__ + table_class = Table if issubclass(model_class, Registration): table_class = RegistrationTable elif issubclass(model_class, Team): @@ -17,8 +18,6 @@ def search_table(results): table_class = ParticipationTable elif issubclass(model_class, Video): table_class = VideoTable - else: - table_class = Table return table_class([result.object for result in results], prefix=model_class._meta.model_name) From 6afa1ea40b3a233b7beb994d072abf0222ce96e8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 15:16:08 +0100 Subject: [PATCH 17/27] Test to change mailing list subscription when an email address got updated --- apps/registration/tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 5991cdb..a2d0947 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -179,6 +179,14 @@ class TestRegistration(TestCase): """ Update the user information, for each type of user. """ + # To test the modification of mailing lists + from participation.models import Team + self.student.registration.team = Team.objects.create( + name="toto", + trigram="TOT", + ) + self.student.registration.save() + for user, data in [(self.user, dict(role="Bot")), (self.student, dict(student_class=11, school="Sky")), (self.coach, dict(professional_activity="God"))]: From aed9f457c3eb38a53cece6c6ea452c2e732fc86e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 15:25:47 +0100 Subject: [PATCH 18/27] Test custom CAS user authentication --- apps/registration/tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/registration/tests.py b/apps/registration/tests.py index a2d0947..0bd1c2b 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,6 +1,7 @@ import os from corres2math.tokens import email_validation_token +from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site @@ -10,6 +11,7 @@ from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from .auth import CustomAuthUser from .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration @@ -325,6 +327,15 @@ class TestRegistration(TestCase): self.assertEqual(response.status_code, 200) self.assertTrue(response.context["object_list"]) + def test_init_cas(self): + """ + Load custom CAS authentication + """ + self.assertEqual(settings.CAS_AUTH_CLASS, "registration.auth.CustomAuthUser") + attr = CustomAuthUser(self.user.username).attributs() + self.assertEqual(attr["matrix_username"], self.user.registration.matrix_username) + self.assertEqual(attr["display_name"], str(self.user.registration)) + def test_not_implemented_error(self): # Only for coverage self.assertRaises(NotImplementedError, lambda: Registration().type) From 52763cb75aee70cf05fc17e426e153949ea63b70 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 15:33:25 +0100 Subject: [PATCH 19/27] Fix video table --- apps/participation/tables.py | 8 +++----- apps/participation/tests.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/participation/tables.py b/apps/participation/tables.py index 2ca91c9..1473b68 100644 --- a/apps/participation/tables.py +++ b/apps/participation/tables.py @@ -72,19 +72,17 @@ class ParticipationTable(tables.Table): class VideoTable(tables.Table): - participationname = tables.LinkColumn( + participation_name = tables.LinkColumn( 'participation:participation_detail', args=[tables.A("participation__pk")], verbose_name=lambda: _("name").capitalize(), + accessor=tables.A("participation__team__name"), ) - def render_participationname(self, record): - return record.participation.team.name - class Meta: attrs = { 'class': 'table table condensed table-striped', } model = Team - fields = ('participationname', 'link',) + fields = ('participation_name', 'link',) template_name = 'django_tables2/bootstrap4.html' diff --git a/apps/participation/tests.py b/apps/participation/tests.py index f242211..b3957e8 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -4,7 +4,7 @@ from django.contrib.sites.models import Site from django.core.management import call_command from django.test import TestCase from django.urls import reverse -from registration.models import StudentRegistration +from registration.models import CoachRegistration, StudentRegistration from .models import Participation, Question, Team @@ -60,6 +60,14 @@ class TestStudentParticipation(TestCase): 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. @@ -356,6 +364,9 @@ class TestStudentParticipation(TestCase): self.user.registration.team = self.team self.user.registration.save() + self.coach.registration.team = self.team + self.coach.registration.save() + response = self.client.get(reverse("participation:update_team", args=(self.team.pk,))) self.assertEqual(response.status_code, 200) From f422212aea0dfaf5068a4204571f52188d554ead Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 16:26:43 +0100 Subject: [PATCH 20/27] Test calendar --- apps/participation/tests.py | 91 ++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index b3957e8..8a21981 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -1,12 +1,16 @@ +from datetime import timedelta + from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django.core.management import call_command +from django.db.models import F from django.test import TestCase from django.urls import reverse +from django.utils import timezone from registration.models import CoachRegistration, StudentRegistration -from .models import Participation, Question, Team +from .models import Participation, Phase, Question, Team class TestStudentParticipation(TestCase): @@ -489,6 +493,91 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) self.assertEqual(response.status_code, 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) + def test_forbidden_access(self): """ Load personnal pages and ensure that these are protected. From e73bb2d18b7d6d8fa3e308ade0310b4064194a9a Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 16:26:43 +0100 Subject: [PATCH 21/27] Test calendar --- apps/participation/forms.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/participation/forms.py b/apps/participation/forms.py index aff330b..e447998 100644 --- a/apps/participation/forms.py +++ b/apps/participation/forms.py @@ -171,10 +171,12 @@ class PhaseForm(forms.ModelForm): def clean(self): # Ensure that dates are in a right order cleaned_data = super().clean() - if cleaned_data["end"] <= cleaned_data["start"]: + start = cleaned_data["start"] + end = cleaned_data["end"] + if end <= start: self.add_error("end", _("Start date must be before the end date.")) - if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=cleaned_data["start"]).exists(): + if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=start).exists(): self.add_error("start", _("This phase must start after the previous phases.")) - if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=cleaned_data["end"]).exists(): + if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=end).exists(): self.add_error("end", _("This phase must end after the next phases.")) return cleaned_data From b6cefc1519596593e37824a053a43726026bcf01 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 17:21:50 +0100 Subject: [PATCH 22/27] Test setting the received participation and the sent participation --- apps/participation/forms.py | 12 +++-- apps/participation/tests.py | 96 ++++++++++++++++++++++++++++++++++--- apps/participation/views.py | 4 +- 3 files changed, 99 insertions(+), 13 deletions(-) diff --git a/apps/participation/forms.py b/apps/participation/forms.py index e447998..47220a6 100644 --- a/apps/participation/forms.py +++ b/apps/participation/forms.py @@ -126,16 +126,20 @@ class SendParticipationForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["sent_participation"].initial = self.instance.sent_participation + try: + self.fields["sent_participation"].initial = self.instance.sent_participation + except: # No sent participation + pass self.fields["sent_participation"].queryset = Participation.objects.filter( ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True) ) def clean(self, commit=True): cleaned_data = super().clean() - participation = cleaned_data["sent_participation"] - participation.received_participation = self.instance - self.instance = participation + if "sent_participation" in cleaned_data: + participation = cleaned_data["sent_participation"] + participation.received_participation = self.instance + self.instance = participation return cleaned_data class Meta: diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 8a21981..59a20be 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -493,6 +493,17 @@ class TestStudentParticipation(TestCase): response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) self.assertEqual(response.status_code, 200) + # Set the second phase + for i in range(1, 5): + Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2), + end=timezone.now() + timedelta(days=i - 1)) + self.assertEqual(Phase.current_phase().phase_number, 2) + + # 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_current_phase(self): """ Ensure that the current phase is the good one. @@ -618,25 +629,96 @@ class TestAdmin(TestCase): ) self.client.force_login(self.user) + self.team1 = Team.objects.create( + name="Toto", + trigram="TOT", + ) + self.team1.participation.valid = True + self.team1.participation.problem = 1 + self.team1.participation.save() + + self.team2 = Team.objects.create( + name="Bliblu", + trigram="BIU", + ) + self.team2.participation.valid = True + self.team2.participation.problem = 1 + self.team2.participation.save() + + self.team3 = Team.objects.create( + name="Zouplop", + trigram="ZPL", + ) + self.team3.participation.valid = True + self.team3.participation.problem = 1 + self.team3.participation.save() + + self.other_team = Team.objects.create( + name="I am different", + trigram="IAD", + ) + self.other_team.participation.valid = True + self.other_team.participation.problem = 2 + self.other_team.participation.save() + def test_research(self): """ Try to search some things. """ - team = Team.objects.create( - name="Best team ever", - trigram="BTE", - ) - call_command("rebuild_index", "--noinput", "--verbosity", 0) - response = self.client.get(reverse("haystack_search") + "?q=" + team.name) + 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=" + team.trigram) + response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram) self.assertEqual(response.status_code, 200) self.assertTrue(response.context["object_list"]) + def test_set_received_video(self): + """ + Try to define the received video of a participation. + """ + response = self.client.get(reverse("participation:participation_receive_participation", + args=(self.team1.participation.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("participation:participation_receive_participation", + args=(self.team1.participation.pk,)), + data=dict(received_participation=self.team2.participation.pk)) + self.assertRedirects(response, reverse("participation:participation_detail", + args=(self.team1.participation.pk,)), 302, 200) + + response = self.client.get(reverse("participation:participation_receive_participation", + args=(self.team1.participation.pk,))) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("participation:participation_send_participation", + args=(self.team1.participation.pk,)), + data=dict(sent_participation=self.team3.participation.pk)) + self.assertRedirects(response, reverse("participation:participation_detail", + args=(self.team1.participation.pk,)), 302, 200) + + self.team1.participation.refresh_from_db() + self.team2.participation.refresh_from_db() + self.team3.participation.refresh_from_db() + + self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk) + self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk) + self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk) + self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk) + + # The other team didn't work on the same problem + response = self.client.post(reverse("participation:participation_receive_participation", + args=(self.team1.participation.pk,)), + data=dict(received_participation=self.other_team.participation.pk)) + self.assertEqual(response.status_code, 200) + + response = self.client.post(reverse("participation:participation_send_participation", + args=(self.team1.participation.pk,)), + data=dict(sent_participation=self.other_team.participation.pk)) + self.assertEqual(response.status_code, 200) + def test_create_team_forbidden(self): """ Ensure that an admin can't create a team. diff --git a/apps/participation/views.py b/apps/participation/views.py index 4923a5d..04a8455 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -388,7 +388,7 @@ class SetParticipationReceiveParticipationView(AdminMixin, UpdateView): template_name = "participation/receive_participation_form.html" def get_success_url(self): - return reverse_lazy("participation:participation_detail", args=(self.object.pk,)) + return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) class SetParticipationSendParticipationView(AdminMixin, UpdateView): @@ -400,7 +400,7 @@ class SetParticipationSendParticipationView(AdminMixin, UpdateView): template_name = "participation/send_participation_form.html" def get_success_url(self): - return reverse_lazy("participation:participation_detail", args=(self.object.pk,)) + return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) class CreateQuestionView(LoginRequiredMixin, CreateView): From c35fb4e996085ef79fee0cce32fee12d2d45cc44 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 17:58:05 +0100 Subject: [PATCH 23/27] Test questions, 100% coverage --- apps/participation/forms.py | 9 +- apps/participation/tests.py | 65 +++++++++++- apps/participation/views.py | 3 + locale/fr/LC_MESSAGES/django.po | 169 ++++++++++++++++++-------------- 4 files changed, 168 insertions(+), 78 deletions(-) diff --git a/apps/participation/forms.py b/apps/participation/forms.py index 47220a6..605601c 100644 --- a/apps/participation/forms.py +++ b/apps/participation/forms.py @@ -2,7 +2,7 @@ import re from bootstrap_datepicker_plus import DateTimePickerInput from django import forms -from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.db.models import Q from django.utils.translation import gettext_lazy as _ @@ -128,7 +128,7 @@ class SendParticipationForm(forms.ModelForm): super().__init__(*args, **kwargs) try: self.fields["sent_participation"].initial = self.instance.sent_participation - except: # No sent participation + except ObjectDoesNotExist: # No sent participation pass self.fields["sent_participation"].queryset = Participation.objects.filter( ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True) @@ -155,6 +155,11 @@ class QuestionForm(forms.ModelForm): super().__init__(*args, **kwargs) self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")}) + def clean(self): + if Phase.current_phase().phase_number != 2: + self.add_error(None, _("You can only create or update a question during the second phase.")) + return super().clean() + class Meta: model = Question fields = ('question',) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 59a20be..4ce5822 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -4,7 +4,6 @@ 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.db.models import F from django.test import TestCase from django.urls import reverse from django.utils import timezone @@ -504,6 +503,70 @@ class TestStudentParticipation(TestCase): 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:delete_question", args=(self.question.pk,)), 302, 200) + response = self.client.get(reverse("participation:add_question", args=(self.question.pk,))) + self.assertRedirects(response, reverse("login") + "?next=" + + reverse("participation:add_question", args=(self.question.pk,)), 302, 200) + def test_current_phase(self): """ Ensure that the current phase is the good one. diff --git a/apps/participation/views.py b/apps/participation/views.py index 04a8455..de74762 100644 --- a/apps/participation/views.py +++ b/apps/participation/views.py @@ -418,6 +418,7 @@ class CreateQuestionView(LoginRequiredMixin, CreateView): self.participation = Participation.objects.get(pk=kwargs["pk"]) if request.user.registration.is_admin or \ request.user.registration.participates and \ + self.participation.valid and \ request.user.registration.team.pk == self.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied @@ -443,6 +444,7 @@ class UpdateQuestionView(LoginRequiredMixin, UpdateView): return self.handle_no_permission() if request.user.registration.is_admin or \ request.user.registration.participates and \ + self.object.participation.valid and \ request.user.registration.team.pk == self.object.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied @@ -464,6 +466,7 @@ class DeleteQuestionView(LoginRequiredMixin, DeleteView): return self.handle_no_permission() if request.user.registration.is_admin or \ request.user.registration.participates and \ + self.object.participation.valid and \ request.user.registration.team.pk == self.object.participation.team_id: return super().dispatch(request, *args, **kwargs) raise PermissionDenied diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 3411a2a..a759138 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Corres2math\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-02 10:56+0100\n" +"POT-Creation-Date: 2020-11-03 17:32+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Yohann D'ANELLO \n" "Language-Team: LANGUAGE \n" @@ -30,7 +30,7 @@ msgid "This task failed successfully." msgstr "Cette tâche a échoué avec succès." #: apps/eastereggs/templates/eastereggs/xp_modal.html:16 -#: templates/base_modal.html:19 +#: corres2math/templates/base_modal.html:19 msgid "Close" msgstr "Fermer" @@ -99,6 +99,16 @@ msgstr "changelogs" msgid "Changelog of type \"{action}\" for model {model} at {timestamp}" msgstr "Changelog de type \"{action}\" pour le modèle {model} le {timestamp}" +#: apps/participation/admin.py:16 apps/participation/models.py:122 +#: apps/participation/tables.py:35 apps/participation/tables.py:62 +msgid "problem number" +msgstr "numéro de problème" + +#: apps/participation/admin.py:21 apps/participation/models.py:128 +#: apps/participation/models.py:182 +msgid "valid" +msgstr "valide" + #: apps/participation/forms.py:20 apps/participation/models.py:33 msgid "The trigram must be composed of three uppercase letters." msgstr "Le trigramme doit être composé de trois lettres majuscules." @@ -123,19 +133,25 @@ msgstr "Vous ne pouvez pas envoyer de vidéo après la date limite." msgid "Send to team" msgstr "Envoyer à l'équipe" -#: apps/participation/forms.py:152 +#: apps/participation/forms.py:156 msgid "How did you get the idea to ...?" msgstr "Comment avez-vous eu l'idée de ... ?" -#: apps/participation/forms.py:175 +#: apps/participation/forms.py:160 +msgid "You can only create or update a question during the second phase." +msgstr "" +"Vous pouvez créer ou modifier une question seulement pendant la seconde " +"phase." + +#: apps/participation/forms.py:186 msgid "Start date must be before the end date." msgstr "La date de début doit être avant la date de fin." -#: apps/participation/forms.py:177 +#: apps/participation/forms.py:188 msgid "This phase must start after the previous phases." msgstr "Cette phase doit commencer après les phases précédentes." -#: apps/participation/forms.py:179 +#: apps/participation/forms.py:190 msgid "This phase must end after the next phases." msgstr "Cette phase doit finir avant les phases suivantes." @@ -187,15 +203,6 @@ msgstr "équipes" msgid "Problem #{problem:d}" msgstr "Problème n°{problem:d}" -#: apps/participation/models.py:122 apps/participation/tables.py:35 -#: apps/participation/tables.py:62 -msgid "problem number" -msgstr "numéro de problème" - -#: apps/participation/models.py:128 apps/participation/models.py:182 -msgid "valid" -msgstr "valide" - #: apps/participation/models.py:129 apps/participation/models.py:183 msgid "The video got the validation of the administrators." msgstr "La vidéo a été validée par les administrateurs." @@ -283,12 +290,12 @@ msgid "phases" msgstr "phases" #: apps/participation/templates/participation/create_team.html:11 -#: templates/base.html:231 +#: corres2math/templates/base.html:231 msgid "Create" msgstr "Créer" #: apps/participation/templates/participation/join_team.html:11 -#: templates/base.html:227 +#: corres2math/templates/base.html:227 msgid "Join" msgstr "Rejoindre" @@ -462,7 +469,7 @@ msgstr "Définir l'équipe qui recevra votre vidéo" #: apps/participation/templates/participation/participation_detail.html:181 #: apps/participation/templates/participation/participation_detail.html:233 -#: apps/participation/views.py:463 +#: apps/participation/views.py:482 msgid "Upload video" msgstr "Envoyer la vidéo" @@ -497,7 +504,7 @@ msgid "Update question" msgstr "Modifier la question" #: apps/participation/templates/participation/participation_detail.html:217 -#: apps/participation/views.py:440 +#: apps/participation/views.py:459 msgid "Delete question" msgstr "Supprimer la question" @@ -507,8 +514,8 @@ msgid "Display synthesis" msgstr "Afficher la synthèse" #: apps/participation/templates/participation/phase_list.html:10 -#: apps/participation/views.py:482 templates/base.html:68 -#: templates/base.html:70 templates/base.html:217 +#: apps/participation/views.py:501 corres2math/templates/base.html:68 +#: corres2math/templates/base.html:70 corres2math/templates/base.html:217 msgid "Calendar" msgstr "Calendrier" @@ -620,7 +627,7 @@ msgid "Update team" msgstr "Modifier l'équipe" #: apps/participation/templates/participation/team_detail.html:127 -#: apps/participation/views.py:296 +#: apps/participation/views.py:314 msgid "Leave team" msgstr "Quitter l'équipe" @@ -628,8 +635,8 @@ msgstr "Quitter l'équipe" msgid "Are you sure that you want to leave this team?" msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?" -#: apps/participation/views.py:36 templates/base.html:77 -#: templates/base.html:230 +#: apps/participation/views.py:36 corres2math/templates/base.html:77 +#: corres2math/templates/base.html:230 msgid "Create team" msgstr "Créer une équipe" @@ -641,80 +648,89 @@ msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe." msgid "You are already in a team." msgstr "Vous êtes déjà dans une équipe." -#: apps/participation/views.py:82 templates/base.html:82 -#: templates/base.html:226 +#: apps/participation/views.py:82 corres2math/templates/base.html:82 +#: corres2math/templates/base.html:226 msgid "Join team" msgstr "Rejoindre une équipe" -#: apps/participation/views.py:133 apps/participation/views.py:302 -#: apps/participation/views.py:335 +#: apps/participation/views.py:133 apps/participation/views.py:320 +#: apps/participation/views.py:353 msgid "You are not in a team." msgstr "Vous n'êtes pas dans une équipe." -#: apps/participation/views.py:134 apps/participation/views.py:336 +#: apps/participation/views.py:134 apps/participation/views.py:354 msgid "You don't participate, so you don't have any team." msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." -#: apps/participation/views.py:155 +#: apps/participation/views.py:156 #, python-brace-format msgid "Detail of team {trigram}" msgstr "Détails de l'équipe {trigram}" -#: apps/participation/views.py:180 +#: apps/participation/views.py:188 msgid "You don't participate, so you can't request the validation of the team." msgstr "" "Vous ne participez pas, vous ne pouvez pas demander la validation de " "l'équipe." -#: apps/participation/views.py:183 +#: apps/participation/views.py:191 msgid "The validation of the team is already done or pending." msgstr "La validation de l'équipe est déjà faite ou en cours." -#: apps/participation/views.py:196 +#: apps/participation/views.py:194 +msgid "" +"The team can't be validated: missing email address confirmations, photo " +"authorizations, people or the chosen problem is not set." +msgstr "" +"L'équipe ne peut pas être validée : il manque soit les confirmations " +"d'adresse e-mail, soit une autorisation parentale, soit des personnes soit " +"le problème n'a pas été choisi." + +#: apps/participation/views.py:213 msgid "You are not an administrator." msgstr "Vous n'êtes pas administrateur." -#: apps/participation/views.py:199 +#: apps/participation/views.py:216 msgid "This team has no pending validation." msgstr "L'équipe n'a pas de validation en attente." -#: apps/participation/views.py:218 +#: apps/participation/views.py:235 msgid "You must specify if you validate the registration or not." msgstr "Vous devez spécifier si vous validez l'inscription ou non." -#: apps/participation/views.py:245 +#: apps/participation/views.py:263 #, python-brace-format msgid "Update team {trigram}" msgstr "Mise à jour de l'équipe {trigram}" -#: apps/participation/views.py:282 apps/registration/views.py:243 +#: apps/participation/views.py:300 apps/registration/views.py:243 #, python-brace-format msgid "Photo authorization of {student}.{ext}" msgstr "Autorisation de droit à l'image de {student}.{ext}" -#: apps/participation/views.py:286 +#: apps/participation/views.py:304 #, python-brace-format msgid "Photo authorizations of team {trigram}.zip" msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip" -#: apps/participation/views.py:304 +#: apps/participation/views.py:322 msgid "The team is already validated or the validation is pending." msgstr "La validation de l'équipe est déjà faite ou en cours." -#: apps/participation/views.py:348 +#: apps/participation/views.py:366 msgid "The team is not validated yet." msgstr "L'équipe n'est pas encore validée." -#: apps/participation/views.py:357 +#: apps/participation/views.py:376 #, python-brace-format msgid "Participation of team {trigram}" msgstr "Participation de l'équipe {trigram}" -#: apps/participation/views.py:394 +#: apps/participation/views.py:413 msgid "Create question" msgstr "Créer une question" -#: apps/participation/views.py:491 +#: apps/participation/views.py:510 msgid "Calendar update" msgstr "Mise à jour du calendrier" @@ -907,9 +923,11 @@ msgid "Your password has been set. You may go ahead and log in now." msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter." #: apps/registration/templates/registration/password_reset_complete.html:10 -#: templates/base.html:130 templates/base.html:221 templates/base.html:222 -#: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:25 +#: corres2math/templates/base.html:130 corres2math/templates/base.html:221 +#: corres2math/templates/base.html:222 +#: corres2math/templates/registration/login.html:7 +#: corres2math/templates/registration/login.html:8 +#: corres2math/templates/registration/login.html:25 msgid "Log in" msgstr "Connexion" @@ -1082,11 +1100,11 @@ msgstr "Anglais" msgid "French" msgstr "Français" -#: templates/400.html:6 +#: corres2math/templates/400.html:6 msgid "Bad request" msgstr "Requête invalide" -#: templates/400.html:7 +#: corres2math/templates/400.html:7 msgid "" "Sorry, your request was bad. Don't know what could be wrong. An email has " "been sent to webmasters with the details of the error. You can now watch " @@ -1096,23 +1114,23 @@ msgstr "" "email a été envoyé aux administrateurs avec les détails de l'erreur. Vous " "pouvez désormais retourner voir des vidéos." -#: templates/403.html:6 +#: corres2math/templates/403.html:6 msgid "Permission denied" msgstr "Permission refusée" -#: templates/403.html:7 +#: corres2math/templates/403.html:7 msgid "You don't have the right to perform this request." msgstr "Vous n'avez pas le droit d'effectuer cette requête." -#: templates/403.html:10 templates/404.html:10 +#: corres2math/templates/403.html:10 corres2math/templates/404.html:10 msgid "Exception message:" msgstr "Message d'erreur :" -#: templates/404.html:6 +#: corres2math/templates/404.html:6 msgid "Page not found" msgstr "Page non trouvée" -#: templates/404.html:7 +#: corres2math/templates/404.html:7 #, python-format msgid "" "The requested path %(request_path)s was not found on the server." @@ -1120,11 +1138,11 @@ msgstr "" "Le chemin demandé %(request_path)s n'a pas été trouvé sur le " "serveur." -#: templates/500.html:6 +#: corres2math/templates/500.html:6 msgid "Server error" msgstr "Erreur du serveur" -#: templates/500.html:7 +#: corres2math/templates/500.html:7 msgid "" "Sorry, an error occurred when processing your request. An email has been " "sent to webmasters with the detail of the error, and this will be fixed " @@ -1135,47 +1153,47 @@ msgstr "" "avec les détails de l'erreur. Vous pouvez désormais retourner voir des " "vidéos." -#: templates/base.html:64 +#: corres2math/templates/base.html:64 msgid "Home" msgstr "Accueil" -#: templates/base.html:88 +#: corres2math/templates/base.html:88 msgid "My team" msgstr "Mon équipe" -#: templates/base.html:93 +#: corres2math/templates/base.html:93 msgid "My participation" msgstr "Ma participation" -#: templates/base.html:100 +#: corres2math/templates/base.html:100 msgid "Chat" msgstr "Chat" -#: templates/base.html:104 +#: corres2math/templates/base.html:104 msgid "Administration" msgstr "Administration" -#: templates/base.html:112 +#: corres2math/templates/base.html:112 msgid "Search..." msgstr "Chercher ..." -#: templates/base.html:121 +#: corres2math/templates/base.html:121 msgid "Return to admin view" msgstr "Retourner à l'interface administrateur" -#: templates/base.html:126 +#: corres2math/templates/base.html:126 msgid "Register" msgstr "S'inscrire" -#: templates/base.html:142 +#: corres2math/templates/base.html:142 msgid "My account" msgstr "Mon compte" -#: templates/base.html:145 +#: corres2math/templates/base.html:145 msgid "Log out" msgstr "Déconnexion" -#: templates/base.html:162 +#: corres2math/templates/base.html:162 #, python-format msgid "" "Your email address is not validated. Please click on the link you received " @@ -1186,23 +1204,23 @@ msgstr "" "avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur ce lien." -#: templates/base.html:186 +#: corres2math/templates/base.html:186 msgid "Contact us" msgstr "Nous contacter" -#: templates/base.html:219 +#: corres2math/templates/base.html:219 msgid "Search results" msgstr "Résultats de la recherche" -#: templates/registration/logged_out.html:8 +#: corres2math/templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "Merci d'avoir utilisé la plateforme des Correspondances." -#: templates/registration/logged_out.html:9 +#: corres2math/templates/registration/logged_out.html:9 msgid "Log in again" msgstr "Se reconnecter" -#: templates/registration/login.html:13 +#: corres2math/templates/registration/login.html:13 #, python-format msgid "" "You are authenticated as %(user)s, but are not authorized to access this " @@ -1211,18 +1229,19 @@ msgstr "" "Vous êtes connectés en tant que %(user)s, mais n'êtes pas autorisés à " "accéder à cette page. Voulez-vous vous reconnecter avec un autre compte ?" -#: templates/registration/login.html:23 +#: corres2math/templates/registration/login.html:23 msgid "Forgotten your password or username?" msgstr "Mot de passe oublié ?" -#: templates/search/search.html:6 templates/search/search.html:11 +#: corres2math/templates/search/search.html:6 +#: corres2math/templates/search/search.html:10 msgid "Search" msgstr "Chercher" -#: templates/search/search.html:16 +#: corres2math/templates/search/search.html:15 msgid "Results" msgstr "Résultats" -#: templates/search/search.html:26 +#: corres2math/templates/search/search.html:25 msgid "No results found." msgstr "Aucun résultat." From fa368a399a0931947334c436a927967346798912 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 18:16:36 +0100 Subject: [PATCH 24/27] More linting --- apps/participation/tests.py | 6 +++--- corres2math/matrix.py | 11 +++++++---- corres2math/middlewares.py | 5 ++--- corres2math/settings.py | 7 +++---- corres2math/urls.py | 4 ++-- tox.ini | 4 ++-- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 4ce5822..21a3c43 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -562,10 +562,10 @@ class TestStudentParticipation(TestCase): 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:delete_question", args=(self.question.pk,)), 302, 200) - response = self.client.get(reverse("participation:add_question", args=(self.question.pk,))) + 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:add_question", args=(self.question.pk,)), 302, 200) + reverse("participation:delete_question", args=(self.question.pk,)), 302, 200) def test_current_phase(self): """ diff --git a/corres2math/matrix.py b/corres2math/matrix.py index ddf76c6..9434baa 100644 --- a/corres2math/matrix.py +++ b/corres2math/matrix.py @@ -1,8 +1,12 @@ import os -from typing import Tuple +from typing import Any, Dict, Optional, Tuple, Union from asgiref.sync import async_to_sync -from nio import * +from nio import AsyncClient, DataProvider, ProfileSetAvatarError, ProfileSetAvatarResponse, \ + ProfileSetDisplayNameError, ProfileSetDisplayNameResponse, RoomCreateError, RoomCreateResponse, \ + RoomInviteError, RoomInviteResponse, RoomKickError, RoomKickResponse, RoomPreset, \ + RoomPutStateError, RoomPutStateResponse, RoomResolveAliasResponse, RoomVisibility, TransferMonitor, \ + UploadError, UploadResponse class Matrix: @@ -263,7 +267,7 @@ class Matrix: @classmethod @async_to_sync - async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomInviteError]: + async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomKickError]: """ Kick a user from a room, or withdraw their invitation. @@ -370,4 +374,3 @@ class FakeMatrixClient: async def func(*_, **_2): return None return func - diff --git a/corres2math/middlewares.py b/corres2math/middlewares.py index 185eda9..fcd29ba 100644 --- a/corres2math/middlewares.py +++ b/corres2math/middlewares.py @@ -1,8 +1,7 @@ -from django.conf import settings -from django.contrib.auth.models import AnonymousUser, User - from threading import local +from django.conf import settings +from django.contrib.auth.models import AnonymousUser, User from django.contrib.sessions.backends.db import SessionStore USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') diff --git a/corres2math/settings.py b/corres2math/settings.py index 1424f6f..73c4a56 100644 --- a/corres2math/settings.py +++ b/corres2math/settings.py @@ -89,8 +89,7 @@ LOGIN_REDIRECT_URL = "index" TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')] - , + 'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -216,6 +215,6 @@ else: } if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": - from .settings_prod import * + from .settings_prod import * # noqa: F401,F403 else: - from .settings_dev import * + from .settings_dev import * # noqa: F401,F403 diff --git a/corres2math/urls.py b/corres2math/urls.py index ae1e0bb..380cd71 100644 --- a/corres2math/urls.py +++ b/corres2math/urls.py @@ -14,8 +14,8 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include -from django.views.defaults import bad_request, permission_denied, page_not_found, server_error +from django.urls import include, path +from django.views.defaults import bad_request, page_not_found, permission_denied, server_error from django.views.generic import TemplateView from registration.views import PhotoAuthorizationView diff --git a/tox.ini b/tox.ini index bb02e3c..27fbf8f 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -r{toxinidir}/requirements.txt coverage commands = - coverage run --omit='*migrations*,apps/scripts*' --source=apps ./manage.py test apps/ + coverage run --omit='apps/scripts*' --source=apps,corres2math ./manage.py test apps/ coverage report -m [testenv:linters] @@ -25,7 +25,7 @@ deps = pep8-naming pyflakes commands = - flake8 apps/ + flake8 apps/ corres2math/ [flake8] exclude = From 1ddf39f29693f5dcebefa926e50e8de14f196b06 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 19:13:33 +0100 Subject: [PATCH 25/27] Cover also settings files, keep 100% coverage by ignoring production files --- .../commands/fix_matrix_channels.py | 8 +++--- apps/participation/migrations/0006_phase.py | 2 +- apps/participation/tests.py | 25 +++++++++++++++++++ apps/registration/models.py | 4 +-- apps/registration/tests.py | 5 ---- corres2math/lists.py | 2 +- corres2math/matrix.py | 19 ++++++++------ corres2math/middlewares.py | 15 +++-------- corres2math/settings.py | 4 +-- corres2math/tests.py | 24 ++++++++++++++++++ tox.ini | 2 +- 11 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 corres2math/tests.py diff --git a/apps/participation/management/commands/fix_matrix_channels.py b/apps/participation/management/commands/fix_matrix_channels.py index 427cc8b..bff9181 100644 --- a/apps/participation/management/commands/fix_matrix_channels.py +++ b/apps/participation/management/commands/fix_matrix_channels.py @@ -11,7 +11,7 @@ class Command(BaseCommand): def handle(self, *args, **options): Matrix.set_display_name("Bot des Correspondances") - if not os.path.isfile(".matrix_avatar"): + if not os.path.isfile(".matrix_avatar"): # pragma: no cover stat_file = os.stat("corres2math/static/logo.png") with open("corres2math/static/logo.png", "rb") as f: resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size) @@ -21,9 +21,9 @@ class Command(BaseCommand): with open(".matrix_avatar", "w") as f: f.write(avatar_uri) Matrix.set_avatar(avatar_uri) - else: - with open(".matrix_avatar", "r") as f: - avatar_uri = f.read().rstrip(" \t\r\n") + + with open(".matrix_avatar", "r") as f: + avatar_uri = f.read().rstrip(" \t\r\n") if not async_to_sync(Matrix.resolve_room_alias)("#faq:correspondances-maths.fr"): Matrix.create_room( diff --git a/apps/participation/migrations/0006_phase.py b/apps/participation/migrations/0006_phase.py index 2653caa..e7b5212 100644 --- a/apps/participation/migrations/0006_phase.py +++ b/apps/participation/migrations/0006_phase.py @@ -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. """ diff --git a/apps/participation/tests.py b/apps/participation/tests.py index 21a3c43..c8b7d4a 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -1,3 +1,4 @@ +import os from datetime import timedelta from django.contrib.auth.models import User @@ -652,6 +653,14 @@ class TestStudentParticipation(TestCase): )) 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. @@ -682,6 +691,22 @@ class TestStudentParticipation(TestCase): 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') + os.remove(".matrix_avatar") + class TestAdmin(TestCase): def setUp(self) -> None: diff --git a/apps/registration/models.py b/apps/registration/models.py index 7fe7292..0a85dc6 100644 --- a/apps/registration/models.py +++ b/apps/registration/models.py @@ -58,11 +58,11 @@ class Registration(PolymorphicModel): self.user.email_user(subject, message, html_message=html) @property - def type(self): + def type(self): # pragma: no cover raise NotImplementedError @property - def form_class(self): + def form_class(self): # pragma: no cover raise NotImplementedError @property diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 0bd1c2b..02170e9 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -335,8 +335,3 @@ class TestRegistration(TestCase): attr = CustomAuthUser(self.user.username).attributs() self.assertEqual(attr["matrix_username"], self.user.registration.matrix_username) self.assertEqual(attr["display_name"], str(self.user.registration)) - - def test_not_implemented_error(self): - # Only for coverage - self.assertRaises(NotImplementedError, lambda: Registration().type) - self.assertRaises(NotImplementedError, lambda: Registration().form_class) diff --git a/corres2math/lists.py b/corres2math/lists.py index a8cbac2..d224392 100644 --- a/corres2math/lists.py +++ b/corres2math/lists.py @@ -6,7 +6,7 @@ _client = None def get_sympa_client(): global _client if _client is None: - if os.getenv("SYMPA_PASSWORD", None) is not None: + if os.getenv("SYMPA_PASSWORD", None) is not None: # pragma: no cover from sympasoap import Client _client = Client("https://" + os.getenv("SYMPA_URL")) _client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) diff --git a/corres2math/matrix.py b/corres2math/matrix.py index 9434baa..00ae20c 100644 --- a/corres2math/matrix.py +++ b/corres2math/matrix.py @@ -22,7 +22,7 @@ class Matrix: _device_id: str = None @classmethod - async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: + async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: # pragma: no cover """ Retrieve the bot account. If not logged, log in and store access token. @@ -133,7 +133,8 @@ class Matrix: If left as ``None``, some servers might refuse the upload. """ client = await cls._get_client() - return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) + return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \ + if isinstance(client, AsyncClient) else UploadResponse("debug mode"), None @classmethod @async_to_sync @@ -219,9 +220,7 @@ class Matrix: """ client = await cls._get_client() resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias) - if isinstance(resp, RoomResolveAliasResponse): - return resp.room_id - return None + return resp.room_id if isinstance(resp, RoomResolveAliasResponse) else None @classmethod @async_to_sync @@ -291,7 +290,7 @@ class Matrix: @classmethod @async_to_sync async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\ - -> Union[RoomPutStateResponse, RoomPutStateError]: + -> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover """ Put a given power level to a user in a certain room. @@ -306,6 +305,9 @@ class Matrix: power_level (int): The target power level to give. """ client = await cls._get_client() + if isinstance(client, FakeMatrixClient): + return RoomPutStateError("debug mode") + if room_id.startswith("#"): room_id = await cls.resolve_room_alias(room_id) resp = await client.room_get_state_event(room_id, "m.room.power_levels") @@ -316,7 +318,7 @@ class Matrix: @classmethod @async_to_sync async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\ - -> Union[RoomPutStateResponse, RoomPutStateError]: + -> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover """ Define the minimal power level to have to send a certain event type in a given room. @@ -332,6 +334,9 @@ class Matrix: power_level (int): The target power level to give. """ client = await cls._get_client() + if isinstance(client, FakeMatrixClient): + return RoomPutStateError("debug mode") + if room_id.startswith("#"): room_id = await cls.resolve_room_alias(room_id) resp = await client.room_get_state_event(room_id, "m.room.power_levels") diff --git a/corres2math/middlewares.py b/corres2math/middlewares.py index fcd29ba..a8ccef6 100644 --- a/corres2math/middlewares.py +++ b/corres2math/middlewares.py @@ -21,19 +21,13 @@ def get_current_user() -> User: return getattr(_thread_locals, USER_ATTR_NAME, None) -def get_current_session() -> SessionStore: - return getattr(_thread_locals, SESSION_ATTR_NAME, None) - - def get_current_ip() -> str: return getattr(_thread_locals, IP_ATTR_NAME, None) def get_current_authenticated_user(): current_user = get_current_user() - if isinstance(current_user, AnonymousUser): - return None - return current_user + return None if isinstance(current_user, AnonymousUser) else current_user class SessionMiddleware(object): @@ -49,10 +43,7 @@ class SessionMiddleware(object): request.user = User.objects.get(pk=request.session["_fake_user_id"]) user = request.user - if 'HTTP_X_REAL_IP' in request.META: - ip = request.META.get('HTTP_X_REAL_IP') - else: - ip = request.META.get('REMOTE_ADDR') + ip = request.META.get('HTTP_X_REAL_IP' if 'HTTP_X_REAL_IP' in request.META else 'REMOTE_ADDR') _set_current_user_and_ip(user, request.session, ip) response = self.get_response(request) @@ -61,7 +52,7 @@ class SessionMiddleware(object): return response -class TurbolinksMiddleware(object): +class TurbolinksMiddleware(object): # pragma: no cover """ Send the `Turbolinks-Location` header in response to a visit that was redirected, and Turbolinks will replace the browser's topmost history entry. diff --git a/corres2math/settings.py b/corres2math/settings.py index 73c4a56..bae7440 100644 --- a/corres2math/settings.py +++ b/corres2math/settings.py @@ -195,7 +195,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' _db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower() -if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': +if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': # pragma: no cover DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2', @@ -214,7 +214,7 @@ else: } } -if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": +if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": # pragma: no cover from .settings_prod import * # noqa: F401,F403 else: from .settings_dev import * # noqa: F401,F403 diff --git a/corres2math/tests.py b/corres2math/tests.py new file mode 100644 index 0000000..c4337de --- /dev/null +++ b/corres2math/tests.py @@ -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) diff --git a/tox.ini b/tox.ini index 27fbf8f..021529b 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ deps = -r{toxinidir}/requirements.txt coverage commands = - coverage run --omit='apps/scripts*' --source=apps,corres2math ./manage.py test apps/ + coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/ coverage report -m [testenv:linters] From 04dd02b88a5c2c3a1284de98b5fc80168069a142 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 20:52:55 +0100 Subject: [PATCH 26/27] Tests should not depend on Matrix-nio, that uses lxml that needs a lot of dependencies and a lot of time to build --- .gitlab-ci.yml | 4 +- Dockerfile | 2 +- .../commands/fix_matrix_channels.py | 31 ++++--- apps/participation/models.py | 3 +- apps/participation/tests.py | 2 - apps/registration/tests.py | 2 +- corres2math/matrix.py | 91 ++++++++++--------- corres2math/middlewares.py | 1 - corres2math/settings.py | 10 +- requirements.txt | 34 +++---- tox.ini | 15 ++- 11 files changed, 110 insertions(+), 85 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 49bdcaf..863abd5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ py38: stage: test image: python:3.8-alpine before_script: - - apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev + - apk add --no-cache libmagic - pip install tox --no-cache-dir script: tox -e py38 @@ -14,7 +14,7 @@ py39: stage: test image: python:3.9-alpine before_script: - - apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev + - apk add --no-cache gcc libmagic - pip install tox --no-cache-dir script: tox -e py39 diff --git a/Dockerfile b/Dockerfile index 9be9d7f..cce2279 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN apk add --no-cache bash RUN mkdir /code WORKDIR /code COPY requirements.txt /code/requirements.txt -RUN pip install -r requirements.txt psycopg2-binary sympasoap --no-cache-dir +RUN pip install -r requirements.txt --no-cache-dir COPY . /code/ diff --git a/apps/participation/management/commands/fix_matrix_channels.py b/apps/participation/management/commands/fix_matrix_channels.py index bff9181..764b41e 100644 --- a/apps/participation/management/commands/fix_matrix_channels.py +++ b/apps/participation/management/commands/fix_matrix_channels.py @@ -1,9 +1,8 @@ import os from asgiref.sync import async_to_sync -from corres2math.matrix import Matrix, RoomVisibility, UploadError +from corres2math.matrix import Matrix, RoomPreset, RoomVisibility from django.core.management import BaseCommand -from nio import RoomPreset from registration.models import AdminRegistration, Registration @@ -11,19 +10,23 @@ class Command(BaseCommand): def handle(self, *args, **options): Matrix.set_display_name("Bot des Correspondances") - if not os.path.isfile(".matrix_avatar"): # pragma: no cover - stat_file = os.stat("corres2math/static/logo.png") - with open("corres2math/static/logo.png", "rb") as f: - resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size) - if isinstance(resp, UploadError): - raise Exception(resp) - avatar_uri = resp.content_uri - with open(".matrix_avatar", "w") as f: - f.write(avatar_uri) - Matrix.set_avatar(avatar_uri) + if not os.getenv("SYNAPSE_PASSWORD"): + avatar_uri = "plop" + else: # pragma: no cover + if not os.path.isfile(".matrix_avatar"): + stat_file = os.stat("corres2math/static/logo.png") + with open("corres2math/static/logo.png", "rb") as f: + resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", + filesize=stat_file.st_size) + if not hasattr(resp, "content_uri"): + raise Exception(resp) + avatar_uri = resp.content_uri + with open(".matrix_avatar", "w") as f: + f.write(avatar_uri) + Matrix.set_avatar(avatar_uri) - with open(".matrix_avatar", "r") as f: - avatar_uri = f.read().rstrip(" \t\r\n") + with open(".matrix_avatar", "r") as f: + avatar_uri = f.read().rstrip(" \t\r\n") if not async_to_sync(Matrix.resolve_room_alias)("#faq:correspondances-maths.fr"): Matrix.create_room( diff --git a/apps/participation/models.py b/apps/participation/models.py index 746550f..8b6af3e 100644 --- a/apps/participation/models.py +++ b/apps/participation/models.py @@ -2,7 +2,7 @@ import os import re from corres2math.lists import get_sympa_client -from corres2math.matrix import Matrix +from corres2math.matrix import Matrix, RoomPreset, RoomVisibility from django.core.exceptions import ObjectDoesNotExist from django.core.validators import RegexValidator from django.db import models @@ -13,7 +13,6 @@ from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ -from nio import RoomPreset, RoomVisibility class Team(models.Model): diff --git a/apps/participation/tests.py b/apps/participation/tests.py index c8b7d4a..4a6d119 100644 --- a/apps/participation/tests.py +++ b/apps/participation/tests.py @@ -1,4 +1,3 @@ -import os from datetime import timedelta from django.contrib.auth.models import User @@ -705,7 +704,6 @@ class TestStudentParticipation(TestCase): call_command('fix_matrix_channels') call_command('setup_third_phase') - os.remove(".matrix_avatar") class TestAdmin(TestCase): diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 02170e9..2db3763 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -12,7 +12,7 @@ from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode from .auth import CustomAuthUser -from .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration +from .models import AdminRegistration, CoachRegistration, StudentRegistration class TestIndexPage(TestCase): diff --git a/corres2math/matrix.py b/corres2math/matrix.py index 00ae20c..d01e44f 100644 --- a/corres2math/matrix.py +++ b/corres2math/matrix.py @@ -1,12 +1,7 @@ +from enum import Enum import os -from typing import Any, Dict, Optional, Tuple, Union from asgiref.sync import async_to_sync -from nio import AsyncClient, DataProvider, ProfileSetAvatarError, ProfileSetAvatarResponse, \ - ProfileSetDisplayNameError, ProfileSetDisplayNameResponse, RoomCreateError, RoomCreateResponse, \ - RoomInviteError, RoomInviteResponse, RoomKickError, RoomKickResponse, RoomPreset, \ - RoomPutStateError, RoomPutStateResponse, RoomResolveAliasResponse, RoomVisibility, TransferMonitor, \ - UploadError, UploadResponse class Matrix: @@ -18,11 +13,11 @@ class Matrix: Tasks are normally asynchronous, but for compatibility we make them synchronous. """ - _token: str = None - _device_id: str = None + _token = None + _device_id = None @classmethod - async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: # pragma: no cover + async def _get_client(cls): # pragma: no cover """ Retrieve the bot account. If not logged, log in and store access token. @@ -30,6 +25,7 @@ class Matrix: if not os.getenv("SYNAPSE_PASSWORD"): return FakeMatrixClient() + from nio import AsyncClient client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr") client.user_id = "@corres2mathbot:correspondances-maths.fr" @@ -53,7 +49,7 @@ class Matrix: @classmethod @async_to_sync - async def set_display_name(cls, name: str) -> Union[ProfileSetDisplayNameResponse, ProfileSetDisplayNameError]: + async def set_display_name(cls, name: str): """ Set the display name of the bot account. """ @@ -62,7 +58,7 @@ class Matrix: @classmethod @async_to_sync - async def set_avatar(cls, avatar_url: str) -> Union[ProfileSetAvatarResponse, ProfileSetAvatarError]: + async def set_avatar(cls, avatar_url: str): # pragma: no cover """ Set the display avatar of the bot account. """ @@ -73,13 +69,13 @@ class Matrix: @async_to_sync async def upload( cls, - data_provider: DataProvider, + data_provider, content_type: str = "application/octet-stream", - filename: Optional[str] = None, + filename: str = None, encrypt: bool = False, - monitor: Optional[TransferMonitor] = None, - filesize: Optional[int] = None, - ) -> Tuple[Union[UploadResponse, UploadError], Optional[Dict[str, Any]]]: + monitor=None, + filesize: int = None, + ): # pragma: no cover """ Upload a file to the content repository. @@ -134,24 +130,24 @@ class Matrix: """ client = await cls._get_client() return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \ - if isinstance(client, AsyncClient) else UploadResponse("debug mode"), None + if not isinstance(client, FakeMatrixClient) else None, None @classmethod @async_to_sync async def create_room( cls, - visibility: RoomVisibility = RoomVisibility.private, - alias: Optional[str] = None, - name: Optional[str] = None, - topic: Optional[str] = None, - room_version: Optional[str] = None, - federate: bool = True, - is_direct: bool = False, - preset: Optional[RoomPreset] = None, + visibility=None, + alias=None, + name=None, + topic=None, + room_version=None, + federate=True, + is_direct=False, + preset=None, invite=(), initial_state=(), - power_level_override: Optional[Dict[str, Any]] = None, - ) -> Union[RoomCreateResponse, RoomCreateError]: + power_level_override=None, + ): """ Create a new room. @@ -213,18 +209,18 @@ class Matrix: power_level_override) @classmethod - async def resolve_room_alias(cls, room_alias: str) -> Optional[str]: + async def resolve_room_alias(cls, room_alias: str): """ Resolve a room alias to a room ID. Return None if the alias does not exist. """ client = await cls._get_client() - resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias) - return resp.room_id if isinstance(resp, RoomResolveAliasResponse) else None + resp = await client.room_resolve_alias(room_alias) + return resp.room_id if resp else None @classmethod @async_to_sync - async def invite(cls, room_id: str, user_id: str) -> Union[RoomInviteResponse, RoomInviteError]: + async def invite(cls, room_id: str, user_id: str): """ Invite a user to a room. @@ -266,13 +262,13 @@ class Matrix: @classmethod @async_to_sync - async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomKickError]: + async def kick(cls, room_id: str, user_id: str, reason: str = None): """ Kick a user from a room, or withdraw their invitation. Kicking a user adjusts their membership to "leave" with an optional reason. - +² Returns either a `RoomKickResponse` if the request was successful or a `RoomKickError` if there was an error with the request. @@ -289,8 +285,7 @@ class Matrix: @classmethod @async_to_sync - async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\ - -> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover + async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover """ Put a given power level to a user in a certain room. @@ -306,7 +301,7 @@ class Matrix: """ client = await cls._get_client() if isinstance(client, FakeMatrixClient): - return RoomPutStateError("debug mode") + return None if room_id.startswith("#"): room_id = await cls.resolve_room_alias(room_id) @@ -317,8 +312,7 @@ class Matrix: @classmethod @async_to_sync - async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\ - -> Union[RoomPutStateResponse, RoomPutStateError]: # pragma: no cover + async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover """ Define the minimal power level to have to send a certain event type in a given room. @@ -335,7 +329,7 @@ class Matrix: """ client = await cls._get_client() if isinstance(client, FakeMatrixClient): - return RoomPutStateError("debug mode") + return None if room_id.startswith("#"): room_id = await cls.resolve_room_alias(room_id) @@ -349,8 +343,7 @@ class Matrix: @classmethod @async_to_sync - async def set_room_avatar(cls, room_id: str, avatar_uri: str)\ - -> Union[RoomPutStateResponse, RoomPutStateError]: + async def set_room_avatar(cls, room_id: str, avatar_uri: str): """ Define the avatar of a room. @@ -370,6 +363,22 @@ class Matrix: }, state_key="") +if os.getenv("SYNAPSE_PASSWORD"): # pragma: no cover + from nio import RoomVisibility, RoomPreset + RoomVisibility = RoomVisibility + RoomPreset = RoomPreset +else: + # When running tests, faking matrix-nio classes to don't include the module + class RoomVisibility(Enum): + private = 'private' + public = 'public' + + class RoomPreset(Enum): + private_chat = "private_chat" + trusted_private_chat = "trusted_private_chat" + public_chat = "public_chat" + + class FakeMatrixClient: """ Simulate a Matrix client to run tests, if no Matrix homeserver is connected. diff --git a/corres2math/middlewares.py b/corres2math/middlewares.py index a8ccef6..40cc04d 100644 --- a/corres2math/middlewares.py +++ b/corres2math/middlewares.py @@ -2,7 +2,6 @@ from threading import local from django.conf import settings from django.contrib.auth.models import AnonymousUser, User -from django.contrib.sessions.backends.db import SessionStore USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') diff --git a/corres2math/settings.py b/corres2math/settings.py index bae7440..25d1362 100644 --- a/corres2math/settings.py +++ b/corres2math/settings.py @@ -51,16 +51,14 @@ INSTALLED_APPS = [ 'django.forms', 'bootstrap_datepicker_plus', + 'cas_server', 'crispy_forms', - 'django_extensions', 'django_tables2', 'haystack', 'logs', - 'mailer', 'polymorphic', 'rest_framework', 'rest_framework.authtoken', - 'cas_server', 'api', 'eastereggs', @@ -68,6 +66,12 @@ INSTALLED_APPS = [ 'participation', ] +if "test" not in sys.argv: # pragma: no cover + INSTALLED_APPS += [ + 'django_extensions', + 'mailer', + ] + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/requirements.txt b/requirements.txt index fe4e57a..49db7a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,19 @@ -Django~=3.0 -django-bootstrap-datepicker-plus -django-cas-server -django-crispy-forms -django-extensions -django-filter~=2.3.0 +Django~=3.1 +django-bootstrap-datepicker-plus~=3.0 +django-cas-server~=1.2 +django-crispy-forms~=1.9 +django-extensions~=3.0 +django-filter~=2.3 django-haystack~=3.0 -django-mailer -django-polymorphic -django-tables2 -djangorestframework~=3.11.1 -django-rest-polymorphic -matrix-nio -ptpython -python-magic~=0.4.18 -gunicorn -whoosh \ No newline at end of file +django-mailer~=2.0 +django-polymorphic~=3.0 +django-tables2~=2.3 +djangorestframework~=3.12 +django-rest-polymorphic~=0.1 +gunicorn~=20.0 +matrix-nio~=0.15 +psycopg2-binary~=2.8 +ptpython~=3.0 +python-magic~=0.4 +sympasoap~=1.0 +whoosh~=2.7 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 021529b..9a86cd3 100644 --- a/tox.ini +++ b/tox.ini @@ -7,10 +7,21 @@ envlist = skipsdist = True [testenv] -sitepackages = True +sitepackages = False deps = - -r{toxinidir}/requirements.txt coverage + Django~=3.1 + django-bootstrap-datepicker-plus~=3.0 + django-cas-server~=1.2 + django-crispy-forms~=1.9 + django-filter~=2.3 + django-haystack~=3.0 + django-polymorphic~=3.0 + django-tables2~=2.3 + djangorestframework~=3.12 + django-rest-polymorphic~=0.1 + python-magic~=0.4 + whoosh~=2.7 commands = coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/ coverage report -m From f146ae2dd28532dc86578287353556e1502c1338 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 3 Nov 2020 21:10:21 +0100 Subject: [PATCH 27/27] Remove django cas server in CI dependencies --- apps/registration/auth.py | 4 ++-- apps/registration/tests.py | 11 ----------- corres2math/settings.py | 2 +- corres2math/urls.py | 8 ++++++-- tox.ini | 1 - 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/apps/registration/auth.py b/apps/registration/auth.py index d2febf4..f84b1f0 100644 --- a/apps/registration/auth.py +++ b/apps/registration/auth.py @@ -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. """ diff --git a/apps/registration/tests.py b/apps/registration/tests.py index 2db3763..4c464c9 100644 --- a/apps/registration/tests.py +++ b/apps/registration/tests.py @@ -1,7 +1,6 @@ import os from corres2math.tokens import email_validation_token -from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site @@ -11,7 +10,6 @@ from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode -from .auth import CustomAuthUser from .models import AdminRegistration, CoachRegistration, StudentRegistration @@ -326,12 +324,3 @@ class TestRegistration(TestCase): self.student.registration.get_student_class_display()) self.assertEqual(response.status_code, 200) self.assertTrue(response.context["object_list"]) - - def test_init_cas(self): - """ - Load custom CAS authentication - """ - self.assertEqual(settings.CAS_AUTH_CLASS, "registration.auth.CustomAuthUser") - attr = CustomAuthUser(self.user.username).attributs() - self.assertEqual(attr["matrix_username"], self.user.registration.matrix_username) - self.assertEqual(attr["display_name"], str(self.user.registration)) diff --git a/corres2math/settings.py b/corres2math/settings.py index 25d1362..7f1e52b 100644 --- a/corres2math/settings.py +++ b/corres2math/settings.py @@ -51,7 +51,6 @@ INSTALLED_APPS = [ 'django.forms', 'bootstrap_datepicker_plus', - 'cas_server', 'crispy_forms', 'django_tables2', 'haystack', @@ -68,6 +67,7 @@ INSTALLED_APPS = [ if "test" not in sys.argv: # pragma: no cover INSTALLED_APPS += [ + 'cas_server', 'django_extensions', 'mailer', ] diff --git a/corres2math/urls.py b/corres2math/urls.py index 380cd71..1947023 100644 --- a/corres2math/urls.py +++ b/corres2math/urls.py @@ -13,6 +13,7 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings from django.contrib import admin from django.urls import include, path from django.views.defaults import bad_request, page_not_found, permission_denied, server_error @@ -35,11 +36,14 @@ urlpatterns = [ path('media/authorization/photo//', PhotoAuthorizationView.as_view(), name='photo_authorization'), - path('cas/', include('cas_server.urls', namespace="cas_server")), - path('', include('eastereggs.urls')), ] +if 'cas_server' in settings.INSTALLED_APPS: # pragma: no cover + urlpatterns += [ + path('cas/', include('cas_server.urls', namespace="cas_server")), + ] + handler400 = bad_request handler403 = permission_denied handler404 = page_not_found diff --git a/tox.ini b/tox.ini index 9a86cd3..37ce64c 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,6 @@ deps = coverage Django~=3.1 django-bootstrap-datepicker-plus~=3.0 - django-cas-server~=1.2 django-crispy-forms~=1.9 django-filter~=2.3 django-haystack~=3.0