mirror of
https://gitlab.com/animath/si/plateforme-corres2math.git
synced 2025-07-04 19:04:03 +02:00
Compare commits
65 Commits
bf32c34d4c
...
master
Author | SHA1 | Date | |
---|---|---|---|
4c9e63be3d | |||
35a197be07
|
|||
01f6d74cae | |||
d55671efd1
|
|||
bcdbc445c6
|
|||
95a69f35d2
|
|||
eec941c816
|
|||
64d471936f
|
|||
02c977264d
|
|||
8f86ea15c8
|
|||
7628387158
|
|||
a82c6b4673 | |||
bf2feb9c35
|
|||
4ee5ac309d | |||
969e53b712
|
|||
84e149e8c9
|
|||
9e6a3eb1ca
|
|||
1f9f60d880
|
|||
205760f2e9
|
|||
8f742b8e14
|
|||
996d00c7f0
|
|||
8f09ca5553
|
|||
b761670133
|
|||
f2ca4b7446 | |||
3faf1294d8 | |||
24e1cf3845 | |||
dbcf15c4f3 | |||
10115a0419 | |||
833f9147ce | |||
91640f8fb1 | |||
55a727612c | |||
de7f51d8da | |||
c658a9767d | |||
ce048a30d6 | |||
d104c2ff1f | |||
982b61fe03 | |||
ace1dbdc75 | |||
731f8711ff | |||
d3e18a8fbb | |||
ece1e800ab | |||
2e0028c063 | |||
d304c3565c | |||
0fe1f9c348 | |||
9867e5e2a8 | |||
48b34e4362 | |||
10932d1cc5 | |||
02ca1d1efe | |||
b0a3a22f83 | |||
4238e49c11 | |||
c3c2c55aca | |||
cba3e56fb8 | |||
f146ae2dd2 | |||
04dd02b88a | |||
1ddf39f296 | |||
fa368a399a | |||
c35fb4e996 | |||
b6cefc1519 | |||
e73bb2d18b | |||
f422212aea | |||
52763cb75a | |||
aed9f457c3 | |||
6afa1ea40b | |||
e98540a2a8 | |||
7353ecfd5f | |||
0a3fffe21e |
@ -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,13 +129,17 @@ class SendParticipationForm(forms.ModelForm):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
try:
|
||||||
self.fields["sent_participation"].initial = self.instance.sent_participation
|
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()
|
||||||
|
if "sent_participation" in cleaned_data:
|
||||||
participation = cleaned_data["sent_participation"]
|
participation = cleaned_data["sent_participation"]
|
||||||
participation.received_participation = self.instance
|
participation.received_participation = self.instance
|
||||||
self.instance = participation
|
self.instance = participation
|
||||||
@ -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',)
|
||||||
@ -171,10 +183,12 @@ class PhaseForm(forms.ModelForm):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
# Ensure that dates are in a right order
|
# Ensure that dates are in a right order
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
if cleaned_data["end"] <= cleaned_data["start"]:
|
start = cleaned_data["start"]
|
||||||
|
end = cleaned_data["end"]
|
||||||
|
if end <= start:
|
||||||
self.add_error("end", _("Start date must be before the end date."))
|
self.add_error("end", _("Start date must be before the end date."))
|
||||||
if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=cleaned_data["start"]).exists():
|
if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=start).exists():
|
||||||
self.add_error("start", _("This phase must start after the previous phases."))
|
self.add_error("start", _("This phase must start after the previous phases."))
|
||||||
if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=cleaned_data["end"]).exists():
|
if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=end).exists():
|
||||||
self.add_error("end", _("This phase must end after the next phases."))
|
self.add_error("end", _("This phase must end after the next phases."))
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
@ -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.getenv("SYNAPSE_PASSWORD"):
|
||||||
|
avatar_uri = "plop"
|
||||||
|
else: # pragma: no cover
|
||||||
if not os.path.isfile(".matrix_avatar"):
|
if not os.path.isfile(".matrix_avatar"):
|
||||||
stat_file = os.stat("corres2math/static/logo.png")
|
stat_file = os.stat("corres2math/static/logo.png")
|
||||||
with open("corres2math/static/logo.png", "rb") as f:
|
with open("corres2math/static/logo.png", "rb") as f:
|
||||||
resp, _ = Matrix.upload(f, filename="logo.png", content_type="image/png", filesize=stat_file.st_size)
|
resp = Matrix.upload(f, filename="logo.png", content_type="image/png",
|
||||||
if isinstance(resp, UploadError):
|
filesize=stat_file.st_size)[0][0]
|
||||||
raise Exception(resp)
|
|
||||||
avatar_uri = resp.content_uri
|
avatar_uri = resp.content_uri
|
||||||
with open(".matrix_avatar", "w") as f:
|
with open(".matrix_avatar", "w") as f:
|
||||||
f.write(avatar_uri)
|
f.write(avatar_uri)
|
||||||
Matrix.set_avatar(avatar_uri)
|
Matrix.set_avatar(avatar_uri)
|
||||||
else:
|
|
||||||
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,14 @@ 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("#je-cherche-une-equipe:correspondances-maths.fr",
|
||||||
|
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'
|
||||||
@ -72,19 +78,17 @@ class ParticipationTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class VideoTable(tables.Table):
|
class VideoTable(tables.Table):
|
||||||
participationname = tables.LinkColumn(
|
participation_name = tables.LinkColumn(
|
||||||
'participation:participation_detail',
|
'participation:participation_detail',
|
||||||
args=[tables.A("participation__pk")],
|
args=[tables.A("participation__pk")],
|
||||||
verbose_name=lambda: _("name").capitalize(),
|
verbose_name=lambda: _("name").capitalize(),
|
||||||
|
accessor=tables.A("participation__team__name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_participationname(self, record):
|
|
||||||
return record.participation.team.name
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'table table condensed table-striped',
|
'class': 'table table condensed table-striped',
|
||||||
}
|
}
|
||||||
model = Team
|
model = Team
|
||||||
fields = ('participationname', 'link',)
|
fields = ('participation_name', 'link',)
|
||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
@ -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,12 +261,13 @@
|
|||||||
});
|
});
|
||||||
{% 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() {
|
||||||
@ -274,13 +276,14 @@
|
|||||||
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,11 +1,18 @@
|
|||||||
|
# Copyright (C) 2020 by Animath
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from registration.models import StudentRegistration
|
from django.utils import timezone
|
||||||
|
from registration.models import CoachRegistration, StudentRegistration
|
||||||
|
|
||||||
from .models import Participation, Question, Team
|
from .models import Participation, Phase, Question, Team
|
||||||
|
|
||||||
|
|
||||||
class TestStudentParticipation(TestCase):
|
class TestStudentParticipation(TestCase):
|
||||||
@ -59,6 +66,14 @@ class TestStudentParticipation(TestCase):
|
|||||||
grant_animath_access_videos=True,
|
grant_animath_access_videos=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.coach = User.objects.create(
|
||||||
|
first_name="Coach",
|
||||||
|
last_name="Coach",
|
||||||
|
email="coach@example.com",
|
||||||
|
password="coach",
|
||||||
|
)
|
||||||
|
CoachRegistration.objects.create(user=self.coach)
|
||||||
|
|
||||||
def test_admin_pages(self):
|
def test_admin_pages(self):
|
||||||
"""
|
"""
|
||||||
Load Django-admin pages.
|
Load Django-admin pages.
|
||||||
@ -173,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.
|
||||||
@ -355,6 +377,9 @@ class TestStudentParticipation(TestCase):
|
|||||||
self.user.registration.team = self.team
|
self.user.registration.team = self.team
|
||||||
self.user.registration.save()
|
self.user.registration.save()
|
||||||
|
|
||||||
|
self.coach.registration.team = self.team
|
||||||
|
self.coach.registration.save()
|
||||||
|
|
||||||
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@ -376,6 +401,39 @@ class TestStudentParticipation(TestCase):
|
|||||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||||
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
|
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
|
||||||
|
|
||||||
|
def test_leave_team(self):
|
||||||
|
"""
|
||||||
|
A user is in a team, and leaves it.
|
||||||
|
"""
|
||||||
|
# User is not in a team
|
||||||
|
response = self.client.post(reverse("participation:team_leave"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.user.registration.team = self.team
|
||||||
|
self.user.registration.save()
|
||||||
|
|
||||||
|
# Team is valid
|
||||||
|
self.team.participation.valid = True
|
||||||
|
self.team.participation.save()
|
||||||
|
response = self.client.post(reverse("participation:team_leave"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
# Unauthenticated users are redirected to login page
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(reverse("participation:team_leave"))
|
||||||
|
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
self.team.participation.valid = None
|
||||||
|
self.team.participation.save()
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:team_leave"))
|
||||||
|
self.assertRedirects(response, reverse("index"), 302, 200)
|
||||||
|
self.user.registration.refresh_from_db()
|
||||||
|
self.assertIsNone(self.user.registration.team)
|
||||||
|
self.assertFalse(Team.objects.filter(pk=self.team.pk).exists())
|
||||||
|
|
||||||
def test_no_myparticipation_redirect_nomyparticipation(self):
|
def test_no_myparticipation_redirect_nomyparticipation(self):
|
||||||
"""
|
"""
|
||||||
Ensure a permission denied when we search my team participation when we are in no team.
|
Ensure a permission denied when we search my team participation when we are in no team.
|
||||||
@ -438,9 +496,177 @@ 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):
|
||||||
|
"""
|
||||||
|
Ensure that the current phase is the good one.
|
||||||
|
"""
|
||||||
|
# We are before the beginning
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=2 * i),
|
||||||
|
end=timezone.now() + timedelta(days=2 * i + 1))
|
||||||
|
self.assertEqual(Phase.current_phase(), None)
|
||||||
|
|
||||||
|
# We are after the end
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() - timedelta(days=2 * i),
|
||||||
|
end=timezone.now() - timedelta(days=2 * i + 1))
|
||||||
|
self.assertEqual(Phase.current_phase().phase_number, Phase.objects.count())
|
||||||
|
|
||||||
|
# First phase
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 1),
|
||||||
|
end=timezone.now() + timedelta(days=i))
|
||||||
|
self.assertEqual(Phase.current_phase().phase_number, 1)
|
||||||
|
|
||||||
|
# Second phase
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
|
||||||
|
end=timezone.now() + timedelta(days=i - 1))
|
||||||
|
self.assertEqual(Phase.current_phase().phase_number, 2)
|
||||||
|
|
||||||
|
# Third phase
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 3),
|
||||||
|
end=timezone.now() + timedelta(days=i - 2))
|
||||||
|
self.assertEqual(Phase.current_phase().phase_number, 3)
|
||||||
|
|
||||||
|
# Fourth phase
|
||||||
|
for i in range(1, 5):
|
||||||
|
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 4),
|
||||||
|
end=timezone.now() + timedelta(days=i - 3))
|
||||||
|
self.assertEqual(Phase.current_phase().phase_number, 4)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("participation:calendar"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("participation:update_phase", args=(4,)))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||||
|
start=timezone.now(),
|
||||||
|
end=timezone.now() + timedelta(days=3),
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
response = self.client.get(reverse("participation:update_phase", args=(4,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||||
|
start=timezone.now(),
|
||||||
|
end=timezone.now() + timedelta(days=3),
|
||||||
|
))
|
||||||
|
self.assertRedirects(response, reverse("participation:calendar"), 302, 200)
|
||||||
|
fourth_phase = Phase.objects.get(phase_number=4)
|
||||||
|
self.assertEqual((fourth_phase.end - fourth_phase.start).days, 3)
|
||||||
|
|
||||||
|
# First phase must be before the other phases
|
||||||
|
response = self.client.post(reverse("participation:update_phase", args=(1,)), data=dict(
|
||||||
|
start=timezone.now() + timedelta(days=8),
|
||||||
|
end=timezone.now() + timedelta(days=9),
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Fourth phase must be after the other phases
|
||||||
|
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||||
|
start=timezone.now() - timedelta(days=9),
|
||||||
|
end=timezone.now() - timedelta(days=8),
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# End must be after start
|
||||||
|
response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
|
||||||
|
start=timezone.now() + timedelta(days=3),
|
||||||
|
end=timezone.now(),
|
||||||
|
))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Unauthenticated user can't update the calendar
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(reverse("participation:calendar"))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
response = self.client.get(reverse("participation:update_phase", args=(2,)))
|
||||||
|
self.assertRedirects(response, reverse("login") + "?next=" +
|
||||||
|
reverse("participation:update_phase", args=(2,)), 302, 200)
|
||||||
|
|
||||||
def test_forbidden_access(self):
|
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()
|
||||||
@ -468,8 +694,23 @@ 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()
|
||||||
|
|
||||||
class TestAdminForbidden(TestCase):
|
call_command('fix_matrix_channels')
|
||||||
|
call_command('setup_third_phase')
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdmin(TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = User.objects.create_superuser(
|
self.user = User.objects.create_superuser(
|
||||||
username="admin@example.com",
|
username="admin@example.com",
|
||||||
@ -478,6 +719,96 @@ class TestAdminForbidden(TestCase):
|
|||||||
)
|
)
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
self.team1 = Team.objects.create(
|
||||||
|
name="Toto",
|
||||||
|
trigram="TOT",
|
||||||
|
)
|
||||||
|
self.team1.participation.valid = True
|
||||||
|
self.team1.participation.problem = 1
|
||||||
|
self.team1.participation.save()
|
||||||
|
|
||||||
|
self.team2 = Team.objects.create(
|
||||||
|
name="Bliblu",
|
||||||
|
trigram="BIU",
|
||||||
|
)
|
||||||
|
self.team2.participation.valid = True
|
||||||
|
self.team2.participation.problem = 1
|
||||||
|
self.team2.participation.save()
|
||||||
|
|
||||||
|
self.team3 = Team.objects.create(
|
||||||
|
name="Zouplop",
|
||||||
|
trigram="ZPL",
|
||||||
|
)
|
||||||
|
self.team3.participation.valid = True
|
||||||
|
self.team3.participation.problem = 1
|
||||||
|
self.team3.participation.save()
|
||||||
|
|
||||||
|
self.other_team = Team.objects.create(
|
||||||
|
name="I am different",
|
||||||
|
trigram="IAD",
|
||||||
|
)
|
||||||
|
self.other_team.participation.valid = True
|
||||||
|
self.other_team.participation.problem = 2
|
||||||
|
self.other_team.participation.save()
|
||||||
|
|
||||||
|
def test_research(self):
|
||||||
|
"""
|
||||||
|
Try to search some things.
|
||||||
|
"""
|
||||||
|
call_command("rebuild_index", "--noinput", "--verbosity", 0)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("haystack_search") + "?q=" + self.team1.name)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["object_list"])
|
||||||
|
|
||||||
|
response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(response.context["object_list"])
|
||||||
|
|
||||||
|
def test_set_received_video(self):
|
||||||
|
"""
|
||||||
|
Try to define the received video of a participation.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||||
|
args=(self.team1.participation.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||||
|
args=(self.team1.participation.pk,)),
|
||||||
|
data=dict(received_participation=self.team2.participation.pk))
|
||||||
|
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||||
|
args=(self.team1.participation.pk,)), 302, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||||
|
args=(self.team1.participation.pk,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:participation_send_participation",
|
||||||
|
args=(self.team1.participation.pk,)),
|
||||||
|
data=dict(sent_participation=self.team3.participation.pk))
|
||||||
|
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||||
|
args=(self.team1.participation.pk,)), 302, 200)
|
||||||
|
|
||||||
|
self.team1.participation.refresh_from_db()
|
||||||
|
self.team2.participation.refresh_from_db()
|
||||||
|
self.team3.participation.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk)
|
||||||
|
self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk)
|
||||||
|
self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk)
|
||||||
|
self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk)
|
||||||
|
|
||||||
|
# The other team didn't work on the same problem
|
||||||
|
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||||
|
args=(self.team1.participation.pk,)),
|
||||||
|
data=dict(received_participation=self.other_team.participation.pk))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.post(reverse("participation:participation_send_participation",
|
||||||
|
args=(self.team1.participation.pk,)),
|
||||||
|
data=dict(sent_participation=self.other_team.participation.pk))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_create_team_forbidden(self):
|
def test_create_team_forbidden(self):
|
||||||
"""
|
"""
|
||||||
Ensure that an admin can't create a team.
|
Ensure that an admin can't create a team.
|
||||||
@ -500,9 +831,23 @@ class TestAdminForbidden(TestCase):
|
|||||||
))
|
))
|
||||||
self.assertTrue(response.status_code, 403)
|
self.assertTrue(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_leave_team_forbidden(self):
|
||||||
|
"""
|
||||||
|
Ensure that an admin can't leave a team.
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("participation:team_leave"))
|
||||||
|
self.assertTrue(response.status_code, 403)
|
||||||
|
|
||||||
def test_my_team_forbidden(self):
|
def test_my_team_forbidden(self):
|
||||||
"""
|
"""
|
||||||
Ensure that an admin can't access to "My team".
|
Ensure that an admin can't access to "My team".
|
||||||
"""
|
"""
|
||||||
response = self.client.get(reverse("participation:my_team_detail"))
|
response = self.client.get(reverse("participation:my_team_detail"))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_my_participation_forbidden(self):
|
||||||
|
"""
|
||||||
|
Ensure that an admin can't access to "My participation".
|
||||||
|
"""
|
||||||
|
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
@ -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
|
||||||
@ -316,9 +342,9 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
if not request.user.registration.team:
|
if not request.user.registration.participates or not request.user.registration.team:
|
||||||
raise PermissionDenied(_("You are not in a team."))
|
raise PermissionDenied(_("You are not in a team."))
|
||||||
if request.user.registration.team.participation.valid is not None:
|
if request.user.registration.team.participation.valid:
|
||||||
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,7 +75,10 @@ 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):
|
||||||
|
if "photo_authorization" in self.files:
|
||||||
file = self.files["photo_authorization"]
|
file = self.files["photo_authorization"]
|
||||||
|
if file.size > 2e6:
|
||||||
|
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
||||||
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
|
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."))
|
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
|
||||||
return self.cleaned_data["photo_authorization"]
|
return self.cleaned_data["photo_authorization"]
|
||||||
@ -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
|
||||||
@ -45,8 +48,9 @@ def invite_to_public_rooms(instance: Registration, created: bool, **_):
|
|||||||
"""
|
"""
|
||||||
When a user got registered, automatically invite the Matrix user into public rooms.
|
When a user got registered, automatically invite the Matrix user into public rooms.
|
||||||
"""
|
"""
|
||||||
if not created:
|
if created:
|
||||||
Matrix.invite("#annonces:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
Matrix.invite("#annonces:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
||||||
Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
Matrix.invite("#faq:correspondances-maths.fr", f"@{instance.matrix_username}:correspondances-maths.fr")
|
||||||
Matrix.invite("#je-cherche-une-equip:correspondances-maths.fr",
|
Matrix.invite("#je-cherche-une-equipe: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
|
||||||
@ -9,6 +12,7 @@ from ..tables import RegistrationTable
|
|||||||
|
|
||||||
def search_table(results):
|
def search_table(results):
|
||||||
model_class = results[0].object.__class__
|
model_class = results[0].object.__class__
|
||||||
|
table_class = Table
|
||||||
if issubclass(model_class, Registration):
|
if issubclass(model_class, Registration):
|
||||||
table_class = RegistrationTable
|
table_class = RegistrationTable
|
||||||
elif issubclass(model_class, Team):
|
elif issubclass(model_class, Team):
|
||||||
@ -17,8 +21,6 @@ def search_table(results):
|
|||||||
table_class = ParticipationTable
|
table_class = ParticipationTable
|
||||||
elif issubclass(model_class, Video):
|
elif issubclass(model_class, Video):
|
||||||
table_class = VideoTable
|
table_class = VideoTable
|
||||||
else:
|
|
||||||
table_class = Table
|
|
||||||
return table_class([result.object for result in results], prefix=model_class._meta.model_name)
|
return table_class([result.object for result in results], prefix=model_class._meta.model_name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +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.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 .models import AdminRegistration, CoachRegistration, Registration, StudentRegistration
|
from .models import AdminRegistration, CoachRegistration, StudentRegistration
|
||||||
|
|
||||||
|
|
||||||
class TestIndexPage(TestCase):
|
class TestIndexPage(TestCase):
|
||||||
@ -28,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:
|
||||||
@ -81,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)
|
||||||
|
|
||||||
@ -109,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)
|
||||||
|
|
||||||
@ -175,10 +225,25 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
# To test the modification of mailing lists
|
||||||
|
from participation.models import Team
|
||||||
|
self.student.registration.team = Team.objects.create(
|
||||||
|
name="toto",
|
||||||
|
trigram="TOT",
|
||||||
|
)
|
||||||
|
self.student.registration.save()
|
||||||
|
|
||||||
for user, data in [(self.user, dict(role="Bot")),
|
for user, data in [(self.user, dict(role="Bot")),
|
||||||
(self.student, dict(student_class=11, school="Sky")),
|
(self.student, dict(student_class=11, school="Sky")),
|
||||||
(self.coach, dict(professional_activity="God"))]:
|
(self.coach, dict(professional_activity="God"))]:
|
||||||
@ -190,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)
|
||||||
|
|
||||||
@ -198,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)
|
||||||
@ -221,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"),
|
||||||
@ -316,8 +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_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 %}
|
||||||
|
{% if 1|current_phase %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
|
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
|
||||||
</li>
|
</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" %}
|
||||||
|
|
||||||
|
{% 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 %}
|
{% trans "Search results" as modal_title %}
|
||||||
{% include "base_modal.html" with modal_id="search" modal_form_method="get" modal_additional_class="modal-lg" %}
|
{% 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 %}
|
|
||||||
{% 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,16 +264,28 @@
|
|||||||
if (!modalBody.html().trim())
|
if (!modalBody.html().trim())
|
||||||
modalBody.load("{% url "participation:calendar" %} #form-content")
|
modalBody.load("{% url "participation:calendar" %} #form-content")
|
||||||
});
|
});
|
||||||
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
|
$('a[data-target="#teamsModal"]').click(function() {
|
||||||
|
let modalBody = $("#teamsModal div.modal-body");
|
||||||
|
if (!modalBody.html().trim())
|
||||||
|
modalBody.load("{% url "participation:team_list" %} #form-content")
|
||||||
|
});
|
||||||
$('button[data-target="#searchModal"]').click(function() {
|
$('button[data-target="#searchModal"]').click(function() {
|
||||||
let modalBody = $("#searchModal div.modal-body");
|
let modalBody = $("#searchModal div.modal-body");
|
||||||
let q = encodeURI($("#search-term").val());
|
let q = encodeURI($("#search-term").val());
|
||||||
modalBody.load("{% url "haystack_search" %}?q=" + q + " #search-results");
|
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")
|
||||||
});
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.is_authenticated and user.registration.participates and not user.registration.team %}
|
||||||
$('a[data-target="#createTeamModal"]').click(function() {
|
$('a[data-target="#createTeamModal"]').click(function() {
|
||||||
let modalBody = $("#createTeamModal div.modal-body");
|
let modalBody = $("#createTeamModal div.modal-body");
|
||||||
if (!modalBody.html().trim())
|
if (!modalBody.html().trim())
|
||||||
@ -263,6 +296,7 @@
|
|||||||
if (!modalBody.html().trim())
|
if (!modalBody.html().trim())
|
||||||
modalBody.load("{% url "participation:join_team" %} #form-content");
|
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