mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2025-06-21 14:38:24 +02:00
Clone Corres2math platform
This commit is contained in:
4
apps/participation/__init__.py
Normal file
4
apps/participation/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'participation.apps.ParticipationConfig'
|
49
apps/participation/admin.py
Normal file
49
apps/participation/admin.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Participation, Phase, Question, Team, Video
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'trigram', 'problem', 'valid',)
|
||||
search_fields = ('name', 'trigram',)
|
||||
list_filter = ('participation__problem', 'participation__valid',)
|
||||
|
||||
def problem(self, team):
|
||||
return team.participation.get_problem_display()
|
||||
|
||||
problem.short_description = _('problem number')
|
||||
|
||||
def valid(self, team):
|
||||
return team.participation.valid
|
||||
|
||||
valid.short_description = _('valid')
|
||||
|
||||
|
||||
@admin.register(Participation)
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'problem', 'valid',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('problem', 'valid',)
|
||||
|
||||
|
||||
@admin.register(Video)
|
||||
class VideoAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'link',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'link',)
|
||||
|
||||
|
||||
@admin.register(Question)
|
||||
class QuestionAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation', 'question',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'question',)
|
||||
|
||||
|
||||
@admin.register(Phase)
|
||||
class PhaseAdmin(admin.ModelAdmin):
|
||||
list_display = ('phase_number', 'start', 'end',)
|
||||
ordering = ('phase_number', 'start',)
|
18
apps/participation/apps.py
Normal file
18
apps/participation/apps.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
|
||||
|
||||
class ParticipationConfig(AppConfig):
|
||||
"""
|
||||
The participation app contains the data about the teams, videos, ...
|
||||
"""
|
||||
name = 'participation'
|
||||
|
||||
def ready(self):
|
||||
from participation.signals import create_team_participation, delete_related_videos, update_mailing_list
|
||||
pre_save.connect(update_mailing_list, "participation.Team")
|
||||
pre_delete.connect(delete_related_videos, "participation.Participation")
|
||||
post_save.connect(create_team_participation, "participation.Team")
|
194
apps/participation/forms.py
Normal file
194
apps/participation/forms.py
Normal file
@ -0,0 +1,194 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from bootstrap_datepicker_plus import DateTimePickerInput
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Participation, Phase, Question, Team, Video
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
"""
|
||||
Form to create a team, with the name and the trigram,
|
||||
and if the team accepts that Animath diffuse the videos.
|
||||
"""
|
||||
def clean_trigram(self):
|
||||
trigram = self.cleaned_data["trigram"].upper()
|
||||
if not re.match("[A-Z]{3}", trigram):
|
||||
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
|
||||
return trigram
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'grant_animath_access_videos',)
|
||||
|
||||
|
||||
class JoinTeamForm(forms.ModelForm):
|
||||
"""
|
||||
Form to join a team by the access code.
|
||||
"""
|
||||
def clean_access_code(self):
|
||||
access_code = self.cleaned_data["access_code"]
|
||||
if not Team.objects.filter(access_code=access_code).exists():
|
||||
raise ValidationError(_("No team was found with this access code."))
|
||||
return access_code
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "access_code" in cleaned_data:
|
||||
team = Team.objects.get(access_code=cleaned_data["access_code"])
|
||||
self.instance = team
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ('access_code',)
|
||||
|
||||
|
||||
class ParticipationForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update the problem of a team participation.
|
||||
"""
|
||||
class Meta:
|
||||
model = Participation
|
||||
fields = ('problem',)
|
||||
|
||||
|
||||
class RequestValidationForm(forms.Form):
|
||||
"""
|
||||
Form to ask about validation.
|
||||
"""
|
||||
_form_type = forms.CharField(
|
||||
initial="RequestValidationForm",
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
|
||||
engagement = forms.BooleanField(
|
||||
label=_("I engage myself to participate to the whole \"Correspondances\"."),
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
class ValidateParticipationForm(forms.Form):
|
||||
"""
|
||||
Form to let administrators to accept or refuse a team.
|
||||
"""
|
||||
_form_type = forms.CharField(
|
||||
initial="ValidateParticipationForm",
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
|
||||
message = forms.CharField(
|
||||
label=_("Message to address to the team:"),
|
||||
widget=forms.Textarea(),
|
||||
)
|
||||
|
||||
|
||||
class UploadVideoForm(forms.ModelForm):
|
||||
"""
|
||||
Form to upload a video, for a solution or a synthesis.
|
||||
"""
|
||||
class Meta:
|
||||
model = Video
|
||||
fields = ('link',)
|
||||
|
||||
def clean(self):
|
||||
if Phase.current_phase().phase_number != 1 and Phase.current_phase().phase_number != 4 and self.instance.link:
|
||||
self.add_error("link", _("You can't upload your video after the deadline."))
|
||||
return super().clean()
|
||||
|
||||
|
||||
class ReceiveParticipationForm(forms.ModelForm):
|
||||
"""
|
||||
Update the received participation of a participation.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["received_participation"].queryset = Participation.objects.filter(
|
||||
~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Participation
|
||||
fields = ('received_participation',)
|
||||
|
||||
|
||||
class SendParticipationForm(forms.ModelForm):
|
||||
"""
|
||||
Update the sent participation of a participation.
|
||||
"""
|
||||
sent_participation = forms.ModelChoiceField(
|
||||
queryset=Participation.objects,
|
||||
label=lambda: _("Send to team"),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
self.fields["sent_participation"].initial = self.instance.sent_participation
|
||||
except ObjectDoesNotExist: # No sent participation
|
||||
pass
|
||||
self.fields["sent_participation"].queryset = Participation.objects.filter(
|
||||
~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
|
||||
)
|
||||
|
||||
def clean(self, commit=True):
|
||||
cleaned_data = super().clean()
|
||||
if "sent_participation" in cleaned_data:
|
||||
participation = cleaned_data["sent_participation"]
|
||||
participation.received_participation = self.instance
|
||||
self.instance = participation
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Participation
|
||||
fields = ('sent_participation',)
|
||||
|
||||
|
||||
class QuestionForm(forms.ModelForm):
|
||||
"""
|
||||
Create or update a question.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")})
|
||||
|
||||
def clean(self):
|
||||
if Phase.current_phase().phase_number != 2:
|
||||
self.add_error(None, _("You can only create or update a question during the second phase."))
|
||||
return super().clean()
|
||||
|
||||
class Meta:
|
||||
model = Question
|
||||
fields = ('question',)
|
||||
|
||||
|
||||
class PhaseForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update the calendar of a phase.
|
||||
"""
|
||||
class Meta:
|
||||
model = Phase
|
||||
fields = ('start', 'end',)
|
||||
widgets = {
|
||||
'start': DateTimePickerInput(format='%d/%m/%Y %H:%M'),
|
||||
'end': DateTimePickerInput(format='%d/%m/%Y %H:%M'),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
# Ensure that dates are in a right order
|
||||
cleaned_data = super().clean()
|
||||
start = cleaned_data["start"]
|
||||
end = cleaned_data["end"]
|
||||
if end <= start:
|
||||
self.add_error("end", _("Start date must be before the end date."))
|
||||
if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=start).exists():
|
||||
self.add_error("start", _("This phase must start after the previous phases."))
|
||||
if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=end).exists():
|
||||
self.add_error("end", _("This phase must end after the next phases."))
|
||||
return cleaned_data
|
2
apps/participation/management/commands/__init__.py
Normal file
2
apps/participation/management/commands/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
@ -0,0 +1,92 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
from django.core.management import BaseCommand
|
||||
from registration.models import AdminRegistration, Registration
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
Matrix.set_display_name("Bot du TFJM²")
|
||||
|
||||
if not os.getenv("SYNAPSE_PASSWORD"):
|
||||
avatar_uri = "plop"
|
||||
else: # pragma: no cover
|
||||
if not os.path.isfile(".matrix_avatar"):
|
||||
stat_file = os.stat("tfjm/static/logo.svg")
|
||||
with open("tfjm/static/logo.svg", "rb") as f:
|
||||
resp = Matrix.upload(f, filename="logo.svg", content_type="image/svg",
|
||||
filesize=stat_file.st_size)[0][0]
|
||||
avatar_uri = resp.content_uri
|
||||
with open(".matrix_avatar", "w") as f:
|
||||
f.write(avatar_uri)
|
||||
Matrix.set_avatar(avatar_uri)
|
||||
|
||||
with open(".matrix_avatar", "r") as f:
|
||||
avatar_uri = f.read().rstrip(" \t\r\n")
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#faq:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="faq",
|
||||
name="FAQ",
|
||||
topic="Posez toutes vos questions ici !",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#annonces:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="annonces",
|
||||
name="Annonces",
|
||||
topic="Informations importantes du TFJM²",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#je-cherche-une-equipe:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="je-cherche-une-equipe",
|
||||
name="Je cherche une équipe",
|
||||
topic="Le Tinder du TFJM²",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#flood:tfjm.org"):
|
||||
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:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
|
||||
|
||||
Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
|
||||
|
||||
for r in Registration.objects.all():
|
||||
Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#je-cherche-une-equipe:tfjm.org",
|
||||
f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
|
||||
for admin in AdminRegistration.objects.all():
|
||||
Matrix.set_room_power_level("#annonces:tfjm.org",
|
||||
f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#faq:tfjm.org",
|
||||
f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#flood:tfjm.org",
|
||||
f"@{admin.matrix_username}:tfjm.org", 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 tfjm.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 du TFJM²", "hotline",
|
||||
"Liste de diffusion pour contacter toutes les équipes validées du TFJM².",
|
||||
"education", raise_error=False)
|
||||
sympa.create_list("equipes-non-valides", "Équipes du TFJM²", "hotline",
|
||||
"Liste de diffusion pour contacter toutes les équipes non validées du TFJM².",
|
||||
"education", raise_error=False)
|
||||
|
||||
for problem in range(1, 4):
|
||||
sympa.create_list(f"probleme-{problem}",
|
||||
f"Équipes du TFJM² participant au problème {problem}", "hotline",
|
||||
f"Liste de diffusion pour contacter les équipes participant au problème {problem}"
|
||||
f" du TFJM².", "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}")
|
44
apps/participation/management/commands/setup_third_phase.py
Normal file
44
apps/participation/management/commands/setup_third_phase.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from tfjm.matrix import Matrix, RoomVisibility
|
||||
from django.core.management import BaseCommand
|
||||
from participation.models import Participation
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
for participation in Participation.objects.filter(valid=True).all():
|
||||
for i, question in enumerate(participation.questions.order_by("id").all()):
|
||||
solution_author = participation.received_participation.team
|
||||
alias = f"equipe-{solution_author.trigram.lower()}-question-{i}"
|
||||
room_id = f"#{alias}:tfjm.org"
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=alias,
|
||||
name=f"Solution équipe {solution_author.trigram} - question {i+1}",
|
||||
topic=f"Échange entre l'équipe {solution_author.name} ({solution_author.trigram}) "
|
||||
f"et l'équipe {participation.team.name} ({participation.team.trigram}) "
|
||||
f"autour de la question {i+1} sur le problème {participation.problem}",
|
||||
federate=False,
|
||||
invite=[f"@{registration.matrix_username}:tfjm.org" for registration in
|
||||
list(participation.team.students.all()) + list(participation.team.coachs.all()) +
|
||||
list(solution_author.students.all()) + list(solution_author.coachs.all())],
|
||||
)
|
||||
Matrix.set_room_power_level_event(room_id, "events_default", 21)
|
||||
for registration in solution_author.students.all():
|
||||
Matrix.set_room_power_level(room_id,
|
||||
f"@{registration.matrix_username}:tfjm.org", 42)
|
||||
|
||||
Matrix.send_message(room_id, "Bienvenue dans la troisième phase du TFJM² !")
|
||||
Matrix.send_message(room_id, f"L'équipe {participation.team.name} a visionné la vidéo de l'équipe "
|
||||
f"{solution_author.name} sur le problème {participation.problem}, et a posé "
|
||||
"une série de questions.")
|
||||
Matrix.send_message(room_id, "L'équipe ayant composé la vidéo doit maintenant proposer une réponse.")
|
||||
Matrix.send_message(room_id, "Une fois la réponse apportée, vous pourrez ensuite échanger plus "
|
||||
"librement autour de la question, au travers de ce canal.")
|
||||
Matrix.send_message(room_id, "**Question posée :**", formatted_body="<strong>Question posée :</strong>")
|
||||
Matrix.send_message(room_id, question.question,
|
||||
formatted_body=f"<font color=\"#ff0000\">{question.question}</font>")
|
||||
|
||||
# TODO Setup the bot the set the power level of all members of the room to 42
|
138
apps/participation/migrations/0001_initial.py
Normal file
138
apps/participation/migrations/0001_initial.py
Normal file
@ -0,0 +1,138 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-04 12:05
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
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):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Participation',
|
||||
fields=[
|
||||
('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')),
|
||||
('valid', models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'participation',
|
||||
'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(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('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')),
|
||||
('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={
|
||||
'verbose_name': 'team',
|
||||
'verbose_name_plural': 'teams',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Video',
|
||||
fields=[
|
||||
('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')),
|
||||
('valid', models.BooleanField(default=None, help_text='The video got the validation of the administrators.', null=True, verbose_name='valid')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'video',
|
||||
'verbose_name_plural': 'videos',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='team',
|
||||
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(
|
||||
model_name='participation',
|
||||
name='received_participation',
|
||||
field=models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sent_participation', to='participation.participation', verbose_name='received participation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
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.AddField(
|
||||
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'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='participation',
|
||||
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,
|
||||
)
|
||||
]
|
2
apps/participation/migrations/__init__.py
Normal file
2
apps/participation/migrations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
307
apps/participation/models.py
Normal file
307
apps/participation/models.py
Normal file
@ -0,0 +1,307 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from tfjm.lists import get_sympa_client
|
||||
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Index
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""
|
||||
The Team model represents a real team that participates to the Correspondances.
|
||||
This only includes the registration detail.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
trigram = models.CharField(
|
||||
max_length=3,
|
||||
verbose_name=_("trigram"),
|
||||
help_text=_("The trigram must be composed of three uppercase letters."),
|
||||
unique=True,
|
||||
validators=[RegexValidator("[A-Z]{3}")],
|
||||
)
|
||||
|
||||
access_code = models.CharField(
|
||||
max_length=6,
|
||||
verbose_name=_("access code"),
|
||||
help_text=_("The access code let other people to join the team."),
|
||||
)
|
||||
|
||||
grant_animath_access_videos = models.BooleanField(
|
||||
verbose_name=_("Grant Animath to publish my video"),
|
||||
help_text=_("Give the authorisation to publish the video on the main website to promote the action."),
|
||||
default=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""
|
||||
:return: The mailing list to contact the team members.
|
||||
"""
|
||||
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
||||
|
||||
def create_mailing_list(self):
|
||||
"""
|
||||
Create a new Sympa mailing list to contact the team.
|
||||
"""
|
||||
get_sympa_client().create_list(
|
||||
f"equipe-{self.trigram.lower()}",
|
||||
f"Équipe {self.name} ({self.trigram})",
|
||||
"hotline", # TODO Use a custom sympa template
|
||||
f"Liste de diffusion pour contacter l'équipe {self.name} du TFJM²",
|
||||
"education",
|
||||
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):
|
||||
"""
|
||||
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}")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.access_code:
|
||||
# if the team got created, generate the access code, create the contact mailing list
|
||||
# and create a dedicated Matrix room.
|
||||
self.access_code = get_random_string(6)
|
||||
self.create_mailing_list()
|
||||
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.private,
|
||||
name=f"#équipe-{self.trigram.lower()}",
|
||||
alias=f"equipe-{self.trigram.lower()}",
|
||||
topic=f"Discussion de l'équipe {self.name}",
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:team_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Team {name} ({trigram})").format(name=self.name, trigram=self.trigram)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("team")
|
||||
verbose_name_plural = _("teams")
|
||||
indexes = [
|
||||
Index(fields=("trigram", )),
|
||||
]
|
||||
|
||||
|
||||
class Participation(models.Model):
|
||||
"""
|
||||
The Participation model contains all data that are related to the participation:
|
||||
chosen problem, validity status, videos,...
|
||||
"""
|
||||
team = models.OneToOneField(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
problem = models.IntegerField(
|
||||
choices=[(i, format_lazy(_("Problem #{problem:d}"), problem=i)) for i in range(1, 4)],
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("problem number"),
|
||||
)
|
||||
|
||||
valid = models.BooleanField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("valid"),
|
||||
help_text=_("The video got the validation of the administrators."),
|
||||
)
|
||||
|
||||
solution = models.OneToOneField(
|
||||
"participation.Video",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="participation_solution",
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("solution video"),
|
||||
)
|
||||
|
||||
received_participation = models.OneToOneField(
|
||||
"participation.Participation",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="sent_participation",
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("received participation"),
|
||||
)
|
||||
|
||||
synthesis = models.OneToOneField(
|
||||
"participation.Video",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="participation_synthesis",
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("synthesis video"),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("participation")
|
||||
verbose_name_plural = _("participations")
|
||||
|
||||
|
||||
class Video(models.Model):
|
||||
"""
|
||||
The Video model only contains a link and a validity status.
|
||||
"""
|
||||
link = models.URLField(
|
||||
verbose_name=_("link"),
|
||||
help_text=_("The full video link."),
|
||||
)
|
||||
|
||||
valid = models.BooleanField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("valid"),
|
||||
help_text=_("The video got the validation of the administrators."),
|
||||
)
|
||||
|
||||
@property
|
||||
def participation(self):
|
||||
"""
|
||||
Retrives the participation that is associated to this video,
|
||||
whatever it is a solution or a synthesis.
|
||||
"""
|
||||
try:
|
||||
# If this is a solution
|
||||
return self.participation_solution
|
||||
except ObjectDoesNotExist:
|
||||
# If this is a synthesis
|
||||
return self.participation_synthesis
|
||||
|
||||
@property
|
||||
def platform(self):
|
||||
"""
|
||||
According to the link, retrieve the platform that is used to upload the video.
|
||||
"""
|
||||
if "youtube.com" in self.link or "youtu.be" in self.link:
|
||||
return "youtube"
|
||||
return "unknown"
|
||||
|
||||
@property
|
||||
def youtube_code(self):
|
||||
"""
|
||||
If the video is uploaded on Youtube, search in the URL the video code.
|
||||
"""
|
||||
return re.compile("(https?://|)(www\\.|)(youtube\\.com/watch\\?v=|youtu\\.be/)([a-zA-Z0-9-_]*)?.*?")\
|
||||
.match(self.link).group(4)
|
||||
|
||||
def as_iframe(self):
|
||||
"""
|
||||
Generate the HTML code to embed the video in an iframe, according to the type of the host platform.
|
||||
"""
|
||||
if self.platform == "youtube":
|
||||
return render_to_string("participation/youtube_iframe.html", context=dict(youtube_code=self.youtube_code))
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return _("Video of team {name} ({trigram})")\
|
||||
.format(name=self.participation.team.name, trigram=self.participation.team.trigram)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("video")
|
||||
verbose_name_plural = _("videos")
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
"""
|
||||
Question to ask to the team that sent a solution.
|
||||
"""
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("participation"),
|
||||
related_name="questions",
|
||||
)
|
||||
|
||||
question = models.TextField(
|
||||
verbose_name=_("question"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.question
|
||||
|
||||
|
||||
class Phase(models.Model):
|
||||
"""
|
||||
The Phase model corresponds to the dates of the phase.
|
||||
"""
|
||||
phase_number = models.AutoField(
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name=_("phase number"),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("phase description"),
|
||||
)
|
||||
|
||||
start = models.DateTimeField(
|
||||
verbose_name=_("start date of the given phase"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
end = models.DateTimeField(
|
||||
verbose_name=_("end date of the given phase"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def current_phase(cls):
|
||||
"""
|
||||
Retrieve the current phase of this day
|
||||
"""
|
||||
qs = Phase.objects.filter(start__lte=timezone.now(), end__gte=timezone.now())
|
||||
if qs.exists():
|
||||
return qs.get()
|
||||
qs = Phase.objects.filter(start__lte=timezone.now()).order_by("phase_number").all()
|
||||
return qs.last() if qs.exists() else None
|
||||
|
||||
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}")\
|
||||
.format(phase_number=self.phase_number, start=self.start, end=self.end)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("phase")
|
||||
verbose_name_plural = _("phases")
|
36
apps/participation/search_indexes.py
Normal file
36
apps/participation/search_indexes.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from haystack import indexes
|
||||
|
||||
from .models import Participation, Team, Video
|
||||
|
||||
|
||||
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all teams by their name and trigram.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
|
||||
|
||||
class ParticipationIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all participations by their team name and team trigram.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Participation
|
||||
|
||||
|
||||
class VideoIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all teams by their team name and team trigram.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Video
|
46
apps/participation/signals.py
Normal file
46
apps/participation/signals.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from tfjm.lists import get_sympa_client
|
||||
from participation.models import Participation, Team, Video
|
||||
|
||||
|
||||
def create_team_participation(instance, created, **_):
|
||||
"""
|
||||
When a team got created, create an associated team and create Video objects.
|
||||
"""
|
||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||
if not participation.solution:
|
||||
participation.solution = Video.objects.create()
|
||||
if not participation.synthesis:
|
||||
participation.synthesis = Video.objects.create()
|
||||
participation.save()
|
||||
if not created:
|
||||
participation.team.create_mailing_list()
|
||||
|
||||
|
||||
def update_mailing_list(instance: Team, **_):
|
||||
"""
|
||||
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
||||
"""
|
||||
if instance.pk:
|
||||
old_team = Team.objects.get(pk=instance.pk)
|
||||
if old_team.name != instance.name or old_team.trigram != instance.trigram:
|
||||
# TODO Rename Matrix room
|
||||
# Delete old mailing list, create a new one
|
||||
old_team.delete_mailing_list()
|
||||
instance.create_mailing_list()
|
||||
# Subscribe all team members in the mailing list
|
||||
for student in instance.students.all():
|
||||
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
f"{student.user.first_name} {student.user.last_name}")
|
||||
for coach in instance.coachs.all():
|
||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
f"{coach.user.first_name} {coach.user.last_name}")
|
||||
|
||||
|
||||
def delete_related_videos(instance: Participation, **_):
|
||||
if instance.solution:
|
||||
instance.solution.delete()
|
||||
if instance.synthesis:
|
||||
instance.synthesis.delete()
|
91
apps/participation/tables.py
Normal file
91
apps/participation/tables.py
Normal file
@ -0,0 +1,91 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
|
||||
from .models import Phase, Team
|
||||
|
||||
|
||||
class CalendarTable(tables.Table):
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
}
|
||||
row_attrs = {
|
||||
'class': lambda record: 'bg-success' if timezone.now() > record.end else
|
||||
'bg-warning' if timezone.now() > record.start else
|
||||
'bg-danger',
|
||||
'data-id': lambda record: str(record.phase_number),
|
||||
}
|
||||
model = Phase
|
||||
fields = ('phase_number', 'description', 'start', 'end',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
order_by = ('phase_number',)
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
class TeamTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'participation:team_detail',
|
||||
args=[tables.A("id")],
|
||||
verbose_name=lambda: _("name").capitalize(),
|
||||
)
|
||||
|
||||
problem = tables.Column(
|
||||
accessor="participation__problem",
|
||||
verbose_name=lambda: _("problem number").capitalize(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'problem',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
class ParticipationTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'participation:participation_detail',
|
||||
args=[tables.A("id")],
|
||||
verbose_name=lambda: _("name").capitalize(),
|
||||
accessor="team__name",
|
||||
)
|
||||
|
||||
trigram = tables.Column(
|
||||
verbose_name=lambda: _("trigram").capitalize(),
|
||||
accessor="team__trigram",
|
||||
)
|
||||
|
||||
problem = tables.Column(
|
||||
verbose_name=lambda: _("problem number").capitalize(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'problem',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
class VideoTable(tables.Table):
|
||||
participation_name = tables.LinkColumn(
|
||||
'participation:participation_detail',
|
||||
args=[tables.A("participation__pk")],
|
||||
verbose_name=lambda: _("name").capitalize(),
|
||||
accessor=tables.A("participation__team__name"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('participation_name', 'link',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
39
apps/participation/templates/participation/chat.html
Normal file
39
apps/participation/templates/participation/chat.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<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.tfjm.org/#/room/#faq:tfjm.org" 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 %}
|
13
apps/participation/templates/participation/create_team.html
Normal file
13
apps/participation/templates/participation/create_team.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
13
apps/participation/templates/participation/join_team.html
Normal file
13
apps/participation/templates/participation/join_team.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Join" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Demande de validation - TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Bonjour {{ user.registration }},
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
|
||||
au {{ team.participation.get_problem_display }} du TFJM² 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 :
|
||||
<a href="https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}">
|
||||
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cordialement,
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,10 @@
|
||||
Bonjour {{ user.registration }},
|
||||
|
||||
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
|
||||
au {{ team.participation.get_problem_display }} du TFJM² 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 :
|
||||
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
|
||||
|
||||
Cordialement,
|
||||
|
||||
L'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Équipe non validée – TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
Bonjour,<br/>
|
||||
<br />
|
||||
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations
|
||||
de droit à l'image sont correctes. Les organisateurs vous adressent ce message :<br />
|
||||
<br />
|
||||
{{ message }}<br />
|
||||
<br />
|
||||
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a>
|
||||
pour plus d'informations.
|
||||
<br/>
|
||||
Cordialement,<br/>
|
||||
<br/>
|
||||
Le comité d'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,12 @@
|
||||
Bonjour,
|
||||
|
||||
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos
|
||||
autorisations de droit à l'image sont correctes. Les organisateurs vous adressent ce message :
|
||||
|
||||
{{ message }}
|
||||
|
||||
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
|
||||
|
||||
Cordialement,
|
||||
|
||||
Le comité d'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Équipe validée – TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
Bonjour,<br/>
|
||||
<br/>
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
|
||||
votre problème. Lorsque les Correspondances auront débutées, vous pourrez soumettre votre vidéo sur la plateforme d'inscription.<br>
|
||||
Les organisateurs vous adressent ce message :<br/>
|
||||
<br/>
|
||||
{{ message }}<br />
|
||||
<br/>
|
||||
Cordialement,<br/>
|
||||
<br/>
|
||||
Le comité d'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,12 @@
|
||||
Bonjour,
|
||||
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
|
||||
votre problème. Lorsque les Correspondances auront débutées, vous pourrez soumettre votre vidéo sur la plateforme d'inscription.
|
||||
|
||||
Les organisateurs vous adressent ce message :
|
||||
|
||||
{{ message }}
|
||||
|
||||
Cordialement,
|
||||
|
||||
Le comité d'organisation du TFJM² des Jeunes Mathématicien·ne·s
|
@ -0,0 +1,300 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "any" as any %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Participation of team" %} {{ participation.team.name }} ({{ participation.team.trigram }})</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-2">{% trans "Team:" %}</dt>
|
||||
<dd class="col-sm-10"><a href="{% url "participation:team_detail" pk=participation.team.pk %}">{{ participation.team }}</a></dd>
|
||||
|
||||
<dt class="col-sm-2">{% trans "Chosen problem:" %}</dt>
|
||||
<dd class="col-sm-10">{{ participation.get_problem_display }}</dd>
|
||||
</dl>
|
||||
|
||||
<div id="solution-container">
|
||||
<dl class="row">
|
||||
{% trans "No video sent" as novideo %}
|
||||
<dt class="col-sm-2">{% trans "Proposed solution:" %}</dt>
|
||||
<dd class="col-sm-10"><a href="{{ participation.solution.link|default:"#" }}"{% if participation.solution.link %} target="_blank"{% endif %}>
|
||||
{{ participation.solution.link|default:novideo }}</a>
|
||||
{% if current_phase.phase_number == 1 or participation.solution.link == "" %}
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadSolutionModal">{% trans "Upload" %}</button>
|
||||
{% endif %}
|
||||
{% if participation.solution.link %}
|
||||
<button class="btn btn-info" data-toggle="modal" data-target="#displaySolutionModal">{% trans "Display" %}</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user.registration.is_admin or current_phase.phase_number >= 2 %}
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Sent solution" %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-xl-5 text-right">{% trans "Team that received your solution:" %}</dt>
|
||||
<dd class="col-md-5">{{ participation.sent_participation.team|default:any }}</dd>
|
||||
{% if user.registration.is_admin %}
|
||||
<dd class="col-xs-2">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#defineSentParticipationModal">{% trans "Change" %}</button>
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
|
||||
{% if current_phase.phase_number == 2 %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
The mentioned team received your video. They are now watching your video,
|
||||
and formulating questions. You would be able to exchange with the other phase during
|
||||
the next phase.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% elif current_phase.phase_number == 3 %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed with user_id=user.pk %}
|
||||
The other team sent you questions about your solution. Your are now able to answer them,
|
||||
then to exchange freely with the other team. You can click on the Chat button, or to
|
||||
connect to your dedicated Matrix account:
|
||||
<code>@tfjm_{{ user_id }}:tfjm.org</code>.
|
||||
You can use your own Matrix client, or use the dedicated Element client:
|
||||
<a href="https://element.tfjm.org">element.correpondances-maths.fr</a>
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% elif current_phase.phase_number == 4 %}
|
||||
<dl class="row">
|
||||
<dt class="col-xl-5 text-right">{% trans "Synthesis from the other team:" %}</dt>
|
||||
<dd class="col-sm-7"><a href="{{ participation.received_participation.synthesis.link|default:"#" }}"{% if participation.received_participation.synthesis.link %} target="_blank"{% endif %}>
|
||||
{{ participation.received_participation.synthesis.link|default:novideo }}</a>
|
||||
{% if participation.received_participation.synthesis.link %}
|
||||
<button class="btn btn-info" data-toggle="modal" data-target="#displayOtherSynthesisModal">{% trans "Display" %}</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Received solution" %}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-xl-5 text-right">{% trans "Team that sent you their solution:" %}</dt>
|
||||
<dd class="col-md-5">{{ participation.received_participation.team|default:any }}</dd>
|
||||
{% if user.registration.is_admin %}
|
||||
<dd class="col-xs-2">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#defineReceivedParticipationModal">{% trans "Change" %}</button>
|
||||
</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt class="col-xl-5 text-right">{% trans "Proposed solution:" %}</dt>
|
||||
<dd class="col-sm-7"><a href="{{ participation.received_participation.solution.link|default:"#" }}"{% if participation.received_participation.solution.link %} target="_blank"{% endif %}>
|
||||
{{ participation.received_participation.solution.link|default:novideo }}</a>
|
||||
{% if participation.received_participation.solution.link %}
|
||||
<button class="btn btn-info" data-toggle="modal" data-target="#displayOtherSolutionModal">{% trans "Display" %}</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
{% if current_phase.phase_number == 2 %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
You received a solution about the same problem that you treated from another team.
|
||||
You are now encouraged to see the video, then to ask from 3 to 6 questions about the video.
|
||||
After that, you will be invited to exchange with the other team about the solution.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% for question in participation.questions.all %}
|
||||
<dd class="col-md-9 text-truncate">{{ question.question }}</dd>
|
||||
<dd class="col-md-3">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updateQuestion{{ forloop.counter }}Modal">{% trans "Change" %}</button>
|
||||
</dd>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
|
||||
{% if user.registration.participates %}
|
||||
<button class="btn btn-success" data-toggle="modal" data-target="#addQuestionModal">
|
||||
<i class="fas fa-plus-circle"></i> {% trans "Add a question" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif current_phase.phase_number == 3 %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed with user_id=user.pk %}
|
||||
You sent your questions to the other team about their solution. When they answer to
|
||||
your questions, you will be able to exchange freely with the other team.
|
||||
You can click on the Chat button, or to connect to your dedicated Matrix account:
|
||||
<code>@tfjm_{{ user_id }}:tfjm.org</code>.
|
||||
You can use your own Matrix client, or use the dedicated Element client:
|
||||
<a href="https://element.tfjm.org">element.correpondances-maths.fr</a>
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% elif current_phase.phase_number == 4 %}
|
||||
<div id="solution-container">
|
||||
<dl class="row">
|
||||
{% trans "No video sent" as novideo %}
|
||||
<dt class="col-sm-5 text-right">{% trans "Your synthesis of the exchange:" %}</dt>
|
||||
<dd class="col-sm-7"><a href="{{ participation.synthesis.link|default:"#" }}"{% if participation.synthesis.link %} target="_blank"{% endif %}>
|
||||
{{ participation.synthesis.link|default:novideo }}</a>
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadSynthesisModal">{% trans "Upload" %}</button>
|
||||
{% if participation.synthesis.link %}
|
||||
<button class="btn btn-info" data-toggle="modal" data-target="#displaySynthesisModal">{% trans "Display" %}</button>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin %}
|
||||
{% trans "Define received video" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:participation_receive_participation" pk=participation.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="defineReceivedParticipation" %}
|
||||
|
||||
{% trans "Define team that receives your video" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:participation_send_participation" pk=participation.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="defineSentParticipation" %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Upload video" as modal_title %}
|
||||
{% trans "Upload" as modal_button %}
|
||||
{% url "participation:upload_video" pk=participation.solution_id as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="uploadSolution" %}
|
||||
|
||||
{% trans "Display solution" as modal_title %}
|
||||
{% trans "This video platform is not supported yet." as unsupported_platform %}
|
||||
{% include "base_modal.html" with modal_id="displaySolution" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.solution.as_iframe|default:unsupported_platform %}
|
||||
|
||||
|
||||
{% if user.registration.is_admin or current_phase.phase_number >= 2 %}
|
||||
{% if participation.received_participation.solution.link %}
|
||||
{% trans "Display solution" as modal_title %}
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.participates and current_phase.phase_number == 2 %}
|
||||
{% trans "Add question" as modal_title %}
|
||||
{% trans "Add" as modal_button %}
|
||||
{% url "participation:add_question" pk=participation.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="addQuestion" modal_button_type="success" %}
|
||||
{% for question in participation.questions.all %}
|
||||
{% with number_str=forloop.counter|stringformat:"d"%}
|
||||
{% with modal_id="updateQuestion"|add:number_str %}
|
||||
{% trans "Delete" as delete %}
|
||||
{% with extra_modal_button='<button class="btn btn-danger" type="button" data-dismiss="modal" data-toggle="modal" data-target="#deleteQuestion'|add:number_str|add:'Modal">'|add:delete|add:"</button>"|safe %}
|
||||
{% trans "Update question" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:update_question" pk=question.pk as modal_action %}
|
||||
{% include "base_modal.html" %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% with modal_id="deleteQuestion"|add:number_str %}
|
||||
{% trans "Delete question" as modal_title %}
|
||||
{% trans "Delete" as modal_button %}
|
||||
{% url "participation:delete_question" pk=question.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_button_type="danger" %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if current_phase.phase_number >= 4 %}
|
||||
{% if participation.received_participation.synthesis.link %}
|
||||
{% trans "Display synthesis" as modal_title %}
|
||||
{% trans "This video platform is not supported yet." as unsupported_platform %}
|
||||
{% include "base_modal.html" with modal_id="displayOtherSynthesis" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.received_participation.synthesis.as_iframe|default:unsupported_platform %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Upload video" as modal_title %}
|
||||
{% trans "Upload" as modal_button %}
|
||||
{% url "participation:upload_video" pk=participation.synthesis_id as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="uploadSynthesis" %}
|
||||
|
||||
{% if participation.synthesis.link %}
|
||||
{% trans "Display synthesis" as modal_title %}
|
||||
{% trans "This video platform is not supported yet." as unsupported_platform %}
|
||||
{% include "base_modal.html" with modal_id="displaySynthesis" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.synthesis.as_iframe|default:unsupported_platform %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
{% if user.registration.is_admin %}
|
||||
$('button[data-target="#defineReceivedParticipationModal"]').click(function() {
|
||||
let modalBody = $("#defineReceivedParticipationModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:participation_receive_participation" pk=participation.pk %} #form-content");
|
||||
});
|
||||
|
||||
$('button[data-target="#defineSentParticipationModal"]').click(function() {
|
||||
let modalBody = $("#defineSentParticipationModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:participation_send_participation" pk=participation.pk %} #form-content");
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.participates and current_phase.phase_number == 2 %}
|
||||
$('button[data-target="#addQuestionModal"]').click(function() {
|
||||
let modalBody = $("#addQuestionModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:add_question" pk=participation.pk %} #form-content");
|
||||
});
|
||||
|
||||
{% for question in participation.questions.all %}
|
||||
$('button[data-target="#updateQuestion{{ forloop.counter }}Modal"]').click(function() {
|
||||
let modalBody = $("#updateQuestion{{ forloop.counter }}Modal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:update_question" pk=question.pk %} #form-content");
|
||||
});
|
||||
|
||||
$('button[data-target="#deleteQuestion{{ forloop.counter }}Modal"]').click(function() {
|
||||
let modalBody = $("#deleteQuestion{{ forloop.counter }}Modal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:delete_question" pk=question.pk %} #form-content");
|
||||
});
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
$('button[data-target="#uploadSolutionModal"]').click(function() {
|
||||
let modalBody = $("#uploadSolutionModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:upload_video" pk=participation.solution_id %} #form-content");
|
||||
});
|
||||
|
||||
{% if current_phase.phase_number == 4 %}
|
||||
$('button[data-target="#uploadSynthesisModal"]').click(function() {
|
||||
let modalBody = $("#uploadSynthesisModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:upload_video" pk=participation.synthesis_id %} #form-content");
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
14
apps/participation/templates/participation/phase_form.html
Normal file
14
apps/participation/templates/participation/phase_form.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="{% url "participation:update_phase" pk=object.pk %}">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
35
apps/participation/templates/participation/phase_list.html
Normal file
35
apps/participation/templates/participation/phase_list.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n static %}
|
||||
|
||||
{% block extracss %}
|
||||
<link rel="stylesheet" href="{% static "bootstrap_datepicker_plus/css/datepicker-widget.css" %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block contenttitle %}
|
||||
<h2>{% trans "Calendar" %}</h2>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-content">
|
||||
{% render_table table %}
|
||||
|
||||
{% trans "Update phase" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% include "base_modal.html" with modal_id="updatePhase" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
{% if user.registration.is_admin %}
|
||||
<script>
|
||||
$("tr").click(function () {
|
||||
let modalBody = $("#updatePhaseModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:calendar" %}" + $(this).data("id") + "/ #form-content");
|
||||
$("#updatePhase-form").attr("action", "{% url "participation:calendar" %}" + $(this).data("id") + "/")
|
||||
$("#updatePhaseModal").modal();
|
||||
})
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
<div class="alert alert-danger">
|
||||
{% trans "Are you sure you want to delete this question?" %}
|
||||
</div>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-danger" type="submit">{% trans "Delete" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Send" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
148
apps/participation/templates/participation/team_detail.html
Normal file
148
apps/participation/templates/participation/team_detail.html
Normal file
@ -0,0 +1,148 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ team.name }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6 text-right">{% trans "Name:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.name }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Trigram:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.trigram }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Email:" %}</dt>
|
||||
<dd class="col-sm-6"><a href="mailto:{{ team.email }}">{{ team.email }}</a></dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Access code:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.access_code }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Coachs:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for coach in team.coachs.all %}
|
||||
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Participants:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for student in team.students.all %}
|
||||
<a href="{% url "registration:user_detail" pk=student.user.pk %}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Chosen problem:" %}</dt>
|
||||
{% trans "any" as any %}
|
||||
<dd class="col-sm-6">{{ team.participation.get_problem_display|default:any }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Grant Animath to publish our video:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.grant_animath_access_videos|yesno }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Authorizations:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for student in team.students.all %}
|
||||
{% if student.photo_authorization %}
|
||||
<a href="{{ student.photo_authorization.url }}" data-turbolinks="false">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% else %}
|
||||
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamModal">{% trans "Update" %}</button>
|
||||
{% if not team.participation.valid %}
|
||||
<button class="btn btn-danger" data-toggle="modal" data-target="#leaveTeamModal">{% trans "Leave" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if team.participation.valid %}
|
||||
<div class="text-center">
|
||||
<a class="btn btn-info" href="{% url "participation:participation_detail" pk=team.participation.pk %}">
|
||||
<i class="fas fa-video"></i> {% trans "Access to team participation" %} <i class="fas fa-video"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% elif team.participation.valid == None %} {# Team did not ask for validation #}
|
||||
{% if user.registration.participates %}
|
||||
{% if can_validate %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "Your team has at least 3 members and all photo authorizations were given: the team can be validated." %}
|
||||
<div class="text-center">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ request_validation_form|crispy }}
|
||||
<button class="btn btn-success" name="request-validation">{% trans "Submit my team to validation" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Your team must be composed of 3 members and each member must upload its photo authorization and confirm its email address." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This team didn't ask for validation yet." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %} {# Team is waiting for validation #}
|
||||
{% if user.registration.participates %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Your validation is pending." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "The team requested to be validated. You may now control the authorizations and confirm that they can participate." %}
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ validation_form|crispy }}
|
||||
<div class="input-group btn-group">
|
||||
<button class="btn btn-success" name="validate" type="submit">{% trans "Validate" %}</button>
|
||||
<button class="btn btn-danger" name="invalidate" type="submit">{% trans "Invalidate" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Update team" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:update_team" pk=team.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateTeam" %}
|
||||
|
||||
{% trans "Leave team" as modal_title %}
|
||||
{% trans "Leave" as modal_button %}
|
||||
{% url "participation:team_leave" as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="leaveTeam" modal_button_type="danger" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('button[data-target="#updateTeamModal"]').click(function() {
|
||||
let modalBody = $("#updateTeamModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:update_team" pk=team.pk %} #form-content");
|
||||
});
|
||||
$('button[data-target="#leaveTeamModal"]').click(function() {
|
||||
let modalBody = $("#leaveTeamModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:team_leave" %} #form-content");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/team_leave.html
Normal file
13
apps/participation/templates/participation/team_leave.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div class="alert alert-warning" id="form-content">
|
||||
{% csrf_token %}
|
||||
{% trans "Are you sure that you want to leave this team?" %}
|
||||
</div>
|
||||
<button class="btn btn-danger" type="submit">{% trans "Leave" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
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 %}
|
15
apps/participation/templates/participation/update_team.html
Normal file
15
apps/participation/templates/participation/update_team.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
{{ participation_form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
14
apps/participation/templates/participation/upload_video.html
Normal file
14
apps/participation/templates/participation/upload_video.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">{% trans "Upload" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div style="position: relative; width: 100%; padding-bottom: 56.25%;">
|
||||
<iframe src="https://www.youtube.com/embed/{{ youtube_code }}"
|
||||
frameborder="0"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
{{ object.team.name }}
|
||||
{{ object.team.trigram }}
|
||||
{{ object.problem }}
|
||||
{{ object.get_problem_display }}
|
@ -0,0 +1,2 @@
|
||||
{{ object.name }}
|
||||
{{ object.trigram }}
|
@ -0,0 +1,5 @@
|
||||
{{ object.link }}
|
||||
{{ object.participation.team.name }}
|
||||
{{ object.participation.team.trigram }}
|
||||
{{ object.participation.problem }}
|
||||
{{ object.participation.get_problem_display }}
|
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)
|
853
apps/participation/tests.py
Normal file
853
apps/participation/tests.py
Normal file
@ -0,0 +1,853 @@
|
||||
# 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.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from registration.models import CoachRegistration, StudentRegistration
|
||||
|
||||
from .models import Participation, Phase, Question, Team
|
||||
|
||||
|
||||
class TestStudentParticipation(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="toto1234",
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
first_name="Toto",
|
||||
last_name="Toto",
|
||||
email="toto@example.com",
|
||||
password="toto",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.user,
|
||||
student_class=12,
|
||||
school="Earth",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.team = Team.objects.create(
|
||||
name="Super team",
|
||||
trigram="AAA",
|
||||
access_code="azerty",
|
||||
grant_animath_access_videos=True,
|
||||
)
|
||||
self.question = Question.objects.create(participation=self.team.participation,
|
||||
question="Pourquoi l'existence précède l'essence ?")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.second_user = User.objects.create(
|
||||
first_name="Lalala",
|
||||
last_name="Lalala",
|
||||
email="lalala@example.com",
|
||||
password="lalala",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.second_user,
|
||||
student_class=11,
|
||||
school="Moon",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.second_team = Team.objects.create(
|
||||
name="Poor team",
|
||||
trigram="FFF",
|
||||
access_code="qwerty",
|
||||
grant_animath_access_videos=True,
|
||||
)
|
||||
|
||||
self.coach = User.objects.create(
|
||||
first_name="Coach",
|
||||
last_name="Coach",
|
||||
email="coach@example.com",
|
||||
password="coach",
|
||||
)
|
||||
CoachRegistration.objects.create(user=self.coach)
|
||||
|
||||
def test_admin_pages(self):
|
||||
"""
|
||||
Load Django-admin pages.
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Test team pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/team/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/team/{self.team.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Team).id}/"
|
||||
f"{self.team.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.get_absolute_url()), 302, 200)
|
||||
|
||||
# Test participation pages
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.get(reverse("admin:index") + "participation/participation/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/participation/{self.team.participation.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Participation).id}/"
|
||||
f"{self.team.participation.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.participation.get_absolute_url()), 302, 200)
|
||||
|
||||
# Test video pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/video/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/video/{self.team.participation.solution.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test question pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/question/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/question/{self.question.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Test phase pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/phase/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index") + "participation/phase/1/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_team(self):
|
||||
"""
|
||||
Try to create a team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:create_team"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="123",
|
||||
grant_animath_access_videos=False,
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="TES",
|
||||
grant_animath_access_videos=False,
|
||||
))
|
||||
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
||||
team = Team.objects.get(trigram="TES")
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
|
||||
|
||||
# Already in a team
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team 2",
|
||||
trigram="TET",
|
||||
grant_animath_access_videos=False,
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_join_team(self):
|
||||
"""
|
||||
Try to join an existing team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:join_team"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
team = Team.objects.create(name="Test", trigram="TES")
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code="éééééé",
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
|
||||
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
||||
|
||||
# Already joined
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
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):
|
||||
"""
|
||||
Test redirection.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertTrue(response.status_code, 200)
|
||||
|
||||
def test_team_detail(self):
|
||||
"""
|
||||
Try to display the information of a team.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other teams
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_request_validate_team(self):
|
||||
"""
|
||||
The team ask for validation.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
second_user = User.objects.create(
|
||||
first_name="Blublu",
|
||||
last_name="Blublu",
|
||||
email="blublu@example.com",
|
||||
password="blublu",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=second_user,
|
||||
student_class=12,
|
||||
school="Jupiter",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/mai-linh",
|
||||
)
|
||||
|
||||
third_user = User.objects.create(
|
||||
first_name="Zupzup",
|
||||
last_name="Zupzup",
|
||||
email="zupzup@example.com",
|
||||
password="zupzup",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=third_user,
|
||||
student_class=10,
|
||||
school="Sun",
|
||||
give_contact_to_animath=False,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/yohann",
|
||||
)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
# Admin users can't ask for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["can_validate"])
|
||||
# Can't validate
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.user.registration.photo_authorization = "authorization/photo/ananas"
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["can_validate"])
|
||||
|
||||
self.team.participation.problem = 2
|
||||
self.team.participation.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.context["can_validate"])
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertFalse(self.team.participation.valid)
|
||||
self.assertIsNotNone(self.team.participation.valid)
|
||||
|
||||
# Team already asked for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_validate_team(self):
|
||||
"""
|
||||
A team asked for validation. Try to validate it.
|
||||
"""
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# No right to do that
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="J'ai 4 ans",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Woops I didn't said anything",
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Test invalidate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Wsh nope",
|
||||
invalidate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
# Team did not ask validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# Test validate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertTrue(self.team.participation.valid)
|
||||
|
||||
def test_update_team(self):
|
||||
"""
|
||||
Try to update team information.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
self.coach.registration.team = self.team
|
||||
self.coach.registration.save()
|
||||
|
||||
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Form is invalid
|
||||
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
|
||||
name="Updated team name",
|
||||
trigram="BBB",
|
||||
grant_animath_access_videos=True,
|
||||
problem=42,
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
|
||||
name="Updated team name",
|
||||
trigram="BBB",
|
||||
grant_animath_access_videos=True,
|
||||
problem=3,
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
|
||||
|
||||
def test_leave_team(self):
|
||||
"""
|
||||
A user is in a team, and leaves it.
|
||||
"""
|
||||
# User is not in a team
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Team is valid
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Unauthenticated users are redirected to login page
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team.participation.valid = None
|
||||
self.team.participation.save()
|
||||
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("index"), 302, 200)
|
||||
self.user.registration.refresh_from_db()
|
||||
self.assertIsNone(self.user.registration.team)
|
||||
self.assertFalse(Team.objects.filter(pk=self.team.pk).exists())
|
||||
|
||||
def test_no_myparticipation_redirect_nomyparticipation(self):
|
||||
"""
|
||||
Ensure a permission denied when we search my team participation when we are in no team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_participation_detail(self):
|
||||
"""
|
||||
Try to display the detail of a team participation.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Can't see the participation if it is not valid
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
|
||||
302, 403)
|
||||
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
|
||||
302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other participations
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_upload_video(self):
|
||||
"""
|
||||
Try to send a solution video link.
|
||||
"""
|
||||
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:upload_video", args=(self.team.participation.solution.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)),
|
||||
data=dict(link="https://youtube.com/watch?v=73nsrixx7eI"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.id,)),
|
||||
302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertEqual(self.team.participation.solution.platform, "youtube")
|
||||
self.assertEqual(self.team.participation.solution.youtube_code, "73nsrixx7eI")
|
||||
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Set the second phase
|
||||
for i in range(1, 5):
|
||||
Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
|
||||
end=timezone.now() + timedelta(days=i - 1))
|
||||
self.assertEqual(Phase.current_phase().phase_number, 2)
|
||||
|
||||
# Can't update the link during the second phase
|
||||
response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)),
|
||||
data=dict(link="https://youtube.com/watch?v=73nsrixx7eI"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_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):
|
||||
"""
|
||||
Load personal pages and ensure that these are protected.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:update_team", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:team_authorizations", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:upload_video",
|
||||
args=(self.second_team.participation.solution.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:upload_video",
|
||||
args=(self.second_team.participation.synthesis.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:add_question", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
question = Question.objects.create(participation=self.second_team.participation,
|
||||
question=self.question.question)
|
||||
resp = self.client.get(reverse("participation:update_question", args=(question.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:delete_question", args=(question.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_cover_matrix(self):
|
||||
"""
|
||||
Load matrix scripts, to cover them and ensure that they can run.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.received_participation = self.second_team.participation
|
||||
self.team.participation.save()
|
||||
|
||||
call_command('fix_matrix_channels')
|
||||
call_command('setup_third_phase')
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_superuser(
|
||||
username="admin@example.com",
|
||||
email="admin@example.com",
|
||||
password="admin",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team1 = Team.objects.create(
|
||||
name="Toto",
|
||||
trigram="TOT",
|
||||
)
|
||||
self.team1.participation.valid = True
|
||||
self.team1.participation.problem = 1
|
||||
self.team1.participation.save()
|
||||
|
||||
self.team2 = Team.objects.create(
|
||||
name="Bliblu",
|
||||
trigram="BIU",
|
||||
)
|
||||
self.team2.participation.valid = True
|
||||
self.team2.participation.problem = 1
|
||||
self.team2.participation.save()
|
||||
|
||||
self.team3 = Team.objects.create(
|
||||
name="Zouplop",
|
||||
trigram="ZPL",
|
||||
)
|
||||
self.team3.participation.valid = True
|
||||
self.team3.participation.problem = 1
|
||||
self.team3.participation.save()
|
||||
|
||||
self.other_team = Team.objects.create(
|
||||
name="I am different",
|
||||
trigram="IAD",
|
||||
)
|
||||
self.other_team.participation.valid = True
|
||||
self.other_team.participation.problem = 2
|
||||
self.other_team.participation.save()
|
||||
|
||||
def test_research(self):
|
||||
"""
|
||||
Try to search some things.
|
||||
"""
|
||||
call_command("rebuild_index", "--noinput", "--verbosity", 0)
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team1.name)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
def test_set_received_video(self):
|
||||
"""
|
||||
Try to define the received video of a participation.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(received_participation=self.team2.participation.pk))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team1.participation.pk,)), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_send_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(sent_participation=self.team3.participation.pk))
|
||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
||||
args=(self.team1.participation.pk,)), 302, 200)
|
||||
|
||||
self.team1.participation.refresh_from_db()
|
||||
self.team2.participation.refresh_from_db()
|
||||
self.team3.participation.refresh_from_db()
|
||||
|
||||
self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk)
|
||||
self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk)
|
||||
self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk)
|
||||
self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk)
|
||||
|
||||
# The other team didn't work on the same problem
|
||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(received_participation=self.other_team.participation.pk))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:participation_send_participation",
|
||||
args=(self.team1.participation.pk,)),
|
||||
data=dict(sent_participation=self.other_team.participation.pk))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_create_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't create a team.
|
||||
"""
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="TES",
|
||||
grant_animath_access_videos=False,
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_join_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't join a team.
|
||||
"""
|
||||
team = Team.objects.create(name="Test", trigram="TES")
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_leave_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't leave a team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_my_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My team".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_my_participation_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My participation".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
37
apps/participation/urls.py
Normal file
37
apps/participation/urls.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .views import CalendarView, CreateQuestionView, CreateTeamView, DeleteQuestionView, JoinTeamView, \
|
||||
MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PhaseUpdateView, \
|
||||
SetParticipationReceiveParticipationView, SetParticipationSendParticipationView, TeamAuthorizationsView, \
|
||||
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, UpdateQuestionView, UploadVideoView
|
||||
|
||||
|
||||
app_name = "participation"
|
||||
|
||||
urlpatterns = [
|
||||
path("create_team/", CreateTeamView.as_view(), name="create_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/<int:pk>/", TeamDetailView.as_view(), name="team_detail"),
|
||||
path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"),
|
||||
path("team/<int:pk>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"),
|
||||
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
|
||||
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
|
||||
path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"),
|
||||
path("detail/upload-video/<int:pk>/", UploadVideoView.as_view(), name="upload_video"),
|
||||
path("detail/<int:pk>/receive-participation/", SetParticipationReceiveParticipationView.as_view(),
|
||||
name="participation_receive_participation"),
|
||||
path("detail/<int:pk>/send-participation/", SetParticipationSendParticipationView.as_view(),
|
||||
name="participation_send_participation"),
|
||||
path("detail/<int:pk>/add-question/", CreateQuestionView.as_view(), name="add_question"),
|
||||
path("update-question/<int:pk>/", UpdateQuestionView.as_view(), name="update_question"),
|
||||
path("delete-question/<int:pk>/", DeleteQuestionView.as_view(), name="delete_question"),
|
||||
path("calendar/", CalendarView.as_view(), name="calendar"),
|
||||
path("calendar/<int:pk>/", PhaseUpdateView.as_view(), name="update_phase"),
|
||||
path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
|
||||
]
|
546
apps/participation/views.py
Normal file
546
apps/participation/views.py
Normal file
@ -0,0 +1,546 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from tfjm.lists import get_sympa_client
|
||||
from tfjm.matrix import Matrix
|
||||
from tfjm.views import AdminMixin
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.mail import send_mail
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, FormView, RedirectView, TemplateView, UpdateView
|
||||
from django.views.generic.edit import FormMixin, ProcessFormView
|
||||
from django_tables2 import SingleTableView
|
||||
from magic import Magic
|
||||
from registration.models import AdminRegistration
|
||||
|
||||
from .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \
|
||||
ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \
|
||||
UploadVideoForm, ValidateParticipationForm
|
||||
from .models import Participation, Phase, Question, Team, Video
|
||||
from .tables import CalendarTable, TeamTable
|
||||
|
||||
|
||||
class CreateTeamView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Display the page to create a team for new users.
|
||||
"""
|
||||
|
||||
model = Team
|
||||
form_class = TeamForm
|
||||
extra_context = dict(title=_("Create team"))
|
||||
template_name = "participation/create_team.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return super().handle_no_permission()
|
||||
registration = user.registration
|
||||
if not registration.participates:
|
||||
raise PermissionDenied(_("You don't participate, so you can't create a team."))
|
||||
elif registration.team:
|
||||
raise PermissionDenied(_("You are already in a team."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
When a team is about to be created, the user automatically
|
||||
joins the team, a mailing list got created and the user is
|
||||
automatically subscribed to this mailing list, and finally
|
||||
a Matrix room is created and the user is invited in this room.
|
||||
"""
|
||||
ret = super().form_valid(form)
|
||||
# The user joins the team
|
||||
user = self.request.user
|
||||
registration = user.registration
|
||||
registration.team = form.instance
|
||||
registration.save()
|
||||
|
||||
# Subscribe the user mail address to the team mailing list
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
||||
f"{user.first_name} {user.last_name}")
|
||||
|
||||
# Invite the user in the team Matrix room
|
||||
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
|
||||
f"@{user.registration.matrix_username}:tfjm.org")
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
|
||||
|
||||
|
||||
class JoinTeamView(LoginRequiredMixin, FormView):
|
||||
"""
|
||||
Participants can join a team with the access code of the team.
|
||||
"""
|
||||
model = Team
|
||||
form_class = JoinTeamForm
|
||||
extra_context = dict(title=_("Join team"))
|
||||
template_name = "participation/create_team.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return super().handle_no_permission()
|
||||
registration = user.registration
|
||||
if not registration.participates:
|
||||
raise PermissionDenied(_("You don't participate, so you can't create a team."))
|
||||
elif registration.team:
|
||||
raise PermissionDenied(_("You are already in a team."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
When a user joins a team, the user is automatically subscribed to
|
||||
the team mailing list,the user is invited in the team Matrix room.
|
||||
"""
|
||||
self.object = form.instance
|
||||
ret = super().form_valid(form)
|
||||
|
||||
# Join the team
|
||||
user = self.request.user
|
||||
registration = user.registration
|
||||
registration.team = form.instance
|
||||
registration.save()
|
||||
|
||||
# Subscribe to the team mailing list
|
||||
get_sympa_client().subscribe(user.email, f"equipe-{form.instance.trigram.lower()}", False,
|
||||
f"{user.first_name} {user.last_name}")
|
||||
|
||||
# Invite the user in the team Matrix room
|
||||
Matrix.invite(f"#equipe-{form.instance.trigram.lower()}:tfjm.org",
|
||||
f"@{user.registration.matrix_username}:tfjm.org")
|
||||
return ret
|
||||
|
||||
def get_success_url(self):
|
||||
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):
|
||||
"""
|
||||
Redirect to the detail of the team in which the user is.
|
||||
"""
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
user = self.request.user
|
||||
registration = user.registration
|
||||
if registration.participates:
|
||||
if registration.team:
|
||||
return reverse_lazy("participation:team_detail", args=(registration.team_id,))
|
||||
raise PermissionDenied(_("You are not in a team."))
|
||||
raise PermissionDenied(_("You don't participate, so you don't have any team."))
|
||||
|
||||
|
||||
class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView):
|
||||
"""
|
||||
Display the detail of a team.
|
||||
"""
|
||||
model = Team
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
self.object = self.get_object()
|
||||
# Ensure that the user is an admin or a member of the team
|
||||
if user.registration.is_admin or user.registration.participates and \
|
||||
user.registration.team and user.registration.team.pk == kwargs["pk"]:
|
||||
return super().get(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
team = self.get_object()
|
||||
context["title"] = _("Detail of team {trigram}").format(trigram=self.object.trigram)
|
||||
context["request_validation_form"] = RequestValidationForm(self.request.POST or None)
|
||||
context["validation_form"] = ValidateParticipationForm(self.request.POST or None)
|
||||
# A team is complete when there are at least 3 members that have sent their photo authorization
|
||||
# and confirmed their email address
|
||||
context["can_validate"] = team.students.count() >= 3 and \
|
||||
all(r.email_confirmed for r in team.students.all()) and \
|
||||
all(r.photo_authorization for r in team.students.all()) and \
|
||||
team.participation.problem
|
||||
|
||||
return context
|
||||
|
||||
def get_form_class(self):
|
||||
if not self.request.POST:
|
||||
return RequestValidationForm
|
||||
elif self.request.POST["_form_type"] == "RequestValidationForm":
|
||||
return RequestValidationForm
|
||||
elif self.request.POST["_form_type"] == "ValidateParticipationForm":
|
||||
return ValidateParticipationForm
|
||||
|
||||
def form_valid(self, form):
|
||||
self.object = self.get_object()
|
||||
if isinstance(form, RequestValidationForm):
|
||||
return self.handle_request_validation(form)
|
||||
elif isinstance(form, ValidateParticipationForm):
|
||||
return self.handle_validate_participation(form)
|
||||
|
||||
def handle_request_validation(self, form):
|
||||
"""
|
||||
A team requests to be validated
|
||||
"""
|
||||
if not self.request.user.registration.participates:
|
||||
form.add_error(None, _("You don't participate, so you can't request the validation of the team."))
|
||||
return self.form_invalid(form)
|
||||
if self.object.participation.valid is not None:
|
||||
form.add_error(None, _("The validation of the team is already done or pending."))
|
||||
return self.form_invalid(form)
|
||||
if not self.get_context_data()["can_validate"]:
|
||||
form.add_error(None, _("The team can't be validated: missing email address confirmations, "
|
||||
"photo authorizations, people or the chosen problem is not set."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
self.object.participation.valid = False
|
||||
self.object.participation.save()
|
||||
|
||||
for admin in AdminRegistration.objects.all():
|
||||
mail_context = dict(user=admin.user, team=self.object, domain=Site.objects.first().domain)
|
||||
mail_plain = render_to_string("participation/mails/request_validation.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/request_validation.html", mail_context)
|
||||
admin.user.email_user("[Corres2math] Validation d'équipe", mail_plain, html_message=mail_html)
|
||||
return super().form_valid(form)
|
||||
|
||||
def handle_validate_participation(self, form):
|
||||
"""
|
||||
An admin validates the team (or not)
|
||||
"""
|
||||
if not self.request.user.registration.is_admin:
|
||||
form.add_error(None, _("You are not an administrator."))
|
||||
return self.form_invalid(form)
|
||||
elif self.object.participation.valid is not False:
|
||||
form.add_error(None, _("This team has no pending validation."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if "validate" in self.request.POST:
|
||||
self.object.participation.valid = True
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe validée", mail_plain, None, [self.object.email], html_message=mail_html)
|
||||
|
||||
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:
|
||||
self.object.participation.valid = None
|
||||
self.object.participation.save()
|
||||
mail_context = dict(team=self.object, message=form.cleaned_data["message"])
|
||||
mail_plain = render_to_string("participation/mails/team_not_validated.txt", mail_context)
|
||||
mail_html = render_to_string("participation/mails/team_not_validated.html", mail_context)
|
||||
send_mail("[Corres2math] Équipe non validée", mail_plain, None, [self.object.email],
|
||||
html_message=mail_html)
|
||||
else:
|
||||
form.add_error(None, _("You must specify if you validate the registration or not."))
|
||||
return self.form_invalid(form)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return self.request.path
|
||||
|
||||
|
||||
class TeamUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update the detail of a team
|
||||
"""
|
||||
model = Team
|
||||
form_class = TeamForm
|
||||
template_name = "participation/update_team.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
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 and \
|
||||
user.registration.team.pk == kwargs["pk"]:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["participation_form"] = ParticipationForm(data=self.request.POST or None,
|
||||
instance=self.object.participation)
|
||||
context["title"] = _("Update team {trigram}").format(trigram=self.object.trigram)
|
||||
return context
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
participation_form = ParticipationForm(data=self.request.POST or None, instance=self.object.participation)
|
||||
if not participation_form.is_valid():
|
||||
return self.form_invalid(form)
|
||||
|
||||
participation_form.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:team_detail", args=(self.object.pk,))
|
||||
|
||||
|
||||
class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Get as a ZIP archive all the authorizations that are sent
|
||||
"""
|
||||
model = Team
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
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"]:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
team = self.get_object()
|
||||
output = BytesIO()
|
||||
zf = ZipFile(output, "w")
|
||||
for student in team.students.all():
|
||||
magic = Magic(mime=True)
|
||||
mime_type = magic.from_file("media/" + student.photo_authorization.name)
|
||||
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
||||
zf.write("media/" + student.photo_authorization.name,
|
||||
_("Photo authorization of {student}.{ext}").format(student=str(student), ext=ext))
|
||||
zf.close()
|
||||
response = HttpResponse(content_type="application/zip")
|
||||
response["Content-Disposition"] = "attachment; filename=\"{filename}\"" \
|
||||
.format(filename=_("Photo authorizations of team {trigram}.zip").format(trigram=team.trigram))
|
||||
response.write(output.getvalue())
|
||||
return response
|
||||
|
||||
|
||||
class TeamLeaveView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
A team member leaves a team
|
||||
"""
|
||||
template_name = "participation/team_leave.html"
|
||||
extra_context = dict(title=_("Leave team"))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
if not request.user.registration.participates or not request.user.registration.team:
|
||||
raise PermissionDenied(_("You are not in a team."))
|
||||
if request.user.registration.team.participation.valid:
|
||||
raise PermissionDenied(_("The team is already validated or the validation is pending."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
When the team is left, the user is unsubscribed from the team mailing list
|
||||
and kicked from the team room.
|
||||
"""
|
||||
team = request.user.registration.team
|
||||
request.user.registration.team = None
|
||||
request.user.registration.save()
|
||||
get_sympa_client().unsubscribe(request.user.email, f"equipe-{team.trigram.lower()}", False)
|
||||
Matrix.kick(f"#equipe-{team.trigram.lower()}:tfjm.org",
|
||||
f"@{request.user.registration.matrix_username}:tfjm.org",
|
||||
"Équipe quittée")
|
||||
if team.students.count() + team.coachs.count() == 0:
|
||||
team.delete()
|
||||
return redirect(reverse_lazy("index"))
|
||||
|
||||
|
||||
class MyParticipationDetailView(LoginRequiredMixin, RedirectView):
|
||||
"""
|
||||
Redirects to the detail view of the participation of the team.
|
||||
"""
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
user = self.request.user
|
||||
registration = user.registration
|
||||
if registration.participates:
|
||||
if registration.team:
|
||||
return reverse_lazy("participation:participation_detail", args=(registration.team.participation.id,))
|
||||
raise PermissionDenied(_("You are not in a team."))
|
||||
raise PermissionDenied(_("You don't participate, so you don't have any team."))
|
||||
|
||||
|
||||
class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Display detail about the participation of a team, and manage the video submission.
|
||||
"""
|
||||
model = Participation
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return super().handle_no_permission()
|
||||
if not self.get_object().valid:
|
||||
raise PermissionDenied(_("The team is not validated yet."))
|
||||
if user.registration.is_admin or user.registration.participates \
|
||||
and user.registration.team.participation \
|
||||
and user.registration.team.participation.pk == kwargs["pk"]:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
|
||||
context["current_phase"] = Phase.current_phase()
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class SetParticipationReceiveParticipationView(AdminMixin, UpdateView):
|
||||
"""
|
||||
Define the solution that a team will receive.
|
||||
"""
|
||||
model = Participation
|
||||
form_class = ReceiveParticipationForm
|
||||
template_name = "participation/receive_participation_form.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
|
||||
|
||||
|
||||
class SetParticipationSendParticipationView(AdminMixin, UpdateView):
|
||||
"""
|
||||
Define the team where the solution will be sent.
|
||||
"""
|
||||
model = Participation
|
||||
form_class = SendParticipationForm
|
||||
template_name = "participation/send_participation_form.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
|
||||
|
||||
|
||||
class CreateQuestionView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Ask a question to another team.
|
||||
"""
|
||||
participation: Participation
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
extra_context = dict(title=_("Create question"))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
self.participation = Participation.objects.get(pk=kwargs["pk"])
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.participation.valid and \
|
||||
request.user.registration.team.pk == self.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.participation = self.participation
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.participation.pk,))
|
||||
|
||||
|
||||
class UpdateQuestionView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Edit a question.
|
||||
"""
|
||||
model = Question
|
||||
form_class = QuestionForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.object.participation.valid and \
|
||||
request.user.registration.team.pk == self.object.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
|
||||
|
||||
|
||||
class DeleteQuestionView(LoginRequiredMixin, DeleteView):
|
||||
"""
|
||||
Remove a question.
|
||||
"""
|
||||
model = Question
|
||||
extra_context = dict(title=_("Delete question"))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
if request.user.registration.is_admin or \
|
||||
request.user.registration.participates and \
|
||||
self.object.participation.valid and \
|
||||
request.user.registration.team.pk == self.object.participation.team_id:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
|
||||
|
||||
|
||||
class UploadVideoView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Upload a solution video for a team.
|
||||
"""
|
||||
model = Video
|
||||
form_class = UploadVideoForm
|
||||
template_name = "participation/upload_video.html"
|
||||
extra_context = dict(title=_("Upload video"))
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
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.participation.pk == self.get_object().participation.pk:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
raise PermissionDenied
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
|
||||
|
||||
|
||||
class CalendarView(SingleTableView):
|
||||
"""
|
||||
Display the calendar of the action.
|
||||
"""
|
||||
table_class = CalendarTable
|
||||
model = Phase
|
||||
extra_context = dict(title=_("Calendar"))
|
||||
|
||||
|
||||
class PhaseUpdateView(AdminMixin, UpdateView):
|
||||
"""
|
||||
Update a phase of the calendar, if we have sufficient rights.
|
||||
"""
|
||||
model = Phase
|
||||
form_class = PhaseForm
|
||||
extra_context = dict(title=_("Calendar update"))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("participation:calendar")
|
Reference in New Issue
Block a user