1
0
mirror of https://gitlab.com/animath/si/plateforme.git synced 2025-02-26 14:26:30 +00:00

Compare commits

..

No commits in common. "0f2c44331c3e44b93356fbed083e0e11d82f2198" and "7f8934a647499ac7ac112704ac10099a1b2ed737" have entirely different histories.

58 changed files with 781 additions and 1298 deletions

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
# Generated by Django 3.0.11 on 2021-01-21 21:06
from django.conf import settings
from django.db import migrations, models
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
('data', models.TextField(blank=True, default='', verbose_name='new data')),
('action', models.CharField(choices=[('create', 'create'), ('edit', 'edit'), ('delete', 'delete')], default='edit', max_length=16, verbose_name='action')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='timestamp')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype', verbose_name='model')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType', verbose_name='model')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={

View File

@ -6,12 +6,14 @@ from io import StringIO
import re
from typing import Iterable
from bootstrap_datepicker_plus.widgets import DatePickerInput, DateTimePickerInput
from django import forms
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.utils import formats
from django.utils.translation import gettext_lazy as _
from pypdf import PdfFileReader
from PyPDF3 import PdfFileReader
from registration.models import VolunteerRegistration
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
@ -123,20 +125,22 @@ class TournamentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["date_start"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
self.fields["date_end"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
self.fields["inscription_limit"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["solution_limit"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["solutions_draw"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["syntheses_first_phase_limit"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["solutions_available_second_phase"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["syntheses_second_phase_limit"].widget = forms.DateTimeInput(attrs={'type': 'datetime-local'},
format='%Y-%m-%d %H:%M')
self.fields["date_start"].widget = DatePickerInput(
format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0])
self.fields["date_end"].widget = DatePickerInput(
format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0])
self.fields["inscription_limit"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["solution_limit"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["solutions_draw"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["syntheses_first_phase_limit"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["solutions_available_second_phase"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["syntheses_second_phase_limit"].widget = DateTimePickerInput(
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
self.fields["organizers"].widget = forms.CheckboxSelectMultiple()
self.fields["organizers"].queryset = VolunteerRegistration.objects.all()

View File

@ -8,7 +8,7 @@ from django.core.management import BaseCommand
from django.utils.http import urlencode
from django.utils.translation import activate
from participation.models import Team, Tournament
from registration.models import Registration, VolunteerRegistration
from registration.models import AdminRegistration, Registration, VolunteerRegistration
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
@ -163,7 +163,7 @@ class Command(BaseCommand):
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in VolunteerRegistration.objects.filter(admin=True).all():
for admin in AdminRegistration.objects.all():
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
await Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
@ -264,7 +264,7 @@ class Command(BaseCommand):
await Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
for admin in AdminRegistration.objects.all():
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
@ -374,7 +374,7 @@ class Command(BaseCommand):
"customwidget", "Tableau", str(pool))
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).all():
for admin in AdminRegistration.objects.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",

View File

@ -4,7 +4,7 @@
from django.core.management import BaseCommand
from django.db.models import Q
from participation.models import Team, Tournament
from registration.models import ParticipantRegistration, VolunteerRegistration
from registration.models import AdminRegistration, ParticipantRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client
@ -71,5 +71,5 @@ class Command(BaseCommand):
slug = jury_in.tournament.name.lower().replace(" ", "-")
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
for admin in VolunteerRegistration.objects.filter(admin=True).all():
for admin in AdminRegistration.objects.all():
sympa.subscribe(admin.user.email, "admins", True)

View File

@ -1,9 +1,8 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
# Generated by Django 3.0.11 on 2021-01-22 18:26
import datetime
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import participation.models
@ -48,8 +47,8 @@ class Migration(migrations.Migration):
name='Passage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('place', models.CharField(default='Non indiqué', help_text='Where the solution is presented?', max_length=255, verbose_name='place')),
('solution_number', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='defended solution')),
('defender_penalties', models.PositiveSmallIntegerField(default=0, help_text='Number of penalties for the defender. The defender will loose a 0.5 coefficient per penalty.', verbose_name='penalties')),
],
options={
'verbose_name': 'passage',
@ -62,7 +61,6 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('round', models.PositiveSmallIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round')),
('bbb_url', models.CharField(blank=True, default='', help_text='The link of the BBB visio for this pool.', max_length=255, verbose_name='BigBlueButton URL')),
('results_available', models.BooleanField(default=False, help_text='Check this case when results become accessible to teams. They stay accessible to you. Only averages are given.', verbose_name='results available')),
],
options={
'verbose_name': 'pool',
@ -75,12 +73,11 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('problem', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='problem')),
('final_solution', models.BooleanField(default=False, verbose_name='solution for the final tournament')),
('file', models.FileField(unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file')),
('file', models.FileField(blank=True, default='', unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file')),
],
options={
'verbose_name': 'solution',
'verbose_name_plural': 'solutions',
'ordering': ('participation__team__trigram', 'final_solution', 'problem'),
},
),
migrations.CreateModel(
@ -88,12 +85,11 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.PositiveSmallIntegerField(choices=[(1, 'opponent'), (2, 'reporter')])),
('file', models.FileField(unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file')),
('file', models.FileField(blank=True, default='', unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file')),
],
options={
'verbose_name': 'synthesis',
'verbose_name_plural': 'syntheses',
'ordering': ('passage__pool__round', 'type'),
},
),
migrations.CreateModel(
@ -117,10 +113,8 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
('date_start', models.DateField(default=datetime.date.today, verbose_name='start')),
('date_end', models.DateField(default=datetime.date.today, verbose_name='end')),
('place', models.CharField(max_length=255, verbose_name='place')),
('max_teams', models.PositiveSmallIntegerField(default=9, verbose_name='max team count')),
('price', models.PositiveSmallIntegerField(default=21, verbose_name='price')),
('remote', models.BooleanField(default=False, verbose_name='remote')),
('inscription_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date for registrations')),
('solution_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload solutions')),
('solutions_draw', models.DateTimeField(default=django.utils.timezone.now, verbose_name='random draw for solutions')),
@ -135,17 +129,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'tournaments',
},
),
migrations.CreateModel(
name='Tweak',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('diff', models.IntegerField(help_text='Score to add/remove on the final score', verbose_name='difference')),
('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tweaks', to='participation.participation', verbose_name='participation')),
('pool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.pool', verbose_name='passage')),
],
options={
'verbose_name': 'tweak',
'verbose_name_plural': 'tweaks',
},
),
]

View File

@ -1,5 +1,6 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
# Generated by Django 3.0.11 on 2021-01-22 18:26
import address.models
from django.db import migrations, models
import django.db.models.deletion
@ -9,8 +10,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('participation', '0001_initial'),
('registration', '0001_initial'),
('address', '0003_auto_20200830_1851'),
('participation', '0001_initial'),
]
operations = [
@ -19,6 +21,11 @@ class Migration(migrations.Migration):
name='organizers',
field=models.ManyToManyField(related_name='organized_tournaments', to='registration.VolunteerRegistration', verbose_name='organizers'),
),
migrations.AddField(
model_name='tournament',
name='place',
field=address.models.AddressField(on_delete=django.db.models.deletion.CASCADE, to='address.Address', verbose_name='place'),
),
migrations.AddIndex(
model_name='team',
index=models.Index(fields=['trigram'], name='participati_trigram_239255_idx'),
@ -26,17 +33,17 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='synthesis',
name='participation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.participation', verbose_name='participation'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.Participation', verbose_name='participation'),
),
migrations.AddField(
model_name='synthesis',
name='passage',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syntheses', to='participation.passage', verbose_name='passage'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syntheses', to='participation.Passage', verbose_name='passage'),
),
migrations.AddField(
model_name='solution',
name='participation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='participation.participation', verbose_name='participation'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='participation.Participation', verbose_name='participation'),
),
migrations.AddField(
model_name='pool',
@ -51,47 +58,47 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='pool',
name='tournament',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pools', to='participation.tournament', verbose_name='tournament'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pools', to='participation.Tournament', verbose_name='tournament'),
),
migrations.AddField(
model_name='passage',
name='defender',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='defender'),
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='defender'),
),
migrations.AddField(
model_name='passage',
name='opponent',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='opponent'),
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='opponent'),
),
migrations.AddField(
model_name='passage',
name='pool',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passages', to='participation.pool', verbose_name='pool'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passages', to='participation.Pool', verbose_name='pool'),
),
migrations.AddField(
model_name='passage',
name='reporter',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.participation', verbose_name='reporter'),
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='reporter'),
),
migrations.AddField(
model_name='participation',
name='team',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.team', verbose_name='team'),
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.Team', verbose_name='team'),
),
migrations.AddField(
model_name='participation',
name='tournament',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='participation.tournament', verbose_name='tournament'),
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='participation.Tournament', verbose_name='tournament'),
),
migrations.AddField(
model_name='note',
name='jury',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='registration.volunteerregistration', verbose_name='jury'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='registration.VolunteerRegistration', verbose_name='jury'),
),
migrations.AddField(
model_name='note',
name='passage',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='participation.passage', verbose_name='passage'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='participation.Passage', verbose_name='passage'),
),
migrations.AddIndex(
model_name='tournament',

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.18 on 2023-02-19 22:13
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0002_initial'),
]
operations = [
migrations.AlterField(
model_name='team',
name='trigram',
field=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}$'), django.core.validators.RegexValidator('^(?!BIT$|CNO$|CRO$|CUL$|FTG$|FCK$|FUC$|FUK$|FYS$|HIV$|IST$|MST$|KKK$|KYS$|SEX$)', message='This trigram is forbidden.')], verbose_name='trigram'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.11 on 2021-01-23 18:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0002_auto_20210122_1926'),
]
operations = [
migrations.AddField(
model_name='tournament',
name='remote',
field=models.BooleanField(default=False, verbose_name='remote'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-03 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0003_tournament_remote'),
]
operations = [
migrations.AddField(
model_name='passage',
name='defender_penalties',
field=models.PositiveSmallIntegerField(default=0, help_text='Number of penalties for the defender. The defender will loose a 0.5 coefficient per penalty.', verbose_name='penalties'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.7 on 2021-04-10 07:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('participation', '0004_passage_defender_penalties'),
]
operations = [
migrations.AddField(
model_name='pool',
name='results_available',
field=models.BooleanField(default=False, help_text='Check this case when results become accessible to teams. They stay accessible to you. Only averages are given.', verbose_name='results available'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.13 on 2022-04-26 11:46
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('participation', '0005_pool_results_available'),
]
operations = [
migrations.AlterModelOptions(
name='solution',
options={'ordering': ('participation__team__trigram', 'final_solution', 'problem'), 'verbose_name': 'solution', 'verbose_name_plural': 'solutions'},
),
migrations.AlterModelOptions(
name='synthesis',
options={'ordering': ('passage__pool__round', 'type'), 'verbose_name': 'synthesis', 'verbose_name_plural': 'syntheses'},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.2.13 on 2022-04-26 19:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('participation', '0006_auto_20220426_1346'),
]
operations = [
migrations.RemoveField(
model_name='passage',
name='place',
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2022-04-29 16:53
from django.db import migrations, models
import participation.models
class Migration(migrations.Migration):
dependencies = [
('participation', '0007_remove_passage_place'),
]
operations = [
migrations.AlterField(
model_name='solution',
name='file',
field=models.FileField(unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file'),
),
migrations.AlterField(
model_name='synthesis',
name='file',
field=models.FileField(unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.2.13 on 2022-05-15 14:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('participation', '0008_auto_20220429_1853'),
]
operations = [
migrations.CreateModel(
name='Tweak',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('diff', models.IntegerField(help_text='Score to add/remove on the final score', verbose_name='difference')),
('participation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tweaks', to='participation.participation', verbose_name='participation')),
('pool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.pool', verbose_name='passage')),
],
options={
'verbose_name': 'tweak',
'verbose_name_plural': 'tweaks',
},
),
]

View File

@ -4,6 +4,7 @@
from datetime import date
import os
from address.models import AddressField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
@ -39,11 +40,7 @@ class Team(models.Model):
verbose_name=_("trigram"),
help_text=_("The trigram must be composed of three uppercase letters."),
unique=True,
validators=[
RegexValidator(r"^[A-Z]{3}$"),
RegexValidator(fr"^(?!{'|'.join(f'{t}$' for t in settings.FORBIDDEN_TRIGRAMS)})",
message=_("This trigram is forbidden.")),
],
validators=[RegexValidator("[A-Z]{3}")],
)
access_code = models.CharField(
@ -146,8 +143,7 @@ class Tournament(models.Model):
default=date.today,
)
place = models.CharField(
max_length=255,
place = AddressField(
verbose_name=_("place"),
)

View File

@ -74,19 +74,6 @@
{% endfor %}
</dd>
<dt class="col-sm-6 text-right">{% trans "Vaccine sheets:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}
{% if student.under_18 %}
{% if student.vaccine_sheet %}
<a href="{{ student.vaccine_sheet.url }}" data-turbolinks="false">{{ student }}</a>{% if not forloop.last %},{% endif %}
{% else %}
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
{% endif %}
{% endif %}
{% endfor %}
</dd>
<dt class="col-sm-6 text-right">{% trans "Parental authorizations:" %}</dt>
<dd class="col-sm-6">
{% for student in team.students.all %}

View File

@ -29,9 +29,6 @@ class TestStudentParticipation(TestCase):
StudentRegistration.objects.create(
user=self.user,
student_class=12,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
school="Earth",
give_contact_to_animath=True,
email_confirmed=True,
@ -52,9 +49,6 @@ class TestStudentParticipation(TestCase):
StudentRegistration.objects.create(
user=self.second_user,
student_class=11,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
school="Moon",
give_contact_to_animath=True,
email_confirmed=True,
@ -71,12 +65,7 @@ class TestStudentParticipation(TestCase):
email="coach@example.com",
password="coach",
)
CoachRegistration.objects.create(
user=self.coach,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
)
CoachRegistration.objects.create(user=self.coach)
self.tournament = Tournament.objects.create(
name="France",
@ -225,12 +214,8 @@ class TestStudentParticipation(TestCase):
give_contact_to_animath=True,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/mai-linh",
health_sheet="authorization/health/mai-linh",
vaccine_sheet="authorization/vaccine/mai-linh",
parental_authorization="authorization/parental/mai-linh",
)
@ -247,13 +232,9 @@ class TestStudentParticipation(TestCase):
give_contact_to_animath=False,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/emmy",
health_sheet="authorization/health/emmy",
vaccine_sheet="authorization/vaccine/emmy",
parental_authorization="authorization/parental/emmy",
photo_authorization="authorization/photo/yohann",
health_sheet="authorization/health/yohann",
parental_authorization="authorization/parental/yohann",
)
fourth_user = User.objects.create(
@ -269,18 +250,13 @@ class TestStudentParticipation(TestCase):
give_contact_to_animath=False,
email_confirmed=True,
team=self.team,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
photo_authorization="authorization/photo/tfjm",
health_sheet="authorization/health/tfjm",
vaccine_sheet="authorization/health/tfjm",
parental_authorization="authorization/parental/tfjm",
)
self.coach.registration.team = self.team
self.coach.registration.health_sheet = "authorization/health/coach"
self.coach.registration.vaccine_sheet = "authorization/vaccine/coach"
self.coach.registration.photo_authorization = "authorization/photo/coach"
self.coach.registration.email_confirmed = True
self.coach.registration.save()
@ -309,7 +285,6 @@ class TestStudentParticipation(TestCase):
self.user.registration.photo_authorization = "authorization/photo/ananas"
self.user.registration.health_sheet = "authorization/health/ananas"
self.user.registration.vaccine_sheet = "authorization/health/ananas"
self.user.registration.parental_authorization = "authorization/parental/ananas"
self.user.registration.save()

View File

@ -180,7 +180,6 @@ class TeamDetailView(LoginRequiredMixin, FormMixin, ProcessFormView, DetailView)
context["validation_form"] = ValidateParticipationForm(self.request.POST or None)
# A team is complete when there are at least 4 members plus a coache that have sent their authorizations,
# their health sheet, they confirmed their email address and under-18 people sent their parental authorization.
# TODO: Add vaccine sheets
context["can_validate"] = team.students.count() >= 4 and team.coaches.exists() and \
team.participation.tournament and \
all(r.photo_authorization for r in team.participants.all()) and \
@ -306,16 +305,12 @@ class TeamUpdateView(LoginRequiredMixin, UpdateView):
context = super().get_context_data(**kwargs)
context["participation_form"] = ParticipationForm(data=self.request.POST or None,
instance=self.object.participation)
if not self.request.user.registration.is_volunteer:
del context["participation_form"].fields['final']
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 self.request.user.registration.is_volunteer:
del participation_form.fields['final']
if not participation_form.is_valid():
return self.form_invalid(form)
@ -415,12 +410,6 @@ class TeamAuthorizationsView(LoginRequiredMixin, DetailView):
zf.write("media/" + participant.health_sheet.name,
_("Health sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
if isinstance(participant, StudentRegistration) and participant.vaccine_sheet:
mime_type = magic.from_file("media/" + participant.vaccine_sheet.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
zf.write("media/" + participant.vaccine_sheet.name,
_("Vaccine sheet of {participant}.{ext}").format(participant=str(participant), ext=ext))
if team.motivation_letter:
mime_type = magic.from_file("media/" + team.motivation_letter.name)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")

View File

@ -5,12 +5,12 @@ from django.contrib import admin
from django.contrib.admin import ModelAdmin
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin
from .models import CoachRegistration, Payment, Registration, StudentRegistration, VolunteerRegistration
from .models import AdminRegistration, CoachRegistration, Payment, Registration, StudentRegistration
@admin.register(Registration)
class RegistrationAdmin(PolymorphicParentModelAdmin):
child_models = (StudentRegistration, CoachRegistration, VolunteerRegistration,)
child_models = (StudentRegistration, CoachRegistration, AdminRegistration,)
list_display = ("user", "type", "email_confirmed",)
polymorphic_list = True
@ -25,8 +25,8 @@ class CoachRegistrationAdmin(PolymorphicChildModelAdmin):
pass
@admin.register(VolunteerRegistration)
class VolunteerRegistrationAdmin(PolymorphicChildModelAdmin):
@admin.register(AdminRegistration)
class AdminRegistrationAdmin(PolymorphicChildModelAdmin):
pass

View File

@ -4,10 +4,16 @@
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from ..models import CoachRegistration, ParticipantRegistration, \
from ..models import AdminRegistration, CoachRegistration, ParticipantRegistration, \
StudentRegistration, VolunteerRegistration
class AdminSerializer(serializers.ModelSerializer):
class Meta:
model = AdminRegistration
fields = '__all__'
class CoachSerializer(serializers.ModelSerializer):
class Meta:
model = CoachRegistration
@ -34,6 +40,7 @@ class VolunteerSerializer(serializers.ModelSerializer):
class RegistrationSerializer(PolymorphicSerializer):
model_serializer_mapping = {
AdminRegistration: AdminSerializer,
CoachRegistration: CoachSerializer,
StudentRegistration: StudentSerializer,
VolunteerRegistration: VolunteerSerializer,

View File

@ -20,3 +20,4 @@ class RegistrationConfig(AppConfig):
post_save.connect(create_payment, "registration.Registration")
post_save.connect(create_payment, "registration.StudentRegistration")
post_save.connect(create_payment, "registration.CoachRegistration")
post_save.connect(create_payment, "registration.AdminRegistration")

View File

@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError
from django.forms import FileInput
from django.utils.translation import gettext_lazy as _
from .models import CoachRegistration, ParticipantRegistration, Payment, \
from .models import AdminRegistration, CoachRegistration, ParticipantRegistration, Payment, \
StudentRegistration, VolunteerRegistration
@ -50,6 +50,14 @@ class AddOrganizerForm(forms.ModelForm):
"""
Signup form to registers volunteers
"""
type = forms.ChoiceField(
label=lambda: _("role").capitalize(),
choices=lambda: [
("volunteer", _("volunteer").capitalize()),
("admin", _("admin").capitalize()),
],
initial="volunteer",
)
def clean_email(self):
"""
@ -68,7 +76,7 @@ class AddOrganizerForm(forms.ModelForm):
class Meta:
model = User
fields = ('first_name', 'last_name', 'email',)
fields = ('first_name', 'last_name', 'email', 'type',)
class UserForm(forms.ModelForm):
@ -91,14 +99,10 @@ class StudentRegistrationForm(forms.ModelForm):
"""
A student can update its class, its school and if it allows Animath to contact him/her later.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["birth_date"].widget = forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d')
class Meta:
model = StudentRegistration
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'phone_number',
'health_issues', 'school', 'responsible_name', 'responsible_phone', 'responsible_email',
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'phone_number', 'health_issues',
'school', 'responsible_name', 'responsible_phone', 'responsible_email',
'give_contact_to_animath', 'email_confirmed',)
@ -146,28 +150,6 @@ class HealthSheetForm(forms.ModelForm):
fields = ('health_sheet',)
class VaccineSheetForm(forms.ModelForm):
"""
Form to send a vaccine sheet.
"""
def clean_vaccine_sheet(self):
if "vaccine_sheet" in self.files:
file = self.files["vaccine_sheet"]
if file.size > 2e6:
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
if file.content_type not in ["application/pdf", "image/png", "image/jpeg"]:
raise ValidationError(_("The uploaded file must be a PDF, PNG of JPEG file."))
return self.cleaned_data["vaccine_sheet"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["vaccine_sheet"].widget = FileInput()
class Meta:
model = StudentRegistration
fields = ('vaccine_sheet',)
class ParentalAuthorizationForm(forms.ModelForm):
"""
Form to send a parental authorization.
@ -196,8 +178,8 @@ class CoachRegistrationForm(forms.ModelForm):
"""
class Meta:
model = CoachRegistration
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'phone_number', 'health_issues',
'professional_activity', 'give_contact_to_animath', 'email_confirmed',)
fields = ('team', 'birth_date', 'gender', 'address', 'phone_number', 'health_issues', 'professional_activity',
'give_contact_to_animath', 'email_confirmed',)
class VolunteerRegistrationForm(forms.ModelForm):
@ -206,7 +188,16 @@ class VolunteerRegistrationForm(forms.ModelForm):
"""
class Meta:
model = VolunteerRegistration
fields = ('professional_activity', 'admin', 'give_contact_to_animath', 'email_confirmed',)
fields = ('professional_activity', 'give_contact_to_animath', 'email_confirmed',)
class AdminRegistrationForm(forms.ModelForm):
"""
Admins can tell everything they want.
"""
class Meta:
model = AdminRegistration
fields = ('role', 'give_contact_to_animath', 'email_confirmed',)
class PaymentForm(forms.ModelForm):

View File

@ -1,8 +1,8 @@
# Generated by Django 3.2.13 on 2023-01-10 19:22
# Generated by Django 3.0.11 on 2021-01-22 18:26
import address.models
import datetime
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
@ -14,9 +14,10 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('participation', '0001_initial'),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('address', '0003_auto_20200830_1851'),
('participation', '0001_initial'),
]
operations = [
@ -26,7 +27,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('give_contact_to_animath', models.BooleanField(default=False, verbose_name='Grant Animath to contact me in the future about other actions')),
('email_confirmed', models.BooleanField(default=False, verbose_name='email confirmed')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_registration.registration_set+', to='contenttypes.contenttype')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_registration.registration_set+', to='contenttypes.ContentType')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
@ -37,16 +38,13 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='ParticipantRegistration',
fields=[
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')),
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.Registration')),
('birth_date', models.DateField(default=datetime.date.today, verbose_name='birth date')),
('gender', models.CharField(choices=[('female', 'Female'), ('male', 'Male'), ('other', 'Other')], default='other', max_length=6, verbose_name='gender')),
('address', models.CharField(max_length=255, verbose_name='address')),
('zip_code', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1000), django.core.validators.MaxValueValidator(99999)], verbose_name='zip code')),
('city', models.CharField(max_length=255, verbose_name='city')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, region=None, verbose_name='phone number')),
('health_issues', models.TextField(blank=True, help_text='You can indicate here your allergies or anything that is important to know for organizers', verbose_name='health issues')),
('photo_authorization', models.FileField(blank=True, default='', upload_to=registration.models.get_random_photo_filename, verbose_name='photo authorization')),
('team', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='participants', to='participation.team', verbose_name='team')),
('address', address.models.AddressField(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='address.Address', verbose_name='address')),
('team', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='participants', to='participation.Team', verbose_name='team')),
],
options={
'abstract': False,
@ -57,7 +55,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='VolunteerRegistration',
fields=[
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.registration')),
('registration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.Registration')),
('professional_activity', models.TextField(verbose_name='professional activity')),
],
options={
@ -69,7 +67,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='AdminRegistration',
fields=[
('volunteerregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.volunteerregistration')),
('volunteerregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.VolunteerRegistration')),
('role', models.TextField(verbose_name='role of the administrator')),
],
options={
@ -81,7 +79,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CoachRegistration',
fields=[
('participantregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.participantregistration')),
('participantregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.ParticipantRegistration')),
('professional_activity', models.TextField(verbose_name='professional activity')),
],
options={
@ -93,7 +91,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='StudentRegistration',
fields=[
('participantregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.participantregistration')),
('participantregistration_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='registration.ParticipantRegistration')),
('student_class', models.IntegerField(choices=[(12, '12th grade'), (11, '11th grade'), (10, '10th grade or lower')], verbose_name='student class')),
('school', models.CharField(max_length=255, verbose_name='school')),
('responsible_name', models.CharField(default='', max_length=255, verbose_name='responsible name')),
@ -112,11 +110,11 @@ class Migration(migrations.Migration):
name='Payment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('other', 'Other (please indicate)'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type')),
('type', models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type')),
('scholarship_file', models.FileField(blank=True, default='', help_text='only if you have a scholarship.', upload_to=registration.models.get_scholarship_filename, verbose_name='scholarship file')),
('additional_information', models.TextField(blank=True, default='', help_text='To help us to find your payment.', verbose_name='additional information')),
('valid', models.BooleanField(default=False, null=True, verbose_name='valid')),
('registration', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='registration.participantregistration', verbose_name='registration')),
('registration', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='payment', to='registration.ParticipantRegistration', verbose_name='registration')),
],
options={
'verbose_name': 'payment',

View File

@ -1,29 +0,0 @@
# Generated by Django 3.2.13 on 2023-01-10 19:31
import datetime
from django.db import migrations, models
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('registration', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='participantregistration',
name='birth_date',
),
migrations.AddField(
model_name='studentregistration',
name='birth_date',
field=models.DateField(default=datetime.date.today, verbose_name='birth date'),
),
migrations.AlterField(
model_name='studentregistration',
name='responsible_phone',
field=phonenumber_field.modelfields.PhoneNumberField(default='', max_length=128, region=None, verbose_name='responsible phone number'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.0.11 on 2021-01-23 20:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='participantregistration',
name='health_issues',
field=models.TextField(blank=True, help_text='You can indicate here your allergies or anything that is important to know for organizers', verbose_name='health issues'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.13 on 2023-01-16 22:13
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0002_auto_20230110_2031'),
]
operations = [
migrations.AlterField(
model_name='participantregistration',
name='zip_code',
field=models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(1000), django.core.validators.MaxValueValidator(99999)], verbose_name='zip code'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2022-04-26 11:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('registration', '0002_participantregistration_health_issues'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='type',
field=models.CharField(blank=True, choices=[('', 'No payment'), ('helloasso', 'Hello Asso'), ('scholarship', 'Scholarship'), ('bank_transfer', 'Bank transfer'), ('other', 'Other (please indicate)'), ('free', 'The tournament is free')], default='', max_length=16, verbose_name='type'),
),
]

View File

@ -1,51 +0,0 @@
# Generated by Django 3.2.18 on 2023-02-19 22:13
from django.contrib.contenttypes.models import ContentType
from django.db import migrations, models
from django.db.models import F
def merge_admins(apps, schema_editor):
AdminRegistration = apps.get_model('registration', 'AdminRegistration')
VolunteerRegistration = apps.get_model('registration', 'VolunteerRegistration')
db_alias = schema_editor.connection.alias
AdminRegistration.objects.using(db_alias).update(admin=True)
for admin in AdminRegistration.objects.all():
admin.professional_activity = admin.role
admin.polymorphic_ctype_id = ContentType.objects.get_for_model(VolunteerRegistration).id
admin.save()
def separate_admins(apps, schema_editor):
AdminRegistration = apps.get_model('registration', 'AdminRegistration')
VolunteerRegistration = apps.get_model('registration', 'VolunteerRegistration')
for admin in VolunteerRegistration.objects.filter(admin=True).all():
admin.delete()
AdminRegistration.objects.create(user=admin.user,
professional_activity=admin.professional_activity,
role=admin.professional_activity)
class Migration(migrations.Migration):
dependencies = [
('participation', '0003_alter_team_trigram'),
('registration', '0003_alter_participantregistration_zip_code'),
]
operations = [
migrations.AddField(
model_name='volunteerregistration',
name='admin',
field=models.BooleanField(
default=False,
help_text="An administrator has all rights. Please don't give this right to all juries and volunteers.",
verbose_name='administrator'),
),
migrations.RunPython(
merge_admins,
separate_admins,
elidable=True,
),
migrations.DeleteModel(
name='AdminRegistration',
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.18 on 2023-02-19 23:38
from django.db import migrations, models
import registration.models
class Migration(migrations.Migration):
dependencies = [
('registration', '0004_volunteer_admin'),
]
operations = [
migrations.AddField(
model_name='studentregistration',
name='vaccine_sheet',
field=models.FileField(blank=True, default='', upload_to=registration.models.get_random_vaccine_filename, verbose_name='vaccine sheet'),
),
]

View File

@ -3,8 +3,8 @@
from datetime import date
from address.models import AddressField
from django.contrib.sites.models import Site
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.template import loader
from django.urls import reverse_lazy
@ -22,7 +22,7 @@ class Registration(PolymorphicModel):
"""
Registrations store extra content that are not asked in the User Model.
This is specific to the role of the user, see StudentRegistration,
CoachRegistration or VolunteerRegistration.
ClassRegistration or AdminRegistration..
"""
user = models.OneToOneField(
"auth.User",
@ -79,7 +79,7 @@ class Registration(PolymorphicModel):
@property
def is_admin(self):
return isinstance(self, VolunteerRegistration) and self.admin or self.user.is_superuser
return isinstance(self, AdminRegistration) or self.user.is_superuser
@property
def is_volunteer(self):
@ -108,10 +108,6 @@ def get_random_health_filename(instance, filename):
return "authorization/health/" + get_random_string(64)
def get_random_vaccine_filename(instance, filename):
return "authorization/vaccine/" + get_random_string(64)
def get_random_parental_filename(instance, filename):
return "authorization/parental/" + get_random_string(64)
@ -127,6 +123,11 @@ class ParticipantRegistration(Registration):
verbose_name=_("team"),
)
birth_date = models.DateField(
verbose_name=_("birth date"),
default=date.today,
)
gender = models.CharField(
max_length=6,
verbose_name=_("gender"),
@ -138,19 +139,10 @@ class ParticipantRegistration(Registration):
default="other",
)
address = models.CharField(
max_length=255,
address = AddressField(
verbose_name=_("address"),
)
zip_code = models.PositiveIntegerField(
verbose_name=_("zip code"),
validators=[MinValueValidator(1000), MaxValueValidator(99999)],
)
city = models.CharField(
max_length=255,
verbose_name=_("city"),
null=True,
default=None,
)
phone_number = PhoneNumberField(
@ -173,8 +165,6 @@ class ParticipantRegistration(Registration):
@property
def under_18(self):
if isinstance(self, CoachRegistration):
return False # In normal case
important_date = timezone.now().date()
if self.team and self.team.participation.tournament:
important_date = self.team.participation.tournament.date_start
@ -197,11 +187,6 @@ class StudentRegistration(ParticipantRegistration):
Specific registration for students.
They have a team, a student class and a school.
"""
birth_date = models.DateField(
verbose_name=_("birth date"),
default=date.today,
)
student_class = models.IntegerField(
choices=[
(12, _("12th grade")),
@ -224,7 +209,7 @@ class StudentRegistration(ParticipantRegistration):
responsible_phone = PhoneNumberField(
verbose_name=_("responsible phone number"),
default="",
blank=True,
)
responsible_email = models.EmailField(
@ -246,13 +231,6 @@ class StudentRegistration(ParticipantRegistration):
default="",
)
vaccine_sheet = models.FileField(
verbose_name=_("vaccine sheet"),
upload_to=get_random_vaccine_filename,
blank=True,
default="",
)
@property
def type(self):
return _("student")
@ -298,19 +276,13 @@ class VolunteerRegistration(Registration):
verbose_name=_("professional activity"),
)
admin = models.BooleanField(
verbose_name=_("administrator"),
help_text=_("An administrator has all rights. Please don't give this right to all juries and volunteers."),
default=False,
)
@property
def interesting_tournaments(self) -> set:
return set(self.organized_tournaments.all()).union(map(lambda pool: pool.tournament, self.jury_in.all()))
@property
def type(self):
return _('admin') if self.is_admin else _('volunteer')
return _('volunteer')
@property
def form_class(self):
@ -318,6 +290,29 @@ class VolunteerRegistration(Registration):
return VolunteerRegistrationForm
class AdminRegistration(VolunteerRegistration):
"""
Specific registration for admins.
They have a field to justify they status.
"""
role = models.TextField(
verbose_name=_("role of the administrator"),
)
@property
def type(self):
return _("admin")
@property
def form_class(self):
from registration.forms import AdminRegistrationForm
return AdminRegistrationForm
class Meta:
verbose_name = _("admin registration")
verbose_name_plural = _("admin registrations")
def get_scholarship_filename(instance, filename):
return f"authorization/scholarship/scholarship_{instance.registration.pk}"

View File

@ -4,7 +4,7 @@
from django.contrib.auth.models import User
from tfjm.lists import get_sympa_client
from .models import Payment, Registration, VolunteerRegistration
from .models import AdminRegistration, Payment, Registration
def set_username(instance, **_):
@ -40,7 +40,7 @@ def create_admin_registration(instance, **_):
ensure that an admin registration is created.
"""
if instance.is_superuser:
VolunteerRegistration.objects.get_or_create(user=instance, admin=True)
AdminRegistration.objects.get_or_create(user=instance)
def create_payment(instance: Registration, **_):

View File

@ -19,7 +19,7 @@ class RegistrationTable(tables.Table):
)
def order_type(self, queryset, desc):
types = ["-volunteerregistration__admin", "volunteerregistration", "participantregistration"]
types = ["volunteerregistration__adminregistration", "volunteerregistration", "participantregistration"]
return queryset.order_by(*(("-" if desc else "") + t for t in types)), True
class Meta:

View File

@ -1,15 +0,0 @@
{% extends "base.html" %}
{% load i18n static crispy_forms_filters %}
{% block content %}
<a class="btn btn-info" href="{% url "registration:user_detail" pk=object.user.pk %}"><i class="fas fa-arrow-left"></i> {% trans "Back to the user detail" %}</a>
<hr>
<form method="post" enctype="multipart/form-data">
<div id="form-content">
{% csrf_token %}
{{ form|crispy }}
</div>
<button class="btn btn-success" type="submit">{% trans "Upload" %}</button>
</form>
{% endblock %}

View File

@ -39,16 +39,14 @@
</a>
</dd>
{% if user_object.registration.studentregistration %}
<dt class="col-sm-6 text-right">{% trans "Birth date:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.birth_date }}</dd>
{% endif %}
<dt class="col-sm-6 text-right">{% trans "Birth date:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.birth_date }}</dd>
<dt class="col-sm-6 text-right">{% trans "Gender:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.get_gender_display }}</dd>
<dt class="col-sm-6 text-right">{% trans "Address:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.address }}, {{ user_object.registration.zip_code|stringformat:'05d' }} {{ user_object.registration.city }}</dd>
<dd class="col-sm-6">{{ user_object.registration.address }}</dd>
<dt class="col-sm-6 text-right">{% trans "Phone number:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd>
@ -79,16 +77,6 @@
{% endif %}
</dd>
<dt class="col-sm-6 text-right">{% trans "Vaccine sheet:" %}</dt>
<dd class="col-sm-6">
{% if user_object.registration.vaccine_sheet %}
<a href="{{ user_object.registration.vaccine_sheet.url }}" data-turbolinks="false">{% trans "Download" %}</a>
{% endif %}
{% if user_object.registration.team and not user_object.registration.team.participation.valid %}
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadVaccineSheetModal">{% trans "Replace" %}</button>
{% endif %}
</dd>
<dt class="col-sm-6 text-right">{% trans "Parental authorization:" %}</dt>
<dd class="col-sm-6">
{% if user_object.registration.parental_authorization %}
@ -117,8 +105,8 @@
<dd class="col-sm-6"><a href="mailto:{{ email }}">{{ email }}</a></dd>
{% endwith %}
{% elif user_object.registration.is_admin %}
<dt class="col-sm-6 text-right">{% trans "Admin:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.is_admin|yesno }}</dd>
<dt class="col-sm-6 text-right">{% trans "Role:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.role }}</dd>
{% elif user_object.registration.coachregistration or user_object.registration.is_volunteer %}
<dt class="col-sm-6 text-right">{% trans "Profesional activity:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.professional_activity }}</dd>
@ -181,11 +169,6 @@
{% url "registration:upload_user_health_sheet" pk=user_object.registration.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadHealthSheet" modal_enctype="multipart/form-data" %}
{% trans "Upload vaccine sheet" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "registration:upload_user_vaccine_sheet" pk=user_object.registration.pk as modal_action %}
{% include "base_modal.html" with modal_id="uploadVaccineSheet" modal_enctype="multipart/form-data" %}
{% trans "Upload parental authorization" as modal_title %}
{% trans "Upload" as modal_button %}
{% url "registration:upload_user_parental_authorization" pk=user_object.registration.pk as modal_action %}

View File

@ -3,9 +3,8 @@
{{ object.user.email }}
{{ object.type }}
{{ object.professional_activity }}
{{ object.birth_date }}
{{ object.address }}
{{ object.zip_code }}
{{ object.city }}
{{ object.phone_number }}
{{ object.team.name }}
{{ object.team.trigram }}

View File

@ -6,8 +6,6 @@
{{ object.school }}
{{ object.birth_date }}
{{ object.address }}
{{ object.zip_code }}
{{ object.city }}
{{ object.phone_number }}
{{ object.responsible_name }}
{{ object.reponsible_phone }}

View File

@ -15,7 +15,7 @@ from django.utils.http import urlsafe_base64_encode
from participation.models import Team
from tfjm.tokens import email_validation_token
from .models import CoachRegistration, StudentRegistration, VolunteerRegistration
from .models import AdminRegistration, CoachRegistration, StudentRegistration
class TestIndexPage(TestCase):
@ -64,22 +64,9 @@ class TestRegistration(TestCase):
self.client.force_login(self.user)
self.student = User.objects.create(email="student@example.com")
StudentRegistration.objects.create(
user=self.student,
student_class=11,
school="Earth",
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
)
StudentRegistration.objects.create(user=self.student, student_class=11, school="Earth")
self.coach = User.objects.create(email="coach@example.com")
CoachRegistration.objects.create(
user=self.coach,
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
professional_activity="Teacher",
)
CoachRegistration.objects.create(user=self.coach, professional_activity="Teacher")
def test_admin_pages(self):
"""
@ -92,7 +79,7 @@ class TestRegistration(TestCase):
+ f"registration/registration/{self.user.registration.pk}/change/")
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse("admin:index") +
f"r/{ContentType.objects.get_for_model(VolunteerRegistration).id}/"
f"r/{ContentType.objects.get_for_model(AdminRegistration).id}/"
f"{self.user.registration.pk}/")
self.assertRedirects(response, "http://" + Site.objects.get().domain +
str(self.user.registration.get_absolute_url()), 302, 200)
@ -144,9 +131,7 @@ class TestRegistration(TestCase):
school="God",
birth_date="2000-01-01",
gender="other",
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
address="1 Rue de Rivoli, 75001 Paris, France",
phone_number="0123456789",
responsible_name="Toto",
responsible_phone="0123456789",
@ -170,9 +155,7 @@ class TestRegistration(TestCase):
school="God",
birth_date="2000-01-01",
gender="other",
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
address="1 Rue de Rivoli, 75001 Paris, France",
phone_number="0123456789",
responsible_name="Toto",
responsible_phone="0123456789",
@ -191,10 +174,9 @@ class TestRegistration(TestCase):
password1="azertyuiopazertyuiop",
password2="azertyuiopazertyuiop",
role="coach",
birth_date="1980-01-01",
gender="other",
address="1 Rue de Rivoli",
zip_code=75001,
city="Paris",
address="1 Rue de Rivoli, 75001 Paris, France",
phone_number="0123456789",
professional_activity="God",
give_contact_to_animath=True,
@ -271,14 +253,12 @@ class TestRegistration(TestCase):
)
self.student.registration.save()
for user, data in [(self.user, dict(professional_activity="Bot", admin=True)),
for user, data in [(self.user, dict(role="Bot")),
(self.student, dict(student_class=11, school="Sky", birth_date="2001-01-01",
gender="female", address="1 Rue de Rivoli", zip_code=75001,
city="Paris", responsible_name="Toto",
responsible_phone="0123456789",
responsible_email="toto@example.com")),
(self.coach, dict(professional_activity="God", gender="male",
address="1 Rue de Rivoli", zip_code=75001, city="Paris"))]:
gender="female", address="1 Rue de Rivoli, 75001 Paris, France",
responsible_name="Toto", responsible_email="toto@example.com")),
(self.coach, dict(professional_activity="God", birth_date="2001-01-01",
gender="male", address="1 Rue de Rivoli, 75001 Paris, France"))]:
response = self.client.get(reverse("registration:update_user", args=(user.pk,)))
self.assertEqual(response.status_code, 200)

View File

@ -7,7 +7,7 @@ from .views import AddOrganizerView, AdultPhotoAuthorizationTemplateView, ChildP
InstructionsTemplateView, MyAccountDetailView, ParentalAuthorizationTemplateView, PaymentUpdateView, \
ResetAdminView, SignupView, UserDetailView, UserImpersonateView, UserListView, UserResendValidationEmailView, \
UserUpdateView, UserUploadHealthSheetView, UserUploadParentalAuthorizationView, UserUploadPhotoAuthorizationView, \
UserUploadVaccineSheetView, UserValidateView, UserValidationEmailSentView
UserValidateView, UserValidationEmailSentView
app_name = "registration"
@ -32,8 +32,6 @@ urlpatterns = [
path("instructions-template/", InstructionsTemplateView.as_view(), name="instructions_template"),
path("user/<int:pk>/upload-health-sheet/", UserUploadHealthSheetView.as_view(),
name="upload_user_health_sheet"),
path("user/<int:pk>/upload-vaccine-sheet/", UserUploadVaccineSheetView.as_view(),
name="upload_user_vaccine_sheet"),
path("user/<int:pk>/upload-parental-authorization/", UserUploadParentalAuthorizationView.as_view(),
name="upload_user_parental_authorization"),
path("update-payment/<int:pk>/", PaymentUpdateView.as_view(), name="update_payment"),

View File

@ -27,9 +27,9 @@ from participation.models import Passage, Solution, Synthesis, Tournament
from tfjm.tokens import email_validation_token
from tfjm.views import UserMixin, UserRegistrationMixin, VolunteerMixin
from .forms import AddOrganizerForm, CoachRegistrationForm, HealthSheetForm, \
from .forms import AddOrganizerForm, AdminRegistrationForm, CoachRegistrationForm, HealthSheetForm, \
ParentalAuthorizationForm, PaymentForm, PhotoAuthorizationForm, SignupForm, StudentRegistrationForm, UserForm, \
VaccineSheetForm, VolunteerRegistrationForm
VolunteerRegistrationForm
from .models import ParticipantRegistration, Payment, Registration, StudentRegistration
from .tables import RegistrationTable
@ -91,21 +91,24 @@ class AddOrganizerView(VolunteerMixin, CreateView):
context = super().get_context_data()
context["volunteer_registration_form"] = VolunteerRegistrationForm(self.request.POST or None)
context["admin_registration_form"] = AdminRegistrationForm(self.request.POST or None)
del context["volunteer_registration_form"].fields["email_confirmed"]
del context["admin_registration_form"].fields["email_confirmed"]
if not self.request.user.registration.is_admin:
context["form"].fields["type"].widget.attrs['readonly'] = True
del context["admin_registration_form"]
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
if not self.request.user.registration.is_admin:
del form.fields["admin"]
return form
@transaction.atomic
def form_valid(self, form):
registration_form = VolunteerRegistrationForm(self.request.POST)
role = form.cleaned_data["type"]
if role == "admin":
registration_form = AdminRegistrationForm(self.request.POST)
else:
registration_form = VolunteerRegistrationForm(self.request.POST)
del registration_form.fields["email_confirmed"]
if not registration_form.is_valid():
@ -344,27 +347,6 @@ class UserUploadHealthSheetView(UserRegistrationMixin, UpdateView):
return reverse_lazy("registration:user_detail", args=(self.object.user.pk,))
class UserUploadVaccineSheetView(UserRegistrationMixin, UpdateView):
"""
A participant can send its vaccine sheet.
"""
model = StudentRegistration
form_class = VaccineSheetForm
template_name = "registration/upload_vaccine_sheet.html"
extra_context = dict(title=_("Upload vaccine sheet"))
@transaction.atomic
def form_valid(self, form):
old_instance = StudentRegistration.objects.get(pk=self.object.pk)
if old_instance.vaccine_sheet:
old_instance.vaccine_sheet.delete()
old_instance.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("registration:user_detail", args=(self.object.user.pk,))
class UserUploadParentalAuthorizationView(UserRegistrationMixin, UpdateView):
"""
A participant can send its parental authorization.
@ -505,29 +487,6 @@ class HealthSheetView(LoginRequiredMixin, View):
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class VaccineSheetView(LoginRequiredMixin, View):
"""
Display the sent health sheet.
"""
def get(self, request, *args, **kwargs):
filename = kwargs["filename"]
path = f"media/authorization/vaccine/{filename}"
if not os.path.exists(path):
raise Http404
student = StudentRegistration.objects.get(vaccine_sheet__endswith=filename)
user = request.user
if not (student.user == user or user.registration.is_admin or user.registration.is_volunteer and student.team
and student.team.participation.tournament in user.registration.organized_tournaments.all()):
raise PermissionDenied
# Guess mime type of the file
mime = Magic(mime=True)
mime_type = mime.from_file(path)
ext = mime_type.split("/")[1].replace("jpeg", "jpg")
# Replace file name
true_file_name = _("Vaccine sheet of {student}.{ext}").format(student=str(student), ext=ext)
return FileResponse(open(path, "rb"), content_type=mime_type, filename=true_file_name)
class ParentalAuthorizationView(LoginRequiredMixin, View):
"""
Display the sent parental authorization.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -18,7 +18,7 @@
# -- Project information -----------------------------------------------------
project = 'Plateforme du TFJM²'
copyright = "2020-2023"
copyright = "2020-2021"
author = "Animath"

View File

@ -1,4 +0,0 @@
Développer la plateforme
========================
Cette page est dédiée aux responsables informatiques qui cherchent à contribuer à la plateforme.

View File

@ -1,21 +1,23 @@
Documentation de la plateforme du TFJM²
=======================================
Ce site vise à documenter l'usage de la plateforme de gestion du TFJM², aussi
bien du côté utilisateur⋅rice que du côté organisateur⋅rice ou bien
administrateur⋅rice.
.. image:: https://gitlab.com/animath/si/plateforme-tfjm/badges/master/pipeline.svg
:target: https://gitlab.com/animath/si/plateforme-tfjm/-/commits/master
:alt: Pipeline status
.. image:: https://gitlab.com/animath/si/plateforme-tfjm/badges/master/coverage.svg
:target: https://gitlab.com/animath/si/plateforme-tfjm/-/commits/master
:alt: Coverage report
.. toctree::
:maxdepth: 3
:caption: Utiliser
user
orga
.. image:: https://img.shields.io/badge/License-GPL%20v3-blue.svg
:target: https://www.gnu.org/licenses/gpl-3.0.txt
:alt: License: GPL v3
.. toctree::
:maxdepth: 3
:caption: Développer
dev/index
.. toctree::
:maxdepth: 3
:caption: Jouer

View File

@ -1,114 +0,0 @@
Partie organisateur⋅rices
=========================
.. contents::
Cette page est dédiée aux organisateur⋅rices qui souhaitent utiliser la plateforme pour gérer
les différentes équipes et les inscriptions.
Ajouter un⋅e nouvelleau organisateur⋅rice
-----------------------------------------
Seul⋅es les actuel⋅les organisateur⋅rices peuvent en ajouter de nouvelleaux. Il n'est pas possible
de s'inscrire.
Pour cela, il faut se connecter, aller dans l'onglet « Utilisateur⋅rices », puis « Ajouter un⋅e organisateur⋅rice ».
Les informations suivantes sont demandées :
* Prénom
* Nom de famille
* Adresse e-mail (préférer une adresse institutionnelle)
* Rôle (Bénévole ou administrateur⋅rice)
* Activité professionnelle (permet de savoir d'où viennent les bénévoles)
Les bénévoles peuvent gérer ce qui les concernent (tournois, jurys,…), les administrateur⋅rices ont un accès
intégral à l'ensemble de la plateforme, non restreint. Ce dernier statut ne devrait être réservé qu'aux membres du CNO.
Une fois le formulaire validé, un mail est envoyé permettant de définir son mot de passe. Iel peut ensuite se
connecter en utilisant son adresse e-mail et son mot de passe.
Gestion des tournois
--------------------
Créer un tournoi
""""""""""""""""
.. important::
Seul⋅es les administrateur⋅rices peuvent créer des tournois.
Pour créer un tournoi, il suffit de cliquer dans l'onglet « Tournois » puis « Ajouter un tournoi ».
Les descriptions des différents paramètres sont dans la section suivante.
Modifier un tournoi
"""""""""""""""""""
.. important::
Seul⋅es les administrateur⋅rices ainsi que les organisateur⋅rices dudit tournoi peuvent modifier le tournoi.
Pour modifier un tournoi, il faut déjà se rendre sur la page du tournoi : onglet « Tournois » puis cliquer sur
le bon tournoi. Le bouton « Modifier le tournoi » devrait être accessible.
.. warning::
Si le bouton n'est pas visible, vérifiez que vous êtes bien connecté⋅e, et que vous êtes bien marqué⋅es parmi
les organisateur⋅rices. N'hésitez pas à les contacter si ce n'est pas le cas.
Les informations suivantes peuvent être modifiées :
* Nom du tournoi
* Date de début (le samedi)
* Date de fin (le dimanche)
* Adresse du lieu physique
* Nombre indicatif maximal d'équipes autorisées
(un multiple de 3, n'est là qu'à titre indicatif et n'est pas bloquant pour la suite)
* Prix demandé aux participant⋅es, normalement 21 € sauf pour la finale 35 € (hors boursièr⋅es et tournois en visio)
* La case « À distance » doit rester décochée tant que les tournois en visio n'ont pas repris
* Date limite d'inscription : date jusqu'à laquelle les équipes peuvent finaliser leur inscription (non bloquant).
En général le mois précédent le tournoi
* Date limite pour envoyer les solutions : date jusqu'à laquelle les équipes peuvent soumettre leurs solutions
(au-delà, le remplacement de solutions déjà soumises n'est plus permis mais l'envoi de nouvelles reste possible en
cas de besoin, à décourager). En général le dimanche avant le tournoi vers 22h
* Tirage au sort : date indicative qui dit quand le tirage au sort va se dérouler. En général le mardi précédent
le tournoi vers 20h
* Date limite pour envoyer les notes de synthèses pour la première/seconde phase : même règle que pour les solutions,
mais pour les notes de synthèse. Généralement le vendredi à 22h pour le premier tour et le dimanche à 10h pour le
second tour
* Date à laquelle les solutions pour le second tour sont accessibles : seules les solutions pour le premier tour sont
directement accessible après le tirage au sort, celles pour le second tour sont libérées automatiquement une fois
cette date passée. Généralement le samedi entre 17h et 18h (à adapter)
* Description
* Organisateur⋅rices : liste des personnes qui organisent le tournoi et peuvent le gérer numériquement. N'inclut pas
les juré⋅es
* Finale (admin uniquement) : cette case ne doit être cochée que pour le tournoi de la finale.
Liste des équipes
"""""""""""""""""
Lorsque les équipes choisissent leur tournoi, elle est répertoriée sur la page du tournoi.
Valider une équipe
""""""""""""""""""
Lorsqu'une équipe a finalisé son inscription et a demandé à être validée, un mail est envoyé à l'ensemble des
organisateur⋅rices. Sur l'interface du tournoi, il est possible de cliquer sur l'équipe et accéder à l'ensemble
de ses informations :
* Nom de l'équipe
* Trigramme
* Encadrant⋅es
* Participant⋅es
* Diverses autorisations
* Lettre de motivation
Lorsqu'il est temps de valider l'équipe, un formulaire dédié apparaît en bas. Un texte peut être envoyé à l'équipe,
et le choix est proposé de valider l'équipe ou non.
.. TODO
.. note::
Cette documentation sera complétée à l'avenir pour prendre en compte les enjeux du tournoi au moment venu.

View File

@ -1,222 +0,0 @@
Utiliser la plateforme
======================
.. contents::
Cette plateforme est conçue pour que les différentes équipes puissent s'inscrire au TFJM² et envoyer
leurs solutions.
Si vous êtes ici, c'est que vous avez des questions sur l'utilisation du site en tant que participant⋅e.
Les inscriptions ouvrent dans le courant du mois de janvier, généralement une semaine après la publication
des problèmes. Pour l'édition 2023, les inscriptions ouvrent le 11 janvier 2023.
Tout se passe sur le site https://inscription.tfjm.org/.
S'inscrire
----------
Il est important de noter que chaque personne d'une équipe doit s'inscrire, y compris les encadrant⋅es.
Rendez-vous sur le site d'inscription, bouton « S'inscrire » en haut à droite.
Les informations suivantes sont requises pour tout le monde :
* Prénom d'usage
* Nom de famille (ou d'usage)
* Adresse électronique (sera vérifiée)
* Mot de passe et confirmation
* Rôle (participant⋅e ou encadrant⋅e)
* Date de naissance
* Genre
* Adresse postale
* Numéro de téléphone de contact
* Problèmes de santé à déclarer
* Donne son consentement pour se faire recontacter par Animath
Informations demandées exclusivement aux élèves :
* Classe (seconde ou avant/première/terminale)
* Établissement scolaire
* Nom du responsable légal
* Numéro de téléphone du responsable légal
* Adresse e-mail du responsable légal
Informations exclusivement demandées aux encadrant⋅es :
* Activité professionnelle
Une fois inscrit⋅e, vous recevrez par mail un lien de confirmation, qu'il vous faudra cliquer.
Connexion
---------
Une fois inscrit⋅e, vous pouvez vous connecter en utilisant le bouton en haut à droite.
Dans le champ « nom d'utilisateur », rentrez votre adresse mail. Si vous avez oublié votre mot de
passe, un formulaire est disponible pour vous aider à le réinitialiser. Vous pouvez ensuite vous
connecter. Votre prénom et votre nom apparaîtra en haut à droite.
Créer une équipe
----------------
Il suffit d'une seule personne (participant⋅e ou encadrant⋅e) pour créer une équipe. Pour créer une
équipe, il faut cliquer sur le bouton « créer une équipe ». Un nom d'équipe et un trigramme seront
demandés. Le trigramme est composé de 3 lettres majuscules, c'est ce qui permettra aux
organisateur⋅rices d'identifier rapidement votre équipe.
.. image:: /_static/img/create_team.png
:alt: Création d'une équipe
Une fois l'équipe créée, vous obtenez un code à 6 caractères, lettres ou chiffre. Ce code est à
transettre à l'ensemble des membres de votre équipe (et seulement à elleux).
.. image:: /_static/img/team_info.png
:alt: Information sur l'équipe nouvellement créée
Rejoindre une équipe
--------------------
Si l'équipe est déjà créée, vous aurez besoin du code d'accès transmis par la personne ayant créé
l'équipe. Vous pouvez cliquer sur « Rejoindre une équipe », et entrer le code.
.. image:: /_static/img/join_team.png
:alt: Rejoindre une équipe par son code d'accès
Vous avez désormais normalement rejoint l'équipe.
En cas de problème, ou si vous ne savez pas de quel code on parle, contactez-nous à l'adresse
contact@tfjm.org.
Informations sur les tournois
-----------------------------
Les tournois peuvent être trouvés dans l'onglet « Tournois ». Vous avez accès, pour chaque tournoi,
à l'ensemble des dates importantes et les lieux des tournois.
.. image:: /_static/img/tournament_info.png
:alt: Informations sur un tournoi
Davantage d'informations peuvent être trouvées sur le site vitrine : https://tfjm.org/infos-tournois/.
Choisir un tournoi
------------------
Pour accéder aux paramètres de votre équipe, vous pouvez aller sur l'onglet « Mon équipe », dans la
barre de navigation.
Pour choisir votre tournoi, il vous suffit de vous rendre sur la page de votre équipe et de cliquer
sur « Modifier ». Un formulaire vous permet alors de choisir votre tournoi.
.. image:: /_static/img/choose_tournament.png
:alt: Formulaire de mise à jour de l'équipe permettant de choisir un tournoi
Attention cependant : cela ne confirme pas votre inscription. Vous devez pour cela envoyer l'ensemble
de vos documents (voir ci-dessous).
Transmettre ses documents
-------------------------
Pour valider votre inscription, vous devez :
* Avoir choisi un tournoi ;
* Que chaque membre de l'équipe ait transmis :
* Autorisation de droit à l'image ;
* Fiche sanitaire de liaison et carnet de vaccination (pour les mineur⋅es) ;
* Autorisation parentale (pour les mineur⋅es) ;
* Transmettre une lettre de motivation.
La lettre de motivation doit être envoyée une seule fois pour toute l'équipe, peut être envoyée
depuis l'interface « Mon équipe », au format PDF, dont le contenu est défini dans l'article 5.3
du guide de læ participant⋅e : https://tfjm.org/reglement/.
Concernant les documents personnels, ils peuvent être envoyés depuis le menu « Mon compte », qui
peut être trouvé en haut à droite dans la barre de navigation. Chaque fichier doit être envoyé
au format PDF et peser moins de 2 Mo.
.. image:: /_static/img/user_info.png
:alt: Informations sur l'utilisateur⋅rice
En cas de besoin, contactez-nous à l'adresse contact@tfjm.org.
Valider son équipe
------------------
Pour prétendre à la validation, il faut que l'équipe compte au moins 1 encadrant⋅e et 4 participant⋅es.
Il faut ensuite que la lettre de motivation soit transmise, le tournoi choisi et que tous les documents
nécessaires ont été transmis (voir section précédente).
Une fois tous les prérequis réunis, sur la page « Mon équipe », il est possible de cliquer sur le bouton
pour demander la validation.
.. image:: /_static/img/validate_team.png
:alt: Formulaire de validation d'équipe
.. warning::
Les places étant limitées, rien ne garantit que vous pourrez avoir votre place dans le tournoi. Nous
vous encourageons à respecter un maximum les critères définis dans le règlement :
https://tfjm.org/reglement/. Selon les disponiblités et votre position géographique, il pourra
vous être proposé de participer à un tournoi voisin.
Une fois les deadlines dépassées, rien ne vous garantit une place au TFJM², alors attention aux dates.
Vous recevrez par mail une réponse des organisateur⋅rices locaux⋅ales. En cas de besoin, contactez-nous
à l'adresse contact@tfjm.org.
Payer son inscription
---------------------
Une fois votre inscription validée, il vous faudra payer votre inscription. Les frais s'élèvent à
23 € par élève, sauf pour les élèves boursièr⋅es qui en sont exonéré⋅es. Les encadrant⋅es n'ont pas
à payer.
.. note::
Ces frais couvrent une partie des frais de restauration et d'hébergement. L'organisation reste
bénévole.
.. TODO
.. note::
Cette section sera mise à jour plus tard.
Envoyer ses solutions
---------------------
.. TODO
.. note::
Cette section sera mise à jour plus tard.
Participer au tirage au sort
----------------------------
.. TODO
.. note::
Cette section sera mise à jour plus tard.
Envoyer ses notes de synthèse
-----------------------------
.. TODO
.. note::
Cette section sera mise à jour plus tard.
Récupérer les solutions adverses
--------------------------------
.. TODO
.. note::
Cette section sera mise à jour plus tard.

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,24 @@
Django>=3.2,<4.0
django-cas-server~=2.0
django-crispy-forms~=1.14
django-extensions~=3.2
django-filter~=22.1
django-haystack~=3.2
django-mailer~=2.2
django-phonenumber-field~=7.0
django-polymorphic~=3.1
django-address~=0.2
django-bootstrap-datepicker-plus~=4.0
django-cas-server~=1.3
django-crispy-forms~=1.9
django-extensions~=3.0
django-filter~=2.4
django-haystack~=3.0
django-mailer~=2.1
django-phonenumber-field~=5.0.0
django-polymorphic~=3.0
django-tables2~=2.4
djangorestframework~=3.14
djangorestframework~=3.12
django-rest-polymorphic~=0.1
gunicorn~=20.1
matrix-nio~=0.20
phonenumbers~=8.12.57
psycopg2-binary~=2.9.5
pypdf~=3.4
ipython~=8.5.0
python-magic~=0.4.26
requests~=2.28.1
sympasoap~=1.1
matrix-nio~=0.16
phonenumbers~=8.9.10
psycopg2-binary~=2.8
PyPDF3~=1.0.2
ipython~=7.19.0
python-magic>=0.4.22
requests~=2.25.1
sympasoap~=1.0
whoosh~=2.7

View File

@ -24,7 +24,7 @@ PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
APPS_DIR = os.path.realpath(os.path.join(BASE_DIR, "apps"))
sys.path.append(APPS_DIR)
ADMINS = [("Emmy D'Anello", "emmy.danello@animath.fr")]
ADMINS = [("Yohann D'ANELLO", "yohann.danello@animath.fr")]
# Quick-start development settings - unsuitable for production
@ -53,6 +53,8 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.forms',
'address',
'bootstrap_datepicker_plus',
'crispy_forms',
'django_filters',
'django_tables2',
@ -224,6 +226,11 @@ else:
}
}
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403
else:
from .settings_dev import * # noqa: F401,F403
# Custom phone number format
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR'
@ -235,25 +242,3 @@ JQUERY_URL = False
# Custom parameters
PROBLEM_COUNT = 8
FORBIDDEN_TRIGRAMS = [
"BIT",
"CNO",
"CRO",
"CUL",
"FTG",
"FCK",
"FUC",
"FUK",
"FYS",
"HIV",
"IST",
"MST",
"KKK",
"KYS",
"SEX",
]
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403
else:
from .settings_dev import * # noqa: F401,F403

View File

@ -8,7 +8,7 @@
<div class="text-justify">
<p>
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2022
par Emmy D'Anello, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants
par Yohann D'ANELLO, bénévole pour l'association Animath. Elle est vouée à être utilisée par les participants
pour intéragir avec les organisateurs et les autres participants.
</p>

View File

@ -5,12 +5,11 @@
<div class="alert alert-success">
<p>
Les inscriptions pour la session 2023 sont à présent ouvertes, vous pouvez créer votre compte.
Prenez garde toutefois aux dates indiquées qui sont pour l'instant provisoires.
Les inscriptions sont à présent ouvertes, vous pouvez créer votre compte. Prenez garde toutefois
aux dates indiquées qui sont pour l'instant provisoires.
</p>
<p>
Une documentation plus complète est disponible à l'adresse
<a href="https://inscription.tfjm.org/doc/">https://inscription.tfjm.org/doc/</a>.
Une documentation plus complète sera disponible dans les jours à venir et régulièrement mise à jour pour apprendre à utiliser la plateforme.
</p>
</div>
@ -37,6 +36,20 @@
</div>
</div>
<div class="alert alert-success">
<h4><strong><i class="fas fa-newspaper"></i> Informations 2022 :</strong></h4>
<p>
Après 2 ans de pandémie, le tournoi devrait (enfin !) revenir en présentiel. Selon les mesures
gouvernementales en vigueur au moment du tournoi et peut-être même selon le lieu d'accueil du tournoi,
le pass sanitaire ou vaccinal pourra être requis.
</p>
<p>
La plateforme <a class="alert-link" href="https://element.tfjm.org">element.tfjm.org</a> reste ouverte
cette année pour faciliter d'éventuelles communications. C'est ici notamment que se déroulera le tirage
au sort. Nous vous invitons à tenter de vous connecter sur la plateforme une fois votre compte créé.
</p>
</div>
<div class="jumbotron">
<h5 class="display-4">Comment ça marche ?</h5>
<p>
@ -46,10 +59,10 @@
<p class="text-justify">
Vous pouvez accéder à votre compte via la rubrique <strong><a href="{% url "login" %}">Connexion</a></strong>.
Une fois connecté⋅e, vous pourrez créer une équipe ou en rejoindre une déjà créée par l'un⋅e de vos camarades
via un code d'accès qui vous aura été transmis. Vous serez ensuite invité⋅e à soumettre une autorisation de droit à l'image,
indispensable au bon déroulement du 𝕋𝔽𝕁𝕄². Une fois que votre équipe comporte au moins 4 participant⋅es (maximum 6)
et un⋅e encadrant⋅e, vous pourrez demander à valider votre équipe pour être apte à travailler sur les problèmes de votre choix.
Une fois connecté, vous pourrez créer une équipe ou en rejoindre une déjà créée par l'un de vos camarades
via un code d'accès qui vous aura été transmis. Vous serez ensuite invité à soumettre une autorisation de droit à l'image,
indispensable au bon déroulement du 𝕋𝔽𝕁𝕄². Une fois que votre équipe comporte au moins 4 participants (maximum 6)
et un encadrant, vous pourrez demander à valider votre équipe pour être apte à travailler sur les problèmes de votre choix.
</p>
<h2>Je ne trouve pas d'équipe, aidez-moi !</h2>
@ -59,13 +72,13 @@
au 𝕋𝔽𝕁𝕄², basée sur le protocole <a href="https://matrix.org/">Matrix</a> et le client
<a href="https://element.io">Element</a>. Cette interface de chat vous permet de communiquer avec les membres
de votre équipe, mais surtout de pouvoir participer au tirage au sort et discuter avec les autres équipes et
les organisateur⋅rices.
les organisateurs.
</p>
<p class="text-justify">
Ce chat contient également un salon <code>#je-cherche-une-equipe</code> où vous pouvez crier à l'aide pour trouver
une équipe, ou compléter la votre s'il vous manque des participant⋅es. C'est un petit coin auprès du feu,
ne cherchez pas à être trop formel⋅le.
une équipe, ou compléter la votre s'il vous manque des participants. C'est un petit coin auprès du feu, un parc à
jeu où vous pouvez descendre en toboggan (en le désinfectant après utilisation), ne cherchez pas à être trop formel.
</p>
<h2>J'ai une question</h2>

View File

@ -23,7 +23,7 @@ from django.views.defaults import bad_request, page_not_found, permission_denied
from django.views.generic import TemplateView
from participation.views import MotivationLetterView
from registration.views import HealthSheetView, ParentalAuthorizationView, PhotoAuthorizationView, \
ScholarshipView, SolutionView, SynthesisView, VaccineSheetView
ScholarshipView, SolutionView, SynthesisView
from .views import AdminSearchView
@ -44,8 +44,6 @@ urlpatterns = [
name='photo_authorization'),
path('media/authorization/health/<str:filename>/', HealthSheetView.as_view(),
name='health_sheet'),
path('media/authorization/vaccine/<str:filename>/', VaccineSheetView.as_view(),
name='vaccine_sheet'),
path('media/authorization/parental/<str:filename>/', ParentalAuthorizationView.as_view(),
name='parental_authorization'),
path('media/authorization/scholarship/<str:filename>/', ScholarshipView.as_view(),

21
tox.ini
View File

@ -12,18 +12,19 @@ sitepackages = False
deps =
coverage
Django>=3.2,<4.0
django-crispy-forms~=1.14
django-filter~=22.1
django-haystack~=3.2
django-phonenumber-field~=7.0
django-polymorphic~=3.1
django-address~=0.2
django-bootstrap-datepicker-plus~=4.0
django-crispy-forms~=1.9
django-filter~=2.4
django-haystack~=3.0
django-phonenumber-field~=5.0.0
django-polymorphic~=3.0
django-tables2~=2.4
djangorestframework~=3.14
djangorestframework~=3.12
django-rest-polymorphic~=0.1
phonenumbers~=8.12.57
pypdf~=3.4
python-magic~=0.4.26
requests~=2.28.1
phonenumbers~=8.9.10
PyPDF3~=1.0.2
python-magic==0.4.22
whoosh~=2.7
commands =
coverage run --source=apps,tfjm ./manage.py test apps/ tfjm/