mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-11-04 04:22:14 +01:00 
			
		
		
		
	Drop a lot of Corres2math content
This commit is contained in:
		@@ -4,19 +4,14 @@
 | 
			
		||||
from django.contrib import admin
 | 
			
		||||
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)
 | 
			
		||||
class TeamAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('name', 'trigram', 'problem', 'valid',)
 | 
			
		||||
    list_display = ('name', 'trigram', 'valid',)
 | 
			
		||||
    search_fields = ('name', 'trigram',)
 | 
			
		||||
    list_filter = ('participation__problem', 'participation__valid',)
 | 
			
		||||
 | 
			
		||||
    def problem(self, team):
 | 
			
		||||
        return team.participation.get_problem_display()
 | 
			
		||||
 | 
			
		||||
    problem.short_description = _('problem number')
 | 
			
		||||
    list_filter = ('participation__valid',)
 | 
			
		||||
 | 
			
		||||
    def valid(self, team):
 | 
			
		||||
        return team.participation.valid
 | 
			
		||||
@@ -26,24 +21,29 @@ class TeamAdmin(admin.ModelAdmin):
 | 
			
		||||
 | 
			
		||||
@admin.register(Participation)
 | 
			
		||||
class ParticipationAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('team', 'problem', 'valid',)
 | 
			
		||||
    list_display = ('team', 'valid',)
 | 
			
		||||
    search_fields = ('team__name', 'team__trigram',)
 | 
			
		||||
    list_filter = ('problem', 'valid',)
 | 
			
		||||
    list_filter = ('valid',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Video)
 | 
			
		||||
class VideoAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('participation', 'link',)
 | 
			
		||||
    search_fields = ('participation__team__name', 'participation__team__trigram', 'link',)
 | 
			
		||||
@admin.register(Pool)
 | 
			
		||||
class PoolAdmin(admin.ModelAdmin):
 | 
			
		||||
    search_fields = ('participations__team__name', 'participations__team__trigram',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Question)
 | 
			
		||||
class QuestionAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('participation', 'question',)
 | 
			
		||||
    search_fields = ('participation__team__name', 'participation__team__trigram', 'question',)
 | 
			
		||||
@admin.register(Solution)
 | 
			
		||||
class SolutionAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('participation',)
 | 
			
		||||
    search_fields = ('participation__team__name', 'participation__team__trigram',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@admin.register(Phase)
 | 
			
		||||
class PhaseAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('phase_number', 'start', 'end',)
 | 
			
		||||
    ordering = ('phase_number', 'start',)
 | 
			
		||||
@admin.register(Synthesis)
 | 
			
		||||
class SynthesisAdmin(admin.ModelAdmin):
 | 
			
		||||
    list_display = ('participation',)
 | 
			
		||||
    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'
 | 
			
		||||
 | 
			
		||||
    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_delete.connect(delete_related_videos, "participation.Participation")
 | 
			
		||||
        post_save.connect(create_team_participation, "participation.Team")
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,11 @@
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from bootstrap_datepicker_plus import DateTimePickerInput
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
 | 
			
		||||
from django.db.models import Q
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
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):
 | 
			
		||||
@@ -25,7 +23,7 @@ class TeamForm(forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Team
 | 
			
		||||
        fields = ('name', 'trigram', 'grant_animath_access_videos',)
 | 
			
		||||
        fields = ('name', 'trigram',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JoinTeamForm(forms.ModelForm):
 | 
			
		||||
@@ -56,7 +54,7 @@ class ParticipationForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Participation
 | 
			
		||||
        fields = ('problem',)
 | 
			
		||||
        fields = ('tournament',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RequestValidationForm(forms.Form):
 | 
			
		||||
@@ -87,108 +85,3 @@ class ValidateParticipationForm(forms.Form):
 | 
			
		||||
        label=_("Message to address to the team:"),
 | 
			
		||||
        widget=forms.Textarea(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UploadVideoForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Form to upload a video, for a solution or a synthesis.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Video
 | 
			
		||||
        fields = ('link',)
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        if Phase.current_phase().phase_number != 1 and Phase.current_phase().phase_number != 4 and self.instance.link:
 | 
			
		||||
            self.add_error("link", _("You can't upload your video after the deadline."))
 | 
			
		||||
        return super().clean()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReceiveParticipationForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Update the received participation of a participation.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.fields["received_participation"].queryset = Participation.objects.filter(
 | 
			
		||||
            ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Participation
 | 
			
		||||
        fields = ('received_participation',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SendParticipationForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Update the sent participation of a participation.
 | 
			
		||||
    """
 | 
			
		||||
    sent_participation = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Participation.objects,
 | 
			
		||||
        label=lambda: _("Send to team"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        try:
 | 
			
		||||
            self.fields["sent_participation"].initial = self.instance.sent_participation
 | 
			
		||||
        except ObjectDoesNotExist:     # No sent participation
 | 
			
		||||
            pass
 | 
			
		||||
        self.fields["sent_participation"].queryset = Participation.objects.filter(
 | 
			
		||||
            ~Q(pk=self.instance.pk) & Q(problem=self.instance.problem, valid=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def clean(self, commit=True):
 | 
			
		||||
        cleaned_data = super().clean()
 | 
			
		||||
        if "sent_participation" in cleaned_data:
 | 
			
		||||
            participation = cleaned_data["sent_participation"]
 | 
			
		||||
            participation.received_participation = self.instance
 | 
			
		||||
            self.instance = participation
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Participation
 | 
			
		||||
        fields = ('sent_participation',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class QuestionForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Create or update a question.
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        self.fields["question"].widget.attrs.update({"placeholder": _("How did you get the idea to ...?")})
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        if Phase.current_phase().phase_number != 2:
 | 
			
		||||
            self.add_error(None, _("You can only create or update a question during the second phase."))
 | 
			
		||||
        return super().clean()
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Question
 | 
			
		||||
        fields = ('question',)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PhaseForm(forms.ModelForm):
 | 
			
		||||
    """
 | 
			
		||||
    Form to update the calendar of a phase.
 | 
			
		||||
    """
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Phase
 | 
			
		||||
        fields = ('start', 'end',)
 | 
			
		||||
        widgets = {
 | 
			
		||||
            'start': DateTimePickerInput(format='%d/%m/%Y %H:%M'),
 | 
			
		||||
            'end': DateTimePickerInput(format='%d/%m/%Y %H:%M'),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def clean(self):
 | 
			
		||||
        # Ensure that dates are in a right order
 | 
			
		||||
        cleaned_data = super().clean()
 | 
			
		||||
        start = cleaned_data["start"]
 | 
			
		||||
        end = cleaned_data["end"]
 | 
			
		||||
        if end <= start:
 | 
			
		||||
            self.add_error("end", _("Start date must be before the end date."))
 | 
			
		||||
        if Phase.objects.filter(phase_number__lt=self.instance.phase_number, end__gt=start).exists():
 | 
			
		||||
            self.add_error("start", _("This phase must start after the previous phases."))
 | 
			
		||||
        if Phase.objects.filter(phase_number__gt=self.instance.phase_number, start__lt=end).exists():
 | 
			
		||||
            self.add_error("end", _("This phase must end after the next phases."))
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 
 | 
			
		||||
@@ -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."),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def students(self):
 | 
			
		||||
        return self.participants.filter(studentregistration__isnull=False)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def coachs(self):
 | 
			
		||||
        return self.participants.filter(coachregistration__isnull=False)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def email(self):
 | 
			
		||||
        """
 | 
			
		||||
@@ -224,33 +232,6 @@ class Participation(models.Model):
 | 
			
		||||
        help_text=_("The video got the validation of the administrators."),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    solution = models.OneToOneField(
 | 
			
		||||
        "participation.Video",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        related_name="participation_solution",
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
        verbose_name=_("solution video"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    received_participation = models.OneToOneField(
 | 
			
		||||
        "participation.Participation",
 | 
			
		||||
        on_delete=models.PROTECT,
 | 
			
		||||
        related_name="sent_participation",
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
        verbose_name=_("received participation"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    synthesis = models.OneToOneField(
 | 
			
		||||
        "participation.Video",
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        related_name="participation_synthesis",
 | 
			
		||||
        null=True,
 | 
			
		||||
        default=None,
 | 
			
		||||
        verbose_name=_("synthesis video"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def get_absolute_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.pk,))
 | 
			
		||||
 | 
			
		||||
@@ -324,7 +305,7 @@ class Solution(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("solution")
 | 
			
		||||
        verbose_name_plural = _("solutions")
 | 
			
		||||
        unique_by = (('participation', 'problem', 'final_solution', ), )
 | 
			
		||||
        unique_together = (('participation', 'problem', 'final_solution', ), )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Synthesis(models.Model):
 | 
			
		||||
@@ -359,4 +340,4 @@ class Synthesis(models.Model):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _("synthesis")
 | 
			
		||||
        verbose_name_plural = _("syntheses")
 | 
			
		||||
        unique_by = (('participation', 'pool', 'type', ), )
 | 
			
		||||
        unique_together = (('participation', 'pool', 'type', ), )
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
 | 
			
		||||
from haystack import indexes
 | 
			
		||||
 | 
			
		||||
from .models import Participation, Team, Video
 | 
			
		||||
from .models import Participation, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
 | 
			
		||||
@@ -24,13 +24,3 @@ class ParticipationIndex(indexes.ModelSearchIndex, indexes.Indexable):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Participation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VideoIndex(indexes.ModelSearchIndex, indexes.Indexable):
 | 
			
		||||
    """
 | 
			
		||||
    Index all teams by their team name and team trigram.
 | 
			
		||||
    """
 | 
			
		||||
    text = indexes.NgramField(document=True, use_template=True)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = Video
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
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, **_):
 | 
			
		||||
@@ -10,10 +10,6 @@ def create_team_participation(instance, created, **_):
 | 
			
		||||
    When a team got created, create an associated team and create Video objects.
 | 
			
		||||
    """
 | 
			
		||||
    participation = Participation.objects.get_or_create(team=instance)[0]
 | 
			
		||||
    if not participation.solution:
 | 
			
		||||
        participation.solution = Video.objects.create()
 | 
			
		||||
    if not participation.synthesis:
 | 
			
		||||
        participation.synthesis = Video.objects.create()
 | 
			
		||||
    participation.save()
 | 
			
		||||
    if not created:
 | 
			
		||||
        participation.team.create_mailing_list()
 | 
			
		||||
@@ -38,9 +34,3 @@ def update_mailing_list(instance: Team, **_):
 | 
			
		||||
                get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
 | 
			
		||||
                                             f"{coach.user.first_name} {coach.user.last_name}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_related_videos(instance: Participation, **_):
 | 
			
		||||
    if instance.solution:
 | 
			
		||||
        instance.solution.delete()
 | 
			
		||||
    if instance.synthesis:
 | 
			
		||||
        instance.synthesis.delete()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +1,10 @@
 | 
			
		||||
# Copyright (C) 2020 by Animath
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
import django_tables2 as tables
 | 
			
		||||
 | 
			
		||||
from .models import Phase, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CalendarTable(tables.Table):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        attrs = {
 | 
			
		||||
            'class': 'table table condensed table-striped',
 | 
			
		||||
        }
 | 
			
		||||
        row_attrs = {
 | 
			
		||||
            'class': lambda record: 'bg-success' if timezone.now() > record.end else
 | 
			
		||||
                                    'bg-warning' if timezone.now() > record.start else
 | 
			
		||||
                                    'bg-danger',
 | 
			
		||||
            'data-id': lambda record: str(record.phase_number),
 | 
			
		||||
        }
 | 
			
		||||
        model = Phase
 | 
			
		||||
        fields = ('phase_number', 'description', 'start', 'end',)
 | 
			
		||||
        template_name = 'django_tables2/bootstrap4.html'
 | 
			
		||||
        order_by = ('phase_number',)
 | 
			
		||||
from .models import Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# noinspection PyTypeChecker
 | 
			
		||||
 
 | 
			
		||||
@@ -12,289 +12,7 @@
 | 
			
		||||
            <dl class="row">
 | 
			
		||||
                <dt class="col-sm-2">{% trans "Team:" %}</dt>
 | 
			
		||||
                <dd class="col-sm-10"><a href="{% url "participation:team_detail" pk=participation.team.pk %}">{{ participation.team }}</a></dd>
 | 
			
		||||
 | 
			
		||||
                <dt class="col-sm-2">{% trans "Chosen problem:" %}</dt>
 | 
			
		||||
                <dd class="col-sm-10">{{ participation.get_problem_display }}</dd>
 | 
			
		||||
            </dl>
 | 
			
		||||
 | 
			
		||||
            <div id="solution-container">
 | 
			
		||||
                <dl class="row">
 | 
			
		||||
                    {% trans "No video sent" as novideo %}
 | 
			
		||||
                    <dt class="col-sm-2">{% trans "Proposed solution:" %}</dt>
 | 
			
		||||
                    <dd class="col-sm-10"><a href="{{ participation.solution.link|default:"#" }}"{% if participation.solution.link %} target="_blank"{% endif %}>
 | 
			
		||||
                        {{ participation.solution.link|default:novideo }}</a>
 | 
			
		||||
                        {% if current_phase.phase_number == 1 or participation.solution.link == "" %}
 | 
			
		||||
                            <button class="btn btn-primary" data-toggle="modal" data-target="#uploadSolutionModal">{% trans "Upload" %}</button>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% if participation.solution.link %}
 | 
			
		||||
                            <button class="btn btn-info" data-toggle="modal" data-target="#displaySolutionModal">{% trans "Display" %}</button>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </dd>
 | 
			
		||||
                </dl>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {% if user.registration.is_admin or current_phase.phase_number >= 2 %}
 | 
			
		||||
    <hr>
 | 
			
		||||
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card bg-light shadow">
 | 
			
		||||
                <div class="card-header text-center">
 | 
			
		||||
                    <h4>{% trans "Sent solution" %}</h4>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <dl class="row">
 | 
			
		||||
                        <dt class="col-xl-5 text-right">{% trans "Team that received your solution:" %}</dt>
 | 
			
		||||
                        <dd class="col-md-5">{{ participation.sent_participation.team|default:any }}</dd>
 | 
			
		||||
                        {% if user.registration.is_admin %}
 | 
			
		||||
                            <dd class="col-xs-2">
 | 
			
		||||
                                <button class="btn btn-primary" data-toggle="modal" data-target="#defineSentParticipationModal">{% trans "Change" %}</button>
 | 
			
		||||
                            </dd>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </dl>
 | 
			
		||||
 | 
			
		||||
                    {% if current_phase.phase_number == 2 %}
 | 
			
		||||
                        <div class="alert alert-info">
 | 
			
		||||
                            {% blocktrans trimmed %}
 | 
			
		||||
                                The mentioned team received your video. They are now watching your video,
 | 
			
		||||
                                and formulating questions. You would be able to exchange with the other phase during
 | 
			
		||||
                                the next phase.
 | 
			
		||||
                            {% endblocktrans %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% elif current_phase.phase_number == 3 %}
 | 
			
		||||
                        <div class="alert alert-info">
 | 
			
		||||
                            {% blocktrans trimmed with user_id=user.pk %}
 | 
			
		||||
                                The other team sent you questions about your solution. Your are now able to answer them,
 | 
			
		||||
                                then to exchange freely with the other team. You can click on the Chat button, or to
 | 
			
		||||
                                connect to your dedicated Matrix account:
 | 
			
		||||
                                <code>@tfjm_{{ user_id }}:tfjm.org</code>.
 | 
			
		||||
                                You can use your own Matrix client, or use the dedicated Element client:
 | 
			
		||||
                                <a href="https://element.tfjm.org">element.correpondances-maths.fr</a>
 | 
			
		||||
                            {% endblocktrans %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% elif current_phase.phase_number == 4 %}
 | 
			
		||||
                        <dl class="row">
 | 
			
		||||
                            <dt class="col-xl-5 text-right">{% trans "Synthesis from the other team:" %}</dt>
 | 
			
		||||
                            <dd class="col-sm-7"><a href="{{ participation.received_participation.synthesis.link|default:"#" }}"{% if participation.received_participation.synthesis.link %} target="_blank"{% endif %}>
 | 
			
		||||
                                {{ participation.received_participation.synthesis.link|default:novideo }}</a>
 | 
			
		||||
                                {% if participation.received_participation.synthesis.link %}
 | 
			
		||||
                                    <button class="btn btn-info" data-toggle="modal" data-target="#displayOtherSynthesisModal">{% trans "Display" %}</button>
 | 
			
		||||
                                {% endif %}
 | 
			
		||||
                            </dd>
 | 
			
		||||
                        </dl>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-md-6">
 | 
			
		||||
            <div class="card bg-light shadow">
 | 
			
		||||
                <div class="card-header text-center">
 | 
			
		||||
                    <h4>{% trans "Received solution" %}</h4>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="card-body">
 | 
			
		||||
                    <dl class="row">
 | 
			
		||||
                        <dt class="col-xl-5 text-right">{% trans "Team that sent you their solution:" %}</dt>
 | 
			
		||||
                        <dd class="col-md-5">{{ participation.received_participation.team|default:any }}</dd>
 | 
			
		||||
                        {% if user.registration.is_admin %}
 | 
			
		||||
                            <dd class="col-xs-2">
 | 
			
		||||
                                <button class="btn btn-primary" data-toggle="modal" data-target="#defineReceivedParticipationModal">{% trans "Change" %}</button>
 | 
			
		||||
                            </dd>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
 | 
			
		||||
                        <dt class="col-xl-5 text-right">{% trans "Proposed solution:" %}</dt>
 | 
			
		||||
                        <dd class="col-sm-7"><a href="{{ participation.received_participation.solution.link|default:"#" }}"{% if participation.received_participation.solution.link %} target="_blank"{% endif %}>
 | 
			
		||||
                            {{ participation.received_participation.solution.link|default:novideo }}</a>
 | 
			
		||||
                            {% if participation.received_participation.solution.link %}
 | 
			
		||||
                                <button class="btn btn-info" data-toggle="modal" data-target="#displayOtherSolutionModal">{% trans "Display" %}</button>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </dd>
 | 
			
		||||
 | 
			
		||||
                    {% if current_phase.phase_number == 2 %}
 | 
			
		||||
                        <div class="alert alert-info">
 | 
			
		||||
                        {% blocktrans trimmed %}
 | 
			
		||||
                            You received a solution about the same problem that you treated from another team.
 | 
			
		||||
                            You are now encouraged to see the video, then to ask from 3 to 6 questions about the video.
 | 
			
		||||
                            After that, you will be invited to exchange with the other team about the solution.
 | 
			
		||||
                        {% endblocktrans %}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        {% for question in participation.questions.all %}
 | 
			
		||||
                            <dd class="col-md-9 text-truncate">{{ question.question }}</dd>
 | 
			
		||||
                            <dd class="col-md-3">
 | 
			
		||||
                                <button class="btn btn-primary" data-toggle="modal" data-target="#updateQuestion{{ forloop.counter }}Modal">{% trans "Change" %}</button>
 | 
			
		||||
                            </dd>
 | 
			
		||||
                            <hr>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
 | 
			
		||||
                        {% if user.registration.participates %}
 | 
			
		||||
                            <button class="btn btn-success" data-toggle="modal" data-target="#addQuestionModal">
 | 
			
		||||
                                <i class="fas fa-plus-circle"></i> {% trans "Add a question" %}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    {% elif current_phase.phase_number == 3 %}
 | 
			
		||||
                        <div class="alert alert-info">
 | 
			
		||||
                            {% blocktrans trimmed with user_id=user.pk %}
 | 
			
		||||
                                You sent your questions to the other team about their solution. When they answer to
 | 
			
		||||
                                your questions, you will be able to exchange freely with the other team.
 | 
			
		||||
                                You can click on the Chat button, or to connect to your dedicated Matrix account:
 | 
			
		||||
                                <code>@tfjm_{{ user_id }}:tfjm.org</code>.
 | 
			
		||||
                                You can use your own Matrix client, or use the dedicated Element client:
 | 
			
		||||
                                <a href="https://element.tfjm.org">element.correpondances-maths.fr</a>
 | 
			
		||||
                            {% endblocktrans %}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% elif current_phase.phase_number == 4 %}
 | 
			
		||||
                        <div id="solution-container">
 | 
			
		||||
                            <dl class="row">
 | 
			
		||||
                                {% trans "No video sent" as novideo %}
 | 
			
		||||
                                <dt class="col-sm-5 text-right">{% trans "Your synthesis of the exchange:" %}</dt>
 | 
			
		||||
                                <dd class="col-sm-7"><a href="{{ participation.synthesis.link|default:"#" }}"{% if participation.synthesis.link %} target="_blank"{% endif %}>
 | 
			
		||||
                                    {{ participation.synthesis.link|default:novideo }}</a>
 | 
			
		||||
                                    <button class="btn btn-primary" data-toggle="modal" data-target="#uploadSynthesisModal">{% trans "Upload" %}</button>
 | 
			
		||||
                                    {% if participation.synthesis.link %}
 | 
			
		||||
                                        <button class="btn btn-info" data-toggle="modal" data-target="#displaySynthesisModal">{% trans "Display" %}</button>
 | 
			
		||||
                                    {% endif %}
 | 
			
		||||
                                </dd>
 | 
			
		||||
                            </dl>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    </dl>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
{% if user.registration.is_admin %}
 | 
			
		||||
    {% trans "Define received video" as modal_title %}
 | 
			
		||||
    {% trans "Update" as modal_button %}
 | 
			
		||||
    {% url "participation:participation_receive_participation" pk=participation.pk as modal_action %}
 | 
			
		||||
    {% include "base_modal.html" with modal_id="defineReceivedParticipation" %}
 | 
			
		||||
 | 
			
		||||
    {% trans "Define team that receives your video" as modal_title %}
 | 
			
		||||
    {% trans "Update" as modal_button %}
 | 
			
		||||
    {% url "participation:participation_send_participation" pk=participation.pk as modal_action %}
 | 
			
		||||
    {% include "base_modal.html" with modal_id="defineSentParticipation" %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% trans "Upload video" as modal_title %}
 | 
			
		||||
{% trans "Upload" as modal_button %}
 | 
			
		||||
{% url "participation:upload_video" pk=participation.solution_id as modal_action %}
 | 
			
		||||
{% include "base_modal.html" with modal_id="uploadSolution" %}
 | 
			
		||||
 | 
			
		||||
{% trans "Display solution" as modal_title %}
 | 
			
		||||
{% trans "This video platform is not supported yet." as unsupported_platform %}
 | 
			
		||||
{% include "base_modal.html" with modal_id="displaySolution" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.solution.as_iframe|default:unsupported_platform %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% if user.registration.is_admin or current_phase.phase_number >= 2 %}
 | 
			
		||||
    {% if participation.received_participation.solution.link %}
 | 
			
		||||
        {% trans "Display solution" as modal_title %}
 | 
			
		||||
        {% trans "This video platform is not supported yet." as unsupported_platform %}
 | 
			
		||||
        {% include "base_modal.html" with modal_id="displayOtherSolution" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.received_participation.solution.as_iframe|default:unsupported_platform %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if user.registration.participates and current_phase.phase_number == 2 %}
 | 
			
		||||
    {% trans "Add question" as modal_title %}
 | 
			
		||||
    {% trans "Add" as modal_button %}
 | 
			
		||||
    {% url "participation:add_question" pk=participation.pk as modal_action %}
 | 
			
		||||
    {% include "base_modal.html" with modal_id="addQuestion" modal_button_type="success" %}
 | 
			
		||||
    {% for question in participation.questions.all %}
 | 
			
		||||
        {% with number_str=forloop.counter|stringformat:"d"%}
 | 
			
		||||
            {% with modal_id="updateQuestion"|add:number_str %}
 | 
			
		||||
                {% trans "Delete" as delete %}
 | 
			
		||||
                {% with extra_modal_button='<button class="btn btn-danger" type="button" data-dismiss="modal" data-toggle="modal" data-target="#deleteQuestion'|add:number_str|add:'Modal">'|add:delete|add:"</button>"|safe %}
 | 
			
		||||
                    {% trans "Update question" as modal_title %}
 | 
			
		||||
                    {% trans "Update" as modal_button %}
 | 
			
		||||
                    {% url "participation:update_question" pk=question.pk as modal_action %}
 | 
			
		||||
                    {% include "base_modal.html" %}
 | 
			
		||||
                {% endwith %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
 | 
			
		||||
            {% with modal_id="deleteQuestion"|add:number_str %}
 | 
			
		||||
                {% trans "Delete question" as modal_title %}
 | 
			
		||||
                {% trans "Delete" as modal_button %}
 | 
			
		||||
                {% url "participation:delete_question" pk=question.pk as modal_action %}
 | 
			
		||||
                {% include "base_modal.html" with modal_button_type="danger" %}
 | 
			
		||||
            {% endwith %}
 | 
			
		||||
        {% endwith %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% if current_phase.phase_number >= 4 %}
 | 
			
		||||
    {% if participation.received_participation.synthesis.link %}
 | 
			
		||||
        {% trans "Display synthesis" as modal_title %}
 | 
			
		||||
        {% trans "This video platform is not supported yet." as unsupported_platform %}
 | 
			
		||||
        {% include "base_modal.html" with modal_id="displayOtherSynthesis" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.received_participation.synthesis.as_iframe|default:unsupported_platform %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    {% trans "Upload video" as modal_title %}
 | 
			
		||||
    {% trans "Upload" as modal_button %}
 | 
			
		||||
    {% url "participation:upload_video" pk=participation.synthesis_id as modal_action %}
 | 
			
		||||
    {% include "base_modal.html" with modal_id="uploadSynthesis" %}
 | 
			
		||||
 | 
			
		||||
    {% if participation.synthesis.link %}
 | 
			
		||||
        {% trans "Display synthesis" as modal_title %}
 | 
			
		||||
        {% trans "This video platform is not supported yet." as unsupported_platform %}
 | 
			
		||||
        {% include "base_modal.html" with modal_id="displaySynthesis" modal_action="" modal_button="" modal_additional_class="modal-lg" modal_content=participation.synthesis.as_iframe|default:unsupported_platform %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block extrajavascript %}
 | 
			
		||||
    <script>
 | 
			
		||||
        $(document).ready(function() {
 | 
			
		||||
            {% if user.registration.is_admin %}
 | 
			
		||||
                $('button[data-target="#defineReceivedParticipationModal"]').click(function() {
 | 
			
		||||
                    let modalBody = $("#defineReceivedParticipationModal div.modal-body");
 | 
			
		||||
                    if (!modalBody.html().trim())
 | 
			
		||||
                        modalBody.load("{% url "participation:participation_receive_participation" pk=participation.pk %} #form-content");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                $('button[data-target="#defineSentParticipationModal"]').click(function() {
 | 
			
		||||
                    let modalBody = $("#defineSentParticipationModal div.modal-body");
 | 
			
		||||
                    if (!modalBody.html().trim())
 | 
			
		||||
                        modalBody.load("{% url "participation:participation_send_participation" pk=participation.pk %} #form-content");
 | 
			
		||||
                });
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            {% if user.registration.participates and current_phase.phase_number == 2 %}
 | 
			
		||||
                $('button[data-target="#addQuestionModal"]').click(function() {
 | 
			
		||||
                    let modalBody = $("#addQuestionModal div.modal-body");
 | 
			
		||||
                    if (!modalBody.html().trim())
 | 
			
		||||
                        modalBody.load("{% url "participation:add_question" pk=participation.pk %} #form-content");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                {% for question in participation.questions.all %}
 | 
			
		||||
                    $('button[data-target="#updateQuestion{{ forloop.counter }}Modal"]').click(function() {
 | 
			
		||||
                        let modalBody = $("#updateQuestion{{ forloop.counter }}Modal div.modal-body");
 | 
			
		||||
                        if (!modalBody.html().trim())
 | 
			
		||||
                            modalBody.load("{% url "participation:update_question" pk=question.pk %} #form-content");
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    $('button[data-target="#deleteQuestion{{ forloop.counter }}Modal"]').click(function() {
 | 
			
		||||
                        let modalBody = $("#deleteQuestion{{ forloop.counter }}Modal div.modal-body");
 | 
			
		||||
                        if (!modalBody.html().trim())
 | 
			
		||||
                            modalBody.load("{% url "participation:delete_question" pk=question.pk %} #form-content");
 | 
			
		||||
                    });
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
 | 
			
		||||
            $('button[data-target="#uploadSolutionModal"]').click(function() {
 | 
			
		||||
                let modalBody = $("#uploadSolutionModal div.modal-body");
 | 
			
		||||
                if (!modalBody.html().trim())
 | 
			
		||||
                    modalBody.load("{% url "participation:upload_video" pk=participation.solution_id %} #form-content");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            {% if current_phase.phase_number == 4 %}
 | 
			
		||||
                $('button[data-target="#uploadSynthesisModal"]').click(function() {
 | 
			
		||||
                    let modalBody = $("#uploadSynthesisModal div.modal-body");
 | 
			
		||||
                    if (!modalBody.html().trim())
 | 
			
		||||
                        modalBody.load("{% url "participation:upload_video" pk=participation.synthesis_id %} #form-content");
 | 
			
		||||
                });
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,9 +45,6 @@
 | 
			
		||||
            {% trans "any" as any %}
 | 
			
		||||
            <dd class="col-sm-6">{{ team.participation.get_problem_display|default:any }}</dd>
 | 
			
		||||
 | 
			
		||||
            <dt class="col-sm-6 text-right">{% trans "Grant Animath to publish our video:" %}</dt>
 | 
			
		||||
            <dd class="col-sm-6">{{ team.grant_animath_access_videos|yesno }}</dd>
 | 
			
		||||
 | 
			
		||||
            <dt class="col-sm-6 text-right">{% trans "Authorizations:" %}</dt>
 | 
			
		||||
            <dd class="col-sm-6">
 | 
			
		||||
                {% for student in team.students.all %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
# SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.contrib.sites.models import Site
 | 
			
		||||
from django.core.management import call_command
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils import timezone
 | 
			
		||||
from registration.models import CoachRegistration, StudentRegistration
 | 
			
		||||
 | 
			
		||||
from .models import Participation, Phase, Question, Team
 | 
			
		||||
from .models import Participation, Team
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestStudentParticipation(TestCase):
 | 
			
		||||
@@ -40,10 +37,7 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
            name="Super team",
 | 
			
		||||
            trigram="AAA",
 | 
			
		||||
            access_code="azerty",
 | 
			
		||||
            grant_animath_access_videos=True,
 | 
			
		||||
        )
 | 
			
		||||
        self.question = Question.objects.create(participation=self.team.participation,
 | 
			
		||||
                                                question="Pourquoi l'existence précède l'essence ?")
 | 
			
		||||
        self.client.force_login(self.user)
 | 
			
		||||
 | 
			
		||||
        self.second_user = User.objects.create(
 | 
			
		||||
@@ -63,7 +57,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
            name="Poor team",
 | 
			
		||||
            trigram="FFF",
 | 
			
		||||
            access_code="qwerty",
 | 
			
		||||
            grant_animath_access_videos=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.coach = User.objects.create(
 | 
			
		||||
@@ -108,29 +101,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        self.assertRedirects(response, "http://" + Site.objects.get().domain +
 | 
			
		||||
                             str(self.team.participation.get_absolute_url()), 302, 200)
 | 
			
		||||
 | 
			
		||||
        # Test video pages
 | 
			
		||||
        response = self.client.get(reverse("admin:index") + "participation/video/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("admin:index")
 | 
			
		||||
                                   + f"participation/video/{self.team.participation.solution.pk}/change/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Test question pages
 | 
			
		||||
        response = self.client.get(reverse("admin:index") + "participation/question/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("admin:index")
 | 
			
		||||
                                   + f"participation/question/{self.question.pk}/change/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Test phase pages
 | 
			
		||||
        response = self.client.get(reverse("admin:index") + "participation/phase/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("admin:index") + "participation/phase/1/change/")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_create_team(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to create a team.
 | 
			
		||||
@@ -141,14 +111,12 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        response = self.client.post(reverse("participation:create_team"), data=dict(
 | 
			
		||||
            name="Test team",
 | 
			
		||||
            trigram="123",
 | 
			
		||||
            grant_animath_access_videos=False,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:create_team"), data=dict(
 | 
			
		||||
            name="Test team",
 | 
			
		||||
            trigram="TES",
 | 
			
		||||
            grant_animath_access_videos=False,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertTrue(Team.objects.filter(trigram="TES").exists())
 | 
			
		||||
        team = Team.objects.get(trigram="TES")
 | 
			
		||||
@@ -158,7 +126,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        response = self.client.post(reverse("participation:create_team"), data=dict(
 | 
			
		||||
            name="Test team 2",
 | 
			
		||||
            trigram="TET",
 | 
			
		||||
            grant_animath_access_videos=False,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
 | 
			
		||||
@@ -286,13 +253,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        self.user.registration.photo_authorization = "authorization/photo/ananas"
 | 
			
		||||
        self.user.registration.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertFalse(resp.context["can_validate"])
 | 
			
		||||
 | 
			
		||||
        self.team.participation.problem = 2
 | 
			
		||||
        self.team.participation.save()
 | 
			
		||||
 | 
			
		||||
        resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 200)
 | 
			
		||||
        self.assertTrue(resp.context["can_validate"])
 | 
			
		||||
@@ -383,23 +343,12 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Form is invalid
 | 
			
		||||
        response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
 | 
			
		||||
            name="Updated team name",
 | 
			
		||||
            trigram="BBB",
 | 
			
		||||
            grant_animath_access_videos=True,
 | 
			
		||||
            problem=42,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
 | 
			
		||||
            name="Updated team name",
 | 
			
		||||
            trigram="BBB",
 | 
			
		||||
            grant_animath_access_videos=True,
 | 
			
		||||
            problem=3,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
 | 
			
		||||
        self.assertTrue(Team.objects.filter(trigram="BBB", participation__problem=3).exists())
 | 
			
		||||
        self.assertTrue(Team.objects.filter(trigram="BBB").exists())
 | 
			
		||||
 | 
			
		||||
    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,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
 | 
			
		||||
    def test_upload_video(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to send a solution video link.
 | 
			
		||||
        """
 | 
			
		||||
        self.user.registration.team = self.team
 | 
			
		||||
        self.user.registration.save()
 | 
			
		||||
 | 
			
		||||
        self.team.participation.valid = True
 | 
			
		||||
        self.team.participation.save()
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)),
 | 
			
		||||
                                    data=dict(link="https://youtube.com/watch?v=73nsrixx7eI"))
 | 
			
		||||
        self.assertRedirects(response,
 | 
			
		||||
                             reverse("participation:participation_detail", args=(self.team.participation.id,)),
 | 
			
		||||
                             302, 200)
 | 
			
		||||
        self.team.participation.refresh_from_db()
 | 
			
		||||
        self.assertEqual(self.team.participation.solution.platform, "youtube")
 | 
			
		||||
        self.assertEqual(self.team.participation.solution.youtube_code, "73nsrixx7eI")
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Set the second phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i - 1))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 2)
 | 
			
		||||
 | 
			
		||||
        # Can't update the link during the second phase
 | 
			
		||||
        response = self.client.post(reverse("participation:upload_video", args=(self.team.participation.solution.pk,)),
 | 
			
		||||
                                    data=dict(link="https://youtube.com/watch?v=73nsrixx7eI"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_questions(self):
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that creating/updating/deleting a question is working.
 | 
			
		||||
        """
 | 
			
		||||
        self.user.registration.team = self.team
 | 
			
		||||
        self.user.registration.save()
 | 
			
		||||
 | 
			
		||||
        self.team.participation.valid = True
 | 
			
		||||
        self.team.participation.save()
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # We are not in second phase
 | 
			
		||||
        response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)),
 | 
			
		||||
                                    data=dict(question="I got censored!"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Set the second phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i - 1))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 2)
 | 
			
		||||
 | 
			
		||||
        # Create a question
 | 
			
		||||
        response = self.client.post(reverse("participation:add_question", args=(self.team.participation.pk,)),
 | 
			
		||||
                                    data=dict(question="I asked a question!"))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:participation_detail",
 | 
			
		||||
                                               args=(self.team.participation.pk,)), 302, 200)
 | 
			
		||||
        qs = Question.objects.filter(participation=self.team.participation, question="I asked a question!")
 | 
			
		||||
        self.assertTrue(qs.exists())
 | 
			
		||||
        question = qs.get()
 | 
			
		||||
 | 
			
		||||
        # Update a question
 | 
			
		||||
        response = self.client.get(reverse("participation:update_question", args=(question.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        response = self.client.post(reverse("participation:update_question", args=(question.pk,)), data=dict(
 | 
			
		||||
            question="The question changed!",
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:participation_detail",
 | 
			
		||||
                                               args=(self.team.participation.pk,)), 302, 200)
 | 
			
		||||
        question.refresh_from_db()
 | 
			
		||||
        self.assertEqual(question.question, "The question changed!")
 | 
			
		||||
 | 
			
		||||
        # Delete the question
 | 
			
		||||
        response = self.client.get(reverse("participation:delete_question", args=(question.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        response = self.client.post(reverse("participation:delete_question", args=(question.pk,)))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:participation_detail",
 | 
			
		||||
                                               args=(self.team.participation.pk,)), 302, 200)
 | 
			
		||||
        self.assertFalse(Question.objects.filter(pk=question.pk).exists())
 | 
			
		||||
 | 
			
		||||
        # Non-authenticated users are redirected to login page
 | 
			
		||||
        self.client.logout()
 | 
			
		||||
        response = self.client.get(reverse("participation:add_question", args=(self.team.participation.pk,)))
 | 
			
		||||
        self.assertRedirects(response, reverse("login") + "?next=" +
 | 
			
		||||
                             reverse("participation:add_question", args=(self.team.participation.pk,)), 302, 200)
 | 
			
		||||
        response = self.client.get(reverse("participation:update_question", args=(self.question.pk,)))
 | 
			
		||||
        self.assertRedirects(response, reverse("login") + "?next=" +
 | 
			
		||||
                             reverse("participation:update_question", args=(self.question.pk,)), 302, 200)
 | 
			
		||||
        response = self.client.get(reverse("participation:delete_question", args=(self.question.pk,)))
 | 
			
		||||
        self.assertRedirects(response, reverse("login") + "?next=" +
 | 
			
		||||
                             reverse("participation:delete_question", args=(self.question.pk,)), 302, 200)
 | 
			
		||||
 | 
			
		||||
    def test_current_phase(self):
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that the current phase is the good one.
 | 
			
		||||
        """
 | 
			
		||||
        # We are before the beginning
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=2 * i),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=2 * i + 1))
 | 
			
		||||
        self.assertEqual(Phase.current_phase(), None)
 | 
			
		||||
 | 
			
		||||
        # We are after the end
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() - timedelta(days=2 * i),
 | 
			
		||||
                                                        end=timezone.now() - timedelta(days=2 * i + 1))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, Phase.objects.count())
 | 
			
		||||
 | 
			
		||||
        # First phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 1),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 1)
 | 
			
		||||
 | 
			
		||||
        # Second phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 2),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i - 1))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 2)
 | 
			
		||||
 | 
			
		||||
        # Third phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 3),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i - 2))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 3)
 | 
			
		||||
 | 
			
		||||
        # Fourth phase
 | 
			
		||||
        for i in range(1, 5):
 | 
			
		||||
            Phase.objects.filter(phase_number=i).update(start=timezone.now() + timedelta(days=i - 4),
 | 
			
		||||
                                                        end=timezone.now() + timedelta(days=i - 3))
 | 
			
		||||
        self.assertEqual(Phase.current_phase().phase_number, 4)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:calendar"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:update_phase", args=(4,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
 | 
			
		||||
            start=timezone.now(),
 | 
			
		||||
            end=timezone.now() + timedelta(days=3),
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
 | 
			
		||||
        self.client.force_login(self.superuser)
 | 
			
		||||
        response = self.client.get(reverse("participation:update_phase", args=(4,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
 | 
			
		||||
            start=timezone.now(),
 | 
			
		||||
            end=timezone.now() + timedelta(days=3),
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:calendar"), 302, 200)
 | 
			
		||||
        fourth_phase = Phase.objects.get(phase_number=4)
 | 
			
		||||
        self.assertEqual((fourth_phase.end - fourth_phase.start).days, 3)
 | 
			
		||||
 | 
			
		||||
        # First phase must be before the other phases
 | 
			
		||||
        response = self.client.post(reverse("participation:update_phase", args=(1,)), data=dict(
 | 
			
		||||
            start=timezone.now() + timedelta(days=8),
 | 
			
		||||
            end=timezone.now() + timedelta(days=9),
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Fourth phase must be after the other phases
 | 
			
		||||
        response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
 | 
			
		||||
            start=timezone.now() - timedelta(days=9),
 | 
			
		||||
            end=timezone.now() - timedelta(days=8),
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # End must be after start
 | 
			
		||||
        response = self.client.post(reverse("participation:update_phase", args=(4,)), data=dict(
 | 
			
		||||
            start=timezone.now() + timedelta(days=3),
 | 
			
		||||
            end=timezone.now(),
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        # Unauthenticated user can't update the calendar
 | 
			
		||||
        self.client.logout()
 | 
			
		||||
        response = self.client.get(reverse("participation:calendar"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        response = self.client.get(reverse("participation:update_phase", args=(2,)))
 | 
			
		||||
        self.assertRedirects(response, reverse("login") + "?next=" +
 | 
			
		||||
                             reverse("participation:update_phase", args=(2,)), 302, 200)
 | 
			
		||||
 | 
			
		||||
    def test_forbidden_access(self):
 | 
			
		||||
        """
 | 
			
		||||
        Load personal pages and ensure that these are protected.
 | 
			
		||||
@@ -679,20 +435,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        resp = self.client.get(reverse("participation:upload_video",
 | 
			
		||||
                                       args=(self.second_team.participation.solution.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        resp = self.client.get(reverse("participation:upload_video",
 | 
			
		||||
                                       args=(self.second_team.participation.synthesis.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        resp = self.client.get(reverse("participation:add_question", args=(self.second_team.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        question = Question.objects.create(participation=self.second_team.participation,
 | 
			
		||||
                                           question=self.question.question)
 | 
			
		||||
        resp = self.client.get(reverse("participation:update_question", args=(question.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
        resp = self.client.get(reverse("participation:delete_question", args=(question.pk,)))
 | 
			
		||||
        self.assertEqual(resp.status_code, 403)
 | 
			
		||||
 | 
			
		||||
    def test_cover_matrix(self):
 | 
			
		||||
        """
 | 
			
		||||
@@ -707,7 +449,6 @@ class TestStudentParticipation(TestCase):
 | 
			
		||||
        self.team.participation.save()
 | 
			
		||||
 | 
			
		||||
        call_command('fix_matrix_channels')
 | 
			
		||||
        call_command('setup_third_phase')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAdmin(TestCase):
 | 
			
		||||
@@ -765,50 +506,6 @@ class TestAdmin(TestCase):
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertTrue(response.context["object_list"])
 | 
			
		||||
 | 
			
		||||
    def test_set_received_video(self):
 | 
			
		||||
        """
 | 
			
		||||
        Try to define the received video of a participation.
 | 
			
		||||
        """
 | 
			
		||||
        response = self.client.get(reverse("participation:participation_receive_participation",
 | 
			
		||||
                                           args=(self.team1.participation.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:participation_receive_participation",
 | 
			
		||||
                                            args=(self.team1.participation.pk,)),
 | 
			
		||||
                                    data=dict(received_participation=self.team2.participation.pk))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:participation_detail",
 | 
			
		||||
                                               args=(self.team1.participation.pk,)), 302, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.get(reverse("participation:participation_receive_participation",
 | 
			
		||||
                                           args=(self.team1.participation.pk,)))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:participation_send_participation",
 | 
			
		||||
                                            args=(self.team1.participation.pk,)),
 | 
			
		||||
                                    data=dict(sent_participation=self.team3.participation.pk))
 | 
			
		||||
        self.assertRedirects(response, reverse("participation:participation_detail",
 | 
			
		||||
                                               args=(self.team1.participation.pk,)), 302, 200)
 | 
			
		||||
 | 
			
		||||
        self.team1.participation.refresh_from_db()
 | 
			
		||||
        self.team2.participation.refresh_from_db()
 | 
			
		||||
        self.team3.participation.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(self.team1.participation.received_participation.pk, self.team2.participation.pk)
 | 
			
		||||
        self.assertEqual(self.team1.participation.sent_participation.pk, self.team3.participation.pk)
 | 
			
		||||
        self.assertEqual(self.team2.participation.sent_participation.pk, self.team1.participation.pk)
 | 
			
		||||
        self.assertEqual(self.team3.participation.received_participation.pk, self.team1.participation.pk)
 | 
			
		||||
 | 
			
		||||
        # The other team didn't work on the same problem
 | 
			
		||||
        response = self.client.post(reverse("participation:participation_receive_participation",
 | 
			
		||||
                                            args=(self.team1.participation.pk,)),
 | 
			
		||||
                                    data=dict(received_participation=self.other_team.participation.pk))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
        response = self.client.post(reverse("participation:participation_send_participation",
 | 
			
		||||
                                            args=(self.team1.participation.pk,)),
 | 
			
		||||
                                    data=dict(sent_participation=self.other_team.participation.pk))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
 | 
			
		||||
    def test_create_team_forbidden(self):
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that an admin can't create a team.
 | 
			
		||||
@@ -816,7 +513,6 @@ class TestAdmin(TestCase):
 | 
			
		||||
        response = self.client.post(reverse("participation:create_team"), data=dict(
 | 
			
		||||
            name="Test team",
 | 
			
		||||
            trigram="TES",
 | 
			
		||||
            grant_animath_access_videos=False,
 | 
			
		||||
        ))
 | 
			
		||||
        self.assertEqual(response.status_code, 403)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,9 @@
 | 
			
		||||
from django.urls import path
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
 | 
			
		||||
from .views import CalendarView, CreateQuestionView, CreateTeamView, DeleteQuestionView, JoinTeamView, \
 | 
			
		||||
    MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, PhaseUpdateView, \
 | 
			
		||||
    SetParticipationReceiveParticipationView, SetParticipationSendParticipationView, TeamAuthorizationsView, \
 | 
			
		||||
    TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, UpdateQuestionView, UploadVideoView
 | 
			
		||||
from .views import CreateTeamView, JoinTeamView, \
 | 
			
		||||
    MyParticipationDetailView, MyTeamDetailView, ParticipationDetailView, TeamAuthorizationsView, \
 | 
			
		||||
    TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app_name = "participation"
 | 
			
		||||
@@ -23,15 +22,5 @@ urlpatterns = [
 | 
			
		||||
    path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
 | 
			
		||||
    path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
 | 
			
		||||
    path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"),
 | 
			
		||||
    path("detail/upload-video/<int:pk>/", UploadVideoView.as_view(), name="upload_video"),
 | 
			
		||||
    path("detail/<int:pk>/receive-participation/", SetParticipationReceiveParticipationView.as_view(),
 | 
			
		||||
         name="participation_receive_participation"),
 | 
			
		||||
    path("detail/<int:pk>/send-participation/", SetParticipationSendParticipationView.as_view(),
 | 
			
		||||
         name="participation_send_participation"),
 | 
			
		||||
    path("detail/<int:pk>/add-question/", CreateQuestionView.as_view(), name="add_question"),
 | 
			
		||||
    path("update-question/<int:pk>/", UpdateQuestionView.as_view(), name="update_question"),
 | 
			
		||||
    path("delete-question/<int:pk>/", DeleteQuestionView.as_view(), name="delete_question"),
 | 
			
		||||
    path("calendar/", CalendarView.as_view(), name="calendar"),
 | 
			
		||||
    path("calendar/<int:pk>/", PhaseUpdateView.as_view(), name="update_phase"),
 | 
			
		||||
    path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -17,17 +17,15 @@ from django.shortcuts import redirect
 | 
			
		||||
from django.template.loader import render_to_string
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django.views.generic import CreateView, DeleteView, DetailView, FormView, RedirectView, TemplateView, UpdateView
 | 
			
		||||
from django.views.generic import CreateView, DetailView, FormView, RedirectView, TemplateView, UpdateView
 | 
			
		||||
from django.views.generic.edit import FormMixin, ProcessFormView
 | 
			
		||||
from django_tables2 import SingleTableView
 | 
			
		||||
from magic import Magic
 | 
			
		||||
from registration.models import AdminRegistration
 | 
			
		||||
 | 
			
		||||
from .forms import JoinTeamForm, ParticipationForm, PhaseForm, QuestionForm, \
 | 
			
		||||
    ReceiveParticipationForm, RequestValidationForm, SendParticipationForm, TeamForm, \
 | 
			
		||||
    UploadVideoForm, ValidateParticipationForm
 | 
			
		||||
from .models import Participation, Phase, Question, Team, Video
 | 
			
		||||
from .tables import CalendarTable, TeamTable
 | 
			
		||||
from .forms import JoinTeamForm, ParticipationForm, RequestValidationForm, TeamForm, ValidateParticipationForm
 | 
			
		||||
from .models import Participation, Team
 | 
			
		||||
from .tables import TeamTable
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateTeamView(LoginRequiredMixin, CreateView):
 | 
			
		||||
@@ -177,8 +175,7 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
 | 
			
		||||
        # and confirmed their email address
 | 
			
		||||
        context["can_validate"] = team.students.count() >= 3 and \
 | 
			
		||||
            all(r.email_confirmed for r in team.students.all()) and \
 | 
			
		||||
            all(r.photo_authorization for r in team.students.all()) and \
 | 
			
		||||
            team.participation.problem
 | 
			
		||||
            all(r.photo_authorization for r in team.students.all())
 | 
			
		||||
 | 
			
		||||
        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().unsubscribe(self.object.email, "equipes-non-valides", False)
 | 
			
		||||
            get_sympa_client().subscribe(self.object.email, f"probleme-{self.object.participation.problem}", False,
 | 
			
		||||
                                         f"Equipe {self.object.name}")
 | 
			
		||||
        elif "invalidate" in self.request.POST:
 | 
			
		||||
            self.object.participation.valid = None
 | 
			
		||||
            self.object.participation.save()
 | 
			
		||||
@@ -318,7 +313,7 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
 | 
			
		||||
        team = self.get_object()
 | 
			
		||||
        output = BytesIO()
 | 
			
		||||
        zf = ZipFile(output, "w")
 | 
			
		||||
        for student in team.students.all():
 | 
			
		||||
        for student in team.participants.all():
 | 
			
		||||
            magic = Magic(mime=True)
 | 
			
		||||
            mime_type = magic.from_file("media/" + student.photo_authorization.name)
 | 
			
		||||
            ext = mime_type.split("/")[1].replace("jpeg", "jpg")
 | 
			
		||||
@@ -402,145 +397,5 @@ class ParticipationDetailView(LoginRequiredMixin, DetailView):
 | 
			
		||||
        context = super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
        context["title"] = lambda: _("Participation of team {trigram}").format(trigram=self.object.team.trigram)
 | 
			
		||||
        context["current_phase"] = Phase.current_phase()
 | 
			
		||||
 | 
			
		||||
        return context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SetParticipationReceiveParticipationView(AdminMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    Define the solution that a team will receive.
 | 
			
		||||
    """
 | 
			
		||||
    model = Participation
 | 
			
		||||
    form_class = ReceiveParticipationForm
 | 
			
		||||
    template_name = "participation/receive_participation_form.html"
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SetParticipationSendParticipationView(AdminMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    Define the team where the solution will be sent.
 | 
			
		||||
    """
 | 
			
		||||
    model = Participation
 | 
			
		||||
    form_class = SendParticipationForm
 | 
			
		||||
    template_name = "participation/send_participation_form.html"
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.kwargs["pk"],))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateQuestionView(LoginRequiredMixin, CreateView):
 | 
			
		||||
    """
 | 
			
		||||
    Ask a question to another team.
 | 
			
		||||
    """
 | 
			
		||||
    participation: Participation
 | 
			
		||||
    model = Question
 | 
			
		||||
    form_class = QuestionForm
 | 
			
		||||
    extra_context = dict(title=_("Create question"))
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        if not request.user.is_authenticated:
 | 
			
		||||
            return self.handle_no_permission()
 | 
			
		||||
        self.participation = Participation.objects.get(pk=kwargs["pk"])
 | 
			
		||||
        if request.user.registration.is_admin or \
 | 
			
		||||
                request.user.registration.participates and \
 | 
			
		||||
                self.participation.valid and \
 | 
			
		||||
                request.user.registration.team.pk == self.participation.team_id:
 | 
			
		||||
            return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def form_valid(self, form):
 | 
			
		||||
        form.instance.participation = self.participation
 | 
			
		||||
        return super().form_valid(form)
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.participation.pk,))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateQuestionView(LoginRequiredMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    Edit a question.
 | 
			
		||||
    """
 | 
			
		||||
    model = Question
 | 
			
		||||
    form_class = QuestionForm
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        self.object = self.get_object()
 | 
			
		||||
        if not request.user.is_authenticated:
 | 
			
		||||
            return self.handle_no_permission()
 | 
			
		||||
        if request.user.registration.is_admin or \
 | 
			
		||||
                request.user.registration.participates and \
 | 
			
		||||
                self.object.participation.valid and \
 | 
			
		||||
                request.user.registration.team.pk == self.object.participation.team_id:
 | 
			
		||||
            return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteQuestionView(LoginRequiredMixin, DeleteView):
 | 
			
		||||
    """
 | 
			
		||||
    Remove a question.
 | 
			
		||||
    """
 | 
			
		||||
    model = Question
 | 
			
		||||
    extra_context = dict(title=_("Delete question"))
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        self.object = self.get_object()
 | 
			
		||||
        if not request.user.is_authenticated:
 | 
			
		||||
            return self.handle_no_permission()
 | 
			
		||||
        if request.user.registration.is_admin or \
 | 
			
		||||
                request.user.registration.participates and \
 | 
			
		||||
                self.object.participation.valid and \
 | 
			
		||||
                request.user.registration.team.pk == self.object.participation.team_id:
 | 
			
		||||
            return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UploadVideoView(LoginRequiredMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    Upload a solution video for a team.
 | 
			
		||||
    """
 | 
			
		||||
    model = Video
 | 
			
		||||
    form_class = UploadVideoForm
 | 
			
		||||
    template_name = "participation/upload_video.html"
 | 
			
		||||
    extra_context = dict(title=_("Upload video"))
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request, *args, **kwargs):
 | 
			
		||||
        user = request.user
 | 
			
		||||
        if not user.is_authenticated:
 | 
			
		||||
            return super().handle_no_permission()
 | 
			
		||||
        if user.registration.is_admin or user.registration.participates \
 | 
			
		||||
                and user.registration.team.participation.pk == self.get_object().participation.pk:
 | 
			
		||||
            return super().dispatch(request, *args, **kwargs)
 | 
			
		||||
        raise PermissionDenied
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:participation_detail", args=(self.object.participation.pk,))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CalendarView(SingleTableView):
 | 
			
		||||
    """
 | 
			
		||||
    Display the calendar of the action.
 | 
			
		||||
    """
 | 
			
		||||
    table_class = CalendarTable
 | 
			
		||||
    model = Phase
 | 
			
		||||
    extra_context = dict(title=_("Calendar"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PhaseUpdateView(AdminMixin, UpdateView):
 | 
			
		||||
    """
 | 
			
		||||
    Update a phase of the calendar, if we have sufficient rights.
 | 
			
		||||
    """
 | 
			
		||||
    model = Phase
 | 
			
		||||
    form_class = PhaseForm
 | 
			
		||||
    extra_context = dict(title=_("Calendar update"))
 | 
			
		||||
 | 
			
		||||
    def get_success_url(self):
 | 
			
		||||
        return reverse_lazy("participation:calendar")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user