Drop a lot of Corres2math content
This commit is contained in:
parent
f5ec9d1054
commit
7ef602c6cd
|
@ -42,3 +42,5 @@ db.sqlite3
|
||||||
|
|
||||||
# Don't git index
|
# Don't git index
|
||||||
whoosh_index/
|
whoosh_index/
|
||||||
|
|
||||||
|
migrations/
|
||||||
|
|
|
@ -4,19 +4,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import Participation, Phase, Question, Team, Video
|
from .models import Participation, Pool, Solution, Synthesis, Team, Tournament
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Team)
|
@admin.register(Team)
|
||||||
class TeamAdmin(admin.ModelAdmin):
|
class TeamAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'trigram', 'problem', 'valid',)
|
list_display = ('name', 'trigram', 'valid',)
|
||||||
search_fields = ('name', 'trigram',)
|
search_fields = ('name', 'trigram',)
|
||||||
list_filter = ('participation__problem', 'participation__valid',)
|
list_filter = ('participation__valid',)
|
||||||
|
|
||||||
def problem(self, team):
|
|
||||||
return team.participation.get_problem_display()
|
|
||||||
|
|
||||||
problem.short_description = _('problem number')
|
|
||||||
|
|
||||||
def valid(self, team):
|
def valid(self, team):
|
||||||
return team.participation.valid
|
return team.participation.valid
|
||||||
|
@ -26,24 +21,29 @@ class TeamAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Participation)
|
@admin.register(Participation)
|
||||||
class ParticipationAdmin(admin.ModelAdmin):
|
class ParticipationAdmin(admin.ModelAdmin):
|
||||||
list_display = ('team', 'problem', 'valid',)
|
list_display = ('team', 'valid',)
|
||||||
search_fields = ('team__name', 'team__trigram',)
|
search_fields = ('team__name', 'team__trigram',)
|
||||||
list_filter = ('problem', 'valid',)
|
list_filter = ('valid',)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Video)
|
@admin.register(Pool)
|
||||||
class VideoAdmin(admin.ModelAdmin):
|
class PoolAdmin(admin.ModelAdmin):
|
||||||
list_display = ('participation', 'link',)
|
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'link',)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Question)
|
@admin.register(Solution)
|
||||||
class QuestionAdmin(admin.ModelAdmin):
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
list_display = ('participation', 'question',)
|
list_display = ('participation',)
|
||||||
search_fields = ('participation__team__name', 'participation__team__trigram', 'question',)
|
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Phase)
|
@admin.register(Synthesis)
|
||||||
class PhaseAdmin(admin.ModelAdmin):
|
class SynthesisAdmin(admin.ModelAdmin):
|
||||||
list_display = ('phase_number', 'start', 'end',)
|
list_display = ('participation',)
|
||||||
ordering = ('phase_number', 'start',)
|
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Tournament)
|
||||||
|
class TournamentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name',)
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
|
@ -12,7 +12,6 @@ class ParticipationConfig(AppConfig):
|
||||||
name = 'participation'
|
name = 'participation'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from participation.signals import create_team_participation, delete_related_videos, update_mailing_list
|
from participation.signals import create_team_participation, update_mailing_list
|
||||||
pre_save.connect(update_mailing_list, "participation.Team")
|
pre_save.connect(update_mailing_list, "participation.Team")
|
||||||
pre_delete.connect(delete_related_videos, "participation.Participation")
|
|
||||||
post_save.connect(create_team_participation, "participation.Team")
|
post_save.connect(create_team_participation, "participation.Team")
|
||||||
|
|
|
@ -3,13 +3,11 @@
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from bootstrap_datepicker_plus import DateTimePickerInput
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .models import Participation, Phase, Question, Team, Video
|
from .models import Participation, Team
|
||||||
|
|
||||||
|
|
||||||
class TeamForm(forms.ModelForm):
|
class TeamForm(forms.ModelForm):
|
||||||
|
@ -25,7 +23,7 @@ class TeamForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Team
|
model = Team
|
||||||
fields = ('name', 'trigram', 'grant_animath_access_videos',)
|
fields = ('name', 'trigram',)
|
||||||
|
|
||||||
|
|
||||||
class JoinTeamForm(forms.ModelForm):
|
class JoinTeamForm(forms.ModelForm):
|
||||||
|
@ -56,7 +54,7 @@ class ParticipationForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Participation
|
model = Participation
|
||||||
fields = ('problem',)
|
fields = ('tournament',)
|
||||||
|
|
||||||
|
|
||||||
class RequestValidationForm(forms.Form):
|
class RequestValidationForm(forms.Form):
|
||||||
|
@ -87,108 +85,3 @@ class ValidateParticipationForm(forms.Form):
|
||||||
label=_("Message to address to the team:"),
|
label=_("Message to address to the team:"),
|
||||||
widget=forms.Textarea(),
|
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
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,138 +0,0 @@
|
||||||
# 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,
|
|
||||||
)
|
|
||||||
]
|
|
|
@ -44,6 +44,14 @@ class Team(models.Model):
|
||||||
help_text=_("The access code let other people to join the team."),
|
help_text=_("The access code let other people to join the team."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def students(self):
|
||||||
|
return self.participants.filter(studentregistration__isnull=False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coachs(self):
|
||||||
|
return self.participants.filter(coachregistration__isnull=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
"""
|
"""
|
||||||
|
@ -224,33 +232,6 @@ class Participation(models.Model):
|
||||||
help_text=_("The video got the validation of the administrators."),
|
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):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy("participation:participation_detail", args=(self.pk,))
|
return reverse_lazy("participation:participation_detail", args=(self.pk,))
|
||||||
|
|
||||||
|
@ -324,7 +305,7 @@ class Solution(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("solution")
|
verbose_name = _("solution")
|
||||||
verbose_name_plural = _("solutions")
|
verbose_name_plural = _("solutions")
|
||||||
unique_by = (('participation', 'problem', 'final_solution', ), )
|
unique_together = (('participation', 'problem', 'final_solution', ), )
|
||||||
|
|
||||||
|
|
||||||
class Synthesis(models.Model):
|
class Synthesis(models.Model):
|
||||||
|
@ -359,4 +340,4 @@ class Synthesis(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("synthesis")
|
verbose_name = _("synthesis")
|
||||||
verbose_name_plural = _("syntheses")
|
verbose_name_plural = _("syntheses")
|
||||||
unique_by = (('participation', 'pool', 'type', ), )
|
unique_together = (('participation', 'pool', 'type', ), )
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from haystack import indexes
|
from haystack import indexes
|
||||||
|
|
||||||
from .models import Participation, Team, Video
|
from .models import Participation, Team
|
||||||
|
|
||||||
|
|
||||||
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||||
|
@ -24,13 +24,3 @@ class ParticipationIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Participation
|
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
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from tfjm.lists import get_sympa_client
|
from tfjm.lists import get_sympa_client
|
||||||
from participation.models import Participation, Team, Video
|
from participation.models import Participation, Team
|
||||||
|
|
||||||
|
|
||||||
def create_team_participation(instance, created, **_):
|
def create_team_participation(instance, created, **_):
|
||||||
|
@ -10,10 +10,6 @@ def create_team_participation(instance, created, **_):
|
||||||
When a team got created, create an associated team and create Video objects.
|
When a team got created, create an associated team and create Video objects.
|
||||||
"""
|
"""
|
||||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
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()
|
participation.save()
|
||||||
if not created:
|
if not created:
|
||||||
participation.team.create_mailing_list()
|
participation.team.create_mailing_list()
|
||||||
|
@ -38,9 +34,3 @@ def update_mailing_list(instance: Team, **_):
|
||||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||||
f"{coach.user.first_name} {coach.user.last_name}")
|
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()
|
|
||||||
|
|
|
@ -1,28 +1,10 @@
|
||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
||||||
from .models import Phase, Team
|
from .models import 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
|
# noinspection PyTypeChecker
|
||||||
|
|
|
@ -12,289 +12,7 @@
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-2">{% trans "Team:" %}</dt>
|
<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>
|
<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>
|
</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>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
|
@ -45,9 +45,6 @@
|
||||||
{% trans "any" as any %}
|
{% trans "any" as any %}
|
||||||
<dd class="col-sm-6">{{ team.participation.get_problem_display|default:any }}</dd>
|
<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>
|
<dt class="col-sm-6 text-right">{% trans "Authorizations:" %}</dt>
|
||||||
<dd class="col-sm-6">
|
<dd class="col-sm-6">
|
||||||
{% for student in team.students.all %}
|
{% for student in team.students.all %}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Copyright (C) 2020 by Animath
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Copyright (C) 2020 by Animath
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django import template
|
|
||||||
|
|
||||||
from ..models import Phase
|
|
||||||
|
|
||||||
|
|
||||||
def current_phase(nb):
|
|
||||||
phase = Phase.current_phase()
|
|
||||||
return phase is not None and phase.phase_number == nb
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
register.filter("current_phase", current_phase)
|
|
|
@ -1,18 +1,15 @@
|
||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
|
||||||
from registration.models import CoachRegistration, StudentRegistration
|
from registration.models import CoachRegistration, StudentRegistration
|
||||||
|
|
||||||
from .models import Participation, Phase, Question, Team
|
from .models import Participation, Team
|
||||||
|
|
||||||
|
|
||||||
class TestStudentParticipation(TestCase):
|
class TestStudentParticipation(TestCase):
|
||||||
|
@ -40,10 +37,7 @@ class TestStudentParticipation(TestCase):
|
||||||
name="Super team",
|
name="Super team",
|
||||||
trigram="AAA",
|
trigram="AAA",
|
||||||
access_code="azerty",
|
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.client.force_login(self.user)
|
||||||
|
|
||||||
self.second_user = User.objects.create(
|
self.second_user = User.objects.create(
|
||||||
|
@ -63,7 +57,6 @@ class TestStudentParticipation(TestCase):
|
||||||
name="Poor team",
|
name="Poor team",
|
||||||
trigram="FFF",
|
trigram="FFF",
|
||||||
access_code="qwerty",
|
access_code="qwerty",
|
||||||
grant_animath_access_videos=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.coach = User.objects.create(
|
self.coach = User.objects.create(
|
||||||
|
@ -108,29 +101,6 @@ class TestStudentParticipation(TestCase):
|
||||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||||
str(self.team.participation.get_absolute_url()), 302, 200)
|
str(self.team.participation.get_absolute_url()), 302, 200)
|
||||||
|
|
||||||
# Test video pages
|
|
||||||
response = self.client.get(reverse("admin:index") + "participation/video/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("admin:index")
|
|
||||||
+ f"participation/video/{self.team.participation.solution.pk}/change/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Test question pages
|
|
||||||
response = self.client.get(reverse("admin:index") + "participation/question/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("admin:index")
|
|
||||||
+ f"participation/question/{self.question.pk}/change/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
# Test phase pages
|
|
||||||
response = self.client.get(reverse("admin:index") + "participation/phase/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("admin:index") + "participation/phase/1/change/")
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_create_team(self):
|
def test_create_team(self):
|
||||||
"""
|
"""
|
||||||
Try to create a team.
|
Try to create a team.
|
||||||
|
@ -141,14 +111,12 @@ class TestStudentParticipation(TestCase):
|
||||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||||
name="Test team",
|
name="Test team",
|
||||||
trigram="123",
|
trigram="123",
|
||||||
grant_animath_access_videos=False,
|
|
||||||
))
|
))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||||
name="Test team",
|
name="Test team",
|
||||||
trigram="TES",
|
trigram="TES",
|
||||||
grant_animath_access_videos=False,
|
|
||||||
))
|
))
|
||||||
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
||||||
team = Team.objects.get(trigram="TES")
|
team = Team.objects.get(trigram="TES")
|
||||||
|
@ -158,7 +126,6 @@ class TestStudentParticipation(TestCase):
|
||||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||||
name="Test team 2",
|
name="Test team 2",
|
||||||
trigram="TET",
|
trigram="TET",
|
||||||
grant_animath_access_videos=False,
|
|
||||||
))
|
))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
@ -286,13 +253,6 @@ class TestStudentParticipation(TestCase):
|
||||||
self.user.registration.photo_authorization = "authorization/photo/ananas"
|
self.user.registration.photo_authorization = "authorization/photo/ananas"
|
||||||
self.user.registration.save()
|
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,)))
|
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertTrue(resp.context["can_validate"])
|
self.assertTrue(resp.context["can_validate"])
|
||||||
|
@ -383,23 +343,12 @@ class TestStudentParticipation(TestCase):
|
||||||
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Form is invalid
|
|
||||||
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
|
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
|
||||||
name="Updated team name",
|
name="Updated team name",
|
||||||
trigram="BBB",
|
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.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||||
self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
|
self.assertTrue(Team.objects.filter(trigram="BBB").exists())
|
||||||
|
|
||||||
def test_leave_team(self):
|
def test_leave_team(self):
|
||||||
"""
|
"""
|
||||||
|
@ -471,199 +420,6 @@ class TestStudentParticipation(TestCase):
|
||||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||||
self.assertEqual(response.status_code, 403)
|
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):
|
def test_forbidden_access(self):
|
||||||
"""
|
"""
|
||||||
Load personal pages and ensure that these are protected.
|
Load personal pages and ensure that these are protected.
|
||||||
|
@ -679,20 +435,6 @@ class TestStudentParticipation(TestCase):
|
||||||
self.assertEqual(resp.status_code, 403)
|
self.assertEqual(resp.status_code, 403)
|
||||||
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
|
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
|
||||||
self.assertEqual(resp.status_code, 403)
|
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):
|
def test_cover_matrix(self):
|
||||||
"""
|
"""
|
||||||
|
@ -707,7 +449,6 @@ class TestStudentParticipation(TestCase):
|
||||||
self.team.participation.save()
|
self.team.participation.save()
|
||||||
|
|
||||||
call_command('fix_matrix_channels')
|
call_command('fix_matrix_channels')
|
||||||
call_command('setup_third_phase')
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdmin(TestCase):
|
class TestAdmin(TestCase):
|
||||||
|
@ -765,50 +506,6 @@ class TestAdmin(TestCase):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue(response.context["object_list"])
|
self.assertTrue(response.context["object_list"])
|
||||||
|
|
||||||
def test_set_received_video(self):
|
|
||||||
"""
|
|
||||||
Try to define the received video of a participation.
|
|
||||||
"""
|
|
||||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
|
||||||
args=(self.team1.participation.pk,)))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
|
||||||
args=(self.team1.participation.pk,)),
|
|
||||||
data=dict(received_participation=self.team2.participation.pk))
|
|
||||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
|
||||||
args=(self.team1.participation.pk,)), 302, 200)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("participation:participation_receive_participation",
|
|
||||||
args=(self.team1.participation.pk,)))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.post(reverse("participation:participation_send_participation",
|
|
||||||
args=(self.team1.participation.pk,)),
|
|
||||||
data=dict(sent_participation=self.team3.participation.pk))
|
|
||||||
self.assertRedirects(response, reverse("participation:participation_detail",
|
|
||||||
args=(self.team1.participation.pk,)), 302, 200)
|
|
||||||
|
|
||||||
self.team1.participation.refresh_from_db()
|
|
||||||
self.team2.participation.refresh_from_db()
|
|
||||||
self.team3.participation.refresh_from_db()
|
|
||||||
|
|
||||||
self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk)
|
|
||||||
self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk)
|
|
||||||
self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk)
|
|
||||||
self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk)
|
|
||||||
|
|
||||||
# The other team didn't work on the same problem
|
|
||||||
response = self.client.post(reverse("participation:participation_receive_participation",
|
|
||||||
args=(self.team1.participation.pk,)),
|
|
||||||
data=dict(received_participation=self.other_team.participation.pk))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
response = self.client.post(reverse("participation:participation_send_participation",
|
|
||||||
args=(self.team1.participation.pk,)),
|
|
||||||
data=dict(sent_participation=self.other_team.participation.pk))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_create_team_forbidden(self):
|
def test_create_team_forbidden(self):
|
||||||
"""
|
"""
|
||||||
Ensure that an admin can't create a team.
|
Ensure that an admin can't create a team.
|
||||||
|
@ -816,7 +513,6 @@ class TestAdmin(TestCase):
|
||||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||||
name="Test team",
|
name="Test team",
|
||||||
trigram="TES",
|
trigram="TES",
|
||||||
grant_animath_access_videos=False,
|
|
||||||
))
|
))
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from .views import CalendarView, CreateQuestionView, CreateTeamView, DeleteQuestionView, JoinTeamView, \
|
from .views import CreateTeamView, JoinTeamView, \
|
||||||
MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PhaseUpdateView, \
|
MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, TeamAuthorizationsView, \
|
||||||
SetParticipationReceiveParticipationView, SetParticipationSendParticipationView, TeamAuthorizationsView, \
|
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView
|
||||||
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, UpdateQuestionView, UploadVideoView
|
|
||||||
|
|
||||||
|
|
||||||
app_name = "participation"
|
app_name = "participation"
|
||||||
|
@ -23,15 +22,5 @@ urlpatterns = [
|
||||||
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
|
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
|
||||||
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
|
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
|
||||||
path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="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")
|
path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
|
||||||
]
|
]
|
||||||
|
|
|
@ -17,17 +17,15 @@ from django.shortcuts import redirect
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DeleteView, DetailView, FormView, RedirectView, TemplateView, UpdateView
|
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView
|
||||||
from django.views.generic.edit import FormMixin, ProcessFormView
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
from registration.models import AdminRegistration
|
from registration.models import AdminRegistration
|
||||||
|
|
||||||
from .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \
|
from .forms import JoinTeamForm, ParticipationForm, RequestValidationForm, TeamForm, ValidateParticipationForm
|
||||||
ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \
|
from .models import Participation, Team
|
||||||
UploadVideoForm, ValidateParticipationForm
|
from .tables import TeamTable
|
||||||
from .models import Participation, Phase, Question, Team, Video
|
|
||||||
from .tables import CalendarTable, TeamTable
|
|
||||||
|
|
||||||
|
|
||||||
class CreateTeamView(LoginRequiredMixin, CreateView):
|
class CreateTeamView(LoginRequiredMixin, CreateView):
|
||||||
|
@ -177,8 +175,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||||
# and confirmed their email address
|
# and confirmed their email address
|
||||||
context["can_validate"] = team.students.count() >= 3 and \
|
context["can_validate"] = team.students.count() >= 3 and \
|
||||||
all(r.email_confirmed for r in team.students.all()) and \
|
all(r.email_confirmed for r in team.students.all()) and \
|
||||||
all(r.photo_authorization for r in team.students.all()) and \
|
all(r.photo_authorization for r in team.students.all())
|
||||||
team.participation.problem
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -243,8 +240,6 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
|
||||||
|
|
||||||
get_sympa_client().subscribe(self.object.email, "equipes", False, f"Equipe {self.object.name}")
|
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().unsubscribe(self.object.email, "equipes-non-valides", False)
|
||||||
get_sympa_client().subscribe(self.object.email, f"probleme-{self.object.participation.problem}", False,
|
|
||||||
f"Equipe {self.object.name}")
|
|
||||||
elif "invalidate" in self.request.POST:
|
elif "invalidate" in self.request.POST:
|
||||||
self.object.participation.valid = None
|
self.object.participation.valid = None
|
||||||
self.object.participation.save()
|
self.object.participation.save()
|
||||||
|
@ -318,7 +313,7 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
|
||||||
team = self.get_object()
|
team = self.get_object()
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
zf = ZipFile(output, "w")
|
zf = ZipFile(output, "w")
|
||||||
for student in team.students.all():
|
for student in team.participants.all():
|
||||||
magic = Magic(mime=True)
|
magic = Magic(mime=True)
|
||||||
mime_type = magic.from_file("media/" + student.photo_authorization.name)
|
mime_type = magic.from_file("media/" + student.photo_authorization.name)
|
||||||
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
|
||||||
|
@ -402,145 +397,5 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
|
context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
|
||||||
context["current_phase"] = Phase.current_phase()
|
|
||||||
|
|
||||||
return context
|
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")
|
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
# Generated by Django 3.1.3 on 2020-11-04 12:05
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import registration.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('participation', '0001_initial'),
|
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Registration',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('give_contact_to_animath', models.BooleanField(default=False, verbose_name='Grant Animath to contact me in the future about other actions')),
|
|
||||||
('email_confirmed', models.BooleanField(default=False, verbose_name='email confirmed')),
|
|
||||||
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_registration.registration_set+', to='contenttypes.contenttype')),
|
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'registration',
|
|
||||||
'verbose_name_plural': 'registrations',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AdminRegistration',
|
|
||||||
fields=[
|
|
||||||
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')),
|
|
||||||
('role', models.TextField(verbose_name='role of the administrator')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'admin registration',
|
|
||||||
'verbose_name_plural': 'admin registrations',
|
|
||||||
},
|
|
||||||
bases=('registration.registration',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='StudentRegistration',
|
|
||||||
fields=[
|
|
||||||
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')),
|
|
||||||
('student_class', models.IntegerField(choices=[(12, '12th grade'), (11, '11th grade'), (10, '10th grade or lower')], verbose_name='student class')),
|
|
||||||
('school', models.CharField(max_length=255, verbose_name='school')),
|
|
||||||
('photo_authorization', models.FileField(blank=True, default='', upload_to=registration.models.get_random_filename, verbose_name='photo authorization')),
|
|
||||||
('team', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='students', to='participation.team', verbose_name='team')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'student registration',
|
|
||||||
'verbose_name_plural': 'student registrations',
|
|
||||||
},
|
|
||||||
bases=('registration.registration',),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='CoachRegistration',
|
|
||||||
fields=[
|
|
||||||
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')),
|
|
||||||
('professional_activity', models.TextField(verbose_name='professional activity')),
|
|
||||||
('team', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='coachs', to='participation.team', verbose_name='team')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'coach registration',
|
|
||||||
'verbose_name_plural': 'coach registrations',
|
|
||||||
},
|
|
||||||
bases=('registration.registration',),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -129,11 +129,11 @@ class ParticipantRegistration(Registration):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self): # pragma: no cover
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form_class(self):
|
def form_class(self): # pragma: no cover
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django_tables2 import Table
|
from django_tables2 import Table
|
||||||
from participation.models import Participation, Team, Video
|
from participation.models import Participation, Team
|
||||||
from participation.tables import ParticipationTable, TeamTable, VideoTable
|
from participation.tables import ParticipationTable, TeamTable, VideoTable
|
||||||
|
|
||||||
from ..models import Registration
|
from ..models import Registration
|
||||||
|
@ -19,8 +19,6 @@ def search_table(results):
|
||||||
table_class = TeamTable
|
table_class = TeamTable
|
||||||
elif issubclass(model_class, Participation):
|
elif issubclass(model_class, Participation):
|
||||||
table_class = ParticipationTable
|
table_class = ParticipationTable
|
||||||
elif issubclass(model_class, Video):
|
|
||||||
table_class = VideoTable
|
|
||||||
return table_class([result.object for result in results], prefix=model_class._meta.model_name)
|
return table_class([result.object for result in results], prefix=model_class._meta.model_name)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# Copyright (C) 2020 by Animath
|
# Copyright (C) 2020 by Animath
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from tfjm.tokens import email_validation_token
|
from tfjm.tokens import email_validation_token
|
||||||
|
@ -12,10 +11,9 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
from django.utils.http import urlsafe_base64_encode
|
from django.utils.http import urlsafe_base64_encode
|
||||||
from participation.models import Phase, Team
|
from participation.models import Team
|
||||||
|
|
||||||
from .models import AdminRegistration, CoachRegistration, StudentRegistration
|
from .models import AdminRegistration, CoachRegistration, StudentRegistration
|
||||||
|
|
||||||
|
@ -54,8 +52,6 @@ class TestIndexPage(TestCase):
|
||||||
response = self.client.get(reverse("participation:participation_detail", args=(1,)))
|
response = self.client.get(reverse("participation:participation_detail", args=(1,)))
|
||||||
self.assertRedirects(response, reverse("login") + "?next="
|
self.assertRedirects(response, reverse("login") + "?next="
|
||||||
+ reverse("participation:participation_detail", args=(1,)))
|
+ reverse("participation:participation_detail", args=(1,)))
|
||||||
response = self.client.get(reverse("participation:upload_video", args=(1,)))
|
|
||||||
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:upload_video", args=(1,)))
|
|
||||||
|
|
||||||
|
|
||||||
class TestRegistration(TestCase):
|
class TestRegistration(TestCase):
|
||||||
|
@ -110,13 +106,6 @@ class TestRegistration(TestCase):
|
||||||
"""
|
"""
|
||||||
Ensure that the signup form is working successfully.
|
Ensure that the signup form is working successfully.
|
||||||
"""
|
"""
|
||||||
# After first phase
|
|
||||||
response = self.client.get(reverse("registration:signup"))
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
Phase.objects.filter(phase_number__gte=2).update(start=timezone.now() + timedelta(days=1),
|
|
||||||
end=timezone.now() + timedelta(days=2))
|
|
||||||
|
|
||||||
response = self.client.get(reverse("registration:signup"))
|
response = self.client.get(reverse("registration:signup"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
@ -300,7 +289,7 @@ class TestRegistration(TestCase):
|
||||||
|
|
||||||
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
||||||
args=(self.student.registration.pk,)), data=dict(
|
args=(self.student.registration.pk,)), data=dict(
|
||||||
photo_authorization=open("tfjm/static/Autorisation de droit à l'image - majeur.pdf", "rb"),
|
photo_authorization=open("tfjm/static/Fiche_sanitaire.pdf", "rb"),
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||||
|
|
||||||
|
@ -323,7 +312,7 @@ class TestRegistration(TestCase):
|
||||||
old_authoratization = self.student.registration.photo_authorization.path
|
old_authoratization = self.student.registration.photo_authorization.path
|
||||||
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
response = self.client.post(reverse("registration:upload_user_photo_authorization",
|
||||||
args=(self.student.registration.pk,)), data=dict(
|
args=(self.student.registration.pk,)), data=dict(
|
||||||
photo_authorization=open("tfjm/static/Autorisation de droit à l'image - majeur.pdf", "rb"),
|
photo_authorization=open("tfjm/static/Fiche_sanitaire.pdf", "rb"),
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
self.assertRedirects(response, reverse("registration:user_detail", args=(self.student.pk,)), 302, 200)
|
||||||
self.assertFalse(os.path.isfile(old_authoratization))
|
self.assertFalse(os.path.isfile(old_authoratization))
|
||||||
|
|
|
@ -18,7 +18,6 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
from django.views.generic import CreateView, DetailView, RedirectView, TemplateView, UpdateView, View
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from magic import Magic
|
from magic import Magic
|
||||||
from participation.models import Phase
|
|
||||||
|
|
||||||
from .forms import CoachRegistrationForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm
|
from .forms import CoachRegistrationForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm
|
||||||
from .models import Registration, StudentRegistration
|
from .models import Registration, StudentRegistration
|
||||||
|
@ -34,15 +33,6 @@ class SignupView(CreateView):
|
||||||
template_name = "registration/signup.html"
|
template_name = "registration/signup.html"
|
||||||
extra_context = dict(title=_("Sign up"))
|
extra_context = dict(title=_("Sign up"))
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
The signup view is available only during the first phase.
|
|
||||||
"""
|
|
||||||
current_phase = Phase.current_phase()
|
|
||||||
if not current_phase or current_phase.phase_number >= 2:
|
|
||||||
raise PermissionDenied(_("You can't register now."))
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% load static i18n static calendar %}
|
{% load static i18n static %}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
{% get_current_language as LANGUAGE_CODE %}{% get_current_language_bidi as LANGUAGE_BIDI %}
|
||||||
|
@ -63,13 +63,6 @@
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a>
|
<a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item active">
|
|
||||||
{% if user.registration.is_admin %}
|
|
||||||
<a href="{% url "participation:calendar" %}" class="nav-link"><i class="fas fa-calendar"></i> {% trans "Calendar" %}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="#" class="nav-link" data-toggle="modal" data-target="#calendarModal"><i class="fas fa-calendar"></i> {% trans "Calendar" %}</a>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% if user.is_authenticated and user.registration.is_admin %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a>
|
<a href="{% url "registration:user_list" %}" class="nav-link"><i class="fas fa-user"></i> {% trans "Users" %}</a>
|
||||||
|
@ -129,11 +122,9 @@
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not user.is_authenticated %}
|
{% if not user.is_authenticated %}
|
||||||
{% if 1|current_phase %}
|
<li class="nav-item active">
|
||||||
<li class="nav-item active">
|
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
|
||||||
<a class="nav-link" href="{% url "registration:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Register" %}</a>
|
</li>
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="#" data-toggle="modal" data-target="#loginModal">
|
<a class="nav-link" href="#" data-toggle="modal" data-target="#loginModal">
|
||||||
<i class="fas fa-sign-in-alt"></i> {% trans "Log in" %}
|
<i class="fas fa-sign-in-alt"></i> {% trans "Log in" %}
|
||||||
|
@ -228,9 +219,6 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
||||||
{% trans "Calendar" as modal_title %}
|
|
||||||
{% include "base_modal.html" with modal_id="calendar" modal_additional_class="modal-lg" %}
|
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% trans "All teams" as modal_title %}
|
{% trans "All teams" as modal_title %}
|
||||||
{% include "base_modal.html" with modal_id="teams" modal_additional_class="modal-lg" %}
|
{% include "base_modal.html" with modal_id="teams" modal_additional_class="modal-lg" %}
|
||||||
|
@ -259,11 +247,6 @@
|
||||||
$(".invalid-feedback").addClass("d-block");
|
$(".invalid-feedback").addClass("d-block");
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('a[data-target="#calendarModal"]').click(function() {
|
|
||||||
let modalBody = $("#calendarModal div.modal-body");
|
|
||||||
if (!modalBody.html().trim())
|
|
||||||
modalBody.load("{% url "participation:calendar" %} #form-content")
|
|
||||||
});
|
|
||||||
{% if user.is_authenticated and user.registration.is_admin %}
|
{% if user.is_authenticated and user.registration.is_admin %}
|
||||||
$('a[data-target="#teamsModal"]').click(function() {
|
$('a[data-target="#teamsModal"]').click(function() {
|
||||||
let modalBody = $("#teamsModal div.modal-body");
|
let modalBody = $("#teamsModal div.modal-body");
|
||||||
|
|
|
@ -8,8 +8,6 @@ from haystack.generic_views import SearchView
|
||||||
|
|
||||||
class AdminMixin(LoginRequiredMixin):
|
class AdminMixin(LoginRequiredMixin):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return self.handle_no_permission()
|
|
||||||
if not request.user.registration.is_admin:
|
if not request.user.registration.is_admin:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue