refactor(validation): un schema aux frontieres tryout et evaluation
ARCH-005, seconde moitie. Meme forme que pour les matchs : des champs lus a la main sur request.form, deux verifies et le reste cru sur parole. Cote tryout : - game pilote la liste des postes et les champs de gamertag montres au joueur qui s inscrit. Il etait accepte tel quel : une faute de frappe produisait une selection pour laquelle personne ne pouvait etre evalue ; - max_players etait int(x) if x else None — un 500 sur « twelve », et un -3 accepte sans broncher ; - coach_ids etait charge par User.id.in_(...) sans filtre de role. Une soumission fabriquee a la main pouvait donc nommer un joueur coach d une selection, ce qui est une attribution de droits : gerer la selection et evaluer ses joueurs. Ce n est pas un formulaire que l interface propose, et ca marchait. Cote evaluation, validate_score transformait tout ce qui sortait de 1..10 — 11, 0, « bien » — en None. Le critere disparaissait de la moyenne et la page annoncait l evaluation enregistree. Rien ne distinguait « non evalue » de « evalue, refuse, et oublie ». Le calcul de la moyenne remonte sur le modele, en Evaluation.overall_from et apply_scores. Il vivait dans la route, additionnant neuf variables locales, et ne pouvait pas etre exerce sans requete HTTP, session authentifiee et base — c est TEST-002, et c est pourquoi le calcul des scores n avait aucun test. Il en a maintenant six, sans rien monter. Une precision qui compte : aucun critere rempli donne None, pas 0. La grille commence a 1, donc un zero serait une note qu aucun joueur ne peut recevoir, et qui le classerait sous tout le monde dans la liste. Les neuf criteres sont ecrits en toutes lettres dans le schema plutot que generes depuis le modele — un schema se lit — et un test verifie que les deux listes coincident. C est la garde qui empeche la derive, pas l astuce. Douze chaines traduites, dont trois que pybabel avait devinees en fuzzy : une entree fuzzy est ignoree a l execution, le piege est consigne dans docs/translations.md. 30 tests neufs. 477 au total.
This commit is contained in:
@@ -31,3 +31,49 @@ class Evaluation(db.Model):
|
|||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: The nine criteria, in the order the form shows them. The overall score
|
||||||
|
#: is their mean; a criterion left blank is left out of the mean rather
|
||||||
|
#: than counted as a zero, which is why this list exists rather than the
|
||||||
|
#: route summing nine named variables (ARCH-005, QUA-003).
|
||||||
|
CRITERIA = (
|
||||||
|
'mecanics_score',
|
||||||
|
'cohesion_score',
|
||||||
|
'communication_score',
|
||||||
|
'gamesense_score',
|
||||||
|
'versatility_score',
|
||||||
|
'discipline_score',
|
||||||
|
'analysis_score',
|
||||||
|
'sport_ethics_score',
|
||||||
|
'mental_score',
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def overall_from(cls, scores):
|
||||||
|
"""Mean of the criteria that were actually filled in.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scores: Mapping of criterion name to score or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float | None: None when nothing was scored — which is not the
|
||||||
|
same as zero, and must not become one. A player nobody could
|
||||||
|
assess has no overall score; a player who scored zero on
|
||||||
|
everything cannot exist, the scale starts at one.
|
||||||
|
"""
|
||||||
|
given = [scores.get(name) for name in cls.CRITERIA]
|
||||||
|
given = [score for score in given if score is not None]
|
||||||
|
if not given:
|
||||||
|
return None
|
||||||
|
return sum(given) / len(given)
|
||||||
|
|
||||||
|
def apply_scores(self, scores):
|
||||||
|
"""Write these criteria onto the record and recompute the overall.
|
||||||
|
|
||||||
|
Every criterion is assigned, including the ones left blank: an edit
|
||||||
|
that clears a score has to clear it, and the mean has to be the mean
|
||||||
|
of what is on the record afterwards.
|
||||||
|
"""
|
||||||
|
for name in self.CRITERIA:
|
||||||
|
setattr(self, name, scores.get(name))
|
||||||
|
self.overall_score = self.overall_from(scores)
|
||||||
|
|||||||
+37
-86
@@ -6,10 +6,12 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
from marshmallow import ValidationError
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import (
|
from app.models import (
|
||||||
GAME_POSITIONS,
|
GAME_POSITIONS,
|
||||||
Admin,
|
Admin,
|
||||||
@@ -19,23 +21,11 @@ from app.models import (
|
|||||||
TryoutRegistration,
|
TryoutRegistration,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from app.validators import EvaluationSchema
|
||||||
|
|
||||||
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
||||||
|
|
||||||
|
|
||||||
def validate_score(score_value):
|
|
||||||
"""Validate that a score is between 1 and 10."""
|
|
||||||
if score_value is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
score = int(score_value)
|
|
||||||
if 1 <= score <= 10:
|
|
||||||
return score
|
|
||||||
return None
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@evaluations_bp.route('')
|
@evaluations_bp.route('')
|
||||||
@login_required
|
@login_required
|
||||||
def list_evaluations():
|
def list_evaluations():
|
||||||
@@ -163,92 +153,53 @@ def evaluate_player(tryout_id, player_id):
|
|||||||
evaluator_id=current_user.id,
|
evaluator_id=current_user.id,
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if request.method == 'POST':
|
def render_evaluation_form():
|
||||||
mecanics = validate_score(request.form.get('mecanics_score'))
|
evaluators = None
|
||||||
cohesion = validate_score(request.form.get('cohesion_score'))
|
if isinstance(current_user, Admin):
|
||||||
communication = validate_score(request.form.get('communication_score'))
|
all_evaluations = Evaluation.query.filter_by(
|
||||||
gamesense = validate_score(request.form.get('gamesense_score'))
|
tryout_id=tryout_id,
|
||||||
versatility = validate_score(request.form.get('versatility_score'))
|
player_id=player_id,
|
||||||
discipline = validate_score(request.form.get('discipline_score'))
|
).all()
|
||||||
analysis = validate_score(request.form.get('analysis_score'))
|
evaluators = [
|
||||||
sport_ethics = validate_score(request.form.get('sport_ethics_score'))
|
{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations
|
||||||
mental = validate_score(request.form.get('mental_score'))
|
|
||||||
comments = request.form.get('comments')
|
|
||||||
position = request.form.get('position_recommendation')
|
|
||||||
|
|
||||||
scores = [
|
|
||||||
s
|
|
||||||
for s in [
|
|
||||||
mecanics,
|
|
||||||
cohesion,
|
|
||||||
communication,
|
|
||||||
gamesense,
|
|
||||||
versatility,
|
|
||||||
discipline,
|
|
||||||
analysis,
|
|
||||||
sport_ethics,
|
|
||||||
mental,
|
|
||||||
]
|
]
|
||||||
if s is not None
|
|
||||||
]
|
|
||||||
overall = sum(scores) / len(scores) if scores else None
|
|
||||||
|
|
||||||
if existing_eval:
|
return render_template(
|
||||||
existing_eval.mecanics_score = mecanics
|
'pages/evaluate_player.html',
|
||||||
existing_eval.cohesion_score = cohesion
|
tryout=tryout,
|
||||||
existing_eval.communication_score = communication
|
player=player,
|
||||||
existing_eval.gamesense_score = gamesense
|
existing_eval=existing_eval,
|
||||||
existing_eval.versatility_score = versatility
|
evaluators=evaluators,
|
||||||
existing_eval.discipline_score = discipline
|
game_positions=GAME_POSITIONS,
|
||||||
existing_eval.analysis_score = analysis
|
)
|
||||||
existing_eval.sport_ethics_score = sport_ethics
|
|
||||||
existing_eval.mental_score = mental
|
if request.method == 'POST':
|
||||||
existing_eval.overall_score = overall
|
try:
|
||||||
existing_eval.comments = comments
|
data = EvaluationSchema().load(form_payload(list_fields=(), optional_blank=()))
|
||||||
existing_eval.position_recommendation = position
|
except ValidationError as err:
|
||||||
flash(_('Evaluation updated!'), 'success')
|
flash_validation_errors(err)
|
||||||
else:
|
return render_evaluation_form()
|
||||||
|
|
||||||
|
evaluation = existing_eval
|
||||||
|
if evaluation is None:
|
||||||
evaluation = Evaluation(
|
evaluation = Evaluation(
|
||||||
tryout_id=tryout_id,
|
tryout_id=tryout_id,
|
||||||
player_id=player_id,
|
player_id=player_id,
|
||||||
evaluator_id=current_user.id,
|
evaluator_id=current_user.id,
|
||||||
mecanics_score=mecanics,
|
|
||||||
cohesion_score=cohesion,
|
|
||||||
communication_score=communication,
|
|
||||||
gamesense_score=gamesense,
|
|
||||||
versatility_score=versatility,
|
|
||||||
discipline_score=discipline,
|
|
||||||
analysis_score=analysis,
|
|
||||||
sport_ethics_score=sport_ethics,
|
|
||||||
mental_score=mental,
|
|
||||||
overall_score=overall,
|
|
||||||
comments=comments,
|
|
||||||
position_recommendation=position,
|
|
||||||
)
|
)
|
||||||
db.session.add(evaluation)
|
db.session.add(evaluation)
|
||||||
flash(_('Evaluation submitted successfully!'), 'success')
|
flash(_('Evaluation submitted successfully!'), 'success')
|
||||||
|
else:
|
||||||
|
flash(_('Evaluation updated!'), 'success')
|
||||||
|
|
||||||
|
evaluation.apply_scores(data)
|
||||||
|
evaluation.comments = data['comments']
|
||||||
|
evaluation.position_recommendation = data['position_recommendation']
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
evaluators = None
|
return render_evaluation_form()
|
||||||
if isinstance(current_user, Admin):
|
|
||||||
all_evaluations = Evaluation.query.filter_by(
|
|
||||||
tryout_id=tryout_id,
|
|
||||||
player_id=player_id,
|
|
||||||
).all()
|
|
||||||
evaluators = [
|
|
||||||
{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations
|
|
||||||
]
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
'pages/evaluate_player.html',
|
|
||||||
tryout=tryout,
|
|
||||||
player=player,
|
|
||||||
existing_eval=existing_eval,
|
|
||||||
evaluators=evaluators,
|
|
||||||
game_positions=GAME_POSITIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@evaluations_bp.route('/<int:tryout_id>/players')
|
@evaluations_bp.route('/<int:tryout_id>/players')
|
||||||
|
|||||||
+71
-139
@@ -9,8 +9,10 @@ from datetime import datetime
|
|||||||
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ESPORT_GAMES,
|
ESPORT_GAMES,
|
||||||
GAME_POSITIONS,
|
GAME_POSITIONS,
|
||||||
@@ -30,6 +32,7 @@ from app.models import (
|
|||||||
TryoutRegistration,
|
TryoutRegistration,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from app.validators import TryoutSchema
|
||||||
|
|
||||||
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts')
|
||||||
|
|
||||||
@@ -39,6 +42,24 @@ def can_manage():
|
|||||||
return isinstance(current_user, (Admin, Manager))
|
return isinstance(current_user, (Admin, Manager))
|
||||||
|
|
||||||
|
|
||||||
|
def tryout_form_payload():
|
||||||
|
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
||||||
|
return form_payload(list_fields=('coach_ids',), optional_blank=())
|
||||||
|
|
||||||
|
|
||||||
|
def coaches_from_ids(coach_ids):
|
||||||
|
"""The coach accounts behind these ids.
|
||||||
|
|
||||||
|
Filtered by role, which the previous `User.id.in_(...)` was not: the form
|
||||||
|
posts a list of ids and nothing stopped a hand-made submission from
|
||||||
|
naming a player, who then appeared as a coach of the tryout and inherited
|
||||||
|
every permission that comes with it.
|
||||||
|
"""
|
||||||
|
if not coach_ids:
|
||||||
|
return []
|
||||||
|
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
||||||
|
|
||||||
|
|
||||||
def _users_by_id(user_ids):
|
def _users_by_id(user_ids):
|
||||||
"""Load these users in one query, keyed by id.
|
"""Load these users in one query, keyed by id.
|
||||||
|
|
||||||
@@ -85,89 +106,46 @@ def create_tryout():
|
|||||||
User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def rerender():
|
||||||
|
return render_template(
|
||||||
|
'pages/tryout_form.html',
|
||||||
|
tryout=None,
|
||||||
|
org_teams=org_teams,
|
||||||
|
managers=managers,
|
||||||
|
coaches=coaches,
|
||||||
|
esport_games=ESPORT_GAMES,
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
title = request.form.get('title')
|
|
||||||
description = request.form.get('description')
|
|
||||||
game = request.form.get('game')
|
|
||||||
date_str = request.form.get('date')
|
|
||||||
end_date_str = request.form.get('end_date')
|
|
||||||
location = request.form.get('location')
|
|
||||||
max_players = request.form.get('max_players')
|
|
||||||
target_org_team_id = request.form.get('target_org_team_id')
|
|
||||||
manager_id = request.form.get('manager_id')
|
|
||||||
coach_ids = request.form.getlist('coach_ids')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
data = TryoutSchema().load(tryout_form_payload())
|
||||||
except (ValueError, TypeError):
|
except ValidationError as err:
|
||||||
flash(_('Invalid start date format.'), 'danger')
|
flash_validation_errors(err)
|
||||||
return render_template(
|
return rerender()
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=None,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_date_obj = None
|
|
||||||
if end_date_str:
|
|
||||||
try:
|
|
||||||
end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
|
||||||
if end_date_obj < date_obj:
|
|
||||||
flash(_('End date cannot be before start date.'), 'danger')
|
|
||||||
return render_template(
|
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=None,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
flash(_('Invalid end date format.'), 'danger')
|
|
||||||
return render_template(
|
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=None,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
tryout = Tryout(
|
tryout = Tryout(
|
||||||
title=title,
|
title=data['title'],
|
||||||
description=description,
|
description=data['description'],
|
||||||
game=game,
|
game=data['game'],
|
||||||
date=date_obj,
|
date=data['date'],
|
||||||
end_date=end_date_obj,
|
end_date=data['end_date'],
|
||||||
location=location,
|
location=data['location'],
|
||||||
max_players=int(max_players) if max_players else None,
|
max_players=data['max_players'],
|
||||||
created_by=current_user.id,
|
created_by=current_user.id,
|
||||||
status='upcoming',
|
status='upcoming',
|
||||||
target_org_team_id=int(target_org_team_id) if target_org_team_id else None,
|
target_org_team_id=data['target_org_team_id'],
|
||||||
manager_id=int(manager_id) if manager_id else None,
|
manager_id=data['manager_id'],
|
||||||
)
|
)
|
||||||
db.session.add(tryout)
|
db.session.add(tryout)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Assign coaches via many-to-many
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
if coach_ids:
|
|
||||||
coach_users = User.query.filter(User.id.in_([int(c) for c in coach_ids])).all()
|
|
||||||
tryout.coaches = coach_users
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout created successfully!'), 'success')
|
flash(_('Tryout created successfully!'), 'success')
|
||||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
|
|
||||||
return render_template(
|
return rerender()
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=None,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
|
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
|
||||||
@@ -192,85 +170,39 @@ def edit_tryout(tryout_id):
|
|||||||
User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all()
|
User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def rerender():
|
||||||
|
return render_template(
|
||||||
|
'pages/tryout_form.html',
|
||||||
|
tryout=tryout,
|
||||||
|
org_teams=org_teams,
|
||||||
|
managers=managers,
|
||||||
|
coaches=coaches,
|
||||||
|
esport_games=ESPORT_GAMES,
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
title = request.form.get('title')
|
|
||||||
description = request.form.get('description')
|
|
||||||
game = request.form.get('game')
|
|
||||||
date_str = request.form.get('date')
|
|
||||||
end_date_str = request.form.get('end_date')
|
|
||||||
location = request.form.get('location')
|
|
||||||
max_players = request.form.get('max_players')
|
|
||||||
target_org_team_id = request.form.get('target_org_team_id')
|
|
||||||
manager_id = request.form.get('manager_id')
|
|
||||||
coach_ids = request.form.getlist('coach_ids')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
data = TryoutSchema().load(tryout_form_payload())
|
||||||
except (ValueError, TypeError):
|
except ValidationError as err:
|
||||||
flash(_('Invalid start date format.'), 'danger')
|
flash_validation_errors(err)
|
||||||
return render_template(
|
return rerender()
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=tryout,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
end_date_obj = None
|
tryout.title = data['title']
|
||||||
if end_date_str:
|
tryout.description = data['description']
|
||||||
try:
|
tryout.game = data['game']
|
||||||
end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
tryout.date = data['date']
|
||||||
if end_date_obj < date_obj:
|
tryout.end_date = data['end_date']
|
||||||
flash(_('End date cannot be before start date.'), 'danger')
|
tryout.location = data['location']
|
||||||
return render_template(
|
tryout.max_players = data['max_players']
|
||||||
'pages/tryout_form.html',
|
tryout.target_org_team_id = data['target_org_team_id']
|
||||||
tryout=tryout,
|
tryout.manager_id = data['manager_id']
|
||||||
org_teams=org_teams,
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
flash(_('Invalid end date format.'), 'danger')
|
|
||||||
return render_template(
|
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=tryout,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
tryout.title = title
|
|
||||||
tryout.description = description
|
|
||||||
tryout.game = game
|
|
||||||
tryout.date = date_obj
|
|
||||||
tryout.end_date = end_date_obj
|
|
||||||
tryout.location = location
|
|
||||||
tryout.max_players = int(max_players) if max_players else None
|
|
||||||
tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None
|
|
||||||
tryout.manager_id = int(manager_id) if manager_id else None
|
|
||||||
|
|
||||||
# Update coaches via many-to-many
|
|
||||||
if coach_ids:
|
|
||||||
coach_users = User.query.filter(User.id.in_([int(c) for c in coach_ids])).all()
|
|
||||||
tryout.coaches = coach_users
|
|
||||||
else:
|
|
||||||
tryout.coaches = []
|
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout updated successfully!'), 'success')
|
flash(_('Tryout updated successfully!'), 'success')
|
||||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
|
|
||||||
return render_template(
|
return rerender()
|
||||||
'pages/tryout_form.html',
|
|
||||||
tryout=tryout,
|
|
||||||
org_teams=org_teams,
|
|
||||||
managers=managers,
|
|
||||||
coaches=coaches,
|
|
||||||
esport_games=ESPORT_GAMES,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tryouts_bp.route('/<int:tryout_id>')
|
@tryouts_bp.route('/<int:tryout_id>')
|
||||||
|
|||||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: team-tryouts VERSION\n"
|
"Project-Id-Version: team-tryouts VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-08-11 12:05-0400\n"
|
"POT-Creation-Date: 2026-08-11 13:32-0400\n"
|
||||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -19,6 +19,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
|
#: app/forms.py:37 app/routes/auth.py:224 app/routes/auth.py:374
|
||||||
|
#: app/routes/users/contracts.py:95
|
||||||
|
#, python-format
|
||||||
|
msgid "%(field)s: %(msg)s"
|
||||||
|
msgstr "%(field)s: %(msg)s"
|
||||||
|
|
||||||
#: app/validators.py:50
|
#: app/validators.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
||||||
@@ -43,68 +49,134 @@ msgstr "Discord User ID must be a 17-20 digit number."
|
|||||||
msgid "Invalid phone number format."
|
msgid "Invalid phone number format."
|
||||||
msgstr "Invalid phone number format."
|
msgstr "Invalid phone number format."
|
||||||
|
|
||||||
#: app/validators.py:164
|
#: app/validators.py:201
|
||||||
msgid "Username is required."
|
msgid "Username is required."
|
||||||
msgstr "Username is required."
|
msgstr "Username is required."
|
||||||
|
|
||||||
#: app/validators.py:168
|
#: app/validators.py:205
|
||||||
msgid "Password is required."
|
msgid "Password is required."
|
||||||
msgstr "Password is required."
|
msgstr "Password is required."
|
||||||
|
|
||||||
#: app/validators.py:190 app/validators.py:262
|
#: app/validators.py:227 app/validators.py:299
|
||||||
msgid "Username must be 3-80 characters."
|
msgid "Username must be 3-80 characters."
|
||||||
msgstr "Username must be 3-80 characters."
|
msgstr "Username must be 3-80 characters."
|
||||||
|
|
||||||
#: app/validators.py:196
|
#: app/validators.py:233
|
||||||
msgid "Email must be 120 characters or less."
|
msgid "Email must be 120 characters or less."
|
||||||
msgstr "Email must be 120 characters or less."
|
msgstr "Email must be 120 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:209 app/validators.py:277 app/validators.py:308
|
#: app/validators.py:246 app/validators.py:314 app/validators.py:345
|
||||||
#: app/validators.py:372
|
#: app/validators.py:409
|
||||||
msgid "Full name is required."
|
msgid "Full name is required."
|
||||||
msgstr "Full name is required."
|
msgstr "Full name is required."
|
||||||
|
|
||||||
#: app/validators.py:244
|
#: app/validators.py:281
|
||||||
msgid "Passwords do not match."
|
msgid "Passwords do not match."
|
||||||
msgstr "Passwords do not match."
|
msgstr "Passwords do not match."
|
||||||
|
|
||||||
#: app/validators.py:281 app/validators.py:316
|
#: app/validators.py:318 app/validators.py:353
|
||||||
msgid "Invalid role selected."
|
msgid "Invalid role selected."
|
||||||
msgstr "Invalid role selected."
|
msgstr "Invalid role selected."
|
||||||
|
|
||||||
#: app/validators.py:417
|
#: app/validators.py:454
|
||||||
msgid "Player must be selected."
|
msgid "Player must be selected."
|
||||||
msgstr "Player must be selected."
|
msgstr "Player must be selected."
|
||||||
|
|
||||||
#: app/validators.py:420
|
#: app/validators.py:457
|
||||||
msgid "Notes must be 2000 characters or less."
|
msgid "Notes must be 2000 characters or less."
|
||||||
msgstr "Notes must be 2000 characters or less."
|
msgstr "Notes must be 2000 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:439
|
#: app/validators.py:476
|
||||||
msgid "Date must be in YYYY-MM-DD format."
|
msgid "Date must be in YYYY-MM-DD format."
|
||||||
msgstr "Date must be in YYYY-MM-DD format."
|
msgstr "Date must be in YYYY-MM-DD format."
|
||||||
|
|
||||||
#: app/validators.py:444 app/validators.py:471
|
#: app/validators.py:481 app/validators.py:508
|
||||||
msgid "Start time must be in HH:MM format."
|
msgid "Start time must be in HH:MM format."
|
||||||
msgstr "Start time must be in HH:MM format."
|
msgstr "Start time must be in HH:MM format."
|
||||||
|
|
||||||
#: app/validators.py:448
|
#: app/validators.py:485
|
||||||
msgid "End time must be in HH:MM format."
|
msgid "End time must be in HH:MM format."
|
||||||
msgstr "End time must be in HH:MM format."
|
msgstr "End time must be in HH:MM format."
|
||||||
|
|
||||||
#: app/validators.py:451
|
#: app/validators.py:488
|
||||||
msgid "Points must be 2000 characters or less."
|
msgid "Points must be 2000 characters or less."
|
||||||
msgstr "Points must be 2000 characters or less."
|
msgstr "Points must be 2000 characters or less."
|
||||||
|
|
||||||
#: app/validators.py:467
|
#: app/validators.py:504
|
||||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
msgstr "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
|
|
||||||
#: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
|
#: app/validators.py:535
|
||||||
#: app/routes/users/contracts.py:95
|
msgid "Player selection is malformed."
|
||||||
#, python-format
|
msgstr "Player selection is malformed."
|
||||||
msgid "%(field)s: %(msg)s"
|
|
||||||
msgstr "%(field)s: %(msg)s"
|
#: app/validators.py:561 app/validators.py:671
|
||||||
|
msgid "A title is required."
|
||||||
|
msgstr "A title is required."
|
||||||
|
|
||||||
|
#: app/validators.py:570
|
||||||
|
msgid "Invalid date format."
|
||||||
|
msgstr "Invalid date format."
|
||||||
|
|
||||||
|
#: app/validators.py:575 app/validators.py:582
|
||||||
|
msgid "Invalid time format."
|
||||||
|
msgstr "Invalid time format."
|
||||||
|
|
||||||
|
#: app/validators.py:576
|
||||||
|
msgid "Start time is required. Please select a time slot."
|
||||||
|
msgstr "Start time is required. Please select a time slot."
|
||||||
|
|
||||||
|
#: app/validators.py:590
|
||||||
|
msgid "Unknown match status."
|
||||||
|
msgstr "Unknown match status."
|
||||||
|
|
||||||
|
#: app/validators.py:606
|
||||||
|
msgid "The end time must come after the start time."
|
||||||
|
msgstr "The end time must come after the start time."
|
||||||
|
|
||||||
|
#: app/validators.py:620
|
||||||
|
msgid "Unknown match type."
|
||||||
|
msgstr "Unknown match type."
|
||||||
|
|
||||||
|
#: app/validators.py:632
|
||||||
|
msgid "A team cannot play against itself."
|
||||||
|
msgstr "A team cannot play against itself."
|
||||||
|
|
||||||
|
#: app/validators.py:680
|
||||||
|
msgid "Unknown game."
|
||||||
|
msgstr "Unknown game."
|
||||||
|
|
||||||
|
#: app/validators.py:685
|
||||||
|
msgid "Invalid start date format."
|
||||||
|
msgstr "Invalid start date format."
|
||||||
|
|
||||||
|
#: app/validators.py:686
|
||||||
|
msgid "A start date is required."
|
||||||
|
msgstr "A start date is required."
|
||||||
|
|
||||||
|
#: app/validators.py:692
|
||||||
|
msgid "Invalid end date format."
|
||||||
|
msgstr "Invalid end date format."
|
||||||
|
|
||||||
|
#: app/validators.py:700
|
||||||
|
msgid "A tryout must allow at least one player."
|
||||||
|
msgstr "A tryout must allow at least one player."
|
||||||
|
|
||||||
|
#: app/validators.py:703
|
||||||
|
msgid "The player limit must be a whole number."
|
||||||
|
msgstr "The player limit must be a whole number."
|
||||||
|
|
||||||
|
#: app/validators.py:715
|
||||||
|
msgid "End date cannot be before start date."
|
||||||
|
msgstr "End date cannot be before start date."
|
||||||
|
|
||||||
|
#: app/validators.py:724
|
||||||
|
msgid "Scores run from 1 to 10."
|
||||||
|
msgstr "Scores run from 1 to 10."
|
||||||
|
|
||||||
|
#: app/validators.py:725
|
||||||
|
msgid "A score must be a whole number from 1 to 10."
|
||||||
|
msgstr "A score must be a whole number from 1 to 10."
|
||||||
|
|
||||||
#: app/routes/auth.py:241
|
#: app/routes/auth.py:241
|
||||||
msgid "This account has been deactivated."
|
msgid "This account has been deactivated."
|
||||||
@@ -175,40 +247,40 @@ msgstr "Discord account connected! Your profile has been pre-filled."
|
|||||||
msgid "You have been logged out."
|
msgid "You have been logged out."
|
||||||
msgstr "You have been logged out."
|
msgstr "You have been logged out."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:46
|
#: app/routes/evaluations.py:36
|
||||||
msgid "You do not have permission to view evaluations."
|
msgid "You do not have permission to view evaluations."
|
||||||
msgstr "You do not have permission to view evaluations."
|
msgstr "You do not have permission to view evaluations."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:136
|
#: app/routes/evaluations.py:126
|
||||||
msgid "You do not have permission to evaluate players."
|
msgid "You do not have permission to evaluate players."
|
||||||
msgstr "You do not have permission to evaluate players."
|
msgstr "You do not have permission to evaluate players."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:141 app/routes/evaluations.py:264
|
#: app/routes/evaluations.py:131 app/routes/evaluations.py:215
|
||||||
msgid "You do not have permission to evaluate players in this tryout."
|
msgid "You do not have permission to evaluate players in this tryout."
|
||||||
msgstr "You do not have permission to evaluate players in this tryout."
|
msgstr "You do not have permission to evaluate players in this tryout."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:152
|
#: app/routes/evaluations.py:142
|
||||||
msgid "Player is not registered for this tryout."
|
msgid "Player is not registered for this tryout."
|
||||||
msgstr "Player is not registered for this tryout."
|
msgstr "Player is not registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:157
|
#: app/routes/evaluations.py:147
|
||||||
msgid "Can only evaluate players."
|
msgid "Can only evaluate players."
|
||||||
msgstr "Can only evaluate players."
|
msgstr "Can only evaluate players."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:209
|
#: app/routes/evaluations.py:191
|
||||||
msgid "Evaluation updated!"
|
|
||||||
msgstr "Evaluation updated!"
|
|
||||||
|
|
||||||
#: app/routes/evaluations.py:229
|
|
||||||
msgid "Evaluation submitted successfully!"
|
msgid "Evaluation submitted successfully!"
|
||||||
msgstr "Evaluation submitted successfully!"
|
msgstr "Evaluation submitted successfully!"
|
||||||
|
|
||||||
#: app/routes/evaluations.py:259 app/routes/teams.py:270
|
#: app/routes/evaluations.py:193
|
||||||
|
msgid "Evaluation updated!"
|
||||||
|
msgstr "Evaluation updated!"
|
||||||
|
|
||||||
|
#: app/routes/evaluations.py:210 app/routes/teams.py:270
|
||||||
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
||||||
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
|
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:437
|
||||||
#: app/routes/tryouts.py:521 app/routes/tryouts.py:541
|
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473
|
||||||
#: app/routes/tryouts.py:580 app/routes/tryouts.py:616
|
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548
|
||||||
#: app/routes/tryouts.py:635
|
#: app/routes/tryouts.py:567
|
||||||
msgid "Permission denied."
|
msgid "Permission denied."
|
||||||
msgstr "Permission denied."
|
msgstr "Permission denied."
|
||||||
|
|
||||||
@@ -216,76 +288,47 @@ msgstr "Permission denied."
|
|||||||
msgid "That language is not available."
|
msgid "That language is not available."
|
||||||
msgstr "That language is not available."
|
msgstr "That language is not available."
|
||||||
|
|
||||||
#: app/routes/matches.py:307
|
#: app/routes/matches.py:361
|
||||||
msgid "You do not have permission to schedule matches for this tryout."
|
msgid "You do not have permission to schedule matches for this tryout."
|
||||||
msgstr "You do not have permission to schedule matches for this tryout."
|
msgstr "You do not have permission to schedule matches for this tryout."
|
||||||
|
|
||||||
#: app/routes/matches.py:311 app/routes/matches.py:473
|
#: app/routes/matches.py:365 app/routes/matches.py:445
|
||||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
msgid "This tryout has ended. Matches can no longer be created or modified."
|
||||||
msgstr "This tryout has ended. Matches can no longer be created or modified."
|
msgstr "This tryout has ended. Matches can no longer be created or modified."
|
||||||
|
|
||||||
#: app/routes/matches.py:332
|
#: app/routes/matches.py:427
|
||||||
msgid "Start time is required. Please select a time slot."
|
|
||||||
msgstr "Start time is required. Please select a time slot."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:344 app/routes/matches.py:496
|
|
||||||
#: app/routes/team_matches.py:153 app/routes/team_matches.py:247
|
|
||||||
msgid "Invalid date format."
|
|
||||||
msgstr "Invalid date format."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:364 app/routes/team_matches.py:174
|
|
||||||
msgid "Invalid time format."
|
|
||||||
msgstr "Invalid time format."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:449
|
|
||||||
msgid "Match scheduled successfully!"
|
msgid "Match scheduled successfully!"
|
||||||
msgstr "Match scheduled successfully!"
|
msgstr "Match scheduled successfully!"
|
||||||
|
|
||||||
#: app/routes/matches.py:469 app/routes/team_matches.py:234
|
#: app/routes/matches.py:441 app/routes/team_matches.py:211
|
||||||
msgid "You do not have permission to edit this match."
|
msgid "You do not have permission to edit this match."
|
||||||
msgstr "You do not have permission to edit this match."
|
msgstr "You do not have permission to edit this match."
|
||||||
|
|
||||||
#: app/routes/matches.py:507
|
#: app/routes/matches.py:536 app/routes/team_matches.py:241
|
||||||
msgid "Start time is required."
|
|
||||||
msgstr "Start time is required."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:616 app/routes/team_matches.py:276
|
|
||||||
msgid "Match updated successfully!"
|
msgid "Match updated successfully!"
|
||||||
msgstr "Match updated successfully!"
|
msgstr "Match updated successfully!"
|
||||||
|
|
||||||
#: app/routes/matches.py:669 app/routes/team_matches.py:291
|
#: app/routes/matches.py:571 app/routes/team_matches.py:256
|
||||||
msgid "You do not have permission to delete this match."
|
msgid "You do not have permission to delete this match."
|
||||||
msgstr "You do not have permission to delete this match."
|
msgstr "You do not have permission to delete this match."
|
||||||
|
|
||||||
#: app/routes/matches.py:672
|
#: app/routes/matches.py:574
|
||||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
msgid "This tryout has ended. Matches can no longer be deleted."
|
||||||
msgstr "This tryout has ended. Matches can no longer be deleted."
|
msgstr "This tryout has ended. Matches can no longer be deleted."
|
||||||
|
|
||||||
#: app/routes/matches.py:685 app/routes/team_matches.py:295
|
#: app/routes/matches.py:587 app/routes/team_matches.py:260
|
||||||
msgid "Match deleted successfully."
|
msgid "Match deleted successfully."
|
||||||
msgstr "Match deleted successfully."
|
msgstr "Match deleted successfully."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:100
|
#: app/routes/team_matches.py:104
|
||||||
msgid "You do not have permission to schedule matches for this team."
|
msgid "You do not have permission to schedule matches for this team."
|
||||||
msgstr "You do not have permission to schedule matches for this team."
|
msgstr "You do not have permission to schedule matches for this team."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:142
|
#: app/routes/team_matches.py:196
|
||||||
msgid "Date is required."
|
|
||||||
msgstr "Date is required."
|
|
||||||
|
|
||||||
#: app/routes/team_matches.py:220
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team match \"%(title)s\" scheduled successfully!"
|
msgid "Team match \"%(title)s\" scheduled successfully!"
|
||||||
msgstr "Team match \"%(title)s\" scheduled successfully!"
|
msgstr "Team match \"%(title)s\" scheduled successfully!"
|
||||||
|
|
||||||
#: app/routes/team_matches.py:259
|
|
||||||
msgid "Invalid start time format."
|
|
||||||
msgstr "Invalid start time format."
|
|
||||||
|
|
||||||
#: app/routes/team_matches.py:267
|
|
||||||
msgid "Invalid end time format."
|
|
||||||
msgstr "Invalid end time format."
|
|
||||||
|
|
||||||
#: app/routes/teams.py:40
|
#: app/routes/teams.py:40
|
||||||
msgid "Use My Team(s) to view your teams."
|
msgid "Use My Team(s) to view your teams."
|
||||||
msgstr "Use My Team(s) to view your teams."
|
msgstr "Use My Team(s) to view your teams."
|
||||||
@@ -380,7 +423,7 @@ msgstr "Coach removed from %(name)s."
|
|||||||
msgid "Manager removed from %(name)s."
|
msgid "Manager removed from %(name)s."
|
||||||
msgstr "Manager removed from %(name)s."
|
msgstr "Manager removed from %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
|
#: app/routes/teams.py:408 app/routes/tryouts.py:477 app/routes/tryouts.py:578
|
||||||
msgid "Please select a player."
|
msgid "Please select a player."
|
||||||
msgstr "Please select a player."
|
msgstr "Please select a player."
|
||||||
|
|
||||||
@@ -426,124 +469,112 @@ msgstr "Can only add notes for players."
|
|||||||
msgid "Note added for %(username)s!"
|
msgid "Note added for %(username)s!"
|
||||||
msgstr "Note added for %(username)s!"
|
msgstr "Note added for %(username)s!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:77
|
#: app/routes/tryouts.py:98
|
||||||
msgid "You do not have permission to create tryouts."
|
msgid "You do not have permission to create tryouts."
|
||||||
msgstr "You do not have permission to create tryouts."
|
msgstr "You do not have permission to create tryouts."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:103 app/routes/tryouts.py:210
|
#: app/routes/tryouts.py:145
|
||||||
msgid "Invalid start date format."
|
|
||||||
msgstr "Invalid start date format."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:118 app/routes/tryouts.py:225
|
|
||||||
msgid "End date cannot be before start date."
|
|
||||||
msgstr "End date cannot be before start date."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:128 app/routes/tryouts.py:235
|
|
||||||
msgid "Invalid end date format."
|
|
||||||
msgstr "Invalid end date format."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:160
|
|
||||||
msgid "Tryout created successfully!"
|
msgid "Tryout created successfully!"
|
||||||
msgstr "Tryout created successfully!"
|
msgstr "Tryout created successfully!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:180
|
#: app/routes/tryouts.py:158
|
||||||
msgid "You do not have permission to edit this tryout."
|
msgid "You do not have permission to edit this tryout."
|
||||||
msgstr "You do not have permission to edit this tryout."
|
msgstr "You do not have permission to edit this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:184
|
#: app/routes/tryouts.py:162
|
||||||
msgid "This tryout has ended and can no longer be modified."
|
msgid "This tryout has ended and can no longer be modified."
|
||||||
msgstr "This tryout has ended and can no longer be modified."
|
msgstr "This tryout has ended and can no longer be modified."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:263
|
#: app/routes/tryouts.py:202
|
||||||
msgid "Tryout updated successfully!"
|
msgid "Tryout updated successfully!"
|
||||||
msgstr "Tryout updated successfully!"
|
msgstr "Tryout updated successfully!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:310
|
#: app/routes/tryouts.py:242
|
||||||
msgid "You do not have permission to view this tryout."
|
msgid "You do not have permission to view this tryout."
|
||||||
msgstr "You do not have permission to view this tryout."
|
msgstr "You do not have permission to view this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:472
|
#: app/routes/tryouts.py:404
|
||||||
msgid "Only players can register for tryouts."
|
msgid "Only players can register for tryouts."
|
||||||
msgstr "Only players can register for tryouts."
|
msgstr "Only players can register for tryouts."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:476
|
#: app/routes/tryouts.py:408
|
||||||
msgid "This tryout is not accepting registrations."
|
msgid "This tryout is not accepting registrations."
|
||||||
msgstr "This tryout is not accepting registrations."
|
msgstr "This tryout is not accepting registrations."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:483
|
#: app/routes/tryouts.py:415
|
||||||
msgid "You are already registered for this tryout."
|
msgid "You are already registered for this tryout."
|
||||||
msgstr "You are already registered for this tryout."
|
msgstr "You are already registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:489 app/routes/tryouts.py:564
|
#: app/routes/tryouts.py:421 app/routes/tryouts.py:496
|
||||||
msgid "This tryout is full."
|
msgid "This tryout is full."
|
||||||
msgstr "This tryout is full."
|
msgstr "This tryout is full."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:495
|
#: app/routes/tryouts.py:427
|
||||||
msgid "Successfully registered for tryout!"
|
msgid "Successfully registered for tryout!"
|
||||||
msgstr "Successfully registered for tryout!"
|
msgstr "Successfully registered for tryout!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:511
|
#: app/routes/tryouts.py:443
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Tryout status updated to %(new_status)s."
|
msgid "Tryout status updated to %(new_status)s."
|
||||||
msgstr "Tryout status updated to %(new_status)s."
|
msgstr "Tryout status updated to %(new_status)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:531
|
#: app/routes/tryouts.py:463
|
||||||
msgid "Registration status updated."
|
msgid "Registration status updated."
|
||||||
msgstr "Registration status updated."
|
msgstr "Registration status updated."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:550
|
#: app/routes/tryouts.py:482
|
||||||
msgid "Can only register players."
|
msgid "Can only register players."
|
||||||
msgstr "Can only register players."
|
msgstr "Can only register players."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:556
|
#: app/routes/tryouts.py:488
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already registered for this tryout."
|
msgid "%(username)s is already registered for this tryout."
|
||||||
msgstr "%(username)s is already registered for this tryout."
|
msgstr "%(username)s is already registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:570
|
#: app/routes/tryouts.py:502
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s registered for tryout!"
|
msgid "%(username)s registered for tryout!"
|
||||||
msgstr "%(username)s registered for tryout!"
|
msgstr "%(username)s registered for tryout!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:606
|
#: app/routes/tryouts.py:538
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from tryout."
|
msgid "%(username)s removed from tryout."
|
||||||
msgstr "%(username)s removed from tryout."
|
msgstr "%(username)s removed from tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:624
|
#: app/routes/tryouts.py:556
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(team_name)s\" created!"
|
msgid "Team \"%(team_name)s\" created!"
|
||||||
msgstr "Team \"%(team_name)s\" created!"
|
msgstr "Team \"%(team_name)s\" created!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:655
|
#: app/routes/tryouts.py:587
|
||||||
msgid "That player is not registered for this tryout."
|
msgid "That player is not registered for this tryout."
|
||||||
msgstr "That player is not registered for this tryout."
|
msgstr "That player is not registered for this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:661
|
#: app/routes/tryouts.py:593
|
||||||
msgid "Player is already on this team."
|
msgid "Player is already on this team."
|
||||||
msgstr "Player is already on this team."
|
msgstr "Player is already on this team."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:666
|
#: app/routes/tryouts.py:598
|
||||||
msgid "Player added to team!"
|
msgid "Player added to team!"
|
||||||
msgstr "Player added to team!"
|
msgstr "Player added to team!"
|
||||||
|
|
||||||
#: app/routes/tryouts.py:676
|
#: app/routes/tryouts.py:608
|
||||||
msgid "You do not have permission to delete this tryout."
|
msgid "You do not have permission to delete this tryout."
|
||||||
msgstr "You do not have permission to delete this tryout."
|
msgstr "You do not have permission to delete this tryout."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:712
|
#: app/routes/tryouts.py:644
|
||||||
msgid "Tryout deleted successfully."
|
msgid "Tryout deleted successfully."
|
||||||
msgstr "Tryout deleted successfully."
|
msgstr "Tryout deleted successfully."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:46
|
#: app/routes/users/_shared.py:51
|
||||||
msgid "No file selected."
|
msgid "No file selected."
|
||||||
msgstr "No file selected."
|
msgstr "No file selected."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:50
|
#: app/routes/users/_shared.py:55
|
||||||
msgid "Only PDF files are allowed for contracts."
|
msgid "Only PDF files are allowed for contracts."
|
||||||
msgstr "Only PDF files are allowed for contracts."
|
msgstr "Only PDF files are allowed for contracts."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:55
|
#: app/routes/users/_shared.py:60
|
||||||
msgid "That file is not a PDF, whatever its name says."
|
msgid "That file is not a PDF, whatever its name says."
|
||||||
msgstr "That file is not a PDF, whatever its name says."
|
msgstr "That file is not a PDF, whatever its name says."
|
||||||
|
|
||||||
@@ -2880,3 +2911,9 @@ msgstr "View Profile"
|
|||||||
#~ msgid "Answer"
|
#~ msgid "Answer"
|
||||||
#~ msgstr "Answer"
|
#~ msgstr "Answer"
|
||||||
|
|
||||||
|
#~ msgid "Invalid start time format."
|
||||||
|
#~ msgstr "Invalid start time format."
|
||||||
|
|
||||||
|
#~ msgid "Invalid end time format."
|
||||||
|
#~ msgstr "Invalid end time format."
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: team-tryouts VERSION\n"
|
"Project-Id-Version: team-tryouts VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2026-08-11 12:05-0400\n"
|
"POT-Creation-Date: 2026-08-11 13:32-0400\n"
|
||||||
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
"PO-Revision-Date: 2026-08-07 20:22-0400\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
@@ -19,6 +19,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.18.0\n"
|
"Generated-By: Babel 2.18.0\n"
|
||||||
|
|
||||||
|
#: app/forms.py:37 app/routes/auth.py:224 app/routes/auth.py:374
|
||||||
|
#: app/routes/users/contracts.py:95
|
||||||
|
#, python-format
|
||||||
|
msgid "%(field)s: %(msg)s"
|
||||||
|
msgstr "%(field)s : %(msg)s"
|
||||||
|
|
||||||
#: app/validators.py:50
|
#: app/validators.py:50
|
||||||
msgid ""
|
msgid ""
|
||||||
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
"Password must be at least 8 characters with uppercase, lowercase, and a "
|
||||||
@@ -45,68 +51,134 @@ msgstr "L’identifiant Discord doit être un nombre de 17 à 20 chiffres."
|
|||||||
msgid "Invalid phone number format."
|
msgid "Invalid phone number format."
|
||||||
msgstr "Format de numéro de téléphone invalide."
|
msgstr "Format de numéro de téléphone invalide."
|
||||||
|
|
||||||
#: app/validators.py:164
|
#: app/validators.py:201
|
||||||
msgid "Username is required."
|
msgid "Username is required."
|
||||||
msgstr "Le nom d’utilisateur est obligatoire."
|
msgstr "Le nom d’utilisateur est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:168
|
#: app/validators.py:205
|
||||||
msgid "Password is required."
|
msgid "Password is required."
|
||||||
msgstr "Le mot de passe est obligatoire."
|
msgstr "Le mot de passe est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:190 app/validators.py:262
|
#: app/validators.py:227 app/validators.py:299
|
||||||
msgid "Username must be 3-80 characters."
|
msgid "Username must be 3-80 characters."
|
||||||
msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
msgstr "Le nom d’utilisateur doit compter de 3 à 80 caractères."
|
||||||
|
|
||||||
#: app/validators.py:196
|
#: app/validators.py:233
|
||||||
msgid "Email must be 120 characters or less."
|
msgid "Email must be 120 characters or less."
|
||||||
msgstr "L’adresse courriel ne doit pas dépasser 120 caractères."
|
msgstr "L’adresse courriel ne doit pas dépasser 120 caractères."
|
||||||
|
|
||||||
#: app/validators.py:209 app/validators.py:277 app/validators.py:308
|
#: app/validators.py:246 app/validators.py:314 app/validators.py:345
|
||||||
#: app/validators.py:372
|
#: app/validators.py:409
|
||||||
msgid "Full name is required."
|
msgid "Full name is required."
|
||||||
msgstr "Le nom complet est obligatoire."
|
msgstr "Le nom complet est obligatoire."
|
||||||
|
|
||||||
#: app/validators.py:244
|
#: app/validators.py:281
|
||||||
msgid "Passwords do not match."
|
msgid "Passwords do not match."
|
||||||
msgstr "Les mots de passe ne concordent pas."
|
msgstr "Les mots de passe ne concordent pas."
|
||||||
|
|
||||||
#: app/validators.py:281 app/validators.py:316
|
#: app/validators.py:318 app/validators.py:353
|
||||||
msgid "Invalid role selected."
|
msgid "Invalid role selected."
|
||||||
msgstr "Rôle sélectionné invalide."
|
msgstr "Rôle sélectionné invalide."
|
||||||
|
|
||||||
#: app/validators.py:417
|
#: app/validators.py:454
|
||||||
msgid "Player must be selected."
|
msgid "Player must be selected."
|
||||||
msgstr "Vous devez choisir un joueur."
|
msgstr "Vous devez choisir un joueur."
|
||||||
|
|
||||||
#: app/validators.py:420
|
#: app/validators.py:457
|
||||||
msgid "Notes must be 2000 characters or less."
|
msgid "Notes must be 2000 characters or less."
|
||||||
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
msgstr "Les notes ne doivent pas dépasser 2000 caractères."
|
||||||
|
|
||||||
#: app/validators.py:439
|
#: app/validators.py:476
|
||||||
msgid "Date must be in YYYY-MM-DD format."
|
msgid "Date must be in YYYY-MM-DD format."
|
||||||
msgstr "La date doit être au format AAAA-MM-JJ."
|
msgstr "La date doit être au format AAAA-MM-JJ."
|
||||||
|
|
||||||
#: app/validators.py:444 app/validators.py:471
|
#: app/validators.py:481 app/validators.py:508
|
||||||
msgid "Start time must be in HH:MM format."
|
msgid "Start time must be in HH:MM format."
|
||||||
msgstr "L’heure de début doit être au format HH:MM."
|
msgstr "L’heure de début doit être au format HH:MM."
|
||||||
|
|
||||||
#: app/validators.py:448
|
#: app/validators.py:485
|
||||||
msgid "End time must be in HH:MM format."
|
msgid "End time must be in HH:MM format."
|
||||||
msgstr "L’heure de fin doit être au format HH:MM."
|
msgstr "L’heure de fin doit être au format HH:MM."
|
||||||
|
|
||||||
#: app/validators.py:451
|
#: app/validators.py:488
|
||||||
msgid "Points must be 2000 characters or less."
|
msgid "Points must be 2000 characters or less."
|
||||||
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
msgstr "Les points ne doivent pas dépasser 2000 caractères."
|
||||||
|
|
||||||
#: app/validators.py:467
|
#: app/validators.py:504
|
||||||
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
msgid "Day must be 0 (Monday) to 6 (Sunday)."
|
||||||
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
msgstr "Le jour doit aller de 0 (lundi) à 6 (dimanche)."
|
||||||
|
|
||||||
#: app/routes/auth.py:224 app/routes/auth.py:374 app/routes/users/_shared.py:64
|
#: app/validators.py:535
|
||||||
#: app/routes/users/contracts.py:95
|
msgid "Player selection is malformed."
|
||||||
#, python-format
|
msgstr "La sélection de joueurs est mal formée."
|
||||||
msgid "%(field)s: %(msg)s"
|
|
||||||
msgstr "%(field)s : %(msg)s"
|
#: app/validators.py:561 app/validators.py:671
|
||||||
|
msgid "A title is required."
|
||||||
|
msgstr "Un titre est requis."
|
||||||
|
|
||||||
|
#: app/validators.py:570
|
||||||
|
msgid "Invalid date format."
|
||||||
|
msgstr "Format de date invalide."
|
||||||
|
|
||||||
|
#: app/validators.py:575 app/validators.py:582
|
||||||
|
msgid "Invalid time format."
|
||||||
|
msgstr "Format d’heure invalide."
|
||||||
|
|
||||||
|
#: app/validators.py:576
|
||||||
|
msgid "Start time is required. Please select a time slot."
|
||||||
|
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
||||||
|
|
||||||
|
#: app/validators.py:590
|
||||||
|
msgid "Unknown match status."
|
||||||
|
msgstr "Statut de match inconnu."
|
||||||
|
|
||||||
|
#: app/validators.py:606
|
||||||
|
msgid "The end time must come after the start time."
|
||||||
|
msgstr "L'heure de fin doit être postérieure à l'heure de début."
|
||||||
|
|
||||||
|
#: app/validators.py:620
|
||||||
|
msgid "Unknown match type."
|
||||||
|
msgstr "Type de match inconnu."
|
||||||
|
|
||||||
|
#: app/validators.py:632
|
||||||
|
msgid "A team cannot play against itself."
|
||||||
|
msgstr "Une équipe ne peut pas jouer contre elle-même."
|
||||||
|
|
||||||
|
#: app/validators.py:680
|
||||||
|
msgid "Unknown game."
|
||||||
|
msgstr "Jeu inconnu."
|
||||||
|
|
||||||
|
#: app/validators.py:685
|
||||||
|
msgid "Invalid start date format."
|
||||||
|
msgstr "Format de date de début invalide."
|
||||||
|
|
||||||
|
#: app/validators.py:686
|
||||||
|
msgid "A start date is required."
|
||||||
|
msgstr "Une date de début est requise."
|
||||||
|
|
||||||
|
#: app/validators.py:692
|
||||||
|
msgid "Invalid end date format."
|
||||||
|
msgstr "Format de date de fin invalide."
|
||||||
|
|
||||||
|
#: app/validators.py:700
|
||||||
|
msgid "A tryout must allow at least one player."
|
||||||
|
msgstr "Une sélection doit accepter au moins un joueur."
|
||||||
|
|
||||||
|
#: app/validators.py:703
|
||||||
|
msgid "The player limit must be a whole number."
|
||||||
|
msgstr "La limite de joueurs doit être un nombre entier."
|
||||||
|
|
||||||
|
#: app/validators.py:715
|
||||||
|
msgid "End date cannot be before start date."
|
||||||
|
msgstr "La date de fin ne peut pas précéder la date de début."
|
||||||
|
|
||||||
|
#: app/validators.py:724
|
||||||
|
msgid "Scores run from 1 to 10."
|
||||||
|
msgstr "Les notes vont de 1 à 10."
|
||||||
|
|
||||||
|
#: app/validators.py:725
|
||||||
|
msgid "A score must be a whole number from 1 to 10."
|
||||||
|
msgstr "Une note doit être un nombre entier de 1 à 10."
|
||||||
|
|
||||||
#: app/routes/auth.py:241
|
#: app/routes/auth.py:241
|
||||||
msgid "This account has been deactivated."
|
msgid "This account has been deactivated."
|
||||||
@@ -177,40 +249,40 @@ msgstr "Compte Discord connecté. Votre profil a été pré-rempli."
|
|||||||
msgid "You have been logged out."
|
msgid "You have been logged out."
|
||||||
msgstr "Vous avez été déconnecté."
|
msgstr "Vous avez été déconnecté."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:46
|
#: app/routes/evaluations.py:36
|
||||||
msgid "You do not have permission to view evaluations."
|
msgid "You do not have permission to view evaluations."
|
||||||
msgstr "Vous n’avez pas les droits pour consulter les évaluations."
|
msgstr "Vous n’avez pas les droits pour consulter les évaluations."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:136
|
#: app/routes/evaluations.py:126
|
||||||
msgid "You do not have permission to evaluate players."
|
msgid "You do not have permission to evaluate players."
|
||||||
msgstr "Vous n’avez pas les droits pour évaluer des joueurs."
|
msgstr "Vous n’avez pas les droits pour évaluer des joueurs."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:141 app/routes/evaluations.py:264
|
#: app/routes/evaluations.py:131 app/routes/evaluations.py:215
|
||||||
msgid "You do not have permission to evaluate players in this tryout."
|
msgid "You do not have permission to evaluate players in this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection."
|
msgstr "Vous n’avez pas les droits pour évaluer des joueurs dans cette sélection."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:152
|
#: app/routes/evaluations.py:142
|
||||||
msgid "Player is not registered for this tryout."
|
msgid "Player is not registered for this tryout."
|
||||||
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:157
|
#: app/routes/evaluations.py:147
|
||||||
msgid "Can only evaluate players."
|
msgid "Can only evaluate players."
|
||||||
msgstr "Seuls des joueurs peuvent être évalués."
|
msgstr "Seuls des joueurs peuvent être évalués."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:209
|
#: app/routes/evaluations.py:191
|
||||||
msgid "Evaluation updated!"
|
|
||||||
msgstr "Évaluation mise à jour."
|
|
||||||
|
|
||||||
#: app/routes/evaluations.py:229
|
|
||||||
msgid "Evaluation submitted successfully!"
|
msgid "Evaluation submitted successfully!"
|
||||||
msgstr "Évaluation enregistrée."
|
msgstr "Évaluation enregistrée."
|
||||||
|
|
||||||
#: app/routes/evaluations.py:259 app/routes/teams.py:270
|
#: app/routes/evaluations.py:193
|
||||||
|
msgid "Evaluation updated!"
|
||||||
|
msgstr "Évaluation mise à jour."
|
||||||
|
|
||||||
|
#: app/routes/evaluations.py:210 app/routes/teams.py:270
|
||||||
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
#: app/routes/teams.py:311 app/routes/teams.py:352 app/routes/teams.py:377
|
||||||
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:505
|
#: app/routes/teams.py:402 app/routes/teams.py:437 app/routes/tryouts.py:437
|
||||||
#: app/routes/tryouts.py:521 app/routes/tryouts.py:541
|
#: app/routes/tryouts.py:453 app/routes/tryouts.py:473
|
||||||
#: app/routes/tryouts.py:580 app/routes/tryouts.py:616
|
#: app/routes/tryouts.py:512 app/routes/tryouts.py:548
|
||||||
#: app/routes/tryouts.py:635
|
#: app/routes/tryouts.py:567
|
||||||
msgid "Permission denied."
|
msgid "Permission denied."
|
||||||
msgstr "Accès refusé."
|
msgstr "Accès refusé."
|
||||||
|
|
||||||
@@ -218,78 +290,49 @@ msgstr "Accès refusé."
|
|||||||
msgid "That language is not available."
|
msgid "That language is not available."
|
||||||
msgstr "Cette langue n’est pas disponible."
|
msgstr "Cette langue n’est pas disponible."
|
||||||
|
|
||||||
#: app/routes/matches.py:307
|
#: app/routes/matches.py:361
|
||||||
msgid "You do not have permission to schedule matches for this tryout."
|
msgid "You do not have permission to schedule matches for this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection."
|
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette sélection."
|
||||||
|
|
||||||
#: app/routes/matches.py:311 app/routes/matches.py:473
|
#: app/routes/matches.py:365 app/routes/matches.py:445
|
||||||
msgid "This tryout has ended. Matches can no longer be created or modified."
|
msgid "This tryout has ended. Matches can no longer be created or modified."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
|
"Cette sélection est terminée. Les matchs ne peuvent plus être créés ni "
|
||||||
"modifiés."
|
"modifiés."
|
||||||
|
|
||||||
#: app/routes/matches.py:332
|
#: app/routes/matches.py:427
|
||||||
msgid "Start time is required. Please select a time slot."
|
|
||||||
msgstr "L’heure de début est obligatoire. Choisissez une plage horaire."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:344 app/routes/matches.py:496
|
|
||||||
#: app/routes/team_matches.py:153 app/routes/team_matches.py:247
|
|
||||||
msgid "Invalid date format."
|
|
||||||
msgstr "Format de date invalide."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:364 app/routes/team_matches.py:174
|
|
||||||
msgid "Invalid time format."
|
|
||||||
msgstr "Format d’heure invalide."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:449
|
|
||||||
msgid "Match scheduled successfully!"
|
msgid "Match scheduled successfully!"
|
||||||
msgstr "Match planifié."
|
msgstr "Match planifié."
|
||||||
|
|
||||||
#: app/routes/matches.py:469 app/routes/team_matches.py:234
|
#: app/routes/matches.py:441 app/routes/team_matches.py:211
|
||||||
msgid "You do not have permission to edit this match."
|
msgid "You do not have permission to edit this match."
|
||||||
msgstr "Vous n’avez pas les droits pour modifier ce match."
|
msgstr "Vous n’avez pas les droits pour modifier ce match."
|
||||||
|
|
||||||
#: app/routes/matches.py:507
|
#: app/routes/matches.py:536 app/routes/team_matches.py:241
|
||||||
msgid "Start time is required."
|
|
||||||
msgstr "L’heure de début est obligatoire."
|
|
||||||
|
|
||||||
#: app/routes/matches.py:616 app/routes/team_matches.py:276
|
|
||||||
msgid "Match updated successfully!"
|
msgid "Match updated successfully!"
|
||||||
msgstr "Match mis à jour."
|
msgstr "Match mis à jour."
|
||||||
|
|
||||||
#: app/routes/matches.py:669 app/routes/team_matches.py:291
|
#: app/routes/matches.py:571 app/routes/team_matches.py:256
|
||||||
msgid "You do not have permission to delete this match."
|
msgid "You do not have permission to delete this match."
|
||||||
msgstr "Vous n’avez pas les droits pour supprimer ce match."
|
msgstr "Vous n’avez pas les droits pour supprimer ce match."
|
||||||
|
|
||||||
#: app/routes/matches.py:672
|
#: app/routes/matches.py:574
|
||||||
msgid "This tryout has ended. Matches can no longer be deleted."
|
msgid "This tryout has ended. Matches can no longer be deleted."
|
||||||
msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
|
msgstr "Cette sélection est terminée. Les matchs ne peuvent plus être supprimés."
|
||||||
|
|
||||||
#: app/routes/matches.py:685 app/routes/team_matches.py:295
|
#: app/routes/matches.py:587 app/routes/team_matches.py:260
|
||||||
msgid "Match deleted successfully."
|
msgid "Match deleted successfully."
|
||||||
msgstr "Match supprimé."
|
msgstr "Match supprimé."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:100
|
#: app/routes/team_matches.py:104
|
||||||
msgid "You do not have permission to schedule matches for this team."
|
msgid "You do not have permission to schedule matches for this team."
|
||||||
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe."
|
msgstr "Vous n’avez pas les droits pour planifier des matchs pour cette équipe."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:142
|
#: app/routes/team_matches.py:196
|
||||||
msgid "Date is required."
|
|
||||||
msgstr "La date est obligatoire."
|
|
||||||
|
|
||||||
#: app/routes/team_matches.py:220
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team match \"%(title)s\" scheduled successfully!"
|
msgid "Team match \"%(title)s\" scheduled successfully!"
|
||||||
msgstr "Match d’équipe « %(title)s » planifié."
|
msgstr "Match d’équipe « %(title)s » planifié."
|
||||||
|
|
||||||
#: app/routes/team_matches.py:259
|
|
||||||
msgid "Invalid start time format."
|
|
||||||
msgstr "Format d’heure de début invalide."
|
|
||||||
|
|
||||||
#: app/routes/team_matches.py:267
|
|
||||||
msgid "Invalid end time format."
|
|
||||||
msgstr "Format d’heure de fin invalide."
|
|
||||||
|
|
||||||
#: app/routes/teams.py:40
|
#: app/routes/teams.py:40
|
||||||
msgid "Use My Team(s) to view your teams."
|
msgid "Use My Team(s) to view your teams."
|
||||||
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
|
msgstr "Utilisez « Mon ou mes équipes » pour consulter vos équipes."
|
||||||
@@ -384,7 +427,7 @@ msgstr "Coach retiré de %(name)s."
|
|||||||
msgid "Manager removed from %(name)s."
|
msgid "Manager removed from %(name)s."
|
||||||
msgstr "Gérant retiré de %(name)s."
|
msgstr "Gérant retiré de %(name)s."
|
||||||
|
|
||||||
#: app/routes/teams.py:408 app/routes/tryouts.py:545 app/routes/tryouts.py:646
|
#: app/routes/teams.py:408 app/routes/tryouts.py:477 app/routes/tryouts.py:578
|
||||||
msgid "Please select a player."
|
msgid "Please select a player."
|
||||||
msgstr "Veuillez choisir un joueur."
|
msgstr "Veuillez choisir un joueur."
|
||||||
|
|
||||||
@@ -430,124 +473,112 @@ msgstr "Il n’est possible d’ajouter des notes que pour des joueurs."
|
|||||||
msgid "Note added for %(username)s!"
|
msgid "Note added for %(username)s!"
|
||||||
msgstr "Note ajoutée pour %(username)s."
|
msgstr "Note ajoutée pour %(username)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:77
|
#: app/routes/tryouts.py:98
|
||||||
msgid "You do not have permission to create tryouts."
|
msgid "You do not have permission to create tryouts."
|
||||||
msgstr "Vous n’avez pas les droits pour créer une sélection."
|
msgstr "Vous n’avez pas les droits pour créer une sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:103 app/routes/tryouts.py:210
|
#: app/routes/tryouts.py:145
|
||||||
msgid "Invalid start date format."
|
|
||||||
msgstr "Format de date de début invalide."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:118 app/routes/tryouts.py:225
|
|
||||||
msgid "End date cannot be before start date."
|
|
||||||
msgstr "La date de fin ne peut pas précéder la date de début."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:128 app/routes/tryouts.py:235
|
|
||||||
msgid "Invalid end date format."
|
|
||||||
msgstr "Format de date de fin invalide."
|
|
||||||
|
|
||||||
#: app/routes/tryouts.py:160
|
|
||||||
msgid "Tryout created successfully!"
|
msgid "Tryout created successfully!"
|
||||||
msgstr "Sélection créée."
|
msgstr "Sélection créée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:180
|
#: app/routes/tryouts.py:158
|
||||||
msgid "You do not have permission to edit this tryout."
|
msgid "You do not have permission to edit this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour modifier cette sélection."
|
msgstr "Vous n’avez pas les droits pour modifier cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:184
|
#: app/routes/tryouts.py:162
|
||||||
msgid "This tryout has ended and can no longer be modified."
|
msgid "This tryout has ended and can no longer be modified."
|
||||||
msgstr "Cette sélection est terminée et ne peut plus être modifiée."
|
msgstr "Cette sélection est terminée et ne peut plus être modifiée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:263
|
#: app/routes/tryouts.py:202
|
||||||
msgid "Tryout updated successfully!"
|
msgid "Tryout updated successfully!"
|
||||||
msgstr "Sélection mise à jour."
|
msgstr "Sélection mise à jour."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:310
|
#: app/routes/tryouts.py:242
|
||||||
msgid "You do not have permission to view this tryout."
|
msgid "You do not have permission to view this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour consulter cette sélection."
|
msgstr "Vous n’avez pas les droits pour consulter cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:472
|
#: app/routes/tryouts.py:404
|
||||||
msgid "Only players can register for tryouts."
|
msgid "Only players can register for tryouts."
|
||||||
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
|
msgstr "Seuls les joueurs peuvent s’inscrire à une sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:476
|
#: app/routes/tryouts.py:408
|
||||||
msgid "This tryout is not accepting registrations."
|
msgid "This tryout is not accepting registrations."
|
||||||
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
msgstr "Cette sélection n’accepte pas d’inscriptions."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:483
|
#: app/routes/tryouts.py:415
|
||||||
msgid "You are already registered for this tryout."
|
msgid "You are already registered for this tryout."
|
||||||
msgstr "Vous êtes déjà inscrit à cette sélection."
|
msgstr "Vous êtes déjà inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:489 app/routes/tryouts.py:564
|
#: app/routes/tryouts.py:421 app/routes/tryouts.py:496
|
||||||
msgid "This tryout is full."
|
msgid "This tryout is full."
|
||||||
msgstr "Cette sélection est complète."
|
msgstr "Cette sélection est complète."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:495
|
#: app/routes/tryouts.py:427
|
||||||
msgid "Successfully registered for tryout!"
|
msgid "Successfully registered for tryout!"
|
||||||
msgstr "Inscription à la sélection réussie."
|
msgstr "Inscription à la sélection réussie."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:511
|
#: app/routes/tryouts.py:443
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Tryout status updated to %(new_status)s."
|
msgid "Tryout status updated to %(new_status)s."
|
||||||
msgstr "Statut de la sélection mis à jour : %(new_status)s."
|
msgstr "Statut de la sélection mis à jour : %(new_status)s."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:531
|
#: app/routes/tryouts.py:463
|
||||||
msgid "Registration status updated."
|
msgid "Registration status updated."
|
||||||
msgstr "Statut d’inscription mis à jour."
|
msgstr "Statut d’inscription mis à jour."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:550
|
#: app/routes/tryouts.py:482
|
||||||
msgid "Can only register players."
|
msgid "Can only register players."
|
||||||
msgstr "Seuls des joueurs peuvent être inscrits."
|
msgstr "Seuls des joueurs peuvent être inscrits."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:556
|
#: app/routes/tryouts.py:488
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s is already registered for this tryout."
|
msgid "%(username)s is already registered for this tryout."
|
||||||
msgstr "%(username)s est déjà inscrit à cette sélection."
|
msgstr "%(username)s est déjà inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:570
|
#: app/routes/tryouts.py:502
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s registered for tryout!"
|
msgid "%(username)s registered for tryout!"
|
||||||
msgstr "%(username)s est inscrit à la sélection."
|
msgstr "%(username)s est inscrit à la sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:606
|
#: app/routes/tryouts.py:538
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(username)s removed from tryout."
|
msgid "%(username)s removed from tryout."
|
||||||
msgstr "%(username)s a été retiré de la sélection."
|
msgstr "%(username)s a été retiré de la sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:624
|
#: app/routes/tryouts.py:556
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Team \"%(team_name)s\" created!"
|
msgid "Team \"%(team_name)s\" created!"
|
||||||
msgstr "Équipe « %(team_name)s » créée."
|
msgstr "Équipe « %(team_name)s » créée."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:655
|
#: app/routes/tryouts.py:587
|
||||||
msgid "That player is not registered for this tryout."
|
msgid "That player is not registered for this tryout."
|
||||||
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
msgstr "Ce joueur n’est pas inscrit à cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:661
|
#: app/routes/tryouts.py:593
|
||||||
msgid "Player is already on this team."
|
msgid "Player is already on this team."
|
||||||
msgstr "Ce joueur est déjà dans cette équipe."
|
msgstr "Ce joueur est déjà dans cette équipe."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:666
|
#: app/routes/tryouts.py:598
|
||||||
msgid "Player added to team!"
|
msgid "Player added to team!"
|
||||||
msgstr "Joueur ajouté à l’équipe."
|
msgstr "Joueur ajouté à l’équipe."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:676
|
#: app/routes/tryouts.py:608
|
||||||
msgid "You do not have permission to delete this tryout."
|
msgid "You do not have permission to delete this tryout."
|
||||||
msgstr "Vous n’avez pas les droits pour supprimer cette sélection."
|
msgstr "Vous n’avez pas les droits pour supprimer cette sélection."
|
||||||
|
|
||||||
#: app/routes/tryouts.py:712
|
#: app/routes/tryouts.py:644
|
||||||
msgid "Tryout deleted successfully."
|
msgid "Tryout deleted successfully."
|
||||||
msgstr "Sélection supprimée."
|
msgstr "Sélection supprimée."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:46
|
#: app/routes/users/_shared.py:51
|
||||||
msgid "No file selected."
|
msgid "No file selected."
|
||||||
msgstr "Aucun fichier sélectionné."
|
msgstr "Aucun fichier sélectionné."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:50
|
#: app/routes/users/_shared.py:55
|
||||||
msgid "Only PDF files are allowed for contracts."
|
msgid "Only PDF files are allowed for contracts."
|
||||||
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
|
msgstr "Seuls les fichiers PDF sont acceptés pour les contrats."
|
||||||
|
|
||||||
#: app/routes/users/_shared.py:55
|
#: app/routes/users/_shared.py:60
|
||||||
msgid "That file is not a PDF, whatever its name says."
|
msgid "That file is not a PDF, whatever its name says."
|
||||||
msgstr "Ce fichier n’est pas un PDF, quel que soit son nom."
|
msgstr "Ce fichier n’est pas un PDF, quel que soit son nom."
|
||||||
|
|
||||||
@@ -2904,3 +2935,9 @@ msgstr "Voir le profil"
|
|||||||
#~ msgid "Answer"
|
#~ msgid "Answer"
|
||||||
#~ msgstr "Réponse"
|
#~ msgstr "Réponse"
|
||||||
|
|
||||||
|
#~ msgid "Invalid start time format."
|
||||||
|
#~ msgstr "Format d’heure de début invalide."
|
||||||
|
|
||||||
|
#~ msgid "Invalid end time format."
|
||||||
|
#~ msgstr "Format d’heure de fin invalide."
|
||||||
|
|
||||||
|
|||||||
+110
-1
@@ -23,7 +23,7 @@ from marshmallow import (
|
|||||||
validates_schema,
|
validates_schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.models import USER_TYPES
|
from app.models import ESPORT_GAMES, USER_TYPES
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Custom Validators
|
# Custom Validators
|
||||||
@@ -652,3 +652,112 @@ class TeamMatchSchema(ScheduledEventSchema):
|
|||||||
load_default=None,
|
load_default=None,
|
||||||
)
|
)
|
||||||
is_practice = fields.Boolean(load_default=False)
|
is_practice = fields.Boolean(load_default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class TryoutSchema(StripMixin):
|
||||||
|
"""A tryout event, created or edited.
|
||||||
|
|
||||||
|
`game` is checked against ESPORT_GAMES: it drives the position list and
|
||||||
|
the gamertag fields shown to registering players, so an unknown value
|
||||||
|
produced a tryout nobody could be evaluated for. It was accepted as any
|
||||||
|
string.
|
||||||
|
|
||||||
|
`max_players` was `int(x) if x else None`, which raised on 'twelve' and
|
||||||
|
happily stored -3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = fields.String(
|
||||||
|
required=True,
|
||||||
|
validate=validate.Length(min=1, max=200, error=_l('A title is required.')),
|
||||||
|
)
|
||||||
|
description = fields.String(
|
||||||
|
validate=validate.Length(max=5000),
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
)
|
||||||
|
game = fields.String(
|
||||||
|
required=True,
|
||||||
|
validate=validate.OneOf(ESPORT_GAMES, error=_l('Unknown game.')),
|
||||||
|
)
|
||||||
|
date = fields.Date(
|
||||||
|
required=True,
|
||||||
|
error_messages={
|
||||||
|
'invalid': _l('Invalid start date format.'),
|
||||||
|
'required': _l('A start date is required.'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
end_date = fields.Date(
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
error_messages={'invalid': _l('Invalid end date format.')},
|
||||||
|
)
|
||||||
|
location = fields.String(
|
||||||
|
validate=validate.Length(max=200),
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
)
|
||||||
|
max_players = fields.Integer(
|
||||||
|
validate=validate.Range(min=1, error=_l('A tryout must allow at least one player.')),
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
error_messages={'invalid': _l('The player limit must be a whole number.')},
|
||||||
|
)
|
||||||
|
target_org_team_id = fields.Integer(allow_none=True, load_default=None)
|
||||||
|
manager_id = fields.Integer(allow_none=True, load_default=None)
|
||||||
|
coach_ids = fields.List(fields.Integer(), load_default=list)
|
||||||
|
|
||||||
|
@validates_schema
|
||||||
|
def validate_span(self, data, **kwargs):
|
||||||
|
"""A tryout cannot end before it starts."""
|
||||||
|
end = data.get('end_date')
|
||||||
|
if end and data.get('date') and end < data['date']:
|
||||||
|
raise ValidationError(
|
||||||
|
_l('End date cannot be before start date.'), field_name='end_date'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def score_field():
|
||||||
|
"""One evaluation criterion: 1 to 10, or not scored at all."""
|
||||||
|
return fields.Integer(
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
validate=validate.Range(min=1, max=10, error=_l('Scores run from 1 to 10.')),
|
||||||
|
error_messages={'invalid': _l('A score must be a whole number from 1 to 10.')},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationSchema(StripMixin):
|
||||||
|
"""A coach's assessment of one player in one tryout (ARCH-005).
|
||||||
|
|
||||||
|
Each criterion is scored 1 to 10, or left blank. `validate_score` used to
|
||||||
|
turn anything else — 11, 0, 'good' — into None: the criterion silently
|
||||||
|
vanished from the average and the page reported the evaluation as
|
||||||
|
submitted. A coach could score a player 11 out of 10 and have it counted
|
||||||
|
as no score at all.
|
||||||
|
|
||||||
|
The nine are spelled out rather than generated from Evaluation.CRITERIA,
|
||||||
|
because a schema is worth reading. test_evaluations.py asserts that the
|
||||||
|
two lists match, so adding a tenth criterion to the model and forgetting
|
||||||
|
this file fails the suite rather than silently dropping the field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mecanics_score = score_field()
|
||||||
|
cohesion_score = score_field()
|
||||||
|
communication_score = score_field()
|
||||||
|
gamesense_score = score_field()
|
||||||
|
versatility_score = score_field()
|
||||||
|
discipline_score = score_field()
|
||||||
|
analysis_score = score_field()
|
||||||
|
sport_ethics_score = score_field()
|
||||||
|
mental_score = score_field()
|
||||||
|
|
||||||
|
comments = fields.String(
|
||||||
|
validate=validate.Length(max=5000),
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
)
|
||||||
|
position_recommendation = fields.String(
|
||||||
|
validate=validate.Length(max=50),
|
||||||
|
allow_none=True,
|
||||||
|
load_default=None,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""Evaluation scoring — the arithmetic and the boundary (ARCH-005, QUA-003).
|
||||||
|
|
||||||
|
The audit's note was short: "le calcul des scores d'évaluation n'a aucun
|
||||||
|
test". It had two defects worth the trouble of writing some.
|
||||||
|
|
||||||
|
`validate_score` mapped anything outside 1..10 to None, so a coach typing 11
|
||||||
|
or 0 had that criterion quietly dropped from the mean and was told the
|
||||||
|
evaluation had been submitted. Nothing distinguished "not assessed" from
|
||||||
|
"assessed, rejected, and forgotten".
|
||||||
|
|
||||||
|
And the mean itself lived inline in the route, summing nine named local
|
||||||
|
variables. It could not be exercised without an HTTP request, an
|
||||||
|
authenticated session and a database — which is TEST-002's point, and the
|
||||||
|
reason it had no tests at all.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import Evaluation, OrgTeam, Tryout, TryoutRegistration
|
||||||
|
from app.validators import EvaluationSchema
|
||||||
|
|
||||||
|
|
||||||
|
class TestOverallScore:
|
||||||
|
"""Pure arithmetic. No request, no session, no database."""
|
||||||
|
|
||||||
|
def test_the_overall_is_the_mean_of_what_was_scored(self):
|
||||||
|
scores = dict.fromkeys(Evaluation.CRITERIA, 6)
|
||||||
|
|
||||||
|
assert Evaluation.overall_from(scores) == 6
|
||||||
|
|
||||||
|
def test_a_blank_criterion_is_left_out_rather_than_counted_as_zero(self):
|
||||||
|
scores = dict.fromkeys(Evaluation.CRITERIA, None)
|
||||||
|
scores['mecanics_score'] = 8
|
||||||
|
scores['mental_score'] = 6
|
||||||
|
|
||||||
|
assert Evaluation.overall_from(scores) == 7
|
||||||
|
|
||||||
|
def test_nothing_scored_is_no_score_at_all(self):
|
||||||
|
"""None, not 0. The scale starts at one, so a zero would be a score
|
||||||
|
no player can be given, sorted below everyone in the listing."""
|
||||||
|
assert Evaluation.overall_from(dict.fromkeys(Evaluation.CRITERIA, None)) is None
|
||||||
|
assert Evaluation.overall_from({}) is None
|
||||||
|
|
||||||
|
def test_the_mean_is_not_rounded(self):
|
||||||
|
scores = dict.fromkeys(Evaluation.CRITERIA, None)
|
||||||
|
scores['mecanics_score'] = 7
|
||||||
|
scores['mental_score'] = 8
|
||||||
|
|
||||||
|
assert Evaluation.overall_from(scores) == 7.5
|
||||||
|
|
||||||
|
def test_a_key_outside_the_criteria_is_ignored(self):
|
||||||
|
"""apply_scores is handed the whole validated payload, comments and
|
||||||
|
all. Only the nine criteria may reach the mean."""
|
||||||
|
scores = dict.fromkeys(Evaluation.CRITERIA, 5)
|
||||||
|
scores['comments'] = 'excellent'
|
||||||
|
scores['position_recommendation'] = 'Support'
|
||||||
|
|
||||||
|
assert Evaluation.overall_from(scores) == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchemaMatchesModel:
|
||||||
|
def test_every_criterion_has_a_field(self):
|
||||||
|
"""The nine are spelled out in the schema for readability. This is
|
||||||
|
what stops the two lists drifting apart: a tenth criterion added to
|
||||||
|
the model and forgotten in validators.py would otherwise be accepted
|
||||||
|
unvalidated and dropped from the mean."""
|
||||||
|
declared = set(EvaluationSchema().fields)
|
||||||
|
|
||||||
|
assert set(Evaluation.CRITERIA) <= declared
|
||||||
|
|
||||||
|
def test_no_field_claims_to_be_a_criterion_and_is_not(self):
|
||||||
|
extra = set(EvaluationSchema().fields) - set(Evaluation.CRITERIA)
|
||||||
|
|
||||||
|
assert extra == {'comments', 'position_recommendation'}
|
||||||
|
|
||||||
|
|
||||||
|
class TestScoreValidation:
|
||||||
|
@pytest.mark.parametrize('bad', ['11', '0', '-3', 'good', '7.5'])
|
||||||
|
def test_a_score_outside_the_scale_is_refused(self, bad):
|
||||||
|
"""It used to become None: silently dropped, evaluation reported as
|
||||||
|
submitted."""
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
EvaluationSchema().load({'mecanics_score': bad})
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('good', ['1', '10', '6'])
|
||||||
|
def test_the_ends_of_the_scale_are_accepted(self, good):
|
||||||
|
assert EvaluationSchema().load({'mecanics_score': good})['mecanics_score'] == int(good)
|
||||||
|
|
||||||
|
def test_a_blank_score_means_not_assessed(self):
|
||||||
|
"""The form submits every criterion it renders, so an untouched one
|
||||||
|
arrives as an empty string rather than not arriving."""
|
||||||
|
data = EvaluationSchema().load({'mecanics_score': '', 'cohesion_score': '4'})
|
||||||
|
|
||||||
|
assert data['mecanics_score'] is None
|
||||||
|
assert data['cohesion_score'] == 4
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def evaluation_setup(app, as_role, make_user):
|
||||||
|
"""A coach who runs a tryout, and a player registered for it."""
|
||||||
|
coach_id = as_role('coach')
|
||||||
|
player_id = make_user('player')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
org_team = OrgTeam(name='Varsity', created_by=coach_id)
|
||||||
|
db.session.add(org_team)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
tryout = Tryout(
|
||||||
|
title='Spring',
|
||||||
|
game='Valorant',
|
||||||
|
date=date(2030, 4, 1),
|
||||||
|
created_by=coach_id,
|
||||||
|
target_org_team_id=org_team.id,
|
||||||
|
)
|
||||||
|
db.session.add(tryout)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
# Attached through the m2m relation, not the inherited coach_id
|
||||||
|
# column: ARCH-002 made the permission read the former.
|
||||||
|
tryout.coaches = [db.session.get(User, coach_id)]
|
||||||
|
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_id))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {'tryout_id': tryout.id, 'player_id': player_id, 'coach_id': coach_id}
|
||||||
|
|
||||||
|
|
||||||
|
class TestThroughTheForm:
|
||||||
|
def _submit(self, client, setup, **fields):
|
||||||
|
return client.post(
|
||||||
|
f'/evaluations/{setup["tryout_id"]}/{setup["player_id"]}',
|
||||||
|
data=fields,
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_a_valid_evaluation_is_stored_with_its_mean(self, app, client, evaluation_setup):
|
||||||
|
self._submit(
|
||||||
|
client,
|
||||||
|
evaluation_setup,
|
||||||
|
mecanics_score='8',
|
||||||
|
mental_score='6',
|
||||||
|
comments='Solid.',
|
||||||
|
)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
evaluation = Evaluation.query.one()
|
||||||
|
assert evaluation.mecanics_score == 8
|
||||||
|
assert evaluation.overall_score == 7
|
||||||
|
assert evaluation.comments == 'Solid.'
|
||||||
|
|
||||||
|
def test_an_out_of_range_score_stores_nothing(self, app, client, evaluation_setup):
|
||||||
|
self._submit(client, evaluation_setup, mecanics_score='11', mental_score='6')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
assert Evaluation.query.count() == 0
|
||||||
|
|
||||||
|
def test_a_second_submission_replaces_the_first(self, app, client, evaluation_setup):
|
||||||
|
self._submit(client, evaluation_setup, mecanics_score='8', mental_score='8')
|
||||||
|
self._submit(client, evaluation_setup, mecanics_score='4', mental_score='4')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
evaluation = Evaluation.query.one()
|
||||||
|
assert evaluation.overall_score == 4
|
||||||
|
|
||||||
|
def test_clearing_a_score_clears_it_and_moves_the_mean(self, app, client, evaluation_setup):
|
||||||
|
"""Every criterion is reassigned on edit, blanks included. Assigning
|
||||||
|
only the ones that came back filled would leave the old value on the
|
||||||
|
record and disagree with the mean beside it."""
|
||||||
|
self._submit(client, evaluation_setup, mecanics_score='10', mental_score='4')
|
||||||
|
self._submit(client, evaluation_setup, mecanics_score='', mental_score='4')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
evaluation = Evaluation.query.one()
|
||||||
|
assert evaluation.mecanics_score is None
|
||||||
|
assert evaluation.overall_score == 4
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Creating and editing a tryout, through the form (ARCH-005).
|
||||||
|
|
||||||
|
Same shape as the match routes: ten fields read off `request.form`, two of
|
||||||
|
them checked and the rest believed.
|
||||||
|
|
||||||
|
- `game` drives the position list and the gamertag fields a registering
|
||||||
|
player is shown. It was accepted as any string, so a typo produced a
|
||||||
|
tryout nobody could be evaluated for;
|
||||||
|
- `max_players` was `int(x) if x else None` — a 500 on 'twelve', and a
|
||||||
|
cheerful -3 otherwise;
|
||||||
|
- `coach_ids` were loaded with `User.id.in_(...)` and no role filter, so a
|
||||||
|
hand-made submission could name a player as coach of a tryout, which is
|
||||||
|
a permission grant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import OrgTeam, Tryout
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def form_context(app, as_role, make_user):
|
||||||
|
admin_id = as_role('admin')
|
||||||
|
coach_id = make_user('coach')
|
||||||
|
player_id = make_user('player')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
org_team = OrgTeam(name='Varsity', created_by=admin_id)
|
||||||
|
db.session.add(org_team)
|
||||||
|
db.session.commit()
|
||||||
|
return {'org_team_id': org_team.id, 'coach_id': coach_id, 'player_id': player_id}
|
||||||
|
|
||||||
|
|
||||||
|
VALID = {
|
||||||
|
'title': 'Spring tryout',
|
||||||
|
'game': 'Valorant',
|
||||||
|
'date': '2030-04-01',
|
||||||
|
'end_date': '2030-04-03',
|
||||||
|
'location': 'Arena',
|
||||||
|
'max_players': '12',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _create(client, **overrides):
|
||||||
|
return client.post('/tryouts/create', data=dict(VALID, **overrides), follow_redirects=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _only_tryout(app):
|
||||||
|
with app.app_context():
|
||||||
|
return Tryout.query.one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreating:
|
||||||
|
def test_a_valid_tryout_is_stored_with_typed_values(self, app, client, form_context):
|
||||||
|
_create(client)
|
||||||
|
|
||||||
|
tryout = _only_tryout(app)
|
||||||
|
assert tryout is not None
|
||||||
|
assert tryout.date == date(2030, 4, 1)
|
||||||
|
assert tryout.end_date == date(2030, 4, 3)
|
||||||
|
assert tryout.max_players == 12
|
||||||
|
|
||||||
|
def test_no_end_date_is_allowed(self, app, client, form_context):
|
||||||
|
_create(client, end_date='')
|
||||||
|
|
||||||
|
assert _only_tryout(app).end_date is None
|
||||||
|
|
||||||
|
def test_no_player_limit_is_allowed(self, app, client, form_context):
|
||||||
|
_create(client, max_players='')
|
||||||
|
|
||||||
|
assert _only_tryout(app).max_players is None
|
||||||
|
|
||||||
|
def test_a_coach_can_be_attached(self, app, client, form_context):
|
||||||
|
_create(client, coach_ids=str(form_context['coach_id']))
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
tryout = Tryout.query.one()
|
||||||
|
assert [c.id for c in tryout.coaches] == [form_context['coach_id']]
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefusals:
|
||||||
|
def test_an_unknown_game_is_refused(self, app, client, form_context):
|
||||||
|
_create(client, game='Pong')
|
||||||
|
|
||||||
|
assert _only_tryout(app) is None
|
||||||
|
|
||||||
|
def test_an_empty_title_is_refused(self, app, client, form_context):
|
||||||
|
_create(client, title='')
|
||||||
|
|
||||||
|
assert _only_tryout(app) is None
|
||||||
|
|
||||||
|
def test_a_non_numeric_player_limit_is_refused_not_crashed(self, app, client, form_context):
|
||||||
|
response = _create(client, max_players='twelve')
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert _only_tryout(app) is None
|
||||||
|
|
||||||
|
def test_a_negative_player_limit_is_refused(self, app, client, form_context):
|
||||||
|
_create(client, max_players='-3')
|
||||||
|
|
||||||
|
assert _only_tryout(app) is None
|
||||||
|
|
||||||
|
def test_an_end_before_the_start_is_refused(self, app, client, form_context):
|
||||||
|
_create(client, date='2030-04-05', end_date='2030-04-01')
|
||||||
|
|
||||||
|
assert _only_tryout(app) is None
|
||||||
|
|
||||||
|
def test_a_player_cannot_be_slipped_in_as_a_coach(self, app, client, form_context):
|
||||||
|
"""Not a form the interface offers — the select lists coaches. It is
|
||||||
|
a hand-made submission, and it used to work: being a coach of a
|
||||||
|
tryout carries the right to manage it and evaluate its players."""
|
||||||
|
_create(client, coach_ids=str(form_context['player_id']))
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
tryout = Tryout.query.one()
|
||||||
|
assert list(tryout.coaches) == []
|
||||||
Reference in New Issue
Block a user