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