refactor(authz): un seul point de verite pour les autorisations d equipe
ARCH-002. La question "a quelles equipes ce coach est-il rattache ?" etait
posee a huit endroits, de cinq facons differentes, et trois d entre elles
donnaient une mauvaise reponse en production.
Le motif fautif, present tel quel dans six routes :
OrgTeam.query.filter_by(coach_id=user.id).first()
Il repond au plus une equipe, et seulement par la colonne heritee. Deux
pannes en decoulaient, silencieuses -- les pages s affichaient, vides :
- un coach rattache uniquement par la relation many-to-many n avait
aucune equipe, donc aucun joueur, aucun contrat, aucune note d equipe,
aucun match a venir sur son tableau de bord ;
- un coach de deux equipes n en voyait qu une. Le formulaire de contrat
lui proposait la moitie de son effectif, alors que la route POST
acceptait l autre moitie.
app/permissions.py devient le module ou la question se pose une fois :
coach_org_teams, manager_org_teams, attached_org_teams, visible_org_teams,
can_manage_org_team, org_team_player_ids, coach_player_ids,
can_manage_player_contract, coach_tryouts, coach_manages_tryout. Toutes
lisent les deux rattachements et toutes les equipes.
Le meme ecart existait dans le modele : Coach.get_visible_tryouts ne lisait
que la relation m2m -- calendrier vide pour un coach rattache par la
colonne -- et can_manage_this_tryout ignorait la colonne pour l equipe
cible. Les deux delegent desormais.
Corrections de portee, au passage
- one_on_one lisait org_team.coach_id : un joueur dont l equipe declare
ses coachs par la relation etait informe qu il n avait pas de coach, et
le formulaire de demande restait ferme. Passe par get_coaches(), qui
retombe deja sur la colonne heritee.
- notes_dashboard conditionnait les notes personnelles du coach a
l existence d une equipe : un coach sans equipe ne voyait pas ses
propres notes.
- can_manage_team_match reformulait can_manage_this_org_team ; la
reformulation avait derive. Elle appelle maintenant la regle.
Limite assumee : le panneau de notes d equipe reste ecrit pour une seule
equipe et affiche donc la premiere. La resolution est corrigee, la mise en
page multi-equipes ne l est pas -- c est un choix produit, pas un bug.
14 tests ajoutes. Cinq echouent sur le code d avant, verifie en remettant
les routes et le modele a leur etat precedent.
ARCH-001 fera disparaitre la colonne heritee ; cela demande une migration
de donnees, donc Alembic. D ici la, ce module est ce qui rend la
duplication inoffensive.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
"""Coach — evaluates, schedules matches, manages their own org teams."""
|
||||
from app.models.user_model.user import User
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class Coach(User):
|
||||
"""Coach — evaluates, schedules matches, manages their own org team."""
|
||||
"""Coach — evaluates, schedules matches, manages their own org teams.
|
||||
|
||||
Every question about *which* teams or tryouts belong to this coach is
|
||||
delegated to ``app.permissions``. Two of the three methods below used to
|
||||
answer it themselves, each considering a different subset of the two
|
||||
ways a coach can be attached to a team: ``can_manage_this_tryout``
|
||||
ignored the legacy ``coach_id`` column when checking the target team,
|
||||
and ``get_visible_tryouts`` ignored it entirely. A coach attached only
|
||||
by that column therefore saw an empty calendar (ARCH-002).
|
||||
"""
|
||||
__mapper_args__ = {'polymorphic_identity': 'coach'}
|
||||
|
||||
def can_evaluate(self):
|
||||
@@ -17,37 +25,14 @@ class Coach(User):
|
||||
return True
|
||||
|
||||
def can_manage_this_tryout(self, tryout):
|
||||
from app.models.org_team.org_team import OrgTeam
|
||||
if tryout.target_org_team_id:
|
||||
is_coach_of_target = OrgTeam.query.filter(
|
||||
OrgTeam.id == tryout.target_org_team_id,
|
||||
OrgTeam.coaches.any(id=self.id),
|
||||
).first() is not None
|
||||
if is_coach_of_target:
|
||||
return True
|
||||
# Check many-to-many coaches relationship
|
||||
if any(c.id == self.id for c in tryout.coaches):
|
||||
return True
|
||||
# Backward compat: check deprecated coach_id
|
||||
if tryout.coach_id == self.id:
|
||||
return True
|
||||
return False
|
||||
from app.permissions import coach_manages_tryout
|
||||
return coach_manages_tryout(self, tryout)
|
||||
|
||||
def can_manage_this_org_team(self, org_team):
|
||||
if org_team.coaches.filter_by(id=self.id).first():
|
||||
return True
|
||||
if org_team.coach_id == self.id:
|
||||
return True
|
||||
return False
|
||||
return org_team.coach_id == self.id
|
||||
|
||||
def get_visible_tryouts(self):
|
||||
from app.models.tryout.tryout import Tryout
|
||||
team_ids = [t.id for t in self.coached_org_teams.all()]
|
||||
conditions = []
|
||||
if team_ids:
|
||||
conditions.append(Tryout.target_org_team_id.in_(team_ids))
|
||||
# Check many-to-many coaches
|
||||
conditions.append(Tryout.coaches.any(id=self.id))
|
||||
# Backward compat: check deprecated coach_id
|
||||
conditions.append(Tryout.coach_id == self.id)
|
||||
return Tryout.query.filter(db.or_(*conditions)).order_by(Tryout.date).all()
|
||||
from app.permissions import coach_tryouts
|
||||
return coach_tryouts(self)
|
||||
|
||||
Reference in New Issue
Block a user