mirror of
				https://gitlab.com/animath/si/plateforme-corres2math.git
				synced 2025-10-26 09:33:17 +01:00 
			
		
		
		
	Compare commits
	
		
			49 Commits
		
	
	
		
			e73bb2d18b
			...
			improvemen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 35a197be07 | ||
|  | d55671efd1 | ||
|  | bcdbc445c6 | ||
|  | 95a69f35d2 | ||
|  | eec941c816 | ||
|  | 64d471936f | ||
|  | 02c977264d | ||
|  | 8f86ea15c8 | ||
|  | 7628387158 | ||
|  | bf2feb9c35 | ||
|  | 969e53b712 | ||
|  | 84e149e8c9 | ||
|  | 9e6a3eb1ca | ||
|  | 1f9f60d880 | ||
|  | 205760f2e9 | ||
|  | 8f742b8e14 | ||
|  | 996d00c7f0 | ||
|  | 8f09ca5553 | ||
|  | b761670133 | ||
|  | f2ca4b7446 | ||
|  | 3faf1294d8 | ||
|  | dbcf15c4f3 | ||
|  | 10115a0419 | ||
|  | 833f9147ce | ||
|  | 55a727612c | ||
|  | de7f51d8da | ||
|  | c658a9767d | ||
|  | 982b61fe03 | ||
|  | ace1dbdc75 | ||
|  | 731f8711ff | ||
|  | d3e18a8fbb | ||
|  | ece1e800ab | ||
|  | 2e0028c063 | ||
|  | d304c3565c | ||
|  | 0fe1f9c348 | ||
|  | 9867e5e2a8 | ||
|  | 48b34e4362 | ||
|  | 10932d1cc5 | ||
|  | 02ca1d1efe | ||
|  | b0a3a22f83 | ||
|  | 4238e49c11 | ||
|  | c3c2c55aca | ||
|  | cba3e56fb8 | ||
|  | f146ae2dd2 | ||
|  | 04dd02b88a | ||
|  | 1ddf39f296 | ||
|  | fa368a399a | ||
|  | c35fb4e996 | ||
|  | b6cefc1519 | 
| @@ -6,7 +6,7 @@ py38: | |||||||
|   stage: test |   stage: test | ||||||
|   image: python:3.8-alpine |   image: python:3.8-alpine | ||||||
|   before_script: |   before_script: | ||||||
|     - apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev |     - apk add --no-cache libmagic | ||||||
|     - pip install tox --no-cache-dir |     - pip install tox --no-cache-dir | ||||||
|   script: tox -e py38 |   script: tox -e py38 | ||||||
|  |  | ||||||
| @@ -14,7 +14,7 @@ py39: | |||||||
|   stage: test |   stage: test | ||||||
|   image: python:3.9-alpine |   image: python:3.9-alpine | ||||||
|   before_script: |   before_script: | ||||||
|     - apk add --no-cache gcc libc-dev libffi-dev libmagic libxml2-dev libxslt-dev libxml2-dev libxslt-dev |     - apk add --no-cache gcc libmagic | ||||||
|     - pip install tox --no-cache-dir |     - pip install tox --no-cache-dir | ||||||
|   script: tox -e py39 |   script: tox -e py39 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM python:3-alpine | FROM python:3.8-alpine | ||||||
|  |  | ||||||
| ENV PYTHONUNBUFFERED 1 | ENV PYTHONUNBUFFERED 1 | ||||||
| ENV DJANGO_ALLOW_ASYNC_UNSAFE 1 | ENV DJANGO_ALLOW_ASYNC_UNSAFE 1 | ||||||
| @@ -11,14 +11,12 @@ RUN apk add --no-cache bash | |||||||
| RUN mkdir /code | RUN mkdir /code | ||||||
| WORKDIR /code | WORKDIR /code | ||||||
| COPY requirements.txt /code/requirements.txt | COPY requirements.txt /code/requirements.txt | ||||||
| RUN pip install -r requirements.txt psycopg2-binary sympasoap --no-cache-dir | RUN pip install -r requirements.txt --no-cache-dir | ||||||
|  |  | ||||||
| COPY . /code/ | COPY . /code/ | ||||||
|  |  | ||||||
| RUN python manage.py collectstatic --noinput && \ | RUN python manage.py collectstatic --noinput && \ | ||||||
|         python manage.py compilemessages && \ |         python manage.py compilemessages | ||||||
|         python manage.py migrate && \ |  | ||||||
|         python manage.py loaddata initial |  | ||||||
|  |  | ||||||
| # Configure nginx | # Configure nginx | ||||||
| RUN mkdir /run/nginx | RUN mkdir /run/nginx | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							| @@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least | |||||||
| the "copyright" line and a pointer to where the full notice is found. | the "copyright" line and a pointer to where the full notice is found. | ||||||
|  |  | ||||||
|     <one line to give the program's name and a brief idea of what it does.> |     <one line to give the program's name and a brief idea of what it does.> | ||||||
|     Copyright (C) <year>  <name of author> |     Copyright (C) 2020  Animath | ||||||
|  |  | ||||||
|     This program is free software: you can redistribute it and/or modify |     This program is free software: you can redistribute it and/or modify | ||||||
|     it under the terms of the GNU General Public License as published by |     it under the terms of the GNU General Public License as published by | ||||||
| @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. | |||||||
|   If the program does terminal interaction, make it output a short |   If the program does terminal interaction, make it output a short | ||||||
| notice like this when it starts in an interactive mode: | notice like this when it starts in an interactive mode: | ||||||
|  |  | ||||||
|     <program>  Copyright (C) <year>  <name of author> |     <program>  Copyright (C) 2020  Animath | ||||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||||
|     This is free software, and you are welcome to redistribute it |     This is free software, and you are welcome to redistribute it | ||||||
|     under certain conditions; type `show c' for details. |     under certain conditions; type `show c' for details. | ||||||
|   | |||||||
| @@ -1 +1,4 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| default_app_config = 'api.apps.APIConfig' | default_app_config = 'api.apps.APIConfig' | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from unittest.case import skipIf | from unittest.case import skipIf | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.conf.urls import include, url | from django.conf.urls import include, url | ||||||
| from rest_framework import routers | from rest_framework import routers | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
| from rest_framework.filters import SearchFilter | from rest_framework.filters import SearchFilter | ||||||
|   | |||||||
| @@ -1 +1,4 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| default_app_config = 'eastereggs.apps.EastereggsConfig' | default_app_config = 'eastereggs.apps.EastereggsConfig' | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-21 15:33 | # Generated by Django 3.1.3 on 2020-11-04 12:05 | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| @@ -11,8 +11,8 @@ class Migration(migrations.Migration): | |||||||
|     initial = True |     initial = True | ||||||
|  |  | ||||||
|     dependencies = [ |     dependencies = [ | ||||||
|         ('contenttypes', '0002_remove_content_type_name'), |  | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |         ('contenttypes', '0002_remove_content_type_name'), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by BDE ENS Paris-Saclay | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1 +1,4 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| default_app_config = 'participation.apps.ParticipationConfig' | default_app_config = 'participation.apps.ParticipationConfig' | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.db.models.signals import post_save, pre_delete, pre_save | from django.db.models.signals import post_save, pre_delete, pre_save | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import re | import re | ||||||
|  |  | ||||||
| from bootstrap_datepicker_plus import DateTimePickerInput | from bootstrap_datepicker_plus import DateTimePickerInput | ||||||
| from django import forms | from django import forms | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ObjectDoesNotExist, ValidationError | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| @@ -126,16 +129,20 @@ class SendParticipationForm(forms.ModelForm): | |||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.fields["sent_participation"].initial = self.instance.sent_participation |         try: | ||||||
|  |             self.fields["sent_participation"].initial = self.instance.sent_participation | ||||||
|  |         except ObjectDoesNotExist:     # No sent participation | ||||||
|  |             pass | ||||||
|         self.fields["sent_participation"].queryset = Participation.objects.filter( |         self.fields["sent_participation"].queryset = Participation.objects.filter( | ||||||
|             ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True) |             ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def clean(self, commit=True): |     def clean(self, commit=True): | ||||||
|         cleaned_data = super().clean() |         cleaned_data = super().clean() | ||||||
|         participation = cleaned_data["sent_participation"] |         if "sent_participation" in cleaned_data: | ||||||
|         participation.received_participation = self.instance |             participation = cleaned_data["sent_participation"] | ||||||
|         self.instance = participation |             participation.received_participation = self.instance | ||||||
|  |             self.instance = participation | ||||||
|         return cleaned_data |         return cleaned_data | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
| @@ -151,6 +158,11 @@ class QuestionForm(forms.ModelForm): | |||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")}) |         self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")}) | ||||||
|  |  | ||||||
|  |     def clean(self): | ||||||
|  |         if Phase.current_phase().phase_number != 2: | ||||||
|  |             self.add_error(None, _("You can only create or update a question during the second phase.")) | ||||||
|  |         return super().clean() | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Question |         model = Question | ||||||
|         fields = ('question',) |         fields = ('question',) | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
| from corres2math.matrix import Matrix, RoomVisibility, UploadError | from corres2math.matrix import Matrix, RoomPreset, RoomVisibility | ||||||
| from django.core.management import BaseCommand | from django.core.management import BaseCommand | ||||||
| from nio import RoomPreset |  | ||||||
| from registration.models import AdminRegistration, Registration | from registration.models import AdminRegistration, Registration | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -11,17 +13,19 @@ class Command(BaseCommand): | |||||||
|     def handle(self, *args, **options): |     def handle(self, *args, **options): | ||||||
|         Matrix.set_display_name("Bot des Correspondances") |         Matrix.set_display_name("Bot des Correspondances") | ||||||
|  |  | ||||||
|         if not os.path.isfile(".matrix_avatar"): |         if not os.getenv("SYNAPSE_PASSWORD"): | ||||||
|             stat_file = os.stat("corres2math/static/logo.png") |             avatar_uri = "plop" | ||||||
|             with open("corres2math/static/logo.png", "rb") as f: |         else:  # pragma: no cover | ||||||
|                 resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size) |             if not os.path.isfile(".matrix_avatar"): | ||||||
|             if isinstance(resp, UploadError): |                 stat_file = os.stat("corres2math/static/logo.png") | ||||||
|                 raise Exception(resp) |                 with open("corres2math/static/logo.png", "rb") as f: | ||||||
|             avatar_uri = resp.content_uri |                     resp = Matrix.upload(f, filename="logo.png", content_type="image/png", | ||||||
|             with open(".matrix_avatar", "w") as f: |                                          filesize=stat_file.st_size)[0][0] | ||||||
|                 f.write(avatar_uri) |                 avatar_uri = resp.content_uri | ||||||
|             Matrix.set_avatar(avatar_uri) |                 with open(".matrix_avatar", "w") as f: | ||||||
|         else: |                     f.write(avatar_uri) | ||||||
|  |                 Matrix.set_avatar(avatar_uri) | ||||||
|  |  | ||||||
|             with open(".matrix_avatar", "r") as f: |             with open(".matrix_avatar", "r") as f: | ||||||
|                 avatar_uri = f.read().rstrip(" \t\r\n") |                 avatar_uri = f.read().rstrip(" \t\r\n") | ||||||
|  |  | ||||||
| @@ -55,9 +59,20 @@ class Command(BaseCommand): | |||||||
|                 preset=RoomPreset.public_chat, |                 preset=RoomPreset.public_chat, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |         if not async_to_sync(Matrix.resolve_room_alias)("#flood:correspondances-maths.fr"): | ||||||
|  |             Matrix.create_room( | ||||||
|  |                 visibility=RoomVisibility.public, | ||||||
|  |                 alias="flood", | ||||||
|  |                 name="Flood", | ||||||
|  |                 topic="Discutez de tout et de rien !", | ||||||
|  |                 federate=False, | ||||||
|  |                 preset=RoomPreset.public_chat, | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         Matrix.set_room_avatar("#annonces:correspondances-maths.fr", avatar_uri) |         Matrix.set_room_avatar("#annonces:correspondances-maths.fr", avatar_uri) | ||||||
|         Matrix.set_room_avatar("#faq:correspondances-maths.fr", avatar_uri) |         Matrix.set_room_avatar("#faq:correspondances-maths.fr", avatar_uri) | ||||||
|         Matrix.set_room_avatar("#je-cherche-une-equipe:correspondances-maths.fr", avatar_uri) |         Matrix.set_room_avatar("#je-cherche-une-equipe:correspondances-maths.fr", avatar_uri) | ||||||
|  |         Matrix.set_room_avatar("#flood:correspondances-maths.fr", avatar_uri) | ||||||
|  |  | ||||||
|         Matrix.set_room_power_level_event("#annonces:correspondances-maths.fr", "events_default", 50) |         Matrix.set_room_power_level_event("#annonces:correspondances-maths.fr", "events_default", 50) | ||||||
|  |  | ||||||
| @@ -66,9 +81,12 @@ class Command(BaseCommand): | |||||||
|             Matrix.invite("#faq:correspondances-maths.fr", f"@{r.matrix_username}:correspondances-maths.fr") |             Matrix.invite("#faq:correspondances-maths.fr", f"@{r.matrix_username}:correspondances-maths.fr") | ||||||
|             Matrix.invite("#je-cherche-une-equipe:correspondances-maths.fr", |             Matrix.invite("#je-cherche-une-equipe:correspondances-maths.fr", | ||||||
|                           f"@{r.matrix_username}:correspondances-maths.fr") |                           f"@{r.matrix_username}:correspondances-maths.fr") | ||||||
|  |             Matrix.invite("#flood:correspondances-maths.fr", f"@{r.matrix_username}:correspondances-maths.fr") | ||||||
|  |  | ||||||
|         for admin in AdminRegistration.objects.all(): |         for admin in AdminRegistration.objects.all(): | ||||||
|             Matrix.set_room_power_level("#annonces:correspondances-maths.fr", |             Matrix.set_room_power_level("#annonces:correspondances-maths.fr", | ||||||
|                                         f"@{admin.matrix_username}:correspondances-maths.fr", 95) |                                         f"@{admin.matrix_username}:correspondances-maths.fr", 95) | ||||||
|             Matrix.set_room_power_level("#faq:correspondances-maths.fr", |             Matrix.set_room_power_level("#faq:correspondances-maths.fr", | ||||||
|                                         f"@{admin.matrix_username}:correspondances-maths.fr", 95) |                                         f"@{admin.matrix_username}:correspondances-maths.fr", 95) | ||||||
|  |             Matrix.set_room_power_level("#flood:correspondances-maths.fr", | ||||||
|  |                                         f"@{admin.matrix_username}:correspondances-maths.fr", 95) | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								apps/participation/management/commands/fix_sympa_lists.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								apps/participation/management/commands/fix_sympa_lists.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from corres2math.lists import get_sympa_client | ||||||
|  | from django.core.management import BaseCommand | ||||||
|  | from django.db.models import Q | ||||||
|  | from participation.models import Team | ||||||
|  | from registration.models import CoachRegistration, StudentRegistration | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Command(BaseCommand): | ||||||
|  |     def handle(self, *args, **options): | ||||||
|  |         """ | ||||||
|  |         Create Sympa mailing lists and register teams. | ||||||
|  |         """ | ||||||
|  |         sympa = get_sympa_client() | ||||||
|  |  | ||||||
|  |         sympa.create_list("equipes", "Équipes des Correspondances", "hotline", | ||||||
|  |                           "Liste de diffusion pour contacter toutes les équipes validées des Correspondances.", | ||||||
|  |                           "education", raise_error=False) | ||||||
|  |         sympa.create_list("equipes-non-valides", "Équipes des Correspondances", "hotline", | ||||||
|  |                           "Liste de diffusion pour contacter toutes les équipes non-validées des Correspondances.", | ||||||
|  |                           "education", raise_error=False) | ||||||
|  |  | ||||||
|  |         for problem in range(1, 4): | ||||||
|  |             sympa.create_list(f"probleme-{problem}", | ||||||
|  |                               f"Équipes des Correspondances participant au problème {problem}", "hotline", | ||||||
|  |                               f"Liste de diffusion pour contacter les équipes participant au problème {problem}" | ||||||
|  |                               f" des Correspondances.", "education", raise_error=False) | ||||||
|  |  | ||||||
|  |         for team in Team.objects.filter(participation__valid=True).all(): | ||||||
|  |             team.create_mailing_list() | ||||||
|  |             sympa.subscribe(team.email, "equipes", f"Equipe {team.name}", True) | ||||||
|  |             sympa.subscribe(team.email, f"probleme-{team.participation.problem}", f"Equipe {team.name}", True) | ||||||
|  |  | ||||||
|  |         for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all(): | ||||||
|  |             team.create_mailing_list() | ||||||
|  |             sympa.subscribe(team.email, "equipes-non-valides", f"Equipe {team.name}", True) | ||||||
|  |  | ||||||
|  |         for student in StudentRegistration.objects.filter(team__isnull=False).all(): | ||||||
|  |             sympa.subscribe(student.user.email, f"equipe-{student.team.trigram.lower()}", True, f"{student}") | ||||||
|  |         for coach in CoachRegistration.objects.filter(team__isnull=False).all(): | ||||||
|  |             sympa.subscribe(coach.user.email, f"equipe-{coach.team.trigram.lower()}", True, f"{coach}") | ||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from corres2math.matrix import Matrix, RoomVisibility | from corres2math.matrix import Matrix, RoomVisibility | ||||||
| from django.core.management import BaseCommand | from django.core.management import BaseCommand | ||||||
| from participation.models import Participation | from participation.models import Participation | ||||||
|   | |||||||
| @@ -1,8 +1,39 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-21 15:51 | # Generated by Django 3.1.3 on 2020-11-04 12:05 | ||||||
|  |  | ||||||
| import django.core.validators | import django.core.validators | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| import django.db.models.deletion | import django.utils.timezone | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def register_phases(apps, _): | ||||||
|  |     """ | ||||||
|  |     Import the different phases of the action | ||||||
|  |     """ | ||||||
|  |     Phase = apps.get_model("participation", "phase") | ||||||
|  |     Phase.objects.get_or_create( | ||||||
|  |         phase_number=1, | ||||||
|  |         description="Soumission des vidéos", | ||||||
|  |     ) | ||||||
|  |     Phase.objects.get_or_create( | ||||||
|  |         phase_number=2, | ||||||
|  |         description="Phase de questions", | ||||||
|  |     ) | ||||||
|  |     Phase.objects.get_or_create( | ||||||
|  |         phase_number=3, | ||||||
|  |         description="Phase d'échanges entre les équipes", | ||||||
|  |     ) | ||||||
|  |     Phase.objects.get_or_create( | ||||||
|  |         phase_number=4, | ||||||
|  |         description="Synthèse de l'échange", | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def reverse_phase_registering(apps, _):  # pragma: no cover | ||||||
|  |     """ | ||||||
|  |     Drop all phases in order to unapply this migration. | ||||||
|  |     """ | ||||||
|  |     Phase = apps.get_model("participation", "phase") | ||||||
|  |     Phase.objects.all().delete() | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
| @@ -18,12 +49,33 @@ class Migration(migrations.Migration): | |||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('problem', models.IntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3')], default=None, null=True, verbose_name='problem number')), |                 ('problem', models.IntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3')], default=None, null=True, verbose_name='problem number')), | ||||||
|  |                 ('valid', models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid')), | ||||||
|             ], |             ], | ||||||
|             options={ |             options={ | ||||||
|                 'verbose_name': 'participation', |                 'verbose_name': 'participation', | ||||||
|                 'verbose_name_plural': 'participations', |                 'verbose_name_plural': 'participations', | ||||||
|             }, |             }, | ||||||
|         ), |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Phase', | ||||||
|  |             fields=[ | ||||||
|  |                 ('phase_number', models.AutoField(primary_key=True, serialize=False, unique=True, verbose_name='phase number')), | ||||||
|  |                 ('description', models.CharField(max_length=255, verbose_name='phase description')), | ||||||
|  |                 ('start', models.DateTimeField(default=django.utils.timezone.now, verbose_name='start date of the given phase')), | ||||||
|  |                 ('end', models.DateTimeField(default=django.utils.timezone.now, verbose_name='end date of the given phase')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'phase', | ||||||
|  |                 'verbose_name_plural': 'phases', | ||||||
|  |             }, | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='Question', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('question', models.TextField(verbose_name='question')), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name='Team', |             name='Team', | ||||||
|             fields=[ |             fields=[ | ||||||
| @@ -31,6 +83,7 @@ class Migration(migrations.Migration): | |||||||
|                 ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), |                 ('name', models.CharField(max_length=255, unique=True, verbose_name='name')), | ||||||
|                 ('trigram', models.CharField(help_text='The trigram must be composed of three uppercase letters.', max_length=3, unique=True, validators=[django.core.validators.RegexValidator('[A-Z]{3}')], verbose_name='trigram')), |                 ('trigram', models.CharField(help_text='The trigram must be composed of three uppercase letters.', max_length=3, unique=True, validators=[django.core.validators.RegexValidator('[A-Z]{3}')], verbose_name='trigram')), | ||||||
|                 ('access_code', models.CharField(help_text='The access code let other people to join the team.', max_length=6, verbose_name='access code')), |                 ('access_code', models.CharField(help_text='The access code let other people to join the team.', max_length=6, verbose_name='access code')), | ||||||
|  |                 ('grant_animath_access_videos', models.BooleanField(default=False, help_text='Give the authorisation to publish the video on the main website to promote the action.', verbose_name='Grant Animath to publish my video')), | ||||||
|             ], |             ], | ||||||
|             options={ |             options={ | ||||||
|                 'verbose_name': 'team', |                 'verbose_name': 'team', | ||||||
| @@ -43,7 +96,6 @@ class Migration(migrations.Migration): | |||||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('link', models.URLField(help_text='The full video link.', verbose_name='link')), |                 ('link', models.URLField(help_text='The full video link.', verbose_name='link')), | ||||||
|                 ('valid', models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid')), |                 ('valid', models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid')), | ||||||
|                 ('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.participation', verbose_name='participation')), |  | ||||||
|             ], |             ], | ||||||
|             options={ |             options={ | ||||||
|                 'verbose_name': 'video', |                 'verbose_name': 'video', | ||||||
| @@ -54,6 +106,11 @@ class Migration(migrations.Migration): | |||||||
|             model_name='team', |             model_name='team', | ||||||
|             index=models.Index(fields=['trigram'], name='participati_trigram_239255_idx'), |             index=models.Index(fields=['trigram'], name='participati_trigram_239255_idx'), | ||||||
|         ), |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='question', | ||||||
|  |             name='participation', | ||||||
|  |             field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='participation.participation', verbose_name='participation'), | ||||||
|  |         ), | ||||||
|         migrations.AddField( |         migrations.AddField( | ||||||
|             model_name='participation', |             model_name='participation', | ||||||
|             name='received_participation', |             name='received_participation', | ||||||
| @@ -62,16 +119,20 @@ class Migration(migrations.Migration): | |||||||
|         migrations.AddField( |         migrations.AddField( | ||||||
|             model_name='participation', |             model_name='participation', | ||||||
|             name='solution', |             name='solution', | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='participation.video', verbose_name='solution video'), |             field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation_solution', to='participation.video', verbose_name='solution video'), | ||||||
|         ), |         ), | ||||||
|         migrations.AddField( |         migrations.AddField( | ||||||
|             model_name='participation', |             model_name='participation', | ||||||
|             name='synthesis', |             name='synthesis', | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='participation.video', verbose_name='synthesis video'), |             field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation_synthesis', to='participation.video', verbose_name='synthesis video'), | ||||||
|         ), |         ), | ||||||
|         migrations.AddField( |         migrations.AddField( | ||||||
|             model_name='participation', |             model_name='participation', | ||||||
|             name='team', |             name='team', | ||||||
|             field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.team', verbose_name='team'), |             field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.team', verbose_name='team'), | ||||||
|         ), |         ), | ||||||
|  |         migrations.RunPython( | ||||||
|  |             register_phases, | ||||||
|  |             reverse_code=reverse_phase_registering, | ||||||
|  |         ) | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,18 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-22 16:21 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0001_initial'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='team', |  | ||||||
|             name='grant_animath_access_videos', |  | ||||||
|             field=models.BooleanField(default=False, help_text='Give the authorisation to publish the video on the main website to promote the action.', verbose_name='Grant Animath to publish my video'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-27 11:09 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0002_team_grant_animath_access_videos'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name='video', |  | ||||||
|             name='participation', |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='participation', |  | ||||||
|             name='solution', |  | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation', to='participation.video', verbose_name='solution video'), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='participation', |  | ||||||
|             name='synthesis', |  | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation_synthesis', to='participation.video', verbose_name='synthesis video'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-27 11:22 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0003_auto_20200927_1309'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='participation', |  | ||||||
|             name='solution', |  | ||||||
|             field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation_solution', to='participation.video', verbose_name='solution video'), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='participation', |  | ||||||
|             name='synthesis', |  | ||||||
|             field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participation_synthesis', to='participation.video', verbose_name='synthesis video'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-10-11 13:58 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0004_auto_20200927_1322'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='participation', |  | ||||||
|             name='valid', |  | ||||||
|             field=models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-10-20 10:39 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.utils.timezone |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def register_phases(apps, schema_editor): |  | ||||||
|     """ |  | ||||||
|     Import the different phases of the action |  | ||||||
|     """ |  | ||||||
|     Phase = apps.get_model("participation", "phase") |  | ||||||
|     Phase.objects.get_or_create( |  | ||||||
|         phase_number=1, |  | ||||||
|         description="Soumission des vidéos", |  | ||||||
|     ) |  | ||||||
|     Phase.objects.get_or_create( |  | ||||||
|         phase_number=2, |  | ||||||
|         description="Phase de questions", |  | ||||||
|     ) |  | ||||||
|     Phase.objects.get_or_create( |  | ||||||
|         phase_number=3, |  | ||||||
|         description="Phase d'échanges entre les équipes", |  | ||||||
|     ) |  | ||||||
|     Phase.objects.get_or_create( |  | ||||||
|         phase_number=4, |  | ||||||
|         description="Synthèse de l'échange", |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def reverse_phase_registering(apps, schema_editor): |  | ||||||
|     """ |  | ||||||
|     Drop all phases in order to unapply this migration. |  | ||||||
|     """ |  | ||||||
|     Phase = apps.get_model("participation", "phase") |  | ||||||
|     Phase.objects.all().delete() |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0005_participation_valid'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name='Phase', |  | ||||||
|             fields=[ |  | ||||||
|                 ('phase_number', models.AutoField(primary_key=True, serialize=False, unique=True, verbose_name='phase number')), |  | ||||||
|                 ('description', models.CharField(max_length=255, verbose_name="phase description")), |  | ||||||
|                 ('start', models.DateTimeField(default=django.utils.timezone.now, verbose_name='start date of the given phase')), |  | ||||||
|                 ('end', models.DateTimeField(default=django.utils.timezone.now, verbose_name='end date of the given phase')), |  | ||||||
|             ], |  | ||||||
|             options={ |  | ||||||
|                 'verbose_name': 'phase', |  | ||||||
|                 'verbose_name_plural': 'phases', |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|         migrations.RunPython( |  | ||||||
|             register_phases, |  | ||||||
|             reverse_phase_registering, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| # Generated by Django 3.0.10 on 2020-10-31 13:36 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0006_phase'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name='Question', |  | ||||||
|             fields=[ |  | ||||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |  | ||||||
|                 ('question', models.TextField(verbose_name='question')), |  | ||||||
|                 ('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='participation.Participation', verbose_name='participation')), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
|  |  | ||||||
| from corres2math.lists import get_sympa_client | from corres2math.lists import get_sympa_client | ||||||
| from corres2math.matrix import Matrix | from corres2math.matrix import Matrix, RoomPreset, RoomVisibility | ||||||
| from django.core.exceptions import ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.core.validators import RegexValidator | from django.core.validators import RegexValidator | ||||||
| from django.db import models | from django.db import models | ||||||
| @@ -13,7 +16,6 @@ from django.utils import timezone | |||||||
| from django.utils.crypto import get_random_string | from django.utils.crypto import get_random_string | ||||||
| from django.utils.text import format_lazy | from django.utils.text import format_lazy | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from nio import RoomPreset, RoomVisibility |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Team(models.Model): | class Team(models.Model): | ||||||
| @@ -66,11 +68,22 @@ class Team(models.Model): | |||||||
|             "education", |             "education", | ||||||
|             raise_error=False, |             raise_error=False, | ||||||
|         ) |         ) | ||||||
|  |         if self.pk and self.participation.valid:  # pragma: no cover | ||||||
|  |             get_sympa_client().subscribe(self.email, "equipes", False, f"Equipe {self.name}") | ||||||
|  |             get_sympa_client().subscribe(self.email, f"probleme-{self.participation.problem}", False, | ||||||
|  |                                          f"Equipe {self.name}") | ||||||
|  |         else: | ||||||
|  |             get_sympa_client().subscribe(self.email, "equipes-non-valides", False) | ||||||
|  |  | ||||||
|     def delete_mailing_list(self): |     def delete_mailing_list(self): | ||||||
|         """ |         """ | ||||||
|         Drop the Sympa mailing list, if the team is empty or if the trigram changed. |         Drop the Sympa mailing list, if the team is empty or if the trigram changed. | ||||||
|         """ |         """ | ||||||
|  |         if self.participation.valid:  # pragma: no cover | ||||||
|  |             get_sympa_client().unsubscribe(self.email, "equipes", False) | ||||||
|  |             get_sympa_client().unsubscribe(self.email, f"probleme-{self.participation.problem}", False) | ||||||
|  |         else: | ||||||
|  |             get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False) | ||||||
|         get_sympa_client().delete_list(f"equipe-{self.trigram}") |         get_sympa_client().delete_list(f"equipe-{self.trigram}") | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
| @@ -275,17 +288,15 @@ class Phase(models.Model): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def current_phase(cls): |     def current_phase(cls) -> "Phase": | ||||||
|         """ |         """ | ||||||
|         Retrieve the current phase of this day |         Retrieve the current phase of this day | ||||||
|         """ |         """ | ||||||
|         qs = Phase.objects.filter(start__lte=timezone.now(), end__gte=timezone.now()) |         qs = Phase.objects.filter(start__lte=timezone.now(), end__gte=timezone.now()) | ||||||
|         if qs.exists(): |         if qs.exists(): | ||||||
|             return qs.get() |             return qs.get() | ||||||
|         qs = Phase.objects.order_by("phase_number").all() |         qs = Phase.objects.filter(start__lte=timezone.now()).order_by("phase_number").all() | ||||||
|         if timezone.now() < qs.first().start: |         return qs.last() if qs.exists() else None | ||||||
|             return qs.first() |  | ||||||
|         return qs.last() |  | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         return _("Phase {phase_number:d} starts on {start:%Y-%m-%d %H:%M} and ends on {end:%Y-%m-%d %H:%M}")\ |         return _("Phase {phase_number:d} starts on {start:%Y-%m-%d %H:%M} and ends on {end:%Y-%m-%d %H:%M}")\ | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from haystack import indexes | from haystack import indexes | ||||||
|  |  | ||||||
| from .models import Participation, Team, Video | from .models import Participation, Team, Video | ||||||
|   | |||||||
| @@ -1,8 +1,11 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from corres2math.lists import get_sympa_client | from corres2math.lists import get_sympa_client | ||||||
| from participation.models import Participation, Team, Video | from participation.models import Participation, Team, Video | ||||||
|  |  | ||||||
|  |  | ||||||
| def create_team_participation(instance, **_): | def create_team_participation(instance, created, **_): | ||||||
|     """ |     """ | ||||||
|     When a team got created, create an associated team and create Video objects. |     When a team got created, create an associated team and create Video objects. | ||||||
|     """ |     """ | ||||||
| @@ -12,6 +15,8 @@ def create_team_participation(instance, **_): | |||||||
|     if not participation.synthesis: |     if not participation.synthesis: | ||||||
|         participation.synthesis = Video.objects.create() |         participation.synthesis = Video.objects.create() | ||||||
|     participation.save() |     participation.save() | ||||||
|  |     if not created: | ||||||
|  |         participation.team.create_mailing_list() | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_mailing_list(instance: Team, **_): | def update_mailing_list(instance: Team, **_): | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| import django_tables2 as tables | import django_tables2 as tables | ||||||
| @@ -39,6 +42,9 @@ class TeamTable(tables.Table): | |||||||
|         attrs = { |         attrs = { | ||||||
|             'class': 'table table condensed table-striped', |             'class': 'table table condensed table-striped', | ||||||
|         } |         } | ||||||
|  |         row_attrs = { | ||||||
|  |             'class': lambda record: '' if record.participation.solution.link else 'bg-warning', | ||||||
|  |         } | ||||||
|         model = Team |         model = Team | ||||||
|         fields = ('name', 'trigram', 'problem',) |         fields = ('name', 'trigram', 'problem',) | ||||||
|         template_name = 'django_tables2/bootstrap4.html' |         template_name = 'django_tables2/bootstrap4.html' | ||||||
|   | |||||||
| @@ -1,16 +1,39 @@ | |||||||
| {% extends "base.html" %} | {% extends "base.html" %} | ||||||
|  |  | ||||||
| {% block extracss %} | {% load i18n %} | ||||||
|     <style> |  | ||||||
|     iframe { |  | ||||||
|         width: 100%; |  | ||||||
|         height: 100vh; |  | ||||||
|         margin: 0; |  | ||||||
|         padding: 0; |  | ||||||
|     } |  | ||||||
|     </style> |  | ||||||
| {% endblock %} |  | ||||||
|  |  | ||||||
| {% block fullcontent %} | {% block content %} | ||||||
|     <iframe frameborder="0" src="https://element.correspondances-maths.fr/#/room/#faq:correspondances-maths.fr"></iframe> |     <div class="alert alert-warning"> | ||||||
|  |     {% blocktrans trimmed %} | ||||||
|  |         The chat is located on the dedicated Matrix server: | ||||||
|  |     {% endblocktrans %} | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="alert text-center"> | ||||||
|  |     <a class="btn btn-success" href="https://element.correspondances-maths.fr/#/room/#faq:correspondances-maths.fr" target="_blank"> | ||||||
|  |         <i class="fas fa-server"></i> {% trans "Access to the Matrix server" %} | ||||||
|  |     </a> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     <div class="alert alert-info"> | ||||||
|  |     <p> | ||||||
|  |     {% blocktrans trimmed %} | ||||||
|  |         To connect to the server, you can select "Log in", then use your credentials of this platform to connect | ||||||
|  |         with the central authentication server, then you must trust the connection between the Matrix account and the | ||||||
|  |         platform. Finally, you will be able to access to the chat platform. | ||||||
|  |     {% endblocktrans %} | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     {% blocktrans trimmed %} | ||||||
|  |         You will be invited in some basic rooms. You must confirm the invitations to join channels. | ||||||
|  |     {% endblocktrans %} | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     {% blocktrans trimmed %} | ||||||
|  |         If you have any trouble, don't hesitate to contact us :) | ||||||
|  |     {% endblocktrans %} | ||||||
|  |     </p> | ||||||
|  |     </div> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|   | |||||||
| @@ -13,7 +13,9 @@ Bonjour {{ user.registration }}, | |||||||
| L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer | L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer | ||||||
| au {{ team.participation.get_problem_display }} des Correspondances des Jeunes Mathématicien·ne·s. | au {{ team.participation.get_problem_display }} des Correspondances des Jeunes Mathématicien·ne·s. | ||||||
| Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe : | Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe : | ||||||
| <a href="{% url "participation:team_detail" pk=team.pk %}">{% url "participation:team_detail" pk=team.pk %}</a> | <a href="https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}"> | ||||||
|  |     https://{{ domain }}{% url "participation:team_detail" pk=team.pk %} | ||||||
|  | </a> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| <p> | <p> | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ Bonjour {{ user.registration }}, | |||||||
| L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer | L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer | ||||||
| au {{ team.participation.get_problem_display }} des Correspondances des Jeunes Mathématicien·ne·s. | au {{ team.participation.get_problem_display }} des Correspondances des Jeunes Mathématicien·ne·s. | ||||||
| Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe : | Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe : | ||||||
| {% url "participation:team_detail" pk=team.pk %} | https://{{ domain }}{% url "participation:team_detail" pk=team.pk %} | ||||||
|  |  | ||||||
| Cordialement, | Cordialement, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -194,13 +194,14 @@ | |||||||
|         {% trans "This video platform is not supported yet." as unsupported_platform %} |         {% trans "This video platform is not supported yet." as unsupported_platform %} | ||||||
|         {% include "base_modal.html" with modal_id="displayOtherSolution" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.received_participation.solution.as_iframe|default:unsupported_platform %} |         {% include "base_modal.html" with modal_id="displayOtherSolution" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.received_participation.solution.as_iframe|default:unsupported_platform %} | ||||||
|     {% endif %} |     {% endif %} | ||||||
| {% endif %} |  | ||||||
|  |  | ||||||
| {% if user.registration.participates and current_phase.phase_number == 2 %} |     {% if current_phase.phase_number == 2 %} | ||||||
|     {% trans "Add question" as modal_title %} |         {% trans "Add question" as modal_title %} | ||||||
|     {% trans "Add" as modal_button %} |         {% trans "Add" as modal_button %} | ||||||
|     {% url "participation:add_question" pk=participation.pk as modal_action %} |         {% url "participation:add_question" pk=participation.pk as modal_action %} | ||||||
|     {% include "base_modal.html" with modal_id="addQuestion" modal_button_type="success" %} |         {% include "base_modal.html" with modal_id="addQuestion" modal_button_type="success" %} | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|     {% for question in participation.questions.all %} |     {% for question in participation.questions.all %} | ||||||
|         {% with number_str=forloop.counter|stringformat:"d"%} |         {% with number_str=forloop.counter|stringformat:"d"%} | ||||||
|             {% with modal_id="updateQuestion"|add:number_str %} |             {% with modal_id="updateQuestion"|add:number_str %} | ||||||
| @@ -260,27 +261,29 @@ | |||||||
|                 }); |                 }); | ||||||
|             {% endif %} |             {% endif %} | ||||||
|  |  | ||||||
|             {% if user.registration.participates and current_phase.phase_number == 2 %} |             {% if current_phase.phase_number == 2 %} | ||||||
|                 $('button[data-target="#addQuestionModal"]').click(function() { |                 $('button[data-target="#addQuestionModal"]').click(function() { | ||||||
|                     let modalBody = $("#addQuestionModal div.modal-body"); |                     let modalBody = $("#addQuestionModal div.modal-body"); | ||||||
|                     if (!modalBody.html().trim()) |                     if (!modalBody.html().trim()) | ||||||
|                         modalBody.load("{% url "participation:add_question" pk=participation.pk %} #form-content"); |                         modalBody.load("{% url "participation:add_question" pk=participation.pk %} #form-content"); | ||||||
|                 }); |                 }); | ||||||
|  |             {% endif %} | ||||||
|  |  | ||||||
|                 {% for question in participation.questions.all %} |             {% for question in participation.questions.all %} | ||||||
|                     $('button[data-target="#updateQuestion{{ forloop.counter }}Modal"]').click(function() { |                 $('button[data-target="#updateQuestion{{ forloop.counter }}Modal"]').click(function() { | ||||||
|                         let modalBody = $("#updateQuestion{{ forloop.counter }}Modal div.modal-body"); |                     let modalBody = $("#updateQuestion{{ forloop.counter }}Modal div.modal-body"); | ||||||
|                         if (!modalBody.html().trim()) |                     if (!modalBody.html().trim()) | ||||||
|                             modalBody.load("{% url "participation:update_question" pk=question.pk %} #form-content"); |                         modalBody.load("{% url "participation:update_question" pk=question.pk %} #form-content"); | ||||||
|                     }); |                 }); | ||||||
|  |  | ||||||
|  |                 {% if current_phase.phase_number == 2 %} | ||||||
|                     $('button[data-target="#deleteQuestion{{ forloop.counter }}Modal"]').click(function() { |                     $('button[data-target="#deleteQuestion{{ forloop.counter }}Modal"]').click(function() { | ||||||
|                         let modalBody = $("#deleteQuestion{{ forloop.counter }}Modal div.modal-body"); |                         let modalBody = $("#deleteQuestion{{ forloop.counter }}Modal div.modal-body"); | ||||||
|                         if (!modalBody.html().trim()) |                         if (!modalBody.html().trim()) | ||||||
|                             modalBody.load("{% url "participation:delete_question" pk=question.pk %} #form-content"); |                             modalBody.load("{% url "participation:delete_question" pk=question.pk %} #form-content"); | ||||||
|                     }); |                     }); | ||||||
|                 {% endfor %} |                 {% endif %} | ||||||
|             {% endif %} |             {% endfor %} | ||||||
|  |  | ||||||
|             $('button[data-target="#uploadSolutionModal"]').click(function() { |             $('button[data-target="#uploadSolutionModal"]').click(function() { | ||||||
|                 let modalBody = $("#uploadSolutionModal div.modal-body"); |                 let modalBody = $("#uploadSolutionModal div.modal-body"); | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								apps/participation/templates/participation/team_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								apps/participation/templates/participation/team_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% load django_tables2 i18n %} | ||||||
|  |  | ||||||
|  | {% block contenttitle %} | ||||||
|  |     <h1>{% trans "All teams" %}</h1> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |     <div id="form-content"> | ||||||
|  |         {% render_table table %} | ||||||
|  |     </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										2
									
								
								apps/participation/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								apps/participation/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
							
								
								
									
										15
									
								
								apps/participation/templatetags/calendar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								apps/participation/templatetags/calendar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from django import template | ||||||
|  |  | ||||||
|  | from ..models import Phase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def current_phase(nb): | ||||||
|  |     phase = Phase.current_phase() | ||||||
|  |     return phase is not None and phase.phase_number == nb | ||||||
|  |  | ||||||
|  |  | ||||||
|  | register = template.Library() | ||||||
|  | register.filter("current_phase", current_phase) | ||||||
| @@ -1,10 +1,12 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  |  | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.contrib.sites.models import Site | from django.contrib.sites.models import Site | ||||||
| from django.core.management import call_command | from django.core.management import call_command | ||||||
| from django.db.models import F |  | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| @@ -186,6 +188,13 @@ class TestStudentParticipation(TestCase): | |||||||
|         )) |         )) | ||||||
|         self.assertEqual(response.status_code, 403) |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|  |     def test_team_list(self): | ||||||
|  |         """ | ||||||
|  |         Test to display the list of teams. | ||||||
|  |         """ | ||||||
|  |         response = self.client.get(reverse("participation:team_list")) | ||||||
|  |         self.assertTrue(response.status_code, 200) | ||||||
|  |  | ||||||
|     def test_no_myteam_redirect_noteam(self): |     def test_no_myteam_redirect_noteam(self): | ||||||
|         """ |         """ | ||||||
|         Test redirection. |         Test redirection. | ||||||
| @@ -403,12 +412,6 @@ class TestStudentParticipation(TestCase): | |||||||
|         self.user.registration.team = self.team |         self.user.registration.team = self.team | ||||||
|         self.user.registration.save() |         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 |         # Team is valid | ||||||
|         self.team.participation.valid = True |         self.team.participation.valid = True | ||||||
|         self.team.participation.save() |         self.team.participation.save() | ||||||
| @@ -493,6 +496,81 @@ class TestStudentParticipation(TestCase): | |||||||
|         response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) |         response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,))) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         # Set the second phase | ||||||
|  |         for i in range(1, 5): | ||||||
|  |             Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2), | ||||||
|  |                                                         end=timezone.now() + timedelta(days=i - 1)) | ||||||
|  |         self.assertEqual(Phase.current_phase().phase_number, 2) | ||||||
|  |  | ||||||
|  |         # Can't update the link during the second phase | ||||||
|  |         response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)), | ||||||
|  |                                     data=dict(link="https://youtube.com/watch?v=73nsrixx7eI")) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_questions(self): | ||||||
|  |         """ | ||||||
|  |         Ensure that creating/updating/deleting a question is working. | ||||||
|  |         """ | ||||||
|  |         self.user.registration.team = self.team | ||||||
|  |         self.user.registration.save() | ||||||
|  |  | ||||||
|  |         self.team.participation.valid = True | ||||||
|  |         self.team.participation.save() | ||||||
|  |  | ||||||
|  |         response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,))) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         # We are not in second phase | ||||||
|  |         response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)), | ||||||
|  |                                     data=dict(question="I got censored!")) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         # Set the second phase | ||||||
|  |         for i in range(1, 5): | ||||||
|  |             Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2), | ||||||
|  |                                                         end=timezone.now() + timedelta(days=i - 1)) | ||||||
|  |         self.assertEqual(Phase.current_phase().phase_number, 2) | ||||||
|  |  | ||||||
|  |         # Create a question | ||||||
|  |         response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)), | ||||||
|  |                                     data=dict(question="I asked a question!")) | ||||||
|  |         self.assertRedirects(response, reverse("participation:participation_detail", | ||||||
|  |                                                args=(self.team.participation.pk,)), 302, 200) | ||||||
|  |         qs = Question.objects.filter(participation=self.team.participation, question="I asked a question!") | ||||||
|  |         self.assertTrue(qs.exists()) | ||||||
|  |         question = qs.get() | ||||||
|  |  | ||||||
|  |         # Update a question | ||||||
|  |         response = self.client.get(reverse("participation:update_question", args=(question.pk,))) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         response = self.client.post(reverse("participation:update_question", args=(question.pk,)), data=dict( | ||||||
|  |             question="The question changed!", | ||||||
|  |         )) | ||||||
|  |         self.assertRedirects(response, reverse("participation:participation_detail", | ||||||
|  |                                                args=(self.team.participation.pk,)), 302, 200) | ||||||
|  |         question.refresh_from_db() | ||||||
|  |         self.assertEqual(question.question, "The question changed!") | ||||||
|  |  | ||||||
|  |         # Delete the question | ||||||
|  |         response = self.client.get(reverse("participation:delete_question", args=(question.pk,))) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         response = self.client.post(reverse("participation:delete_question", args=(question.pk,))) | ||||||
|  |         self.assertRedirects(response, reverse("participation:participation_detail", | ||||||
|  |                                                args=(self.team.participation.pk,)), 302, 200) | ||||||
|  |         self.assertFalse(Question.objects.filter(pk=question.pk).exists()) | ||||||
|  |  | ||||||
|  |         # Non-authenticated users are redirected to login page | ||||||
|  |         self.client.logout() | ||||||
|  |         response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + | ||||||
|  |                              reverse("participation:add_question", args=(self.team.participation.pk,)), 302, 200) | ||||||
|  |         response = self.client.get(reverse("participation:update_question", args=(self.question.pk,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + | ||||||
|  |                              reverse("participation:update_question", args=(self.question.pk,)), 302, 200) | ||||||
|  |         response = self.client.get(reverse("participation:delete_question", args=(self.question.pk,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + | ||||||
|  |                              reverse("participation:delete_question", args=(self.question.pk,)), 302, 200) | ||||||
|  |  | ||||||
|     def test_current_phase(self): |     def test_current_phase(self): | ||||||
|         """ |         """ | ||||||
|         Ensure that the current phase is the good one. |         Ensure that the current phase is the good one. | ||||||
| @@ -501,7 +579,7 @@ class TestStudentParticipation(TestCase): | |||||||
|         for i in range(1, 5): |         for i in range(1, 5): | ||||||
|             Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=2 * i), |             Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=2 * i), | ||||||
|                                                         end=timezone.now() + timedelta(days=2 * i + 1)) |                                                         end=timezone.now() + timedelta(days=2 * i + 1)) | ||||||
|         self.assertEqual(Phase.current_phase().phase_number, 1) |         self.assertEqual(Phase.current_phase(), None) | ||||||
|  |  | ||||||
|         # We are after the end |         # We are after the end | ||||||
|         for i in range(1, 5): |         for i in range(1, 5): | ||||||
| @@ -578,9 +656,17 @@ class TestStudentParticipation(TestCase): | |||||||
|         )) |         )) | ||||||
|         self.assertEqual(response.status_code, 200) |         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): |     def test_forbidden_access(self): | ||||||
|         """ |         """ | ||||||
|         Load personnal pages and ensure that these are protected. |         Load personal pages and ensure that these are protected. | ||||||
|         """ |         """ | ||||||
|         self.user.registration.team = self.team |         self.user.registration.team = self.team | ||||||
|         self.user.registration.save() |         self.user.registration.save() | ||||||
| @@ -608,6 +694,21 @@ class TestStudentParticipation(TestCase): | |||||||
|         resp = self.client.get(reverse("participation:delete_question", args=(question.pk,))) |         resp = self.client.get(reverse("participation:delete_question", args=(question.pk,))) | ||||||
|         self.assertEqual(resp.status_code, 403) |         self.assertEqual(resp.status_code, 403) | ||||||
|  |  | ||||||
|  |     def test_cover_matrix(self): | ||||||
|  |         """ | ||||||
|  |         Load matrix scripts, to cover them and ensure that they can run. | ||||||
|  |         """ | ||||||
|  |         self.user.registration.team = self.team | ||||||
|  |         self.user.registration.save() | ||||||
|  |         self.second_user.registration.team = self.second_team | ||||||
|  |         self.second_user.registration.save() | ||||||
|  |         self.team.participation.valid = True | ||||||
|  |         self.team.participation.received_participation = self.second_team.participation | ||||||
|  |         self.team.participation.save() | ||||||
|  |  | ||||||
|  |         call_command('fix_matrix_channels') | ||||||
|  |         call_command('setup_third_phase') | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestAdmin(TestCase): | class TestAdmin(TestCase): | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
| @@ -618,25 +719,96 @@ class TestAdmin(TestCase): | |||||||
|         ) |         ) | ||||||
|         self.client.force_login(self.user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|  |         self.team1 = Team.objects.create( | ||||||
|  |             name="Toto", | ||||||
|  |             trigram="TOT", | ||||||
|  |         ) | ||||||
|  |         self.team1.participation.valid = True | ||||||
|  |         self.team1.participation.problem = 1 | ||||||
|  |         self.team1.participation.save() | ||||||
|  |  | ||||||
|  |         self.team2 = Team.objects.create( | ||||||
|  |             name="Bliblu", | ||||||
|  |             trigram="BIU", | ||||||
|  |         ) | ||||||
|  |         self.team2.participation.valid = True | ||||||
|  |         self.team2.participation.problem = 1 | ||||||
|  |         self.team2.participation.save() | ||||||
|  |  | ||||||
|  |         self.team3 = Team.objects.create( | ||||||
|  |             name="Zouplop", | ||||||
|  |             trigram="ZPL", | ||||||
|  |         ) | ||||||
|  |         self.team3.participation.valid = True | ||||||
|  |         self.team3.participation.problem = 1 | ||||||
|  |         self.team3.participation.save() | ||||||
|  |  | ||||||
|  |         self.other_team = Team.objects.create( | ||||||
|  |             name="I am different", | ||||||
|  |             trigram="IAD", | ||||||
|  |         ) | ||||||
|  |         self.other_team.participation.valid = True | ||||||
|  |         self.other_team.participation.problem = 2 | ||||||
|  |         self.other_team.participation.save() | ||||||
|  |  | ||||||
|     def test_research(self): |     def test_research(self): | ||||||
|         """ |         """ | ||||||
|         Try to search some things. |         Try to search some things. | ||||||
|         """ |         """ | ||||||
|         team = Team.objects.create( |  | ||||||
|             name="Best team ever", |  | ||||||
|             trigram="BTE", |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         call_command("rebuild_index", "--noinput", "--verbosity", 0) |         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.assertEqual(response.status_code, 200) | ||||||
|         self.assertTrue(response.context["object_list"]) |         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.assertEqual(response.status_code, 200) | ||||||
|         self.assertTrue(response.context["object_list"]) |         self.assertTrue(response.context["object_list"]) | ||||||
|  |  | ||||||
|  |     def test_set_received_video(self): | ||||||
|  |         """ | ||||||
|  |         Try to define the received video of a participation. | ||||||
|  |         """ | ||||||
|  |         response = self.client.get(reverse("participation:participation_receive_participation", | ||||||
|  |                                            args=(self.team1.participation.pk,))) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         response = self.client.post(reverse("participation:participation_receive_participation", | ||||||
|  |                                             args=(self.team1.participation.pk,)), | ||||||
|  |                                     data=dict(received_participation=self.team2.participation.pk)) | ||||||
|  |         self.assertRedirects(response, reverse("participation:participation_detail", | ||||||
|  |                                                args=(self.team1.participation.pk,)), 302, 200) | ||||||
|  |  | ||||||
|  |         response = self.client.get(reverse("participation:participation_receive_participation", | ||||||
|  |                                            args=(self.team1.participation.pk,))) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         response = self.client.post(reverse("participation:participation_send_participation", | ||||||
|  |                                             args=(self.team1.participation.pk,)), | ||||||
|  |                                     data=dict(sent_participation=self.team3.participation.pk)) | ||||||
|  |         self.assertRedirects(response, reverse("participation:participation_detail", | ||||||
|  |                                                args=(self.team1.participation.pk,)), 302, 200) | ||||||
|  |  | ||||||
|  |         self.team1.participation.refresh_from_db() | ||||||
|  |         self.team2.participation.refresh_from_db() | ||||||
|  |         self.team3.participation.refresh_from_db() | ||||||
|  |  | ||||||
|  |         self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk) | ||||||
|  |         self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk) | ||||||
|  |         self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk) | ||||||
|  |         self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk) | ||||||
|  |  | ||||||
|  |         # The other team didn't work on the same problem | ||||||
|  |         response = self.client.post(reverse("participation:participation_receive_participation", | ||||||
|  |                                             args=(self.team1.participation.pk,)), | ||||||
|  |                                     data=dict(received_participation=self.other_team.participation.pk)) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         response = self.client.post(reverse("participation:participation_send_participation", | ||||||
|  |                                             args=(self.team1.participation.pk,)), | ||||||
|  |                                     data=dict(sent_participation=self.other_team.participation.pk)) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|     def test_create_team_forbidden(self): |     def test_create_team_forbidden(self): | ||||||
|         """ |         """ | ||||||
|         Ensure that an admin can't create a team. |         Ensure that an admin can't create a team. | ||||||
|   | |||||||
| @@ -1,10 +1,13 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
|  |  | ||||||
| from .views import CalendarView, CreateQuestionView, CreateTeamView, DeleteQuestionView, JoinTeamView, \ | from .views import CalendarView, CreateQuestionView, CreateTeamView, DeleteQuestionView, JoinTeamView, \ | ||||||
|     MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PhaseUpdateView, \ |     MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PhaseUpdateView, \ | ||||||
|     SetParticipationReceiveParticipationView, SetParticipationSendParticipationView, TeamAuthorizationsView, \ |     SetParticipationReceiveParticipationView, SetParticipationSendParticipationView, TeamAuthorizationsView, \ | ||||||
|     TeamDetailView, TeamLeaveView, TeamUpdateView, UpdateQuestionView, UploadVideoView |     TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, UpdateQuestionView, UploadVideoView | ||||||
|  |  | ||||||
|  |  | ||||||
| app_name = "participation" | app_name = "participation" | ||||||
| @@ -12,6 +15,7 @@ app_name = "participation" | |||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("create_team/", CreateTeamView.as_view(), name="create_team"), |     path("create_team/", CreateTeamView.as_view(), name="create_team"), | ||||||
|     path("join_team/", JoinTeamView.as_view(), name="join_team"), |     path("join_team/", JoinTeamView.as_view(), name="join_team"), | ||||||
|  |     path("teams/", TeamListView.as_view(), name="team_list"), | ||||||
|     path("team/", MyTeamDetailView.as_view(), name="my_team_detail"), |     path("team/", MyTeamDetailView.as_view(), name="my_team_detail"), | ||||||
|     path("team/<int:pk>/", TeamDetailView.as_view(), name="team_detail"), |     path("team/<int:pk>/", TeamDetailView.as_view(), name="team_detail"), | ||||||
|     path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"), |     path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"), | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| from zipfile import ZipFile | from zipfile import ZipFile | ||||||
|  |  | ||||||
| @@ -5,6 +8,7 @@ from corres2math.lists import get_sympa_client | |||||||
| from corres2math.matrix import Matrix | from corres2math.matrix import Matrix | ||||||
| from corres2math.views import AdminMixin | from corres2math.views import AdminMixin | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
|  | from django.contrib.sites.models import Site | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from django.core.mail import send_mail | from django.core.mail import send_mail | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| @@ -23,7 +27,7 @@ from .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \ | |||||||
|     ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \ |     ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \ | ||||||
|     UploadVideoForm, ValidateParticipationForm |     UploadVideoForm, ValidateParticipationForm | ||||||
| from .models import Participation, Phase, Question, Team, Video | from .models import Participation, Phase, Question, Team, Video | ||||||
| from .tables import CalendarTable | from .tables import CalendarTable, TeamTable | ||||||
|  |  | ||||||
|  |  | ||||||
| class CreateTeamView(LoginRequiredMixin, CreateView): | class CreateTeamView(LoginRequiredMixin, CreateView): | ||||||
| @@ -38,6 +42,8 @@ class CreateTeamView(LoginRequiredMixin, CreateView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         registration = user.registration |         registration = user.registration | ||||||
|         if not registration.participates: |         if not registration.participates: | ||||||
|             raise PermissionDenied(_("You don't participate, so you can't create a team.")) |             raise PermissionDenied(_("You don't participate, so you can't create a team.")) | ||||||
| @@ -84,6 +90,8 @@ class JoinTeamView(LoginRequiredMixin, FormView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         registration = user.registration |         registration = user.registration | ||||||
|         if not registration.participates: |         if not registration.participates: | ||||||
|             raise PermissionDenied(_("You don't participate, so you can't create a team.")) |             raise PermissionDenied(_("You don't participate, so you can't create a team.")) | ||||||
| @@ -119,6 +127,15 @@ class JoinTeamView(LoginRequiredMixin, FormView): | |||||||
|         return reverse_lazy("participation:team_detail", args=(self.object.pk,)) |         return reverse_lazy("participation:team_detail", args=(self.object.pk,)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TeamListView(AdminMixin, SingleTableView): | ||||||
|  |     """ | ||||||
|  |     Display the whole list of teams | ||||||
|  |     """ | ||||||
|  |     model = Team | ||||||
|  |     table_class = TeamTable | ||||||
|  |     ordering = ('participation__problem', 'trigram',) | ||||||
|  |  | ||||||
|  |  | ||||||
| class MyTeamDetailView(LoginRequiredMixin, RedirectView): | class MyTeamDetailView(LoginRequiredMixin, RedirectView): | ||||||
|     """ |     """ | ||||||
|     Redirect to the detail of the team in which the user is. |     Redirect to the detail of the team in which the user is. | ||||||
| @@ -199,7 +216,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) | |||||||
|         self.object.participation.save() |         self.object.participation.save() | ||||||
|  |  | ||||||
|         for admin in AdminRegistration.objects.all(): |         for admin in AdminRegistration.objects.all(): | ||||||
|             mail_context = dict(user=admin.user, team=self.object) |             mail_context = dict(user=admin.user, team=self.object, domain=Site.objects.first().domain) | ||||||
|             mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) |             mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context) | ||||||
|             mail_html = render_to_string("participation/mails/request_validation.html", 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) |             admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html) | ||||||
| @@ -223,6 +240,11 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView) | |||||||
|             mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context) |             mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context) | ||||||
|             mail_html = render_to_string("participation/mails/team_validated.html", 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) |             send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html) | ||||||
|  |  | ||||||
|  |             get_sympa_client().subscribe(self.object.email, "equipes", False, f"Equipe {self.object.name}") | ||||||
|  |             get_sympa_client().unsubscribe(self.object.email, "equipes-non-valides", False) | ||||||
|  |             get_sympa_client().subscribe(self.object.email, f"probleme-{self.object.participation.problem}", False, | ||||||
|  |                                          f"Equipe {self.object.name}") | ||||||
|         elif "invalidate" in self.request.POST: |         elif "invalidate" in self.request.POST: | ||||||
|             self.object.participation.valid = None |             self.object.participation.valid = None | ||||||
|             self.object.participation.save() |             self.object.participation.save() | ||||||
| @@ -250,6 +272,8 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         if user.registration.is_admin or user.registration.participates and \ |         if user.registration.is_admin or user.registration.participates and \ | ||||||
|                 user.registration.team and \ |                 user.registration.team and \ | ||||||
|                 user.registration.team.pk == kwargs["pk"]: |                 user.registration.team.pk == kwargs["pk"]: | ||||||
| @@ -284,6 +308,8 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         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.pk == kwargs["pk"]: | ||||||
|             return super().dispatch(request, *args, **kwargs) |             return super().dispatch(request, *args, **kwargs) | ||||||
|         raise PermissionDenied |         raise PermissionDenied | ||||||
| @@ -318,7 +344,7 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView): | |||||||
|             return self.handle_no_permission() |             return self.handle_no_permission() | ||||||
|         if not request.user.registration.participates or not request.user.registration.team: |         if not request.user.registration.participates or not request.user.registration.team: | ||||||
|             raise PermissionDenied(_("You are not in a team.")) |             raise PermissionDenied(_("You are not in a team.")) | ||||||
|         if request.user.registration.team.participation.valid is not None: |         if request.user.registration.team.participation.valid: | ||||||
|             raise PermissionDenied(_("The team is already validated or the validation is pending.")) |             raise PermissionDenied(_("The team is already validated or the validation is pending.")) | ||||||
|         return super().dispatch(request, *args, **kwargs) |         return super().dispatch(request, *args, **kwargs) | ||||||
|  |  | ||||||
| @@ -362,6 +388,8 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         if not self.get_object().valid: |         if not self.get_object().valid: | ||||||
|             raise PermissionDenied(_("The team is not validated yet.")) |             raise PermissionDenied(_("The team is not validated yet.")) | ||||||
|         if user.registration.is_admin or user.registration.participates \ |         if user.registration.is_admin or user.registration.participates \ | ||||||
| @@ -388,7 +416,7 @@ class SetParticipationReceiveParticipationView(AdminMixin, UpdateView): | |||||||
|     template_name = "participation/receive_participation_form.html" |     template_name = "participation/receive_participation_form.html" | ||||||
|  |  | ||||||
|     def get_success_url(self): |     def get_success_url(self): | ||||||
|         return reverse_lazy("participation:participation_detail", args=(self.object.pk,)) |         return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SetParticipationSendParticipationView(AdminMixin, UpdateView): | class SetParticipationSendParticipationView(AdminMixin, UpdateView): | ||||||
| @@ -400,7 +428,7 @@ class SetParticipationSendParticipationView(AdminMixin, UpdateView): | |||||||
|     template_name = "participation/send_participation_form.html" |     template_name = "participation/send_participation_form.html" | ||||||
|  |  | ||||||
|     def get_success_url(self): |     def get_success_url(self): | ||||||
|         return reverse_lazy("participation:participation_detail", args=(self.object.pk,)) |         return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],)) | ||||||
|  |  | ||||||
|  |  | ||||||
| class CreateQuestionView(LoginRequiredMixin, CreateView): | class CreateQuestionView(LoginRequiredMixin, CreateView): | ||||||
| @@ -418,6 +446,7 @@ class CreateQuestionView(LoginRequiredMixin, CreateView): | |||||||
|         self.participation = Participation.objects.get(pk=kwargs["pk"]) |         self.participation = Participation.objects.get(pk=kwargs["pk"]) | ||||||
|         if request.user.registration.is_admin or \ |         if request.user.registration.is_admin or \ | ||||||
|                 request.user.registration.participates and \ |                 request.user.registration.participates and \ | ||||||
|  |                 self.participation.valid and \ | ||||||
|                 request.user.registration.team.pk == self.participation.team_id: |                 request.user.registration.team.pk == self.participation.team_id: | ||||||
|             return super().dispatch(request, *args, **kwargs) |             return super().dispatch(request, *args, **kwargs) | ||||||
|         raise PermissionDenied |         raise PermissionDenied | ||||||
| @@ -443,10 +472,17 @@ class UpdateQuestionView(LoginRequiredMixin, UpdateView): | |||||||
|             return self.handle_no_permission() |             return self.handle_no_permission() | ||||||
|         if request.user.registration.is_admin or \ |         if request.user.registration.is_admin or \ | ||||||
|                 request.user.registration.participates and \ |                 request.user.registration.participates and \ | ||||||
|  |                 self.object.participation.valid and \ | ||||||
|                 request.user.registration.team.pk == self.object.participation.team_id: |                 request.user.registration.team.pk == self.object.participation.team_id: | ||||||
|             return super().dispatch(request, *args, **kwargs) |             return super().dispatch(request, *args, **kwargs) | ||||||
|         raise PermissionDenied |         raise PermissionDenied | ||||||
|  |  | ||||||
|  |     def form_valid(self, form): | ||||||
|  |         if not self.request.user.registration.is_admin and Phase.current_phase().phase_number != 2: | ||||||
|  |             form.add_error(None, _("You can update your questions now.")) | ||||||
|  |             return self.form_invalid(form) | ||||||
|  |         return super().form_valid(form) | ||||||
|  |  | ||||||
|     def get_success_url(self): |     def get_success_url(self): | ||||||
|         return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) |         return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) | ||||||
|  |  | ||||||
| @@ -464,10 +500,16 @@ class DeleteQuestionView(LoginRequiredMixin, DeleteView): | |||||||
|             return self.handle_no_permission() |             return self.handle_no_permission() | ||||||
|         if request.user.registration.is_admin or \ |         if request.user.registration.is_admin or \ | ||||||
|                 request.user.registration.participates and \ |                 request.user.registration.participates and \ | ||||||
|  |                 self.object.participation.valid and \ | ||||||
|                 request.user.registration.team.pk == self.object.participation.team_id: |                 request.user.registration.team.pk == self.object.participation.team_id: | ||||||
|             return super().dispatch(request, *args, **kwargs) |             return super().dispatch(request, *args, **kwargs) | ||||||
|         raise PermissionDenied |         raise PermissionDenied | ||||||
|  |  | ||||||
|  |     def delete(self, request, *args, **kwargs): | ||||||
|  |         if not request.user.registration.is_admin and Phase.current_phase().phase_number != 2: | ||||||
|  |             raise PermissionDenied(_("You can update your questions now.")) | ||||||
|  |         return super().delete(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def get_success_url(self): |     def get_success_url(self): | ||||||
|         return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) |         return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,)) | ||||||
|  |  | ||||||
| @@ -483,6 +525,8 @@ class UploadVideoView(LoginRequiredMixin, UpdateView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return super().handle_no_permission() | ||||||
|         if user.registration.is_admin or user.registration.participates \ |         if user.registration.is_admin or user.registration.participates \ | ||||||
|                 and user.registration.team.participation.pk == self.get_object().participation.pk: |                 and user.registration.team.participation.pk == self.get_object().participation.pk: | ||||||
|             return super().dispatch(request, *args, **kwargs) |             return super().dispatch(request, *args, **kwargs) | ||||||
|   | |||||||
| @@ -1 +1,4 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| default_app_config = 'registration.apps.RegistrationConfig' | default_app_config = 'registration.apps.RegistrationConfig' | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin | from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.db.models.signals import post_save, pre_save | from django.db.models.signals import post_save, pre_save | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| from cas_server.auth import DjangoAuthUser | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from cas_server.auth import DjangoAuthUser  # pragma: no cover | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomAuthUser(DjangoAuthUser): | class CustomAuthUser(DjangoAuthUser):  # pragma: no cover | ||||||
|     """ |     """ | ||||||
|     Override Django Auth User model to define a custom Matrix username. |     Override Django Auth User model to define a custom Matrix username. | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -13,5 +13,14 @@ | |||||||
|             "single_log_out": true, |             "single_log_out": true, | ||||||
|             "single_log_out_callback": "" |             "single_log_out_callback": "" | ||||||
|         } |         } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         "model": "cas_server.replaceattributname", | ||||||
|  |         "pk": 1, | ||||||
|  |         "fields": { | ||||||
|  |             "name": "display_name", | ||||||
|  |             "replace": "", | ||||||
|  |             "service_pattern": 1 | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django import forms | from django import forms | ||||||
| from django.contrib.auth.forms import UserCreationForm | from django.contrib.auth.forms import UserCreationForm | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| @@ -22,6 +25,15 @@ class SignupForm(UserCreationForm): | |||||||
|         ], |         ], | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     def clean_email(self): | ||||||
|  |         """ | ||||||
|  |         Ensure that the email address is unique. | ||||||
|  |         """ | ||||||
|  |         email = self.data["email"] | ||||||
|  |         if User.objects.filter(email=email).exists(): | ||||||
|  |             self.add_error("email", _("This email address is already used.")) | ||||||
|  |         return email | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
|         self.fields["first_name"].required = True |         self.fields["first_name"].required = True | ||||||
| @@ -55,7 +67,7 @@ class StudentRegistrationForm(forms.ModelForm): | |||||||
|     """ |     """ | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = StudentRegistration |         model = StudentRegistration | ||||||
|         fields = ('student_class', 'school', 'give_contact_to_animath',) |         fields = ('team', 'student_class', 'school', 'give_contact_to_animath', 'email_confirmed',) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PhotoAuthorizationForm(forms.ModelForm): | class PhotoAuthorizationForm(forms.ModelForm): | ||||||
| @@ -63,10 +75,13 @@ class PhotoAuthorizationForm(forms.ModelForm): | |||||||
|     Form to send a photo authorization. |     Form to send a photo authorization. | ||||||
|     """ |     """ | ||||||
|     def clean_photo_authorization(self): |     def clean_photo_authorization(self): | ||||||
|         file = self.files["photo_authorization"] |         if "photo_authorization" in self.files: | ||||||
|         if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]: |             file = self.files["photo_authorization"] | ||||||
|             raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file.")) |             if file.size > 2e6: | ||||||
|         return self.cleaned_data["photo_authorization"] |                 raise ValidationError(_("The uploaded file size must be under 2 Mo.")) | ||||||
|  |             if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]: | ||||||
|  |                 raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file.")) | ||||||
|  |             return self.cleaned_data["photo_authorization"] | ||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         super().__init__(*args, **kwargs) |         super().__init__(*args, **kwargs) | ||||||
| @@ -83,7 +98,7 @@ class CoachRegistrationForm(forms.ModelForm): | |||||||
|     """ |     """ | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = CoachRegistration |         model = CoachRegistration | ||||||
|         fields = ('professional_activity', 'give_contact_to_animath',) |         fields = ('team', 'professional_activity', 'give_contact_to_animath', 'email_confirmed',) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AdminRegistrationForm(forms.ModelForm): | class AdminRegistrationForm(forms.ModelForm): | ||||||
| @@ -92,4 +107,4 @@ class AdminRegistrationForm(forms.ModelForm): | |||||||
|     """ |     """ | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = AdminRegistration |         model = AdminRegistration | ||||||
|         fields = ('role', 'give_contact_to_animath',) |         fields = ('role', 'give_contact_to_animath', 'email_confirmed',) | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-21 15:51 | # Generated by Django 3.1.3 on 2020-11-04 12:05 | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
|  | import registration.models | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
| @@ -11,8 +12,8 @@ class Migration(migrations.Migration): | |||||||
|  |  | ||||||
|     dependencies = [ |     dependencies = [ | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|         ('contenttypes', '0002_remove_content_type_name'), |  | ||||||
|         ('participation', '0001_initial'), |         ('participation', '0001_initial'), | ||||||
|  |         ('contenttypes', '0002_remove_content_type_name'), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     operations = [ |     operations = [ | ||||||
| @@ -21,8 +22,8 @@ class Migration(migrations.Migration): | |||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('give_contact_to_animath', models.BooleanField(default=False, verbose_name='Grant Animath to contact me in the future about other actions')), |                 ('give_contact_to_animath', models.BooleanField(default=False, verbose_name='Grant Animath to contact me in the future about other actions')), | ||||||
|  |                 ('email_confirmed', models.BooleanField(default=False, verbose_name='email confirmed')), | ||||||
|                 ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_registration.registration_set+', to='contenttypes.contenttype')), |                 ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_registration.registration_set+', to='contenttypes.contenttype')), | ||||||
|                 ('team', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='participation.team', verbose_name='team')), |  | ||||||
|                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), |                 ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')), | ||||||
|             ], |             ], | ||||||
|             options={ |             options={ | ||||||
| @@ -42,24 +43,14 @@ class Migration(migrations.Migration): | |||||||
|             }, |             }, | ||||||
|             bases=('registration.registration',), |             bases=('registration.registration',), | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |  | ||||||
|             name='CoachRegistration', |  | ||||||
|             fields=[ |  | ||||||
|                 ('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')), |  | ||||||
|                 ('professional_activity', models.TextField(verbose_name='professional activity')), |  | ||||||
|             ], |  | ||||||
|             options={ |  | ||||||
|                 'verbose_name': 'coach registration', |  | ||||||
|                 'verbose_name_plural': 'coach registrations', |  | ||||||
|             }, |  | ||||||
|             bases=('registration.registration',), |  | ||||||
|         ), |  | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name='StudentRegistration', |             name='StudentRegistration', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')), |                 ('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')), | ||||||
|                 ('student_class', models.IntegerField(choices=[(12, '12th grade'), (11, '11th grade'), (10, '10th grade or lower')], verbose_name='student class')), |                 ('student_class', models.IntegerField(choices=[(12, '12th grade'), (11, '11th grade'), (10, '10th grade or lower')], verbose_name='student class')), | ||||||
|                 ('school', models.CharField(max_length=255, verbose_name='school')), |                 ('school', models.CharField(max_length=255, verbose_name='school')), | ||||||
|  |                 ('photo_authorization', models.FileField(blank=True, default='', upload_to=registration.models.get_random_filename, verbose_name='photo authorization')), | ||||||
|  |                 ('team', models.ForeignKey(default=None, blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='students', to='participation.team', verbose_name='team')), | ||||||
|             ], |             ], | ||||||
|             options={ |             options={ | ||||||
|                 'verbose_name': 'student registration', |                 'verbose_name': 'student registration', | ||||||
| @@ -67,4 +58,17 @@ class Migration(migrations.Migration): | |||||||
|             }, |             }, | ||||||
|             bases=('registration.registration',), |             bases=('registration.registration',), | ||||||
|         ), |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='CoachRegistration', | ||||||
|  |             fields=[ | ||||||
|  |                 ('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')), | ||||||
|  |                 ('professional_activity', models.TextField(verbose_name='professional activity')), | ||||||
|  |                 ('team', models.ForeignKey(default=None, blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='coachs', to='participation.team', verbose_name='team')), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'verbose_name': 'coach registration', | ||||||
|  |                 'verbose_name_plural': 'coach registrations', | ||||||
|  |             }, | ||||||
|  |             bases=('registration.registration',), | ||||||
|  |         ), | ||||||
|     ] |     ] | ||||||
|   | |||||||
| @@ -1,29 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-21 17:48 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('participation', '0001_initial'), |  | ||||||
|         ('registration', '0001_initial'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name='registration', |  | ||||||
|             name='team', |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='coachregistration', |  | ||||||
|             name='team', |  | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='coachs', to='participation.team', verbose_name='team'), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='studentregistration', |  | ||||||
|             name='team', |  | ||||||
|             field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='students', to='participation.team', verbose_name='team'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-22 16:56 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('registration', '0002_auto_20200921_1948'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='registration', |  | ||||||
|             name='email_confirmed', |  | ||||||
|             field=models.BooleanField(default=False, verbose_name='email confirmed'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| # Generated by Django 3.1.1 on 2020-09-24 20:36 |  | ||||||
|  |  | ||||||
| from django.db import migrations, models |  | ||||||
| import registration.models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
|  |  | ||||||
|     dependencies = [ |  | ||||||
|         ('registration', '0003_registration_email_confirmed'), |  | ||||||
|     ] |  | ||||||
|  |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='studentregistration', |  | ||||||
|             name='photo_authorization', |  | ||||||
|             field=models.FileField(blank=True, default='', upload_to=registration.models.get_random_filename, verbose_name='photo authorization'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from corres2math.tokens import email_validation_token | from corres2math.tokens import email_validation_token | ||||||
| from django.contrib.sites.models import Site | from django.contrib.sites.models import Site | ||||||
| from django.db import models | from django.db import models | ||||||
| @@ -58,11 +61,11 @@ class Registration(PolymorphicModel): | |||||||
|         self.user.email_user(subject, message, html_message=html) |         self.user.email_user(subject, message, html_message=html) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def type(self): |     def type(self):  # pragma: no cover | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def form_class(self): |     def form_class(self):  # pragma: no cover | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
| @@ -102,6 +105,7 @@ class StudentRegistration(Registration): | |||||||
|         related_name="students", |         related_name="students", | ||||||
|         on_delete=models.PROTECT, |         on_delete=models.PROTECT, | ||||||
|         null=True, |         null=True, | ||||||
|  |         blank=True, | ||||||
|         default=None, |         default=None, | ||||||
|         verbose_name=_("team"), |         verbose_name=_("team"), | ||||||
|     ) |     ) | ||||||
| @@ -151,6 +155,7 @@ class CoachRegistration(Registration): | |||||||
|         related_name="coachs", |         related_name="coachs", | ||||||
|         on_delete=models.PROTECT, |         on_delete=models.PROTECT, | ||||||
|         null=True, |         null=True, | ||||||
|  |         blank=True, | ||||||
|         default=None, |         default=None, | ||||||
|         verbose_name=_("team"), |         verbose_name=_("team"), | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from haystack import indexes | from haystack import indexes | ||||||
|  |  | ||||||
| from .models import Registration | from .models import Registration | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from corres2math.lists import get_sympa_client | from corres2math.lists import get_sympa_client | ||||||
| from corres2math.matrix import Matrix | from corres2math.matrix import Matrix | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| @@ -50,3 +53,4 @@ def invite_to_public_rooms(instance: Registration, created: bool, **_): | |||||||
|         Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr") |         Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr") | ||||||
|         Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr", |         Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr", | ||||||
|                       f"@{instance.matrix_username}:correspondances-maths.fr") |                       f"@{instance.matrix_username}:correspondances-maths.fr") | ||||||
|  |         Matrix.invite("#flood:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr") | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| import django_tables2 as tables | import django_tables2 as tables | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,7 +19,11 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|         </p> |         </p> | ||||||
|         {% else %} |         {% else %} | ||||||
|         <p> |         <p> | ||||||
|             {% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %} |             {% if user.is_authenticated and user.registration.email_confirmed %} | ||||||
|  |                 {% trans "The link was invalid. The token may have expired, or your account is already activated. However, your account seems to be already valid." %} | ||||||
|  |             {% else %} | ||||||
|  |                 {% trans "The link was invalid. The token may have expired, or your account is already activated. Please send us an email to activate your account." %} | ||||||
|  |             {% endif %} | ||||||
|         </p> |         </p> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|      </div> |      </div> | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								apps/registration/templates/registration/user_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								apps/registration/templates/registration/user_list.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% load django_tables2 %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |     {% render_table table %} | ||||||
|  | {% endblock %} | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django import template | from django import template | ||||||
| from django_tables2 import Table | from django_tables2 import Table | ||||||
| from participation.models import Participation, Team, Video | from participation.models import Participation, Team, Video | ||||||
|   | |||||||
| @@ -1,18 +1,23 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from datetime import timedelta | ||||||
| import os | import os | ||||||
|  |  | ||||||
| from corres2math.tokens import email_validation_token | from corres2math.tokens import email_validation_token | ||||||
| from django.conf import settings |  | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.contrib.contenttypes.models import ContentType | from django.contrib.contenttypes.models import ContentType | ||||||
| from django.contrib.sites.models import Site | from django.contrib.sites.models import Site | ||||||
|  | from django.core.files.uploadedfile import SimpleUploadedFile | ||||||
| from django.core.management import call_command | from django.core.management import call_command | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | from django.utils import timezone | ||||||
| from django.utils.encoding import force_bytes | from django.utils.encoding import force_bytes | ||||||
| from django.utils.http import urlsafe_base64_encode | from django.utils.http import urlsafe_base64_encode | ||||||
|  | from participation.models import Phase, Team | ||||||
|  |  | ||||||
| from .auth import CustomAuthUser | from .models import AdminRegistration, CoachRegistration, StudentRegistration | ||||||
| from .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestIndexPage(TestCase): | class TestIndexPage(TestCase): | ||||||
| @@ -30,6 +35,28 @@ class TestIndexPage(TestCase): | |||||||
|         response = self.client.get(reverse("registration:reset_admin")) |         response = self.client.get(reverse("registration:reset_admin")) | ||||||
|         self.assertRedirects(response, reverse("login") + "?next=" + reverse("registration:reset_admin"), 302, 200) |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("registration:reset_admin"), 302, 200) | ||||||
|  |  | ||||||
|  |         User.objects.create() | ||||||
|  |         response = self.client.get(reverse("registration:user_detail", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("registration:user_detail", args=(1,))) | ||||||
|  |  | ||||||
|  |         Team.objects.create() | ||||||
|  |         response = self.client.get(reverse("participation:team_detail", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_detail", args=(1,))) | ||||||
|  |         response = self.client.get(reverse("participation:update_team", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:update_team", args=(1,))) | ||||||
|  |         response = self.client.get(reverse("participation:create_team")) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:create_team")) | ||||||
|  |         response = self.client.get(reverse("participation:join_team")) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:join_team")) | ||||||
|  |         response = self.client.get(reverse("participation:team_authorizations", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" | ||||||
|  |                              + reverse("participation:team_authorizations", args=(1,))) | ||||||
|  |         response = self.client.get(reverse("participation:participation_detail", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" | ||||||
|  |                              + reverse("participation:participation_detail", args=(1,))) | ||||||
|  |         response = self.client.get(reverse("participation:upload_video", args=(1,))) | ||||||
|  |         self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:upload_video", args=(1,))) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestRegistration(TestCase): | class TestRegistration(TestCase): | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
| @@ -83,6 +110,13 @@ class TestRegistration(TestCase): | |||||||
|         """ |         """ | ||||||
|         Ensure that the signup form is working successfully. |         Ensure that the signup form is working successfully. | ||||||
|         """ |         """ | ||||||
|  |         # After first phase | ||||||
|  |         response = self.client.get(reverse("registration:signup")) | ||||||
|  |         self.assertEqual(response.status_code, 403) | ||||||
|  |  | ||||||
|  |         Phase.objects.filter(phase_number__gte=2).update(start=timezone.now() + timedelta(days=1), | ||||||
|  |                                                          end=timezone.now() + timedelta(days=2)) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("registration:signup")) |         response = self.client.get(reverse("registration:signup")) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
| @@ -111,6 +145,20 @@ class TestRegistration(TestCase): | |||||||
|         self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200) |         self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200) | ||||||
|         self.assertTrue(User.objects.filter(email="toto@example.com").exists()) |         self.assertTrue(User.objects.filter(email="toto@example.com").exists()) | ||||||
|  |  | ||||||
|  |         # Email is already used | ||||||
|  |         response = self.client.post(reverse("registration:signup"), data=dict( | ||||||
|  |             last_name="Toto", | ||||||
|  |             first_name="Toto", | ||||||
|  |             email="toto@example.com", | ||||||
|  |             password1="azertyuiopazertyuiop", | ||||||
|  |             password2="azertyuiopazertyuiop", | ||||||
|  |             role="participant", | ||||||
|  |             student_class=12, | ||||||
|  |             school="God", | ||||||
|  |             give_contact_to_animath=False, | ||||||
|  |         )) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|         response = self.client.get(reverse("registration:email_validation_sent")) |         response = self.client.get(reverse("registration:email_validation_sent")) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
| @@ -177,6 +225,13 @@ class TestRegistration(TestCase): | |||||||
|         response = self.client.get(reverse("registration:user_detail", args=(self.user.pk,))) |         response = self.client.get(reverse("registration:user_detail", args=(self.user.pk,))) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_user_list(self): | ||||||
|  |         """ | ||||||
|  |         Display the list of all users. | ||||||
|  |         """ | ||||||
|  |         response = self.client.get(reverse("registration:user_list")) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|     def test_update_user(self): |     def test_update_user(self): | ||||||
|         """ |         """ | ||||||
|         Update the user information, for each type of user. |         Update the user information, for each type of user. | ||||||
| @@ -200,6 +255,8 @@ class TestRegistration(TestCase): | |||||||
|                 last_name="Name", |                 last_name="Name", | ||||||
|                 email="new_" + user.email, |                 email="new_" + user.email, | ||||||
|                 give_contact_to_animath=True, |                 give_contact_to_animath=True, | ||||||
|  |                 email_confirmed=True, | ||||||
|  |                 team_id="", | ||||||
|             )) |             )) | ||||||
|             self.assertEqual(response.status_code, 200) |             self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
| @@ -208,6 +265,8 @@ class TestRegistration(TestCase): | |||||||
|                 last_name="Name", |                 last_name="Name", | ||||||
|                 email="new_" + user.email, |                 email="new_" + user.email, | ||||||
|                 give_contact_to_animath=True, |                 give_contact_to_animath=True, | ||||||
|  |                 email_confirmed=True, | ||||||
|  |                 team_id="", | ||||||
|             ) |             ) | ||||||
|             response = self.client.post(reverse("registration:update_user", args=(user.pk,)), data=data) |             response = self.client.post(reverse("registration:update_user", args=(user.pk,)), data=data) | ||||||
|             self.assertRedirects(response, reverse("registration:user_detail", args=(user.pk,)), 302, 200) |             self.assertRedirects(response, reverse("registration:user_detail", args=(user.pk,)), 302, 200) | ||||||
| @@ -231,6 +290,14 @@ class TestRegistration(TestCase): | |||||||
|         )) |         )) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |         # Don't send too large files | ||||||
|  |         response = self.client.post(reverse("registration:upload_user_photo_authorization", | ||||||
|  |                                             args=(self.student.registration.pk,)), data=dict( | ||||||
|  |             photo_authorization=SimpleUploadedFile("file.pdf", content=int(0).to_bytes(2000001, "big"), | ||||||
|  |                                                    content_type="application/pdf"), | ||||||
|  |         )) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|         response = self.client.post(reverse("registration:upload_user_photo_authorization", |         response = self.client.post(reverse("registration:upload_user_photo_authorization", | ||||||
|                                             args=(self.student.registration.pk,)), data=dict( |                                             args=(self.student.registration.pk,)), data=dict( | ||||||
|             photo_authorization=open("corres2math/static/Autorisation de droit à l'image - majeur.pdf", "rb"), |             photo_authorization=open("corres2math/static/Autorisation de droit à l'image - majeur.pdf", "rb"), | ||||||
| @@ -326,17 +393,3 @@ class TestRegistration(TestCase): | |||||||
|                                    self.student.registration.get_student_class_display()) |                                    self.student.registration.get_student_class_display()) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|         self.assertTrue(response.context["object_list"]) |         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) |  | ||||||
|         self.assertRaises(NotImplementedError, lambda: Registration().form_class) |  | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.urls import path | from django.urls import path | ||||||
|  |  | ||||||
| from .views import MyAccountDetailView, ResetAdminView, SignupView, UserDetailView, UserImpersonateView, \ | from .views import MyAccountDetailView, ResetAdminView, SignupView, UserDetailView, UserImpersonateView, \ | ||||||
|     UserResendValidationEmailView, UserUpdateView, UserUploadPhotoAuthorizationView, UserValidateView, \ |     UserListView, UserResendValidationEmailView, UserUpdateView, UserUploadPhotoAuthorizationView, UserValidateView, \ | ||||||
|     UserValidationEmailSentView |     UserValidationEmailSentView | ||||||
|  |  | ||||||
| app_name = "registration" | app_name = "registration" | ||||||
| @@ -18,5 +21,6 @@ urlpatterns = [ | |||||||
|     path("user/<int:pk>/upload-photo-authorization/", UserUploadPhotoAuthorizationView.as_view(), |     path("user/<int:pk>/upload-photo-authorization/", UserUploadPhotoAuthorizationView.as_view(), | ||||||
|          name="upload_user_photo_authorization"), |          name="upload_user_photo_authorization"), | ||||||
|     path("user/<int:pk>/impersonate/", UserImpersonateView.as_view(), name="user_impersonate"), |     path("user/<int:pk>/impersonate/", UserImpersonateView.as_view(), name="user_impersonate"), | ||||||
|  |     path("user/list/", UserListView.as_view(), name="user_list"), | ||||||
|     path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"), |     path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"), | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
| from corres2math.tokens import email_validation_token | from corres2math.tokens import email_validation_token | ||||||
|  | from corres2math.views import AdminMixin | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| @@ -12,10 +16,13 @@ from django.urls import reverse_lazy | |||||||
| from django.utils.http import urlsafe_base64_decode | from django.utils.http import urlsafe_base64_decode | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View | from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View | ||||||
|  | from django_tables2 import SingleTableView | ||||||
| from magic import Magic | from magic import Magic | ||||||
|  | from participation.models import Phase | ||||||
|  |  | ||||||
| from .forms import CoachRegistrationForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm | from .forms import CoachRegistrationForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm | ||||||
| from .models import StudentRegistration | from .models import Registration, StudentRegistration | ||||||
|  | from .tables import RegistrationTable | ||||||
|  |  | ||||||
|  |  | ||||||
| class SignupView(CreateView): | class SignupView(CreateView): | ||||||
| @@ -27,12 +34,26 @@ class SignupView(CreateView): | |||||||
|     template_name = "registration/signup.html" |     template_name = "registration/signup.html" | ||||||
|     extra_context = dict(title=_("Sign up")) |     extra_context = dict(title=_("Sign up")) | ||||||
|  |  | ||||||
|  |     def dispatch(self, request, *args, **kwargs): | ||||||
|  |         """ | ||||||
|  |         The signup view is available only during the first phase. | ||||||
|  |         """ | ||||||
|  |         current_phase = Phase.current_phase() | ||||||
|  |         if not current_phase or current_phase.phase_number >= 2: | ||||||
|  |             raise PermissionDenied(_("You can't register now.")) | ||||||
|  |         return super().dispatch(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def get_context_data(self, **kwargs): |     def get_context_data(self, **kwargs): | ||||||
|         context = super().get_context_data() |         context = super().get_context_data() | ||||||
|  |  | ||||||
|         context["student_registration_form"] = StudentRegistrationForm(self.request.POST or None) |         context["student_registration_form"] = StudentRegistrationForm(self.request.POST or None) | ||||||
|         context["coach_registration_form"] = CoachRegistrationForm(self.request.POST or None) |         context["coach_registration_form"] = CoachRegistrationForm(self.request.POST or None) | ||||||
|  |  | ||||||
|  |         del context["student_registration_form"].fields["team"] | ||||||
|  |         del context["student_registration_form"].fields["email_confirmed"] | ||||||
|  |         del context["coach_registration_form"].fields["team"] | ||||||
|  |         del context["coach_registration_form"].fields["email_confirmed"] | ||||||
|  |  | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
| @@ -42,6 +63,8 @@ class SignupView(CreateView): | |||||||
|             registration_form = StudentRegistrationForm(self.request.POST) |             registration_form = StudentRegistrationForm(self.request.POST) | ||||||
|         else: |         else: | ||||||
|             registration_form = CoachRegistrationForm(self.request.POST) |             registration_form = CoachRegistrationForm(self.request.POST) | ||||||
|  |         del registration_form.fields["team"] | ||||||
|  |         del registration_form.fields["email_confirmed"] | ||||||
|  |  | ||||||
|         if not registration_form.is_valid(): |         if not registration_form.is_valid(): | ||||||
|             return self.form_invalid(form) |             return self.form_invalid(form) | ||||||
| @@ -148,6 +171,8 @@ class UserDetailView(LoginRequiredMixin, DetailView): | |||||||
|  |  | ||||||
|     def dispatch(self, request, *args, **kwargs): |     def dispatch(self, request, *args, **kwargs): | ||||||
|         user = request.user |         user = request.user | ||||||
|  |         if not user.is_authenticated: | ||||||
|  |             return self.handle_no_permission() | ||||||
|         # Only an admin or the concerned user can see the information |         # Only an admin or the concerned user can see the information | ||||||
|         if not user.registration.is_admin and user.pk != kwargs["pk"]: |         if not user.registration.is_admin and user.pk != kwargs["pk"]: | ||||||
|             raise PermissionDenied |             raise PermissionDenied | ||||||
| @@ -159,6 +184,15 @@ class UserDetailView(LoginRequiredMixin, DetailView): | |||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UserListView(AdminMixin, SingleTableView): | ||||||
|  |     """ | ||||||
|  |     Display the list of all registered users. | ||||||
|  |     """ | ||||||
|  |     model = Registration | ||||||
|  |     table_class = RegistrationTable | ||||||
|  |     template_name = "registration/user_list.html" | ||||||
|  |  | ||||||
|  |  | ||||||
| class UserUpdateView(LoginRequiredMixin, UpdateView): | class UserUpdateView(LoginRequiredMixin, UpdateView): | ||||||
|     """ |     """ | ||||||
|     Update the detail about a user and its registration. |     Update the detail about a user and its registration. | ||||||
| @@ -179,6 +213,10 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): | |||||||
|         context["title"] = _("Update user {user}").format(user=str(self.object.registration)) |         context["title"] = _("Update user {user}").format(user=str(self.object.registration)) | ||||||
|         context["registration_form"] = user.registration.form_class(data=self.request.POST or None, |         context["registration_form"] = user.registration.form_class(data=self.request.POST or None, | ||||||
|                                                                     instance=self.object.registration) |                                                                     instance=self.object.registration) | ||||||
|  |         if not self.request.user.registration.is_admin: | ||||||
|  |             if "team" in context["registration_form"].fields: | ||||||
|  |                 del context["registration_form"].fields["team"] | ||||||
|  |             del context["registration_form"].fields["email_confirmed"] | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
| @@ -186,6 +224,11 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): | |||||||
|         user = form.instance |         user = form.instance | ||||||
|         registration_form = user.registration.form_class(data=self.request.POST or None, |         registration_form = user.registration.form_class(data=self.request.POST or None, | ||||||
|                                                          instance=self.object.registration) |                                                          instance=self.object.registration) | ||||||
|  |         if not self.request.user.registration.is_admin: | ||||||
|  |             if "team" in registration_form.fields: | ||||||
|  |                 del registration_form.fields["team"] | ||||||
|  |             del registration_form.fields["email_confirmed"] | ||||||
|  |  | ||||||
|         if not registration_form.is_valid(): |         if not registration_form.is_valid(): | ||||||
|             return self.form_invalid(form) |             return self.form_invalid(form) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,3 +3,6 @@ | |||||||
| *       *       *       *       *       cd /code && python manage.py send_mail -c 1 | *       *       *       *       *       cd /code && python manage.py send_mail -c 1 | ||||||
| *       *       *       *       *       cd /code && python manage.py retry_deferred -c 1 | *       *       *       *       *       cd /code && python manage.py retry_deferred -c 1 | ||||||
| 0       0       *       *       *       cd /code && python manage.py purge_mail_log 7 -c 1 | 0       0       *       *       *       cd /code && python manage.py purge_mail_log 7 -c 1 | ||||||
|  |  | ||||||
|  | # Rebuild search index | ||||||
|  | 0       *       *       *       *       cd /code && python manage.py update_index -v 0 | ||||||
|   | |||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| """ | """ | ||||||
| ASGI config for corres2math project. | ASGI config for corres2math project. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
| _client = None | _client = None | ||||||
| @@ -6,7 +9,7 @@ _client = None | |||||||
| def get_sympa_client(): | def get_sympa_client(): | ||||||
|     global _client |     global _client | ||||||
|     if _client is None: |     if _client is None: | ||||||
|         if os.getenv("SYMPA_PASSWORD", None) is not None: |         if os.getenv("SYMPA_PASSWORD", None) is not None:  # pragma: no cover | ||||||
|             from sympasoap import Client |             from sympasoap import Client | ||||||
|             _client = Client("https://" + os.getenv("SYMPA_URL")) |             _client = Client("https://" + os.getenv("SYMPA_URL")) | ||||||
|             _client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) |             _client.login(os.getenv("SYMPA_EMAIL"), os.getenv("SYMPA_PASSWORD")) | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | from enum import Enum | ||||||
| import os | import os | ||||||
| from typing import Tuple |  | ||||||
|  |  | ||||||
| from asgiref.sync import async_to_sync | from asgiref.sync import async_to_sync | ||||||
| from nio import * |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Matrix: | class Matrix: | ||||||
| @@ -14,11 +16,11 @@ class Matrix: | |||||||
|     Tasks are normally asynchronous, but for compatibility we make |     Tasks are normally asynchronous, but for compatibility we make | ||||||
|     them synchronous. |     them synchronous. | ||||||
|     """ |     """ | ||||||
|     _token: str = None |     _token = None | ||||||
|     _device_id: str = None |     _device_id = None | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     async def _get_client(cls) -> Union[AsyncClient, "FakeMatrixClient"]: |     async def _get_client(cls):  # pragma: no cover | ||||||
|         """ |         """ | ||||||
|         Retrieve the bot account. |         Retrieve the bot account. | ||||||
|         If not logged, log in and store access token. |         If not logged, log in and store access token. | ||||||
| @@ -26,6 +28,7 @@ class Matrix: | |||||||
|         if not os.getenv("SYNAPSE_PASSWORD"): |         if not os.getenv("SYNAPSE_PASSWORD"): | ||||||
|             return FakeMatrixClient() |             return FakeMatrixClient() | ||||||
|  |  | ||||||
|  |         from nio import AsyncClient | ||||||
|         client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr") |         client = AsyncClient("https://correspondances-maths.fr", "@corres2mathbot:correspondances-maths.fr") | ||||||
|         client.user_id = "@corres2mathbot:correspondances-maths.fr" |         client.user_id = "@corres2mathbot:correspondances-maths.fr" | ||||||
|  |  | ||||||
| @@ -49,7 +52,7 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def set_display_name(cls, name: str) -> Union[ProfileSetDisplayNameResponse, ProfileSetDisplayNameError]: |     async def set_display_name(cls, name: str): | ||||||
|         """ |         """ | ||||||
|         Set the display name of the bot account. |         Set the display name of the bot account. | ||||||
|         """ |         """ | ||||||
| @@ -58,7 +61,7 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def set_avatar(cls, avatar_url: str) -> Union[ProfileSetAvatarResponse, ProfileSetAvatarError]: |     async def set_avatar(cls, avatar_url: str):  # pragma: no cover | ||||||
|         """ |         """ | ||||||
|         Set the display avatar of the bot account. |         Set the display avatar of the bot account. | ||||||
|         """ |         """ | ||||||
| @@ -69,13 +72,13 @@ class Matrix: | |||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def upload( |     async def upload( | ||||||
|             cls, |             cls, | ||||||
|             data_provider: DataProvider, |             data_provider, | ||||||
|             content_type: str = "application/octet-stream", |             content_type: str = "application/octet-stream", | ||||||
|             filename: Optional[str] = None, |             filename: str = None, | ||||||
|             encrypt: bool = False, |             encrypt: bool = False, | ||||||
|             monitor: Optional[TransferMonitor] = None, |             monitor=None, | ||||||
|             filesize: Optional[int] = None, |             filesize: int = None, | ||||||
|     ) -> Tuple[Union[UploadResponse, UploadError], Optional[Dict[str, Any]]]: |     ):  # pragma: no cover | ||||||
|         """ |         """ | ||||||
|         Upload a file to the content repository. |         Upload a file to the content repository. | ||||||
|  |  | ||||||
| @@ -129,24 +132,25 @@ class Matrix: | |||||||
|                 If left as ``None``, some servers might refuse the upload. |                 If left as ``None``, some servers might refuse the upload. | ||||||
|         """ |         """ | ||||||
|         client = await cls._get_client() |         client = await cls._get_client() | ||||||
|         return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) |         return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \ | ||||||
|  |             if not isinstance(client, FakeMatrixClient) else None, None | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def create_room( |     async def create_room( | ||||||
|             cls, |             cls, | ||||||
|             visibility: RoomVisibility = RoomVisibility.private, |             visibility=None, | ||||||
|             alias: Optional[str] = None, |             alias=None, | ||||||
|             name: Optional[str] = None, |             name=None, | ||||||
|             topic: Optional[str] = None, |             topic=None, | ||||||
|             room_version: Optional[str] = None, |             room_version=None, | ||||||
|             federate: bool = True, |             federate=True, | ||||||
|             is_direct: bool = False, |             is_direct=False, | ||||||
|             preset: Optional[RoomPreset] = None, |             preset=None, | ||||||
|             invite=(), |             invite=(), | ||||||
|             initial_state=(), |             initial_state=(), | ||||||
|             power_level_override: Optional[Dict[str, Any]] = None, |             power_level_override=None, | ||||||
|     ) -> Union[RoomCreateResponse, RoomCreateError]: |     ): | ||||||
|         """ |         """ | ||||||
|         Create a new room. |         Create a new room. | ||||||
|  |  | ||||||
| @@ -208,20 +212,18 @@ class Matrix: | |||||||
|             power_level_override) |             power_level_override) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     async def resolve_room_alias(cls, room_alias: str) -> Optional[str]: |     async def resolve_room_alias(cls, room_alias: str): | ||||||
|         """ |         """ | ||||||
|         Resolve a room alias to a room ID. |         Resolve a room alias to a room ID. | ||||||
|         Return None if the alias does not exist. |         Return None if the alias does not exist. | ||||||
|         """ |         """ | ||||||
|         client = await cls._get_client() |         client = await cls._get_client() | ||||||
|         resp: RoomResolveAliasResponse = await client.room_resolve_alias(room_alias) |         resp = await client.room_resolve_alias(room_alias) | ||||||
|         if isinstance(resp, RoomResolveAliasResponse): |         return resp.room_id if resp and hasattr(resp, "room_id") else None | ||||||
|             return resp.room_id |  | ||||||
|         return None |  | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def invite(cls, room_id: str, user_id: str) -> Union[RoomInviteResponse, RoomInviteError]: |     async def invite(cls, room_id: str, user_id: str): | ||||||
|         """ |         """ | ||||||
|         Invite a user to a room. |         Invite a user to a room. | ||||||
|  |  | ||||||
| @@ -263,13 +265,13 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def kick(cls, room_id: str, user_id: str, reason: str = None) -> Union[RoomKickResponse, RoomInviteError]: |     async def kick(cls, room_id: str, user_id: str, reason: str = None): | ||||||
|         """ |         """ | ||||||
|         Kick a user from a room, or withdraw their invitation. |         Kick a user from a room, or withdraw their invitation. | ||||||
|  |  | ||||||
|         Kicking a user adjusts their membership to "leave" with an optional |         Kicking a user adjusts their membership to "leave" with an optional | ||||||
|         reason. |         reason. | ||||||
|  | ² | ||||||
|         Returns either a `RoomKickResponse` if the request was successful or |         Returns either a `RoomKickResponse` if the request was successful or | ||||||
|         a `RoomKickError` if there was an error with the request. |         a `RoomKickError` if there was an error with the request. | ||||||
|  |  | ||||||
| @@ -286,8 +288,7 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int)\ |     async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int):  # pragma: no cover | ||||||
|             -> Union[RoomPutStateResponse, RoomPutStateError]: |  | ||||||
|         """ |         """ | ||||||
|         Put a given power level to a user in a certain room. |         Put a given power level to a user in a certain room. | ||||||
|  |  | ||||||
| @@ -302,6 +303,9 @@ class Matrix: | |||||||
|             power_level (int): The target power level to give. |             power_level (int): The target power level to give. | ||||||
|         """ |         """ | ||||||
|         client = await cls._get_client() |         client = await cls._get_client() | ||||||
|  |         if isinstance(client, FakeMatrixClient): | ||||||
|  |             return None | ||||||
|  |  | ||||||
|         if room_id.startswith("#"): |         if room_id.startswith("#"): | ||||||
|             room_id = await cls.resolve_room_alias(room_id) |             room_id = await cls.resolve_room_alias(room_id) | ||||||
|         resp = await client.room_get_state_event(room_id, "m.room.power_levels") |         resp = await client.room_get_state_event(room_id, "m.room.power_levels") | ||||||
| @@ -311,8 +315,7 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int)\ |     async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int):  # pragma: no cover | ||||||
|             -> Union[RoomPutStateResponse, RoomPutStateError]: |  | ||||||
|         """ |         """ | ||||||
|         Define the minimal power level to have to send a certain event type |         Define the minimal power level to have to send a certain event type | ||||||
|          in a given room. |          in a given room. | ||||||
| @@ -328,6 +331,9 @@ class Matrix: | |||||||
|             power_level (int): The target power level to give. |             power_level (int): The target power level to give. | ||||||
|         """ |         """ | ||||||
|         client = await cls._get_client() |         client = await cls._get_client() | ||||||
|  |         if isinstance(client, FakeMatrixClient): | ||||||
|  |             return None | ||||||
|  |  | ||||||
|         if room_id.startswith("#"): |         if room_id.startswith("#"): | ||||||
|             room_id = await cls.resolve_room_alias(room_id) |             room_id = await cls.resolve_room_alias(room_id) | ||||||
|         resp = await client.room_get_state_event(room_id, "m.room.power_levels") |         resp = await client.room_get_state_event(room_id, "m.room.power_levels") | ||||||
| @@ -340,8 +346,7 @@ class Matrix: | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     @async_to_sync |     @async_to_sync | ||||||
|     async def set_room_avatar(cls, room_id: str, avatar_uri: str)\ |     async def set_room_avatar(cls, room_id: str, avatar_uri: str): | ||||||
|             -> Union[RoomPutStateResponse, RoomPutStateError]: |  | ||||||
|         """ |         """ | ||||||
|         Define the avatar of a room. |         Define the avatar of a room. | ||||||
|  |  | ||||||
| @@ -361,6 +366,22 @@ class Matrix: | |||||||
|         }, state_key="") |         }, state_key="") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if os.getenv("SYNAPSE_PASSWORD"):  # pragma: no cover | ||||||
|  |     from nio import RoomVisibility, RoomPreset | ||||||
|  |     RoomVisibility = RoomVisibility | ||||||
|  |     RoomPreset = RoomPreset | ||||||
|  | else: | ||||||
|  |     # When running tests, faking matrix-nio classes to don't include the module | ||||||
|  |     class RoomVisibility(Enum): | ||||||
|  |         private = 'private' | ||||||
|  |         public = 'public' | ||||||
|  |  | ||||||
|  |     class RoomPreset(Enum): | ||||||
|  |         private_chat = "private_chat" | ||||||
|  |         trusted_private_chat = "trusted_private_chat" | ||||||
|  |         public_chat = "public_chat" | ||||||
|  |  | ||||||
|  |  | ||||||
| class FakeMatrixClient: | class FakeMatrixClient: | ||||||
|     """ |     """ | ||||||
|     Simulate a Matrix client to run tests, if no Matrix homeserver is connected. |     Simulate a Matrix client to run tests, if no Matrix homeserver is connected. | ||||||
| @@ -370,4 +391,3 @@ class FakeMatrixClient: | |||||||
|         async def func(*_, **_2): |         async def func(*_, **_2): | ||||||
|             return None |             return None | ||||||
|         return func |         return func | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| from django.conf import settings | # Copyright (C) 2020 by Animath | ||||||
| from django.contrib.auth.models import AnonymousUser, User | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from threading import local | from threading import local | ||||||
|  |  | ||||||
| from django.contrib.sessions.backends.db import SessionStore | from django.conf import settings | ||||||
|  | from django.contrib.auth.models import AnonymousUser, User | ||||||
|  |  | ||||||
| USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') | USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') | ||||||
| SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') | SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') | ||||||
| @@ -22,19 +23,13 @@ def get_current_user() -> User: | |||||||
|     return getattr(_thread_locals, USER_ATTR_NAME, None) |     return getattr(_thread_locals, USER_ATTR_NAME, None) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_current_session() -> SessionStore: |  | ||||||
|     return getattr(_thread_locals, SESSION_ATTR_NAME, None) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_current_ip() -> str: | def get_current_ip() -> str: | ||||||
|     return getattr(_thread_locals, IP_ATTR_NAME, None) |     return getattr(_thread_locals, IP_ATTR_NAME, None) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_current_authenticated_user(): | def get_current_authenticated_user(): | ||||||
|     current_user = get_current_user() |     current_user = get_current_user() | ||||||
|     if isinstance(current_user, AnonymousUser): |     return None if isinstance(current_user, AnonymousUser) else current_user | ||||||
|         return None |  | ||||||
|     return current_user |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SessionMiddleware(object): | class SessionMiddleware(object): | ||||||
| @@ -50,10 +45,7 @@ class SessionMiddleware(object): | |||||||
|             request.user = User.objects.get(pk=request.session["_fake_user_id"]) |             request.user = User.objects.get(pk=request.session["_fake_user_id"]) | ||||||
|  |  | ||||||
|         user = request.user |         user = request.user | ||||||
|         if 'HTTP_X_REAL_IP' in request.META: |         ip = request.META.get('HTTP_X_REAL_IP' if 'HTTP_X_REAL_IP' in request.META else 'REMOTE_ADDR') | ||||||
|             ip = request.META.get('HTTP_X_REAL_IP') |  | ||||||
|         else: |  | ||||||
|             ip = request.META.get('REMOTE_ADDR') |  | ||||||
|  |  | ||||||
|         _set_current_user_and_ip(user, request.session, ip) |         _set_current_user_and_ip(user, request.session, ip) | ||||||
|         response = self.get_response(request) |         response = self.get_response(request) | ||||||
| @@ -62,7 +54,7 @@ class SessionMiddleware(object): | |||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  |  | ||||||
| class TurbolinksMiddleware(object): | class TurbolinksMiddleware(object):  # pragma: no cover | ||||||
|     """ |     """ | ||||||
|     Send the `Turbolinks-Location` header in response to a visit that was redirected, |     Send the `Turbolinks-Location` header in response to a visit that was redirected, | ||||||
|     and Turbolinks will replace the browser's topmost history entry. |     and Turbolinks will replace the browser's topmost history entry. | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| """ | """ | ||||||
| Django settings for corres2math project. | Django settings for corres2math project. | ||||||
|  |  | ||||||
| @@ -52,15 +55,12 @@ INSTALLED_APPS = [ | |||||||
|  |  | ||||||
|     'bootstrap_datepicker_plus', |     'bootstrap_datepicker_plus', | ||||||
|     'crispy_forms', |     'crispy_forms', | ||||||
|     'django_extensions', |  | ||||||
|     'django_tables2', |     'django_tables2', | ||||||
|     'haystack', |     'haystack', | ||||||
|     'logs', |     'logs', | ||||||
|     'mailer', |  | ||||||
|     'polymorphic', |     'polymorphic', | ||||||
|     'rest_framework', |     'rest_framework', | ||||||
|     'rest_framework.authtoken', |     'rest_framework.authtoken', | ||||||
|     'cas_server', |  | ||||||
|  |  | ||||||
|     'api', |     'api', | ||||||
|     'eastereggs', |     'eastereggs', | ||||||
| @@ -68,6 +68,13 @@ INSTALLED_APPS = [ | |||||||
|     'participation', |     'participation', | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | if "test" not in sys.argv:  # pragma: no cover | ||||||
|  |     INSTALLED_APPS += [ | ||||||
|  |         'cas_server', | ||||||
|  |         'django_extensions', | ||||||
|  |         'mailer', | ||||||
|  |     ] | ||||||
|  |  | ||||||
| MIDDLEWARE = [ | MIDDLEWARE = [ | ||||||
|     'django.middleware.security.SecurityMiddleware', |     'django.middleware.security.SecurityMiddleware', | ||||||
|     'django.contrib.sessions.middleware.SessionMiddleware', |     'django.contrib.sessions.middleware.SessionMiddleware', | ||||||
| @@ -89,8 +96,7 @@ LOGIN_REDIRECT_URL = "index" | |||||||
| TEMPLATES = [ | TEMPLATES = [ | ||||||
|     { |     { | ||||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', |         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||||
|         'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')] |         'DIRS': [os.path.join(BASE_DIR, 'corres2math/templates')], | ||||||
|         , |  | ||||||
|         'APP_DIRS': True, |         'APP_DIRS': True, | ||||||
|         'OPTIONS': { |         'OPTIONS': { | ||||||
|             'context_processors': [ |             'context_processors': [ | ||||||
| @@ -196,7 +202,7 @@ HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' | |||||||
|  |  | ||||||
| _db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower() | _db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower() | ||||||
|  |  | ||||||
| if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': | if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql':  # pragma: no cover | ||||||
|     DATABASES = { |     DATABASES = { | ||||||
|         'default': { |         'default': { | ||||||
|             'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2', |             'ENGINE': 'django.db.backends.mysql' if _db_type == 'mysql' else 'django.db.backends.postgresql_psycopg2', | ||||||
| @@ -215,7 +221,7 @@ else: | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| if os.getenv("CORRES2MATH_STAGE", "dev") == "prod": | if os.getenv("CORRES2MATH_STAGE", "dev") == "prod":  # pragma: no cover | ||||||
|     from .settings_prod import * |     from .settings_prod import *    # noqa: F401,F403 | ||||||
| else: | else: | ||||||
|     from .settings_dev import * |     from .settings_dev import *     # noqa: F401,F403 | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| # Database | # Database | ||||||
| # https://docs.djangoproject.com/en/3.0/ref/settings/#databases | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
| # Break it, fix it! | # Break it, fix it! | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								corres2math/templates/about.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								corres2math/templates/about.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | {% extends "base.html" %} | ||||||
|  |  | ||||||
|  | {% block contenttitle %} | ||||||
|  |     <h1>À propos</h1> | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block content %} | ||||||
|  |     <p> | ||||||
|  |     La plateforme d'inscription des Correspondances des Jeunes Mathématiciennes a été développée entre 2019 et 2021 | ||||||
|  |     par Yohann D'ANELLO, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants | ||||||
|  |     pour intéragir avec les organisateurs et les autres participants. | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     La plateforme est développée avec le framework <a href="https://www.djangoproject.com/">Django</a> et le code | ||||||
|  |     source est accessible librement sur <a href="https://gitlab.com/animath/si/plateforme-corres2math">Gitlab</a>. | ||||||
|  |     Le code est distribué sous la licence <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU GPL v3</a>, | ||||||
|  |     qui vous autorise à consulter le code, à le partager, à réutiliser des parties du code et à contribuer. | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     Le site principal présent sur <a href="https://inscription.correspondances-maths.fr/">https://inscription.correspondances-maths.fr</a> | ||||||
|  |     est hébergé chez <a href="https://www.scaleway.com/fr/">Scaleway</a>. | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     Les données collectées par cette plateforme sont utilisées uniquement dans le cadre des Correspondances et sont | ||||||
|  |     détruites dès l'action touche à sa fin, soit au plus tard 1 an après le début de l'action. Sur autorisation | ||||||
|  |     explicite, des informations de contact peuvent être conservées afin d'être tenu au courant des actions futures | ||||||
|  |     de l'association Animath. Aucune information personnelle n'est collectée à votre insu. Aucune information | ||||||
|  |     personnelle n'est cédée à des tiers. | ||||||
|  |     </p> | ||||||
|  |  | ||||||
|  |     <p> | ||||||
|  |     Pour toute demande ou réclammation, merci de nous contacter à l'adresse | ||||||
|  |     <a target="_blank" href="mailto:contact@correspondances-maths.fr"> | ||||||
|  |         contact@correspondances-maths.fr | ||||||
|  |     </a>. | ||||||
|  |     </p> | ||||||
|  | {% endblock %} | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| {% load static i18n static %} | {% load static i18n static calendar %} | ||||||
|  |  | ||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} | {% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %} | ||||||
| @@ -70,7 +70,14 @@ | |||||||
|                         <a href="#" class="nav-link" data-toggle="modal" data-target="#calendarModal"><i class="fas fa-calendar"></i> {% trans "Calendar" %}</a> |                         <a href="#" class="nav-link" data-toggle="modal" data-target="#calendarModal"><i class="fas fa-calendar"></i> {% trans "Calendar" %}</a> | ||||||
|                     {% endif %} |                     {% endif %} | ||||||
|                 </li> |                 </li> | ||||||
|                 {% if user.is_authenticated and user.registration.participates %} |                 {% if user.is_authenticated and user.registration.is_admin %} | ||||||
|  |                     <li class="nav-item active"> | ||||||
|  |                         <a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a> | ||||||
|  |                     </li> | ||||||
|  |                     <li class="nav-item active"> | ||||||
|  |                         <a href="#" class="nav-link" data-toggle="modal" data-target="#teamsModal"><i class="fas fa-users"></i> {% trans "Teams" %}</a> | ||||||
|  |                     </li> | ||||||
|  |                 {% elif user.is_authenticated and user.registration.participates %} | ||||||
|                     {% if not user.registration.team %} |                     {% if not user.registration.team %} | ||||||
|                         <li class="nav-item active"> |                         <li class="nav-item active"> | ||||||
|                             <a href="#" class="nav-link" data-toggle="modal" data-target="#createTeamModal"> |                             <a href="#" class="nav-link" data-toggle="modal" data-target="#createTeamModal"> | ||||||
| @@ -122,9 +129,11 @@ | |||||||
|                     </li> |                     </li> | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|                 {% if not user.is_authenticated %} |                 {% if not user.is_authenticated %} | ||||||
|                     <li class="nav-item active"> |                     {% if 1|current_phase %} | ||||||
|                         <a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a> |                         <li class="nav-item active"> | ||||||
|                     </li> |                             <a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a> | ||||||
|  |                         </li> | ||||||
|  |                     {% endif %} | ||||||
|                     <li class="nav-item active"> |                     <li class="nav-item active"> | ||||||
|                         <a class="nav-link" href="#" data-toggle="modal" data-target="#loginModal"> |                         <a class="nav-link" href="#" data-toggle="modal" data-target="#loginModal"> | ||||||
|                             <i class="fas fa-sign-in-alt"></i> {% trans "Log in" %} |                             <i class="fas fa-sign-in-alt"></i> {% trans "Log in" %} | ||||||
| @@ -183,7 +192,7 @@ | |||||||
|                       class="form-inline"> |                       class="form-inline"> | ||||||
|                     <span class="text-muted mr-1"> |                     <span class="text-muted mr-1"> | ||||||
|                         <a target="_blank" href="mailto:contact@correspondances-maths.fr" |                         <a target="_blank" href="mailto:contact@correspondances-maths.fr" | ||||||
|                            class="text-muted">{% trans "Contact us" %}</a> — |                            class="text-muted"><i class="fas fa-envelope"></i> {% trans "Contact us" %}</a> | ||||||
|                     </span> |                     </span> | ||||||
|                     {% csrf_token %} |                     {% csrf_token %} | ||||||
|                     <select title="language" name="language" |                     <select title="language" name="language" | ||||||
| @@ -198,10 +207,15 @@ | |||||||
|                                 {{ lang_name }} ({{ lang_code }}) |                                 {{ lang_name }} ({{ lang_code }}) | ||||||
|                             </option> |                             </option> | ||||||
|                         {% endfor %} |                         {% endfor %} | ||||||
|                     </select> |                     </select>   | ||||||
|                     <noscript> |                     <noscript> | ||||||
|                         <input type="submit"> |                         <input type="submit"> | ||||||
|                     </noscript> |                     </noscript>   | ||||||
|  |                     <a target="_blank" class="text-muted" href="{% url "about" %}">{% trans "About" %}</a>   —   | ||||||
|  |                     <a target="_blank" class="text-muted" | ||||||
|  |                        href="https://gitlab.com/animath/si/plateforme-corres2math"> | ||||||
|  |                         <i class="fab fa-gitlab"></i> | ||||||
|  |                     </a> | ||||||
|                 </form> |                 </form> | ||||||
|             </div> |             </div> | ||||||
|             <div class="col text-right"> |             <div class="col text-right"> | ||||||
| @@ -216,21 +230,28 @@ | |||||||
|  |  | ||||||
| {% trans "Calendar" as modal_title %} | {% trans "Calendar" as modal_title %} | ||||||
| {% include "base_modal.html" with modal_id="calendar" modal_additional_class="modal-lg" %} | {% include "base_modal.html" with modal_id="calendar" modal_additional_class="modal-lg" %} | ||||||
| {% trans "Search results" as modal_title %} |  | ||||||
| {% include "base_modal.html" with modal_id="search" modal_form_method="get" modal_additional_class="modal-lg" %} |  | ||||||
| {% trans "Log in" as modal_title %} |  | ||||||
| {% trans "Log in" as modal_button %} |  | ||||||
| {% url "login" as modal_action %} |  | ||||||
| {% include "base_modal.html" with modal_id="login" %} |  | ||||||
| {% if user.is_authenticated %} | {% if user.is_authenticated %} | ||||||
|  |     {% trans "All teams" as modal_title %} | ||||||
|  |     {% include "base_modal.html" with modal_id="teams" modal_additional_class="modal-lg" %} | ||||||
|  |  | ||||||
|  |     {% trans "Search results" as modal_title %} | ||||||
|  |     {% include "base_modal.html" with modal_id="search" modal_form_method="get" modal_additional_class="modal-lg" %} | ||||||
|  |  | ||||||
|     {% trans "Join team" as modal_title %} |     {% trans "Join team" as modal_title %} | ||||||
|     {% trans "Join" as modal_button %} |     {% trans "Join" as modal_button %} | ||||||
|     {% url "participation:join_team" as modal_action %} |     {% url "participation:join_team" as modal_action %} | ||||||
|  |  | ||||||
|     {% include "base_modal.html" with modal_id="joinTeam" %} |     {% include "base_modal.html" with modal_id="joinTeam" %} | ||||||
|     {% trans "Create team" as modal_title %} |     {% trans "Create team" as modal_title %} | ||||||
|     {% trans "Create" as modal_button %} |     {% trans "Create" as modal_button %} | ||||||
|     {% url "participation:create_team" as modal_action %} |     {% url "participation:create_team" as modal_action %} | ||||||
|     {% include "base_modal.html" with modal_id="createTeam" modal_button_type="success" %} |     {% include "base_modal.html" with modal_id="createTeam" modal_button_type="success" %} | ||||||
|  | {% else %} | ||||||
|  |     {% trans "Log in" as modal_title %} | ||||||
|  |     {% trans "Log in" as modal_button %} | ||||||
|  |     {% url "login" as modal_action %} | ||||||
|  |     {% include "base_modal.html" with modal_id="login" %} | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| @@ -243,26 +264,39 @@ | |||||||
|             if (!modalBody.html().trim()) |             if (!modalBody.html().trim()) | ||||||
|                 modalBody.load("{% url "participation:calendar" %} #form-content") |                 modalBody.load("{% url "participation:calendar" %} #form-content") | ||||||
|         }); |         }); | ||||||
|         $('button[data-target="#searchModal"]').click(function() { |         {% if user.is_authenticated and user.registration.is_admin %} | ||||||
|             let modalBody = $("#searchModal div.modal-body"); |             $('a[data-target="#teamsModal"]').click(function() { | ||||||
|             let q = encodeURI($("#search-term").val()); |                 let modalBody = $("#teamsModal div.modal-body"); | ||||||
|             modalBody.load("{% url "haystack_search" %}?q=" + q + " #search-results"); |                 if (!modalBody.html().trim()) | ||||||
|         }); |                     modalBody.load("{% url "participation:team_list" %} #form-content") | ||||||
|  |             }); | ||||||
|  |             $('button[data-target="#searchModal"]').click(function() { | ||||||
|  |                 let modalBody = $("#searchModal div.modal-body"); | ||||||
|  |                 let q = encodeURI($("#search-term").val()); | ||||||
|  |                 modalBody.load("{% url "haystack_search" %}?q=" + q + " #search-results"); | ||||||
|  |             }); | ||||||
|  |         {% endif %} | ||||||
|  |  | ||||||
|  |         {% if not user.is_authenticated %} | ||||||
|         $('a[data-target="#loginModal"]').click(function() { |         $('a[data-target="#loginModal"]').click(function() { | ||||||
|             let modalBody = $("#loginModal div.modal-body"); |                 let modalBody = $("#loginModal div.modal-body"); | ||||||
|             if (!modalBody.html().trim()) |                 if (!modalBody.html().trim()) | ||||||
|                 modalBody.load("{% url "login" %} #form-content") |                     modalBody.load("{% url "login" %} #form-content") | ||||||
|         }); |             }); | ||||||
|         $('a[data-target="#createTeamModal"]').click(function() { |         {% endif %} | ||||||
|             let modalBody = $("#createTeamModal div.modal-body"); |  | ||||||
|             if (!modalBody.html().trim()) |         {% if user.is_authenticated and user.registration.participates and not user.registration.team %} | ||||||
|                 modalBody.load("{% url "participation:create_team" %} #form-content"); |             $('a[data-target="#createTeamModal"]').click(function() { | ||||||
|         }); |                 let modalBody = $("#createTeamModal div.modal-body"); | ||||||
|         $('a[data-target="#joinTeamModal"]').click(function() { |                 if (!modalBody.html().trim()) | ||||||
|             let modalBody = $("#joinTeamModal div.modal-body"); |                     modalBody.load("{% url "participation:create_team" %} #form-content"); | ||||||
|             if (!modalBody.html().trim()) |             }); | ||||||
|                 modalBody.load("{% url "participation:join_team" %} #form-content"); |             $('a[data-target="#joinTeamModal"]').click(function() { | ||||||
|         }); |                 let modalBody = $("#joinTeamModal div.modal-body"); | ||||||
|  |                 if (!modalBody.html().trim()) | ||||||
|  |                     modalBody.load("{% url "participation:join_team" %} #form-content"); | ||||||
|  |             }); | ||||||
|  |         {% endif %} | ||||||
|     }); |     }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,6 +27,11 @@ | |||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  |         <div class="alert alert-danger"> | ||||||
|  |             Avertissement : certains rencontrent des difficultés à recevoir les mails automatiques. Merci de vérifier | ||||||
|  |             régulièrement votre boîte spam, et à nous contacter en cas de problème. | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|     <div class="jumbotron"> |     <div class="jumbotron"> | ||||||
|         <h5 class="display-4">Comment ça marche ?</h5> |         <h5 class="display-4">Comment ça marche ?</h5> | ||||||
|         <p> |         <p> | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								corres2math/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								corres2math/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
|  | 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) | ||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib.auth.tokens import PasswordResetTokenGenerator | from django.contrib.auth.tokens import PasswordResetTokenGenerator | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| """corres2math URL Configuration | """corres2math URL Configuration | ||||||
|  |  | ||||||
| The `urlpatterns` list routes URLs to views. For more information please see: | The `urlpatterns` list routes URLs to views. For more information please see: | ||||||
| @@ -13,16 +16,18 @@ Including another URLconf | |||||||
|     1. Import the include() function: from django.urls import include, path |     1. Import the include() function: from django.urls import include, path | ||||||
|     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) |     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) | ||||||
| """ | """ | ||||||
|  | from django.conf import settings | ||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from django.urls import path, include | from django.urls import include, path | ||||||
| from django.views.defaults import bad_request, permission_denied, page_not_found, server_error | from django.views.defaults import bad_request, page_not_found, permission_denied, server_error | ||||||
| from django.views.generic import TemplateView | from django.views.generic import TemplateView | ||||||
| from registration.views import PhotoAuthorizationView | from registration.views import PhotoAuthorizationView | ||||||
|  |  | ||||||
| from .views import AdminSearchView | from .views import AdminSearchView | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path('', TemplateView.as_view(template_name="index.html", extra_context=dict(title="Accueil")), name='index'), |     path('', TemplateView.as_view(template_name="index.html"), name='index'), | ||||||
|  |     path('about/', TemplateView.as_view(template_name="about.html"), name='about'), | ||||||
|     path('i18n/', include('django.conf.urls.i18n')), |     path('i18n/', include('django.conf.urls.i18n')), | ||||||
|     path('admin/doc/', include('django.contrib.admindocs.urls')), |     path('admin/doc/', include('django.contrib.admindocs.urls')), | ||||||
|     path('admin/', admin.site.urls, name="admin"), |     path('admin/', admin.site.urls, name="admin"), | ||||||
| @@ -35,11 +40,14 @@ urlpatterns = [ | |||||||
|  |  | ||||||
|     path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), name='photo_authorization'), |     path('media/authorization/photo/<str:filename>/', PhotoAuthorizationView.as_view(), name='photo_authorization'), | ||||||
|  |  | ||||||
|     path('cas/', include('cas_server.urls', namespace="cas_server")), |  | ||||||
|  |  | ||||||
|     path('', include('eastereggs.urls')), |     path('', include('eastereggs.urls')), | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | if 'cas_server' in settings.INSTALLED_APPS:  # pragma: no cover | ||||||
|  |     urlpatterns += [ | ||||||
|  |         path('cas/', include('cas_server.urls', namespace="cas_server")), | ||||||
|  |     ] | ||||||
|  |  | ||||||
| handler400 = bad_request | handler400 = bad_request | ||||||
| handler403 = permission_denied | handler403 = permission_denied | ||||||
| handler404 = page_not_found | handler404 = page_not_found | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from haystack.generic_views import SearchView | from haystack.generic_views import SearchView | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | # Copyright (C) 2020 by Animath | ||||||
|  | # SPDX-License-Identifier: GPL-3.0-or-later | ||||||
|  |  | ||||||
| """ | """ | ||||||
| WSGI config for corres2math project. | WSGI config for corres2math project. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,10 +2,13 @@ | |||||||
|  |  | ||||||
| crond -l 0 | crond -l 0 | ||||||
|  |  | ||||||
|  | python manage.py migrate | ||||||
|  | python manage.py loaddata initial | ||||||
|  |  | ||||||
| nginx | nginx | ||||||
|  |  | ||||||
| if [ "$CORRES2MATH_STAGE" = "prod" ]; then | if [ "$CORRES2MATH_STAGE" = "prod" ]; then | ||||||
|     gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread corres2math.wsgi --access-logfile '-' --error-logfile '-'; |     gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread corres2math.wsgi --access-logfile '-' --error-logfile '-'; | ||||||
| else | else | ||||||
|     ./manage.py runserver 0.0.0.0:8000; |     python manage.py runserver 0.0.0.0:8000; | ||||||
| fi | fi | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ msgid "" | |||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: Corres2math\n" | "Project-Id-Version: Corres2math\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2020-11-02 10:56+0100\n" | "POT-Creation-Date: 2020-12-22 21:30+0100\n" | ||||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||||
| "Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n" | "Last-Translator: Yohann D'ANELLO <yohann.danello@animath.fr>\n" | ||||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | "Language-Team: LANGUAGE <LL@li.org>\n" | ||||||
| @@ -30,7 +30,7 @@ msgid "This task failed successfully." | |||||||
| msgstr "Cette tâche a échoué avec succès." | msgstr "Cette tâche a échoué avec succès." | ||||||
|  |  | ||||||
| #: apps/eastereggs/templates/eastereggs/xp_modal.html:16 | #: apps/eastereggs/templates/eastereggs/xp_modal.html:16 | ||||||
| #: templates/base_modal.html:19 | #: corres2math/templates/base_modal.html:19 | ||||||
| msgid "Close" | msgid "Close" | ||||||
| msgstr "Fermer" | msgstr "Fermer" | ||||||
|  |  | ||||||
| @@ -99,7 +99,17 @@ msgstr "changelogs" | |||||||
| msgid "Changelog of type \"{action}\" for model {model} at {timestamp}" | msgid "Changelog of type \"{action}\" for model {model} at {timestamp}" | ||||||
| msgstr "Changelog de type \"{action}\" pour le modèle {model} le {timestamp}" | msgstr "Changelog de type \"{action}\" pour le modèle {model} le {timestamp}" | ||||||
|  |  | ||||||
| #: apps/participation/forms.py:20 apps/participation/models.py:33 | #: apps/participation/admin.py:16 apps/participation/models.py:132 | ||||||
|  | #: 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:138 | ||||||
|  | #: apps/participation/models.py:192 | ||||||
|  | msgid "valid" | ||||||
|  | msgstr "valide" | ||||||
|  |  | ||||||
|  | #: apps/participation/forms.py:20 apps/participation/models.py:32 | ||||||
| msgid "The trigram must be composed of three uppercase letters." | msgid "The trigram must be composed of three uppercase letters." | ||||||
| msgstr "Le trigramme doit être composé de trois lettres majuscules." | msgstr "Le trigramme doit être composé de trois lettres majuscules." | ||||||
|  |  | ||||||
| @@ -123,44 +133,50 @@ msgstr "Vous ne pouvez pas envoyer de vidéo après la date limite." | |||||||
| msgid "Send to team" | msgid "Send to team" | ||||||
| msgstr "Envoyer à l'équipe" | msgstr "Envoyer à l'équipe" | ||||||
|  |  | ||||||
| #: apps/participation/forms.py:152 | #: apps/participation/forms.py:156 | ||||||
| msgid "How did you get the idea to ...?" | msgid "How did you get the idea to ...?" | ||||||
| msgstr "Comment avez-vous eu l'idée de ... ?" | msgstr "Comment avez-vous eu l'idée de ... ?" | ||||||
|  |  | ||||||
| #: apps/participation/forms.py:175 | #: apps/participation/forms.py:160 | ||||||
|  | msgid "You can only create or update a question during the second phase." | ||||||
|  | msgstr "" | ||||||
|  | "Vous pouvez créer ou modifier une question seulement pendant la seconde " | ||||||
|  | "phase." | ||||||
|  |  | ||||||
|  | #: apps/participation/forms.py:186 | ||||||
| msgid "Start date must be before the end date." | msgid "Start date must be before the end date." | ||||||
| msgstr "La date de début doit être avant la date de fin." | msgstr "La date de début doit être avant la date de fin." | ||||||
|  |  | ||||||
| #: apps/participation/forms.py:177 | #: apps/participation/forms.py:188 | ||||||
| msgid "This phase must start after the previous phases." | msgid "This phase must start after the previous phases." | ||||||
| msgstr "Cette phase doit commencer après les phases précédentes." | msgstr "Cette phase doit commencer après les phases précédentes." | ||||||
|  |  | ||||||
| #: apps/participation/forms.py:179 | #: apps/participation/forms.py:190 | ||||||
| msgid "This phase must end after the next phases." | msgid "This phase must end after the next phases." | ||||||
| msgstr "Cette phase doit finir avant les phases suivantes." | msgstr "Cette phase doit finir avant les phases suivantes." | ||||||
|  |  | ||||||
| #: apps/participation/models.py:26 apps/participation/tables.py:30 | #: apps/participation/models.py:25 apps/participation/tables.py:30 | ||||||
| #: apps/participation/tables.py:52 apps/participation/tables.py:78 | #: apps/participation/tables.py:52 apps/participation/tables.py:78 | ||||||
| msgid "name" | msgid "name" | ||||||
| msgstr "nom" | msgstr "nom" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:32 apps/participation/tables.py:57 | #: apps/participation/models.py:31 apps/participation/tables.py:57 | ||||||
| msgid "trigram" | msgid "trigram" | ||||||
| msgstr "trigramme" | msgstr "trigramme" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:40 | #: apps/participation/models.py:39 | ||||||
| msgid "access code" | msgid "access code" | ||||||
| msgstr "code d'accès" | msgstr "code d'accès" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:41 | #: apps/participation/models.py:40 | ||||||
| msgid "The access code let other people to join the team." | msgid "The access code let other people to join the team." | ||||||
| msgstr "Le code d'accès permet aux autres participants de rejoindre l'équipe." | msgstr "Le code d'accès permet aux autres participants de rejoindre l'équipe." | ||||||
|  |  | ||||||
| #: apps/participation/models.py:45 | #: apps/participation/models.py:44 | ||||||
| msgid "Grant Animath to publish my video" | msgid "Grant Animath to publish my video" | ||||||
| msgstr "Autoriser Animath à publier ma vidéo" | msgstr "Autoriser Animath à publier ma vidéo" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:46 | #: apps/participation/models.py:45 | ||||||
| msgid "" | msgid "" | ||||||
| "Give the authorisation to publish the video on the main website to promote " | "Give the authorisation to publish the video on the main website to promote " | ||||||
| "the action." | "the action." | ||||||
| @@ -168,105 +184,96 @@ msgstr "" | |||||||
| "Donner l'autorisation de publier la vidéo sur le site principal pour " | "Donner l'autorisation de publier la vidéo sur le site principal pour " | ||||||
| "promouvoir les Correspondances." | "promouvoir les Correspondances." | ||||||
|  |  | ||||||
| #: apps/participation/models.py:97 | #: apps/participation/models.py:107 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Team {name} ({trigram})" | msgid "Team {name} ({trigram})" | ||||||
| msgstr "Équipe {name} ({trigram})" | msgstr "Équipe {name} ({trigram})" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:100 apps/participation/models.py:115 | #: apps/participation/models.py:110 apps/participation/models.py:125 | ||||||
| #: apps/registration/models.py:106 apps/registration/models.py:155 | #: apps/registration/models.py:106 apps/registration/models.py:155 | ||||||
| msgid "team" | msgid "team" | ||||||
| msgstr "équipe" | msgstr "équipe" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:101 | #: apps/participation/models.py:111 | ||||||
| msgid "teams" | msgid "teams" | ||||||
| msgstr "équipes" | msgstr "équipes" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:119 | #: apps/participation/models.py:129 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Problem #{problem:d}" | msgid "Problem #{problem:d}" | ||||||
| msgstr "Problème n°{problem:d}" | msgstr "Problème n°{problem:d}" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:122 apps/participation/tables.py:35 | #: apps/participation/models.py:139 apps/participation/models.py:193 | ||||||
| #: 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." | msgid "The video got the validation of the administrators." | ||||||
| msgstr "La vidéo a été validée par les administrateurs." | msgstr "La vidéo a été validée par les administrateurs." | ||||||
|  |  | ||||||
| #: apps/participation/models.py:138 | #: apps/participation/models.py:148 | ||||||
| msgid "solution video" | msgid "solution video" | ||||||
| msgstr "vidéo de solution" | msgstr "vidéo de solution" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:147 | #: apps/participation/models.py:157 | ||||||
| msgid "received participation" | msgid "received participation" | ||||||
| msgstr "participation reçue" | msgstr "participation reçue" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:156 | #: apps/participation/models.py:166 | ||||||
| msgid "synthesis video" | msgid "synthesis video" | ||||||
| msgstr "vidéo de synthèse" | msgstr "vidéo de synthèse" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:163 | #: apps/participation/models.py:173 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Participation of the team {name} ({trigram})" | msgid "Participation of the team {name} ({trigram})" | ||||||
| msgstr "Participation de l'équipe {name} ({trigram})" | msgstr "Participation de l'équipe {name} ({trigram})" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:166 apps/participation/models.py:240 | #: apps/participation/models.py:176 apps/participation/models.py:250 | ||||||
| msgid "participation" | msgid "participation" | ||||||
| msgstr "participation" | msgstr "participation" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:167 | #: apps/participation/models.py:177 | ||||||
| msgid "participations" | msgid "participations" | ||||||
| msgstr "participations" | msgstr "participations" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:175 | #: apps/participation/models.py:185 | ||||||
| msgid "link" | msgid "link" | ||||||
| msgstr "lien" | msgstr "lien" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:176 | #: apps/participation/models.py:186 | ||||||
| msgid "The full video link." | msgid "The full video link." | ||||||
| msgstr "Le lien complet de la vidéo." | msgstr "Le lien complet de la vidéo." | ||||||
|  |  | ||||||
| #: apps/participation/models.py:225 | #: apps/participation/models.py:235 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Video of team {name} ({trigram})" | msgid "Video of team {name} ({trigram})" | ||||||
| msgstr "Vidéo de l'équipe {name} ({trigram})" | msgstr "Vidéo de l'équipe {name} ({trigram})" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:229 | #: apps/participation/models.py:239 | ||||||
| msgid "video" | msgid "video" | ||||||
| msgstr "vidéo" | msgstr "vidéo" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:230 | #: apps/participation/models.py:240 | ||||||
| msgid "videos" | msgid "videos" | ||||||
| msgstr "vidéos" | msgstr "vidéos" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:245 | #: apps/participation/models.py:255 | ||||||
| msgid "question" | msgid "question" | ||||||
| msgstr "question" | msgstr "question" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:259 | #: apps/participation/models.py:269 | ||||||
| msgid "phase number" | msgid "phase number" | ||||||
| msgstr "phase" | msgstr "phase" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:264 | #: apps/participation/models.py:274 | ||||||
| msgid "phase description" | msgid "phase description" | ||||||
| msgstr "description" | msgstr "description" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:268 | #: apps/participation/models.py:278 | ||||||
| msgid "start date of the given phase" | msgid "start date of the given phase" | ||||||
| msgstr "début de la phase" | msgstr "début de la phase" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:273 | #: apps/participation/models.py:283 | ||||||
| msgid "end date of the given phase" | msgid "end date of the given phase" | ||||||
| msgstr "fin de la phase" | msgstr "fin de la phase" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:291 | #: apps/participation/models.py:299 | ||||||
| msgid "" | msgid "" | ||||||
| "Phase {phase_number:d} starts on {start:%Y-%m-%d %H:%M} and ends on {end:%Y-" | "Phase {phase_number:d} starts on {start:%Y-%m-%d %H:%M} and ends on {end:%Y-" | ||||||
| "%m-%d %H:%M}" | "%m-%d %H:%M}" | ||||||
| @@ -274,21 +281,55 @@ msgstr "" | |||||||
| "Phase {phase_number:d} démarrant le {start:%d/%m/%Y %H:%M} et finissant le " | "Phase {phase_number:d} démarrant le {start:%d/%m/%Y %H:%M} et finissant le " | ||||||
| "{end:%d/%m/%Y %H:%M}" | "{end:%d/%m/%Y %H:%M}" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:295 | #: apps/participation/models.py:303 | ||||||
| msgid "phase" | msgid "phase" | ||||||
| msgstr "phase" | msgstr "phase" | ||||||
|  |  | ||||||
| #: apps/participation/models.py:296 | #: apps/participation/models.py:304 | ||||||
| msgid "phases" | msgid "phases" | ||||||
| msgstr "phases" | msgstr "phases" | ||||||
|  |  | ||||||
|  | #: apps/participation/templates/participation/chat.html:7 | ||||||
|  | msgid "The chat is located on the dedicated Matrix server:" | ||||||
|  | msgstr "Le chat est situé sur le serveur Matrix dédié aux Correspondances :" | ||||||
|  |  | ||||||
|  | #: apps/participation/templates/participation/chat.html:14 | ||||||
|  | msgid "Access to the Matrix server" | ||||||
|  | msgstr "Accéder au serveur Matrix" | ||||||
|  |  | ||||||
|  | #: apps/participation/templates/participation/chat.html:20 | ||||||
|  | msgid "" | ||||||
|  | "To connect to the server, you can select \"Log in\", then use your " | ||||||
|  | "credentials of this platform to connect with the central authentication " | ||||||
|  | "server, then you must trust the connection between the Matrix account and " | ||||||
|  | "the platform. Finally, you will be able to access to the chat platform." | ||||||
|  | msgstr "" | ||||||
|  | "Pour se connecter au serveur, vous pouvez sélectionner \"Se connecter\", " | ||||||
|  | "puis utiliser vos identifiants de cette plateforme via l'authentication " | ||||||
|  | "centralisée. Vous devrez ensuite approuver la connexion entre le compte " | ||||||
|  | "Matrix et la plateforme. Enfin, vous pourrez accéder à la plateforme de chat." | ||||||
|  |  | ||||||
|  | #: apps/participation/templates/participation/chat.html:28 | ||||||
|  | msgid "" | ||||||
|  | "You will be invited in some basic rooms. You must confirm the invitations to " | ||||||
|  | "join channels." | ||||||
|  | msgstr "" | ||||||
|  | "Vous serez invités dans quelques salons basiques. Vous devez confirmer les " | ||||||
|  | "invitations pour rejoindre les canaux." | ||||||
|  |  | ||||||
|  | #: apps/participation/templates/participation/chat.html:34 | ||||||
|  | msgid "If you have any trouble, don't hesitate to contact us :)" | ||||||
|  | msgstr "" | ||||||
|  | "Si vous rencontrez le moindre problème, n'hésitez surtout pas à nous " | ||||||
|  | "contacter :)" | ||||||
|  |  | ||||||
| #: apps/participation/templates/participation/create_team.html:11 | #: apps/participation/templates/participation/create_team.html:11 | ||||||
| #: templates/base.html:231 | #: corres2math/templates/base.html:247 | ||||||
| msgid "Create" | msgid "Create" | ||||||
| msgstr "Créer" | msgstr "Créer" | ||||||
|  |  | ||||||
| #: apps/participation/templates/participation/join_team.html:11 | #: apps/participation/templates/participation/join_team.html:11 | ||||||
| #: templates/base.html:227 | #: corres2math/templates/base.html:242 | ||||||
| msgid "Join" | msgid "Join" | ||||||
| msgstr "Rejoindre" | msgstr "Rejoindre" | ||||||
|  |  | ||||||
| @@ -462,7 +503,7 @@ msgstr "Définir l'équipe qui recevra votre vidéo" | |||||||
|  |  | ||||||
| #: apps/participation/templates/participation/participation_detail.html:181 | #: apps/participation/templates/participation/participation_detail.html:181 | ||||||
| #: apps/participation/templates/participation/participation_detail.html:233 | #: apps/participation/templates/participation/participation_detail.html:233 | ||||||
| #: apps/participation/views.py:463 | #: apps/participation/views.py:510 | ||||||
| msgid "Upload video" | msgid "Upload video" | ||||||
| msgstr "Envoyer la vidéo" | msgstr "Envoyer la vidéo" | ||||||
|  |  | ||||||
| @@ -497,7 +538,7 @@ msgid "Update question" | |||||||
| msgstr "Modifier la question" | msgstr "Modifier la question" | ||||||
|  |  | ||||||
| #: apps/participation/templates/participation/participation_detail.html:217 | #: apps/participation/templates/participation/participation_detail.html:217 | ||||||
| #: apps/participation/views.py:440 | #: apps/participation/views.py:486 | ||||||
| msgid "Delete question" | msgid "Delete question" | ||||||
| msgstr "Supprimer la question" | msgstr "Supprimer la question" | ||||||
|  |  | ||||||
| @@ -507,8 +548,8 @@ msgid "Display synthesis" | |||||||
| msgstr "Afficher la synthèse" | msgstr "Afficher la synthèse" | ||||||
|  |  | ||||||
| #: apps/participation/templates/participation/phase_list.html:10 | #: apps/participation/templates/participation/phase_list.html:10 | ||||||
| #: apps/participation/views.py:482 templates/base.html:68 | #: apps/participation/views.py:531 corres2math/templates/base.html:68 | ||||||
| #: templates/base.html:70 templates/base.html:217 | #: corres2math/templates/base.html:70 corres2math/templates/base.html:231 | ||||||
| msgid "Calendar" | msgid "Calendar" | ||||||
| msgstr "Calendrier" | msgstr "Calendrier" | ||||||
|  |  | ||||||
| @@ -620,7 +661,7 @@ msgid "Update team" | |||||||
| msgstr "Modifier l'équipe" | msgstr "Modifier l'équipe" | ||||||
|  |  | ||||||
| #: apps/participation/templates/participation/team_detail.html:127 | #: apps/participation/templates/participation/team_detail.html:127 | ||||||
| #: apps/participation/views.py:296 | #: apps/participation/views.py:337 | ||||||
| msgid "Leave team" | msgid "Leave team" | ||||||
| msgstr "Quitter l'équipe" | msgstr "Quitter l'équipe" | ||||||
|  |  | ||||||
| @@ -628,93 +669,107 @@ msgstr "Quitter l'équipe" | |||||||
| msgid "Are you sure that you want to leave this team?" | msgid "Are you sure that you want to leave this team?" | ||||||
| msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?" | msgstr "Êtes-vous sûr·e de vouloir quitter cette équipe ?" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:36 templates/base.html:77 | #: apps/participation/templates/participation/team_list.html:6 | ||||||
| #: templates/base.html:230 | #: corres2math/templates/base.html:235 | ||||||
|  | msgid "All teams" | ||||||
|  | msgstr "Toutes les équipes" | ||||||
|  |  | ||||||
|  | #: apps/participation/views.py:37 corres2math/templates/base.html:84 | ||||||
|  | #: corres2math/templates/base.html:246 | ||||||
| msgid "Create team" | msgid "Create team" | ||||||
| msgstr "Créer une équipe" | msgstr "Créer une équipe" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:43 apps/participation/views.py:89 | #: apps/participation/views.py:46 apps/participation/views.py:94 | ||||||
| msgid "You don't participate, so you can't create a team." | msgid "You don't participate, so you can't create a team." | ||||||
| msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe." | msgstr "Vous ne participez pas, vous ne pouvez pas créer d'équipe." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:45 apps/participation/views.py:91 | #: apps/participation/views.py:48 apps/participation/views.py:96 | ||||||
| msgid "You are already in a team." | msgid "You are already in a team." | ||||||
| msgstr "Vous êtes déjà dans une équipe." | msgstr "Vous êtes déjà dans une équipe." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:82 templates/base.html:82 | #: apps/participation/views.py:85 corres2math/templates/base.html:89 | ||||||
| #: templates/base.html:226 | #: corres2math/templates/base.html:241 | ||||||
| msgid "Join team" | msgid "Join team" | ||||||
| msgstr "Rejoindre une équipe" | msgstr "Rejoindre une équipe" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:133 apps/participation/views.py:302 | #: apps/participation/views.py:147 apps/participation/views.py:343 | ||||||
| #: apps/participation/views.py:335 | #: apps/participation/views.py:376 | ||||||
| msgid "You are not in a team." | msgid "You are not in a team." | ||||||
| msgstr "Vous n'êtes pas dans une équipe." | msgstr "Vous n'êtes pas dans une équipe." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:134 apps/participation/views.py:336 | #: apps/participation/views.py:148 apps/participation/views.py:377 | ||||||
| msgid "You don't participate, so you don't have any team." | msgid "You don't participate, so you don't have any team." | ||||||
| msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." | msgstr "Vous ne participez pas, vous n'avez donc pas d'équipe." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:155 | #: apps/participation/views.py:170 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Detail of team {trigram}" | msgid "Detail of team {trigram}" | ||||||
| msgstr "Détails de l'équipe {trigram}" | msgstr "Détails de l'équipe {trigram}" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:180 | #: apps/participation/views.py:202 | ||||||
| msgid "You don't participate, so you can't request the validation of the team." | msgid "You don't participate, so you can't request the validation of the team." | ||||||
| msgstr "" | msgstr "" | ||||||
| "Vous ne participez pas, vous ne pouvez pas demander la validation de " | "Vous ne participez pas, vous ne pouvez pas demander la validation de " | ||||||
| "l'équipe." | "l'équipe." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:183 | #: apps/participation/views.py:205 | ||||||
| msgid "The validation of the team is already done or pending." | msgid "The validation of the team is already done or pending." | ||||||
| msgstr "La validation de l'équipe est déjà faite ou en cours." | msgstr "La validation de l'équipe est déjà faite ou en cours." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:196 | #: apps/participation/views.py:208 | ||||||
|  | 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:227 | ||||||
| msgid "You are not an administrator." | msgid "You are not an administrator." | ||||||
| msgstr "Vous n'êtes pas administrateur." | msgstr "Vous n'êtes pas administrateur." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:199 | #: apps/participation/views.py:230 | ||||||
| msgid "This team has no pending validation." | msgid "This team has no pending validation." | ||||||
| msgstr "L'équipe n'a pas de validation en attente." | msgstr "L'équipe n'a pas de validation en attente." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:218 | #: apps/participation/views.py:254 | ||||||
| msgid "You must specify if you validate the registration or not." | msgid "You must specify if you validate the registration or not." | ||||||
| msgstr "Vous devez spécifier si vous validez l'inscription ou non." | msgstr "Vous devez spécifier si vous validez l'inscription ou non." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:245 | #: apps/participation/views.py:284 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Update team {trigram}" | msgid "Update team {trigram}" | ||||||
| msgstr "Mise à jour de l'équipe {trigram}" | msgstr "Mise à jour de l'équipe {trigram}" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:282 apps/registration/views.py:243 | #: apps/participation/views.py:323 apps/registration/views.py:283 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Photo authorization of {student}.{ext}" | msgid "Photo authorization of {student}.{ext}" | ||||||
| msgstr "Autorisation de droit à l'image de {student}.{ext}" | msgstr "Autorisation de droit à l'image de {student}.{ext}" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:286 | #: apps/participation/views.py:327 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Photo authorizations of team {trigram}.zip" | msgid "Photo authorizations of team {trigram}.zip" | ||||||
| msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip" | msgstr "Autorisations de droit à l'image de l'équipe {trigram}.zip" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:304 | #: apps/participation/views.py:345 | ||||||
| msgid "The team is already validated or the validation is pending." | msgid "The team is already validated or the validation is pending." | ||||||
| msgstr "La validation de l'équipe est déjà faite ou en cours." | msgstr "La validation de l'équipe est déjà faite ou en cours." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:348 | #: apps/participation/views.py:391 | ||||||
| msgid "The team is not validated yet." | msgid "The team is not validated yet." | ||||||
| msgstr "L'équipe n'est pas encore validée." | msgstr "L'équipe n'est pas encore validée." | ||||||
|  |  | ||||||
| #: apps/participation/views.py:357 | #: apps/participation/views.py:401 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Participation of team {trigram}" | msgid "Participation of team {trigram}" | ||||||
| msgstr "Participation de l'équipe {trigram}" | msgstr "Participation de l'équipe {trigram}" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:394 | #: apps/participation/views.py:438 | ||||||
| msgid "Create question" | msgid "Create question" | ||||||
| msgstr "Créer une question" | msgstr "Créer une question" | ||||||
|  |  | ||||||
| #: apps/participation/views.py:491 | #: apps/participation/views.py:540 | ||||||
| msgid "Calendar update" | msgid "Calendar update" | ||||||
| msgstr "Mise à jour du calendrier" | msgstr "Mise à jour du calendrier" | ||||||
|  |  | ||||||
| @@ -730,7 +785,15 @@ msgstr "participant" | |||||||
| msgid "coach" | msgid "coach" | ||||||
| msgstr "encadrant" | msgstr "encadrant" | ||||||
|  |  | ||||||
| #: apps/registration/forms.py:68 | #: apps/registration/forms.py:31 | ||||||
|  | msgid "This email address is already used." | ||||||
|  | msgstr "Cette adresse e-mail est déjà utilisée." | ||||||
|  |  | ||||||
|  | #: apps/registration/forms.py:78 | ||||||
|  | msgid "The uploaded file size must be under 2 Mo." | ||||||
|  | msgstr "Le fichier envoyé doit peser moins de 2 Mo." | ||||||
|  |  | ||||||
|  | #: apps/registration/forms.py:80 | ||||||
| msgid "The uploaded file must be a PDF, PNG of JPEG file." | msgid "The uploaded file must be a PDF, PNG of JPEG file." | ||||||
| msgstr "Le fichier envoyé doit être au format PDF, PNG ou JPEG." | msgstr "Le fichier envoyé doit être au format PDF, PNG ou JPEG." | ||||||
|  |  | ||||||
| @@ -832,13 +895,21 @@ msgstr "Votre email a été validé avec succès." | |||||||
| msgid "You can now <a href=\"%(login_url)s\">log in</a>." | msgid "You can now <a href=\"%(login_url)s\">log in</a>." | ||||||
| msgstr "Vous pouvez désormais vous <a href=\"%(login_url)s\">connecter</a>." | msgstr "Vous pouvez désormais vous <a href=\"%(login_url)s\">connecter</a>." | ||||||
|  |  | ||||||
| #: apps/registration/templates/registration/email_validation_complete.html:22 | #: apps/registration/templates/registration/email_validation_complete.html:23 | ||||||
| msgid "" | msgid "" | ||||||
| "The link was invalid. The token may have expired. Please send us an email to " | "The link was invalid. The token may have expired, or your account is already " | ||||||
| "activate your account." | "activated. However, your account seems to be already valid." | ||||||
| msgstr "" | msgstr "" | ||||||
| "Le lien est invalide. Le jeton a peut-être expiré. Merci de nous envoyer un " | "Le lien est invalide. Le jeton a peut-être expiré, ou votre compte est déjà " | ||||||
| "mail pour activer votre compte." | "activé. Toutefois, il semble que votre compte est déjà valide." | ||||||
|  |  | ||||||
|  | #: apps/registration/templates/registration/email_validation_complete.html:25 | ||||||
|  | msgid "" | ||||||
|  | "The link was invalid. The token may have expired, or your account is already " | ||||||
|  | "activated. Please send us an email to activate your account." | ||||||
|  | msgstr "" | ||||||
|  | "Le lien est invalide. Le jeton a peut-être expiré, ou votre compte est déjà " | ||||||
|  | "activé. Merci de nous envoyer un mail pour activer votre compte." | ||||||
|  |  | ||||||
| #: apps/registration/templates/registration/email_validation_email_sent.html:10 | #: apps/registration/templates/registration/email_validation_email_sent.html:10 | ||||||
| msgid "Account activation" | msgid "Account activation" | ||||||
| @@ -907,9 +978,11 @@ msgid "Your password has been set. You may go ahead and log in now." | |||||||
| msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter." | msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter." | ||||||
|  |  | ||||||
| #: apps/registration/templates/registration/password_reset_complete.html:10 | #: apps/registration/templates/registration/password_reset_complete.html:10 | ||||||
| #: templates/base.html:130 templates/base.html:221 templates/base.html:222 | #: corres2math/templates/base.html:139 corres2math/templates/base.html:251 | ||||||
| #: templates/registration/login.html:7 templates/registration/login.html:8 | #: corres2math/templates/base.html:252 | ||||||
| #: templates/registration/login.html:25 | #: corres2math/templates/registration/login.html:7 | ||||||
|  | #: corres2math/templates/registration/login.html:8 | ||||||
|  | #: corres2math/templates/registration/login.html:25 | ||||||
| msgid "Log in" | msgid "Log in" | ||||||
| msgstr "Connexion" | msgstr "Connexion" | ||||||
|  |  | ||||||
| @@ -963,7 +1036,7 @@ msgstr "Réinitialiser mon mot de passe" | |||||||
| #: apps/registration/templates/registration/signup.html:5 | #: apps/registration/templates/registration/signup.html:5 | ||||||
| #: apps/registration/templates/registration/signup.html:8 | #: apps/registration/templates/registration/signup.html:8 | ||||||
| #: apps/registration/templates/registration/signup.html:20 | #: apps/registration/templates/registration/signup.html:20 | ||||||
| #: apps/registration/views.py:28 | #: apps/registration/views.py:32 | ||||||
| msgid "Sign up" | msgid "Sign up" | ||||||
| msgstr "Inscription" | msgstr "Inscription" | ||||||
|  |  | ||||||
| @@ -1040,53 +1113,57 @@ msgid "Update user" | |||||||
| msgstr "Modifier l'utilisateur" | msgstr "Modifier l'utilisateur" | ||||||
|  |  | ||||||
| #: apps/registration/templates/registration/user_detail.html:77 | #: apps/registration/templates/registration/user_detail.html:77 | ||||||
| #: apps/registration/views.py:206 | #: apps/registration/views.py:246 | ||||||
| msgid "Upload photo authorization" | msgid "Upload photo authorization" | ||||||
| msgstr "Téléverser l'autorisation de droit à l'image" | msgstr "Téléverser l'autorisation de droit à l'image" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:64 | #: apps/registration/views.py:40 | ||||||
|  | msgid "You can't register now." | ||||||
|  | msgstr "Vous ne pouvez pas vous inscrire maintenant." | ||||||
|  |  | ||||||
|  | #: apps/registration/views.py:84 | ||||||
| msgid "Email validation" | msgid "Email validation" | ||||||
| msgstr "Validation de l'adresse mail" | msgstr "Validation de l'adresse mail" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:66 | #: apps/registration/views.py:86 | ||||||
| msgid "Validate email" | msgid "Validate email" | ||||||
| msgstr "Valider l'adresse mail" | msgstr "Valider l'adresse mail" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:105 | #: apps/registration/views.py:125 | ||||||
| msgid "Email validation unsuccessful" | msgid "Email validation unsuccessful" | ||||||
| msgstr "Échec de la validation de l'adresse mail" | msgstr "Échec de la validation de l'adresse mail" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:116 | #: apps/registration/views.py:136 | ||||||
| msgid "Email validation email sent" | msgid "Email validation email sent" | ||||||
| msgstr "Mail de confirmation de l'adresse mail envoyé" | msgstr "Mail de confirmation de l'adresse mail envoyé" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:124 | #: apps/registration/views.py:144 | ||||||
| msgid "Resend email validation link" | msgid "Resend email validation link" | ||||||
| msgstr "Renvoyé le lien de validation de l'adresse mail" | msgstr "Renvoyé le lien de validation de l'adresse mail" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:158 | #: apps/registration/views.py:180 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Detail of user {user}" | msgid "Detail of user {user}" | ||||||
| msgstr "Détails de l'utilisateur {user}" | msgstr "Détails de l'utilisateur {user}" | ||||||
|  |  | ||||||
| #: apps/registration/views.py:179 | #: apps/registration/views.py:210 | ||||||
| #, python-brace-format | #, python-brace-format | ||||||
| msgid "Update user {user}" | msgid "Update user {user}" | ||||||
| msgstr "Mise à jour de l'utilisateur {user}" | msgstr "Mise à jour de l'utilisateur {user}" | ||||||
|  |  | ||||||
| #: corres2math/settings.py:154 | #: corres2math/settings.py:157 | ||||||
| msgid "English" | msgid "English" | ||||||
| msgstr "Anglais" | msgstr "Anglais" | ||||||
|  |  | ||||||
| #: corres2math/settings.py:155 | #: corres2math/settings.py:158 | ||||||
| msgid "French" | msgid "French" | ||||||
| msgstr "Français" | msgstr "Français" | ||||||
|  |  | ||||||
| #: templates/400.html:6 | #: corres2math/templates/400.html:6 | ||||||
| msgid "Bad request" | msgid "Bad request" | ||||||
| msgstr "Requête invalide" | msgstr "Requête invalide" | ||||||
|  |  | ||||||
| #: templates/400.html:7 | #: corres2math/templates/400.html:7 | ||||||
| msgid "" | msgid "" | ||||||
| "Sorry, your request was bad. Don't know what could be wrong. An email has " | "Sorry, your request was bad. Don't know what could be wrong. An email has " | ||||||
| "been sent to webmasters with the details of the error. You can now watch " | "been sent to webmasters with the details of the error. You can now watch " | ||||||
| @@ -1096,23 +1173,23 @@ msgstr "" | |||||||
| "email a été envoyé aux administrateurs avec les détails de l'erreur. Vous " | "email a été envoyé aux administrateurs avec les détails de l'erreur. Vous " | ||||||
| "pouvez désormais retourner voir des vidéos." | "pouvez désormais retourner voir des vidéos." | ||||||
|  |  | ||||||
| #: templates/403.html:6 | #: corres2math/templates/403.html:6 | ||||||
| msgid "Permission denied" | msgid "Permission denied" | ||||||
| msgstr "Permission refusée" | msgstr "Permission refusée" | ||||||
|  |  | ||||||
| #: templates/403.html:7 | #: corres2math/templates/403.html:7 | ||||||
| msgid "You don't have the right to perform this request." | msgid "You don't have the right to perform this request." | ||||||
| msgstr "Vous n'avez pas le droit d'effectuer cette requête." | msgstr "Vous n'avez pas le droit d'effectuer cette requête." | ||||||
|  |  | ||||||
| #: templates/403.html:10 templates/404.html:10 | #: corres2math/templates/403.html:10 corres2math/templates/404.html:10 | ||||||
| msgid "Exception message:" | msgid "Exception message:" | ||||||
| msgstr "Message d'erreur :" | msgstr "Message d'erreur :" | ||||||
|  |  | ||||||
| #: templates/404.html:6 | #: corres2math/templates/404.html:6 | ||||||
| msgid "Page not found" | msgid "Page not found" | ||||||
| msgstr "Page non trouvée" | msgstr "Page non trouvée" | ||||||
|  |  | ||||||
| #: templates/404.html:7 | #: corres2math/templates/404.html:7 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "" | msgid "" | ||||||
| "The requested path <code>%(request_path)s</code> was not found on the server." | "The requested path <code>%(request_path)s</code> was not found on the server." | ||||||
| @@ -1120,11 +1197,11 @@ msgstr "" | |||||||
| "Le chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le " | "Le chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le " | ||||||
| "serveur." | "serveur." | ||||||
|  |  | ||||||
| #: templates/500.html:6 | #: corres2math/templates/500.html:6 | ||||||
| msgid "Server error" | msgid "Server error" | ||||||
| msgstr "Erreur du serveur" | msgstr "Erreur du serveur" | ||||||
|  |  | ||||||
| #: templates/500.html:7 | #: corres2math/templates/500.html:7 | ||||||
| msgid "" | msgid "" | ||||||
| "Sorry, an error occurred when processing your request. An email has been " | "Sorry, an error occurred when processing your request. An email has been " | ||||||
| "sent to webmasters with the detail of the error, and this will be fixed " | "sent to webmasters with the detail of the error, and this will be fixed " | ||||||
| @@ -1135,47 +1212,55 @@ msgstr "" | |||||||
| "avec les détails de l'erreur. Vous pouvez désormais retourner voir des " | "avec les détails de l'erreur. Vous pouvez désormais retourner voir des " | ||||||
| "vidéos." | "vidéos." | ||||||
|  |  | ||||||
| #: templates/base.html:64 | #: corres2math/templates/base.html:64 | ||||||
| msgid "Home" | msgid "Home" | ||||||
| msgstr "Accueil" | msgstr "Accueil" | ||||||
|  |  | ||||||
| #: templates/base.html:88 | #: corres2math/templates/base.html:75 | ||||||
|  | msgid "Users" | ||||||
|  | msgstr "Utilisateurs" | ||||||
|  |  | ||||||
|  | #: corres2math/templates/base.html:78 | ||||||
|  | msgid "Teams" | ||||||
|  | msgstr "Équipes" | ||||||
|  |  | ||||||
|  | #: corres2math/templates/base.html:95 | ||||||
| msgid "My team" | msgid "My team" | ||||||
| msgstr "Mon équipe" | msgstr "Mon équipe" | ||||||
|  |  | ||||||
| #: templates/base.html:93 | #: corres2math/templates/base.html:100 | ||||||
| msgid "My participation" | msgid "My participation" | ||||||
| msgstr "Ma participation" | msgstr "Ma participation" | ||||||
|  |  | ||||||
| #: templates/base.html:100 | #: corres2math/templates/base.html:107 | ||||||
| msgid "Chat" | msgid "Chat" | ||||||
| msgstr "Chat" | msgstr "Chat" | ||||||
|  |  | ||||||
| #: templates/base.html:104 | #: corres2math/templates/base.html:111 | ||||||
| msgid "Administration" | msgid "Administration" | ||||||
| msgstr "Administration" | msgstr "Administration" | ||||||
|  |  | ||||||
| #: templates/base.html:112 | #: corres2math/templates/base.html:119 | ||||||
| msgid "Search..." | msgid "Search..." | ||||||
| msgstr "Chercher ..." | msgstr "Chercher ..." | ||||||
|  |  | ||||||
| #: templates/base.html:121 | #: corres2math/templates/base.html:128 | ||||||
| msgid "Return to admin view" | msgid "Return to admin view" | ||||||
| msgstr "Retourner à l'interface administrateur" | msgstr "Retourner à l'interface administrateur" | ||||||
|  |  | ||||||
| #: templates/base.html:126 | #: corres2math/templates/base.html:134 | ||||||
| msgid "Register" | msgid "Register" | ||||||
| msgstr "S'inscrire" | msgstr "S'inscrire" | ||||||
|  |  | ||||||
| #: templates/base.html:142 | #: corres2math/templates/base.html:151 | ||||||
| msgid "My account" | msgid "My account" | ||||||
| msgstr "Mon compte" | msgstr "Mon compte" | ||||||
|  |  | ||||||
| #: templates/base.html:145 | #: corres2math/templates/base.html:154 | ||||||
| msgid "Log out" | msgid "Log out" | ||||||
| msgstr "Déconnexion" | msgstr "Déconnexion" | ||||||
|  |  | ||||||
| #: templates/base.html:162 | #: corres2math/templates/base.html:171 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "" | msgid "" | ||||||
| "Your email address is not validated. Please click on the link you received " | "Your email address is not validated. Please click on the link you received " | ||||||
| @@ -1186,23 +1271,27 @@ msgstr "" | |||||||
| "avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a href=" | "avez reçu par mail. Vous pouvez renvoyer un mail en cliquant sur <a href=" | ||||||
| "\"%(send_email_url)s\">ce lien</a>." | "\"%(send_email_url)s\">ce lien</a>." | ||||||
|  |  | ||||||
| #: templates/base.html:186 | #: corres2math/templates/base.html:195 | ||||||
| msgid "Contact us" | msgid "Contact us" | ||||||
| msgstr "Nous contacter" | msgstr "Nous contacter" | ||||||
|  |  | ||||||
| #: templates/base.html:219 | #: corres2math/templates/base.html:214 | ||||||
|  | msgid "About" | ||||||
|  | msgstr "À propos" | ||||||
|  |  | ||||||
|  | #: corres2math/templates/base.html:238 | ||||||
| msgid "Search results" | msgid "Search results" | ||||||
| msgstr "Résultats de la recherche" | msgstr "Résultats de la recherche" | ||||||
|  |  | ||||||
| #: templates/registration/logged_out.html:8 | #: corres2math/templates/registration/logged_out.html:8 | ||||||
| msgid "Thanks for spending some quality time with the Web site today." | msgid "Thanks for spending some quality time with the Web site today." | ||||||
| msgstr "Merci d'avoir utilisé la plateforme des Correspondances." | msgstr "Merci d'avoir utilisé la plateforme des Correspondances." | ||||||
|  |  | ||||||
| #: templates/registration/logged_out.html:9 | #: corres2math/templates/registration/logged_out.html:9 | ||||||
| msgid "Log in again" | msgid "Log in again" | ||||||
| msgstr "Se reconnecter" | msgstr "Se reconnecter" | ||||||
|  |  | ||||||
| #: templates/registration/login.html:13 | #: corres2math/templates/registration/login.html:13 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "" | msgid "" | ||||||
| "You are authenticated as %(user)s, but are not authorized to access this " | "You are authenticated as %(user)s, but are not authorized to access this " | ||||||
| @@ -1211,18 +1300,19 @@ msgstr "" | |||||||
| "Vous êtes connectés en tant que %(user)s, mais n'êtes pas autorisés à " | "Vous êtes connectés en tant que %(user)s, mais n'êtes pas autorisés à " | ||||||
| "accéder à cette page. Voulez-vous vous reconnecter avec un autre compte ?" | "accéder à cette page. Voulez-vous vous reconnecter avec un autre compte ?" | ||||||
|  |  | ||||||
| #: templates/registration/login.html:23 | #: corres2math/templates/registration/login.html:23 | ||||||
| msgid "Forgotten your password or username?" | msgid "Forgotten your password or username?" | ||||||
| msgstr "Mot de passe oublié ?" | msgstr "Mot de passe oublié ?" | ||||||
|  |  | ||||||
| #: templates/search/search.html:6 templates/search/search.html:11 | #: corres2math/templates/search/search.html:6 | ||||||
|  | #: corres2math/templates/search/search.html:10 | ||||||
| msgid "Search" | msgid "Search" | ||||||
| msgstr "Chercher" | msgstr "Chercher" | ||||||
|  |  | ||||||
| #: templates/search/search.html:16 | #: corres2math/templates/search/search.html:15 | ||||||
| msgid "Results" | msgid "Results" | ||||||
| msgstr "Résultats" | msgstr "Résultats" | ||||||
|  |  | ||||||
| #: templates/search/search.html:26 | #: corres2math/templates/search/search.html:25 | ||||||
| msgid "No results found." | msgid "No results found." | ||||||
| msgstr "Aucun résultat." | msgstr "Aucun résultat." | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ upstream corres2math { | |||||||
| server { | server { | ||||||
|     listen 80; |     listen 80; | ||||||
|     server_name corres2math; |     server_name corres2math; | ||||||
|  |     client_max_body_size 50M; | ||||||
|  |  | ||||||
|     location / { |     location / { | ||||||
|             proxy_pass http://corres2math; |             proxy_pass http://corres2math; | ||||||
|   | |||||||
| @@ -1,17 +1,19 @@ | |||||||
| Django~=3.0 | Django~=3.0 | ||||||
| django-bootstrap-datepicker-plus | django-bootstrap-datepicker-plus~=3.0 | ||||||
| django-cas-server | django-cas-server~=1.2 | ||||||
| django-crispy-forms | django-crispy-forms~=1.9 | ||||||
| django-extensions | django-extensions~=3.0 | ||||||
| django-filter~=2.3.0 | django-filter~=2.3 | ||||||
| django-haystack~=3.0 | django-haystack~=3.0 | ||||||
| django-mailer | django-mailer~=2.0 | ||||||
| django-polymorphic | django-polymorphic~=3.0 | ||||||
| django-tables2 | django-tables2~=2.3 | ||||||
| djangorestframework~=3.11.1 | djangorestframework~=3.12 | ||||||
| django-rest-polymorphic | django-rest-polymorphic~=0.1 | ||||||
| matrix-nio | gunicorn~=20.0 | ||||||
| ptpython | matrix-nio~=0.15 | ||||||
| python-magic~=0.4.18 | psycopg2-binary~=2.8 | ||||||
| gunicorn | ptpython~=3.0 | ||||||
| whoosh | python-magic~=0.4 | ||||||
|  | sympasoap~=1.0 | ||||||
|  | whoosh~=2.7 | ||||||
							
								
								
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -7,12 +7,22 @@ envlist = | |||||||
| skipsdist = True | skipsdist = True | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| sitepackages = True | sitepackages = False | ||||||
| deps = | deps = | ||||||
|     -r{toxinidir}/requirements.txt |  | ||||||
|     coverage |     coverage | ||||||
|  |     Django~=3.1 | ||||||
|  |     django-bootstrap-datepicker-plus~=3.0 | ||||||
|  |     django-crispy-forms~=1.9 | ||||||
|  |     django-filter~=2.3 | ||||||
|  |     django-haystack~=3.0 | ||||||
|  |     django-polymorphic~=3.0 | ||||||
|  |     django-tables2~=2.3 | ||||||
|  |     djangorestframework~=3.12 | ||||||
|  |     django-rest-polymorphic~=0.1 | ||||||
|  |     python-magic~=0.4 | ||||||
|  |     whoosh~=2.7 | ||||||
| commands = | commands = | ||||||
|     coverage run --omit='*migrations*,apps/scripts*' --source=apps ./manage.py test apps/ |     coverage run --source=apps,corres2math ./manage.py test apps/ corres2math/ | ||||||
|     coverage report -m |     coverage report -m | ||||||
|  |  | ||||||
| [testenv:linters] | [testenv:linters] | ||||||
| @@ -25,7 +35,7 @@ deps = | |||||||
|     pep8-naming |     pep8-naming | ||||||
|     pyflakes |     pyflakes | ||||||
| commands = | commands = | ||||||
|     flake8 apps/ |     flake8 apps/ corres2math/ | ||||||
|  |  | ||||||
| [flake8] | [flake8] | ||||||
| exclude = | exclude = | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user