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

Compare commits

..

No commits in common. "9ec35c917f098afbb8c462b4b862ddd92a014141" and "2f4755ffc7e849456fb752352b7bd4e4f5ad55d4" have entirely different histories.

32 changed files with 1409 additions and 462 deletions

View File

@ -2,24 +2,22 @@ stages:
- test
- quality-assurance
py310:
stage: test
image: python:3.10-alpine
before_script:
- apk add --no-cache libmagic
- pip install tox --no-cache-dir
script: tox -e py310
py311:
stage: test
image: python:3.11-alpine
before_script:
- apk add --no-cache libmagic
- apk add --no-cache git # Useful for django-haystack, remove when the newer versions are in PyPI
- pip install tox --no-cache-dir
script: tox -e py311
py312:
stage: test
image: python:3.12-alpine
before_script:
- apk add --no-cache libmagic
- apk add --no-cache git # Useful for django-haystack, remove when the newer versions are in PyPI
- pip install tox --no-cache-dir
script: tox -e py312
linters:
stage: quality-assurance
image: python:3-alpine

View File

@ -1,4 +1,4 @@
FROM python:3.12-alpine
FROM python:3.11-alpine
ENV PYTHONUNBUFFERED 1
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
@ -13,6 +13,8 @@ COPY requirements.txt /code/requirements.txt
COPY docs/requirements.txt /code/docs/requirements.txt
RUN pip install -r requirements.txt --no-cache-dir
RUN pip install -r docs/requirements.txt --no-cache-dir
# FIXME Remove this line when all dependencies will be ready
RUN pip install "Django>=4.2,<5.0"
COPY . /code/

View File

@ -54,13 +54,14 @@ SERVER_EMAIL=contact@tfjm.org # Adresse e-mail expéditrice
SYMPA_URL=lists.example.com # Serveur Sympa à utiliser
SYMPA_EMAIL= # Adresse e-mail du compte administrateur de Sympa
SYMPA_PASSWORD= # Mot de passe du compte administrateur de Sympa
SYNAPSE_PASSWORD= # Mot de passe du robot Matrix
```
Si le type de base de données sélectionné est SQLite, la variable `DJANGO_DB_HOST` sera utilisée en guise de chemin vers
le fichier de base de données (par défaut, `db.sqlite3`).
En développement, il est recommandé d'utiliser SQLite pour des raisons de simplicité. Les paramètres de mail ne seront
pas utilisés, et les mails qui doivent être envoyés seront envoyés dans la console. L'intégration mail
pas utilisés, et les mails qui doivent être envoyés seront envoyés dans la console. Les intégrations mail et Matrix
seront également désactivées.
En production, il est recommandé de ne pas utiliser SQLite pour des raisons de performances.

View File

@ -9,7 +9,7 @@ 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 2024, les inscriptions ouvrent le 17 janvier 2024.
des problèmes. Pour l'édition 2023, les inscriptions ouvrent le 11 janvier 2023.
Tout se passe sur le site https://inscription.tfjm.org/.

View File

@ -1,31 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-13 16:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("draw", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="teamdraw",
name="purposed",
field=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"),
],
default=None,
null=True,
verbose_name="purposed problem",
),
),
]

View File

@ -4,7 +4,6 @@ crond -l 0
python manage.py migrate
python manage.py loaddata initial
python manage.py update_index
nginx

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,477 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
import asyncio
import os
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 tfjm.matrix import Matrix, RoomPreset, RoomVisibility
class Command(BaseCommand):
def handle(self, *args, **options): # noqa: C901
activate("fr")
async def main():
await 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"):
avatar_uri = await Matrix.get_avatar()
if isinstance(avatar_uri, str):
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
else:
stat_file = os.stat("tfjm/static/logo.png")
with open("tfjm/static/logo.png", "rb") as f:
resp = (await Matrix.upload(f, filename="../../../tfjm/static/logo.png", content_type="image/png",
filesize=stat_file.st_size))[0][0]
avatar_uri = resp.content_uri
with open(".matrix_avatar", "w") as f:
f.write(avatar_uri)
await Matrix.set_avatar(avatar_uri)
with open(".matrix_avatar", "r") as f:
avatar_uri = f.read().rstrip(" \t\r\n")
# Create basic channels
if not await Matrix.resolve_room_alias("#aide-jurys-orgas:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="aide-jurys-orgas",
name="Aide jurys & orgas",
topic="Pour discuter de propblèmes d'organisation",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#annonces:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="annonces",
name="Annonces",
topic="Informations importantes du TFJM²",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bienvenue:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bienvenue",
name="Bienvenue",
topic="Bienvenue au TFJM² 2023 !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="bot",
name="Bot",
topic="Vive les r0b0ts",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#cno:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="cno",
name="CNO",
topic="Channel des dieux",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#dev-bot:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="dev-bot",
name="Bot - développement",
topic="Vive le bot",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias("#faq:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="faq",
name="FAQ",
topic="Posez toutes vos questions ici !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#flood:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias="flood",
name="Flood",
topic="Discutez de tout et de rien !",
federate=False,
preset=RoomPreset.public_chat,
)
if not await Matrix.resolve_room_alias("#je-cherche-une-equipe:tfjm.org"):
await 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,
)
# Setup avatars
await Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
await Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
# Read-only channels
await Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
await Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
# Invite everyone to public channels
for r in Registration.objects.all():
await Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
await Matrix.invite("#je-cherche-une-equipe:tfjm.org",
f"@{r.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {r} in most common channels...")
# Volunteers have access to the help channel
for volunteer in VolunteerRegistration.objects.all():
await Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
# Admins are admins
for admin in VolunteerRegistration.objects.filter(admin=True).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")
self.stdout.write(f"Give admin permissions for {admin}...")
await Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#annonces:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bienvenue:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#cno:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#dev-bot:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#faq:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#flood:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Create tournament-specific channels
for tournament in Tournament.objects.all():
self.stdout.write(f"Managing tournament of {tournament.name}.")
name = tournament.name
slug = name.lower().replace(" ", "-")
if not await Matrix.resolve_room_alias(f"#annonces-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"annonces-{slug}",
name=f"{name} - Annonces",
topic=f"Annonces du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#general-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"general-{slug}",
name=f"{name} - Général",
topic=f"Accueil du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#flood-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"flood-{slug}",
name=f"{name} - Flood",
topic=f"Discussion libre du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#jury-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"jury-{slug}",
name=f"{name} - Jury",
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#orga-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"orga-{slug}",
name=f"{name} - Organisateurs",
topic=f"Discussion entre les orgas du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#tirage-au-sort-{slug}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"tirage-au-sort-{slug}",
name=f"{name} - Tirage au sort",
topic=f"Tirage au sort du tournoi de {name}",
federate=False,
preset=RoomPreset.private_chat,
)
# Setup avatars
await Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
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():
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")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#general-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite participants
for participation in tournament.participations.filter(valid=True).all():
for participant in participation.team.participants.all():
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
await Matrix.invite(f"#annonces-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create pool-specific channels
for pool in tournament.pools.all():
self.stdout.write(f"Managing {pool}...")
five = pool.participations.count() >= 5
for i in range(2 if five else 1):
# Fix for five teams-pools
suffix = f"-{chr(ord('A') + i)}" if five else ""
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}",
name=f"{name} - Jour {pool.round} - Poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les équipes - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
if not await Matrix.resolve_room_alias(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"poule-{slug}-{pool.id}{suffix}-jurys",
name=f"{name} - Jour {pool.round}{suffix} - Jurys poule " +
', '.join(participation.team.trigram
for participation in pool.participations.all()) + suffix,
topic=f"Discussion avec les jurys - {pool}{suffix}",
federate=False,
preset=RoomPreset.private_chat,
)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org", avatar_uri)
await Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org", avatar_uri)
bbb_url = pool.bbb_url.strip()
if five and ';' in bbb_url:
bbb_url = bbb_url.split(";")[i].strip()
url_params = urlencode(dict(url=bbb_url,
isAudioConf='false', displayName='$matrix_display_name',
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
.replace("%24", "$")
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
f"bbb-{slug}-{pool.id}{suffix}", "bigbluebutton", "BigBlueButton", str(pool))
await Matrix.add_integration(
f"#poule-{slug}-{pool.id}:tfjm.org",
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
"customwidget", "Tableau", str(pool))
# Invite admins and give permissions
for admin in VolunteerRegistration.objects.filter(admin=True).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",
f"@{admin.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{admin.matrix_username}:tfjm.org", 95)
# Invite organizers and give permissions
for orga in tournament.organizers.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org")
if not orga.is_admin:
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{orga.matrix_username}:tfjm.org", 50)
# Invite the jury, give good permissions
for jury in pool.juries.all():
await Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
await Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org")
if not jury.is_admin:
await Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
await Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}{suffix}-jurys:tfjm.org",
f"@{jury.matrix_username}:tfjm.org", 50)
# Invite participants to the right pool
for participation in pool.participations.all():
for participant in participation.team.participants.all():
await Matrix.invite(f"#poule-{slug}-{pool.id}{suffix}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
# Create private channels for teams
for team in Team.objects.all():
self.stdout.write(f"Create private channel for {team}...")
if not await Matrix.resolve_room_alias(f"#equipe-{team.trigram.lower()}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"equipe-{team.trigram.lower()}",
name=f"Équipe {team.trigram}",
topic=f"Discussion interne de l'équipe {team.name}",
federate=False,
preset=RoomPreset.private_chat,
)
for participant in team.participants.all():
await Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org")
await Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
f"@{participant.matrix_username}:tfjm.org", 50)
"""
# Manage channels to discuss about problems
for i in range(9):
self.stdout.write(f"Create channel for problem {i}...")
if not await Matrix.resolve_room_alias(f"#mec-{i}:tfjm.org"):
await Matrix.create_room(
visibility=RoomVisibility.public,
alias=f"mec-{i}",
name=f"Mise en commun - {'Général' if i == 0 else f'Problème {i}'}",
topic=f"Discussion autour des problèmes",
federate=False,
preset=RoomPreset.public_chat,
invite=[f"@{registration.matrix_username}:tfjm.org"
for registration in Registration.objects.all()],
power_level_override={
f"@{registration.matrix_username}:tfjm.org": (95 if registration.is_admin else 50)
for registration in VolunteerRegistration.objects.all()
},
)
await Matrix.set_room_avatar(f"#mec-{i}:tfjm.org", avatar_uri)
for registration in Registration.objects.all():
await Matrix.invite(f"#mec-{i}:tfjm.org", registration.matrix_username)
for registration in VolunteerRegistration.objects.all():
await Matrix.set_room_power_level(f"#mec-{i}:tfjm.org",
f"@{registration.matrix_username}:tfjm.org",
95 if registration.is_admin else 50)
"""
asyncio.get_event_loop().run_until_complete(main())

View File

@ -16,6 +16,7 @@ from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy as _
from registration.models import VolunteerRegistration
from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
def get_motivation_letter_filename(instance, filename):
@ -100,9 +101,18 @@ class Team(models.Model):
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):

View File

@ -19,11 +19,12 @@ def create_team_participation(instance, created, raw, **_):
def update_mailing_list(instance: Team, raw, **_):
"""
When a team name or trigram got updated, update mailing lists
When a team name or trigram got updated, update mailing lists and Matrix rooms
"""
if instance.pk and not raw:
old_team = Team.objects.get(pk=instance.pk)
if 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()

View File

@ -5,9 +5,35 @@
{% block content %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
The chat feature is now out of usage. If you feel that having a chat
feature between participants is important, for example to build a
team, please contact us.
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 %}

View File

@ -32,6 +32,7 @@ from odf.table import CoveredTableCell, Table, TableCell, TableColumn, TableRow
from odf.text import P
from registration.models import StudentRegistration, VolunteerRegistration
from tfjm.lists import get_sympa_client
from tfjm.matrix import Matrix
from tfjm.views import AdminMixin, VolunteerMixin
from .forms import AddJuryForm, JoinTeamForm, MotivationLetterForm, NoteForm, ParticipationForm, PassageForm, \
@ -67,7 +68,8 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
"""
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.
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
@ -80,6 +82,9 @@ class CreateTeamView(LoginRequiredMixin, CreateView):
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
@ -107,7 +112,7 @@ class JoinTeamView(LoginRequiredMixin, FormView):
def form_valid(self, form):
"""
When a user joins a team, the user is automatically subscribed to
the team mailing list.
the team mailing list,the user is invited in the team Matrix room.
"""
self.object = form.instance
ret = super().form_valid(form)
@ -122,6 +127,9 @@ class JoinTeamView(LoginRequiredMixin, FormView):
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):
@ -460,6 +468,9 @@ class TeamLeaveView(LoginRequiredMixin, TemplateView):
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.coaches.count() == 0:
team.delete()
return redirect(reverse_lazy("index"))

17
registration/auth.py Normal file
View File

@ -0,0 +1,17 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover
class CustomAuthUser(DjangoAuthUser): # pragma: no cover
"""
Override Django Auth User model to define a custom Matrix username.
"""
def attributs(self):
d = super().attributs()
if self.user:
d["matrix_username"] = self.user.registration.matrix_username
d["display_name"] = str(self.user.registration)
return d

View File

@ -0,0 +1,26 @@
[
{
"model": "cas_server.servicepattern",
"pk": 1,
"fields": {
"pos": 100,
"name": "Plateforme du TFJM²",
"pattern": "^https://tfjm.org(:8448)?/.*$",
"user_field": "matrix_username",
"restrict_users": false,
"proxy": true,
"proxy_callback": true,
"single_log_out": true,
"single_log_out_callback": ""
}
},
{
"model": "cas_server.replaceattributname",
"pk": 1,
"fields": {
"name": "display_name",
"replace": "",
"service_pattern": 1
}
}
]

View File

@ -107,8 +107,8 @@ class StudentRegistrationForm(forms.ModelForm):
class Meta:
model = StudentRegistration
fields = ('team', 'student_class', 'birth_date', 'gender', 'address', 'zip_code', 'city', 'phone_number',
'school', 'health_issues', 'housing_constraints', 'responsible_name', 'responsible_phone',
'responsible_email', 'give_contact_to_animath', 'email_confirmed',)
'health_issues', 'school', 'responsible_name', 'responsible_phone', 'responsible_email',
'give_contact_to_animath', 'email_confirmed',)
class PhotoAuthorizationForm(forms.ModelForm):
@ -205,9 +205,8 @@ class CoachRegistrationForm(forms.ModelForm):
"""
class Meta:
model = CoachRegistration
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'phone_number',
'professional_activity', 'health_issues', 'housing_constraints',
'give_contact_to_animath', 'email_confirmed',)
fields = ('team', 'gender', 'address', 'zip_code', 'city', 'phone_number', 'health_issues',
'professional_activity', 'give_contact_to_animath', 'email_confirmed',)
class VolunteerRegistrationForm(forms.ModelForm):

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-16 21:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registration", "0008_alter_payment_valid"),
]
operations = [
migrations.AddField(
model_name="participantregistration",
name="housing_constraints",
field=models.TextField(
blank=True,
help_text="You can fill in something here if you have any housing constraints, e.g. medical problems, scheduling issues, gender issues, or anything else you feel is relevant to the organizers. Leave empty if you have nothing specific to declare.",
verbose_name="housing constraints",
),
),
migrations.AlterField(
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

@ -85,6 +85,10 @@ class Registration(PolymorphicModel):
def is_volunteer(self):
return isinstance(self, VolunteerRegistration)
@property
def matrix_username(self):
return f"tfjm_{self.user.pk}"
def get_absolute_url(self):
return reverse_lazy("registration:user_detail", args=(self.user_id,))
@ -157,16 +161,7 @@ class ParticipantRegistration(Registration):
health_issues = models.TextField(
verbose_name=_("health issues"),
blank=True,
help_text=_("You can indicate here your allergies or anything that is important to know for organizers."),
)
housing_constraints = models.TextField(
verbose_name=_("housing constraints"),
blank=True,
help_text=_("You can fill in something here if you have any housing constraints, "
"e.g. medical problems, scheduling issues, gender issues, "
"or anything else you feel is relevant to the organizers. "
"Leave empty if you have nothing specific to declare."),
help_text=_("You can indicate here your allergies or anything that is important to know for organizers"),
)
photo_authorization = models.FileField(

View File

@ -53,15 +53,8 @@
<dt class="col-sm-6 text-end">{% trans "Phone number:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.phone_number }}</dd>
{% if user_object.registration.health_issues %}
<dt class="col-sm-6 text-end">{% trans "Health issues:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.health_issues }}</dd>
{% endif %}
{% if user_object.registration.housing_constraints %}
<dt class="col-sm-6 text-end">{% trans "Housing constraints:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.housing_constraints }}</dd>
{% endif %}
<dt class="col-sm-6 text-end">{% trans "Health issues:" %}</dt>
<dd class="col-sm-6">{{ user_object.registration.health_issues|default:any }}</dd>
<dt class="col-sm-6 text-end">{% trans "Photo authorization:" %}</dt>
<dd class="col-sm-6">

View File

@ -7,6 +7,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.test import TestCase
from django.urls import reverse
from django.utils.encoding import force_bytes
@ -412,18 +413,18 @@ class TestRegistration(TestCase):
"""
Try to search some things.
"""
call_command("rebuild_index", "--noinput", "-v", 0)
response = self.client.get(reverse("haystack_search") + "?q=" + self.user.email + "&models=auth.user")
response = self.client.get(reverse("haystack_search") + "?q=" + self.user.email)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["object_list"])
response = self.client.get(reverse("haystack_search") + "?q=" +
str(self.coach.registration.professional_activity)
+ "&models=registration.CoachRegistration")
str(self.coach.registration.professional_activity))
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["object_list"])
response = self.client.get(reverse("haystack_search") + "?q=" +
self.student.registration.school + "&models=registration.StudentRegistration")
self.student.registration.get_student_class_display())
self.assertEqual(response.status_code, 200)
self.assertTrue(response.context["object_list"])

View File

@ -1,26 +1,28 @@
channels[daphne]~=4.0.0
channels-redis~=4.2.0
crispy-bootstrap5~=2023.10
Django>=5.0,<6.0
django-crispy-forms~=2.1
django-extensions~=3.2.3
django-filter~=23.5
elasticsearch~=7.17.9
git+https://github.com/django-haystack/django-haystack.git#v3.3b1
django-mailer~=2.3.1
django-phonenumber-field~=7.3.0
django-polymorphic~=3.1.0
django-tables2~=2.7.0
djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10
gunicorn~=21.2.0
channels-redis~=4.0.0
crispy-bootstrap5~=0.7
Django>=4.1,<5.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-tables2~=2.4
djangorestframework~=3.14
django-rest-polymorphic~=0.1
gunicorn~=20.1
matrix-nio~=0.20
odfpy~=1.4.1
phonenumbers~=8.13.27
psycopg2-binary~=2.9.9
pypdf~=3.17.4
ipython~=8.20.0
python-magic~=0.4.27
requests~=2.31.0
phonenumbers~=8.12.57
psycopg2-binary~=2.9.5
pypdf~=3.4
ipython~=8.11.0
python-magic~=0.4.26
requests~=2.28.1
sympasoap~=1.1
uvicorn~=0.25.0
websockets~=12.0
uvicorn~=0.17
websockets~=10.4
whoosh~=2.7

View File

@ -4,9 +4,15 @@
* * * * * cd /code && python manage.py retry_deferred -c 1
0 0 * * * cd /code && python manage.py purge_mail_log 7 -c 1
# Rebuild search index
0 * * * * cd /code && python manage.py update_index -v 0
# Recreate sympa lists
*/2 * * * * cd /code && python manage.py fix_sympa_lists &> /dev/null
# Update matrix channels
03 */6 * * * cd /code && python manage.py fix_matrix_channels &> /dev/null
# Check payments from Hello Asso
*/6 * * * * cd /code && python manage.py check_hello_asso &> /dev/null

432
tfjm/matrix.py Normal file
View File

@ -0,0 +1,432 @@
# Copyright (C) 2020 by Animath
# SPDX-License-Identifier: GPL-3.0-or-later
from enum import Enum
import os
class Matrix:
"""
Utility class to manage interaction with the Matrix homeserver.
This log in the @tfjmbot account (must be created before).
The access token is then stored.
All is done with this bot account, that is a server administrator.
Tasks are normally asynchronous, but for compatibility we make
them synchronous.
"""
_token = None
_device_id = None
@classmethod
async def _get_client(cls): # pragma: no cover
"""
Retrieve the bot account.
If not logged, log in and store access token.
"""
if not os.getenv("SYNAPSE_PASSWORD") and not os.getenv("SYNAPSE_TOKEN"):
return FakeMatrixClient()
from nio import AsyncClient
client = AsyncClient("https://tfjm.org", "@tfjmbot:tfjm.org")
client.user_id = "@tfjmbot:tfjm.org"
if os.getenv("SYNAPSE_TOKEN"):
client.access_token = os.getenv("SYNAPSE_TOKEN")
client.device_id = os.getenv("SYNAPSE_DEVICE")
return client
elif os.path.isfile(".matrix_token"):
with open(".matrix_device", "r") as f:
cls._device_id = f.read().rstrip(" \t\r\n")
client.device_id = cls._device_id
with open(".matrix_token", "r") as f:
cls._token = f.read().rstrip(" \t\r\n")
client.access_token = cls._token
return client
await client.login(password=os.getenv("SYNAPSE_PASSWORD"), device_name="Plateforme")
cls._token = client.access_token
cls._device_id = client.device_id
with open(".matrix_token", "w") as f:
f.write(cls._token)
with open(".matrix_device", "w") as f:
f.write(cls._device_id)
return client
@classmethod
async def set_display_name(cls, name: str):
"""
Set the display name of the bot account.
"""
client = await cls._get_client()
return await client.set_displayname(name)
@classmethod
async def set_avatar(cls, avatar_url: str): # pragma: no cover
"""
Set the display avatar of the bot account.
"""
client = await cls._get_client()
return await client.set_avatar(avatar_url)
@classmethod
async def get_avatar(cls): # pragma: no cover
"""
Set the display avatar of the bot account.
"""
client = await cls._get_client()
resp = await client.get_avatar()
return resp.avatar_url if hasattr(resp, "avatar_url") else resp
@classmethod
async def upload(
cls,
data_provider,
content_type: str = "application/octet-stream",
filename: str = None,
encrypt: bool = False,
monitor=None,
filesize: int = None,
): # pragma: no cover
"""
Upload a file to the content repository.
Returns a tuple containing:
- Either a `UploadResponse` if the request was successful, or a
`UploadError` if there was an error with the request
- A dict with file decryption info if encrypt is ``True``,
else ``None``.
Args:
data_provider (Callable, SynchronousFile, AsyncFile): A function
returning the data to upload or a file object. File objects
must be opened in binary mode (``mode="r+b"``). Callables
returning a path string, Path, async iterable or aiofiles
open binary file object allow the file data to be read in an
asynchronous and lazy way (without reading the entire file
into memory). Returning a synchronous iterable or standard
open binary file object will still allow the data to be read
lazily, but not asynchronously.
The function will be called again if the upload fails
due to a server timeout, in which case it must restart
from the beginning.
Callables receive two arguments: the total number of
429 "Too many request" errors that occured, and the total
number of server timeout exceptions that occured, thus
cleanup operations can be performed for retries if necessary.
content_type (str): The content MIME type of the file,
e.g. "image/png".
Defaults to "application/octet-stream", corresponding to a
generic binary file.
Custom values are ignored if encrypt is ``True``.
filename (str, optional): The file's original name.
encrypt (bool): If the file's content should be encrypted,
necessary for files that will be sent to encrypted rooms.
Defaults to ``False``.
monitor (TransferMonitor, optional): If a ``TransferMonitor``
object is passed, it will be updated by this function while
uploading.
From this object, statistics such as currently
transferred bytes or estimated remaining time can be gathered
while the upload is running as a task; it also allows
for pausing and cancelling.
filesize (int, optional): Size in bytes for the file to transfer.
If left as ``None``, some servers might refuse the upload.
"""
client = await cls._get_client()
return await client.upload(data_provider, content_type, filename, encrypt, monitor, filesize) \
if not isinstance(client, FakeMatrixClient) else None, None
@classmethod
async def create_room(
cls,
visibility=None,
alias=None,
name=None,
topic=None,
room_version=None,
federate=True,
is_direct=False,
preset=None,
invite=(),
initial_state=(),
power_level_override=None,
):
"""
Create a new room.
Returns either a `RoomCreateResponse` if the request was successful or
a `RoomCreateError` if there was an error with the request.
Args:
visibility (RoomVisibility): whether to have the room published in
the server's room directory or not.
Defaults to ``RoomVisibility.private``.
alias (str, optional): The desired canonical alias local part.
For example, if set to "foo" and the room is created on the
"example.com" server, the room alias will be
"#foo:example.com".
name (str, optional): A name to set for the room.
topic (str, optional): A topic to set for the room.
room_version (str, optional): The room version to set.
If not specified, the homeserver will use its default setting.
If a version not supported by the homeserver is specified,
a 400 ``M_UNSUPPORTED_ROOM_VERSION`` error will be returned.
federate (bool): Whether to allow users from other homeservers from
joining the room. Defaults to ``True``.
Cannot be changed later.
is_direct (bool): If this should be considered a
direct messaging room.
If ``True``, the server will set the ``is_direct`` flag on
``m.room.member events`` sent to the users in ``invite``.
Defaults to ``False``.
preset (RoomPreset, optional): The selected preset will set various
rules for the room.
If unspecified, the server will choose a preset from the
``visibility``: ``RoomVisibility.public`` equates to
``RoomPreset.public_chat``, and
``RoomVisibility.private`` equates to a
``RoomPreset.private_chat``.
invite (list): A list of user id to invite to the room.
initial_state (list): A list of state event dicts to send when
the room is created.
For example, a room could be made encrypted immediatly by
having a ``m.room.encryption`` event dict.
power_level_override (dict): A ``m.room.power_levels content`` dict
to override the default.
The dict will be applied on top of the generated
``m.room.power_levels`` event before it is sent to the room.
"""
client = await cls._get_client()
return await client.room_create(
visibility, alias, name, topic, room_version, federate, is_direct, preset, invite, initial_state,
power_level_override)
@classmethod
async def resolve_room_alias(cls, room_alias: str):
"""
Resolve a room alias to a room ID.
Return None if the alias does not exist.
"""
client = await cls._get_client()
resp = await client.room_resolve_alias(room_alias)
return resp.room_id if resp and hasattr(resp, "room_id") else None
@classmethod
async def invite(cls, room_id: str, user_id: str):
"""
Invite a user to a room.
Returns either a `RoomInviteResponse` if the request was successful or
a `RoomInviteError` if there was an error with the request.
Args:
room_id (str): The room id of the room that the user will be
invited to.
user_id (str): The user id of the user that should be invited.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_invite(room_id, user_id)
@classmethod
async def send_message(cls, room_id: str, body: str, formatted_body: str = None,
msgtype: str = "m.text", html: bool = True):
"""
Send a message to a room.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
content = {
"msgtype": msgtype,
"body": body,
"formatted_body": formatted_body or body,
}
if html:
content["format"] = "org.matrix.custom.html"
return await client.room_send(
room_id=room_id,
message_type="m.room.message",
content=content,
)
@classmethod
async def add_integration(cls, room_id: str, widget_url: str, state_key: str,
widget_type: str = "customwidget", widget_name: str = "Custom widget",
widget_title: str = ""):
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
content = {
"type": widget_type,
"url": widget_url,
"name": widget_name,
"data": {
"curl": widget_url,
"title": widget_title,
},
"creatorUserId": client.user,
"roomId": room_id,
"id": state_key,
}
return await client.room_put_state(
room_id=room_id,
event_type="im.vector.modular.widgets",
content=content,
state_key=state_key,
)
@classmethod
async def remove_integration(cls, room_id: str, state_key: str):
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_put_state(
room_id=room_id,
event_type="im.vector.modular.widgets",
content={},
state_key=state_key,
)
@classmethod
async def kick(cls, room_id: str, user_id: str, reason: str = None):
"""
Kick a user from a room, or withdraw their invitation.
Kicking a user adjusts their membership to "leave" with an optional
reason.
²
Returns either a `RoomKickResponse` if the request was successful or
a `RoomKickError` if there was an error with the request.
Args:
room_id (str): The room id of the room that the user will be
kicked from.
user_id (str): The user_id of the user that should be kicked.
reason (str, optional): A reason for which the user is kicked.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_kick(room_id, user_id, reason)
@classmethod
async def set_room_power_level(cls, room_id: str, user_id: str, power_level: int): # pragma: no cover
"""
Put a given power level to a user in a certain room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the power level
of the user should be updated.
user_id (str): The user_id of the user which power level should
be updated.
power_level (int): The target power level to give.
"""
client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
content = resp.content
content["users"][user_id] = power_level
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
async def set_room_power_level_event(cls, room_id: str, event: str, power_level: int): # pragma: no cover
"""
Define the minimal power level to have to send a certain event type
in a given room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the power level
of the event should be updated.
event (str): The event name which minimal power level should
be updated.
power_level (int): The target power level to give.
"""
client = await cls._get_client()
if isinstance(client, FakeMatrixClient):
return None
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
resp = await client.room_get_state_event(room_id, "m.room.power_levels")
content = resp.content
if event.startswith("m."):
content["events"][event] = power_level
else:
content[event] = power_level
return await client.room_put_state(room_id, "m.room.power_levels", content=content, state_key=resp.state_key)
@classmethod
async def set_room_avatar(cls, room_id: str, avatar_uri: str):
"""
Define the avatar of a room.
Returns either a `RoomPutStateResponse` if the request was successful or
a `RoomPutStateError` if there was an error with the request.
Args:
room_id (str): The room id of the room where the avatar
should be changed.
avatar_uri (str): The internal avatar URI to apply.
"""
client = await cls._get_client()
if room_id.startswith("#"):
room_id = await cls.resolve_room_alias(room_id)
return await client.room_put_state(room_id, "m.room.avatar", content={
"url": avatar_uri
}, state_key="")
if os.getenv("SYNAPSE_PASSWORD"): # pragma: no cover
from nio import RoomVisibility, RoomPreset
RoomVisibility = RoomVisibility
RoomPreset = RoomPreset
else:
# When running tests, faking matrix-nio classes to don't include the module
class RoomVisibility(Enum):
private = 'private'
public = 'public'
class RoomPreset(Enum):
private_chat = "private_chat"
trusted_private_chat = "trusted_private_chat"
public_chat = "public_chat"
class FakeMatrixClient:
"""
Simulate a Matrix client to run tests, if no Matrix homeserver is connected.
"""
def __getattribute__(self, item):
async def func(*_, **_2):
return None
return func

View File

@ -24,11 +24,12 @@ PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
ADMINS = [("Emmy D'Anello", "emmy.danello@animath.fr")]
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
SECRET_KEY = '6$wl1=ehfoiymin3m3i-wyx5d3t=1h7g4(j2izn*my)*yiq#he'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
@ -37,6 +38,7 @@ SITE_ID = 1
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
@ -72,6 +74,7 @@ INSTALLED_APPS = [
if "test" not in sys.argv: # pragma: no cover
INSTALLED_APPS += [
'cas_server',
'django_extensions',
'mailer',
]
@ -120,6 +123,7 @@ FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
ASGI_APPLICATION = 'tfjm.asgi.application'
WSGI_APPLICATION = 'tfjm.wsgi.application'
# Password validation
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
@ -143,6 +147,8 @@ PASSWORD_HASHERS = [
'django.contrib.auth.hashers.BCryptPasswordHasher',
]
CAS_AUTH_CLASS = 'registration.auth.CustomAuthUser'
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAdminUser'
@ -175,6 +181,7 @@ USE_TZ = True
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
@ -199,13 +206,13 @@ DJANGO_TABLES2_TEMPLATE = 'django_tables2/bootstrap5.html'
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'haystack.backends.elasticsearch7_backend.Elasticsearch7SearchEngine'
if os.getenv("HAYSTACK_INDEX_NAME", None) else 'haystack.backends.simple_backend.SimpleEngine',
'URL': 'http://elasticsearch:9200/',
'INDEX_NAME': os.getenv('HAYSTACK_INDEX_NAME', 'inscription-tfjm'),
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
}
}
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
_db_type = os.getenv('DJANGO_DB_TYPE', 'sqlite').lower()
if _db_type == 'mysql' or _db_type.startswith('postgres') or _db_type == 'psql': # pragma: no cover
@ -239,14 +246,14 @@ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
# Custom parameters
PROBLEMS = [
"Triominos",
"Rassemblements mathématiques",
"Tournoi de ping-pong",
"Dépollution de la Seine",
"Électron libre",
"Pièces truquées",
"Drôles de cookies",
"Création d'un jeu",
"Philatélie",
"La montagne aux ruisseaux",
"Appoint monétaire",
"Musique déformée",
"Angles entiers",
"Tribus hiérarchiques",
"Miroirs et lasers",
"Graine de deck",
]
FORBIDDEN_TRIGRAMS = [
"BIT",
@ -273,6 +280,6 @@ CHANNEL_LAYERS = {
}
if os.getenv("TFJM_STAGE", "dev") == "prod": # pragma: no cover
from .settings_prod import * # noqa: F401,F403
from .settings_prod import * # noqa: F401,F403
else:
from .settings_dev import * # noqa: F401,F403
from .settings_dev import * # noqa: F401,F403

View File

@ -9,6 +9,8 @@ DEBUG = False
# Mandatory !
ALLOWED_HOSTS = ['inscription.tfjm.org', 'plateforme.tfjm.org']
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'CHANGE_ME_IN_ENV_SETTINGS')
# Emails
EMAIL_BACKEND = 'mailer.backend.DbBackend'
MAILER_EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
@ -37,5 +39,3 @@ CHANNEL_LAYERS = {
},
},
}
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
{% block content %}
<div class="text-justify">
<p>
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2024
La plateforme d'inscription du TFJM² a été développée entre 2019 et 2023
par Emmy 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

@ -100,10 +100,9 @@
</li>
{% endif %}
{% endif %}
<li class="nav-item active d-none">
<li class="nav-item active">
<a class="nav-link" href="{% url "participation:chat" %}">
<i class="fas fa-comments"></i> {% trans "Chat" %}
</a>
<i class="fas fa-comments"></i> {% trans "Chat" %}</a>
</li>
{% if user.registration.is_admin %}
<li class="nav-item active">

View File

@ -5,19 +5,14 @@
<div class="alert alert-success">
<p>
Les inscriptions pour la session 2024 sont à présent ouvertes, vous pouvez créer votre compte.
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.
</p>
<p>
Une documentation plus complète est disponible à l'adresse
<a href="https://inscription.tfjm.org/doc/">https://inscription.tfjm.org/doc/</a>
et sera progressivement actualisée.
<a href="https://inscription.tfjm.org/doc/">https://inscription.tfjm.org/doc/</a>.
</p>
</div>
<div class="alert alert-warning">
Diverses améliorations fonctionnelles pourront être apportées au site au cours des prochaines semaines.
</div>
<div class="jumbotron p-5">
<div class="row text-center">
@ -60,10 +55,18 @@
<h2>Je ne trouve pas d'équipe, aidez-moi !</h2>
<p class="text-justify">
En ayant créé un compte sur cette plateforme, vous créez également un compte sur la plateforme de chat dédiée
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.
</p>
<p class="text-justify">
Vous pouvez nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a> pour que nous
puissions vous aider à vous mettre en relation avec d'autres participant⋅es qui cherchent également une équipe.
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.
</p>
<h2>J'ai une question</h2>

32
tox.ini
View File

@ -1,7 +1,7 @@
[tox]
envlist =
py310
py311
py312
linters
skipsdist = True
@ -11,22 +11,22 @@ sitepackages = False
deps =
coverage
channels[daphne]~=4.0.0
crispy-bootstrap5~=2023.10
Django>=5.0,<6.0
django-crispy-forms~=2.1
django-filter~=23.5
git+https://github.com/django-haystack/django-haystack.git#v3.3b1
django-phonenumber-field~=7.3.0
django-polymorphic~=3.1.0
django-tables2~=2.7.0
djangorestframework~=3.14.0
django-rest-polymorphic~=0.1.10
crispy-bootstrap5~=0.7
Django>=4.2,<5.0
django-crispy-forms~=1.14
django-filter~=22.1
django-haystack~=3.2
django-phonenumber-field~=7.0
django-polymorphic~=3.1
django-tables2~=2.4
djangorestframework~=3.14
django-rest-polymorphic~=0.1
odfpy~=1.4.1
phonenumbers~=8.13.27
pypdf~=3.17.4
python-magic~=0.4.27
requests~=2.31.0
phonenumbers~=8.12.57
pypdf~=3.4
python-magic~=0.4.26
requests~=2.28.1
whoosh~=2.7
commands =
coverage run --source=api,draw,logs,participation,registration,tfjm ./manage.py test api/ draw/ logs/ participation/ registration/ tfjm/
coverage report -m