ARCH-004 et la moitie NotificationService d ARCH-003. app/routes/users.py faisait 1 699 lignes et couvrait six sujets qui ne partageaient rien d autre qu un prefixe d URL. Il devient un paquet : blueprint.py l objet Blueprint, seul _shared.py helpers de formulaire, gamertags, validation de PDF accounts.py 348 l. liste, creation, edition, suppression, fiche availability.py 233 l. disponibilites joueur et creneaux coach contracts.py 206 l. depot, signature, telechargement notes.py 376 l. notes d equipe et notes nominatives one_on_one.py 259 l. demandes de seance individuelle profile.py 132 l. profil de la personne connectee Aucun fichier ne depasse 400 lignes -- le critere d acceptation de l audit. **Un seul blueprint, pas six.** Les endpoints restent `users.*`. Les renommer aurait touche 137 appels `url_for` dans les gabarits, pour un benefice nul : l objectif est un fichier qu on peut lire, pas une carte d URL a reapprendre. Les 30 endpoints sont identiques avant et apres, verifie sur url_map. send_discord_notification part dans app/services/notifications.py. Elle tirait `requests`, `logging` et le bot Discord dans un module dont le sujet est le traitement HTTP, et se trouvait coincee entre deux definitions de route. Son `except Exception` est conserve et documente : une notification qui n arrive pas ne doit pas annuler la transaction qu elle annoncait. tests/test_route_map.py, nouveau : il parcourt gabarits et code, releve tout endpoint nomme litteralement dans un url_for, et verifie qu il existe dans la carte. C est le mode de defaillance de ce genre de decoupage -- pas une erreur a l import, mais un BuildError chez la premiere personne qui ouvre la page concernee. Un test de garde verifie aussi que le scan trouve quelque chose, sinon le reste serait vide de sens. Piege rencontre, et corrige : test_role_change patchait `app.routes.users.update_user_gamertags`. Apres le decoupage ce nom est un reexport, pas celui qu accounts.py resout -- le patch aurait pu laisser la route appeler la vraie fonction et le test passer sans rien verifier. Ici monkeypatch a echoue bruyamment, mais la cible est desormais explicite et le test enregistre que la doublure a bien ete appelee. 347 tests passent (263 + 84, dont 82 parametres par la carte des routes). Co-Authored-By: Claude Opus 5 <[email protected]>
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
"""Outbound notifications.
|
|
|
|
Extracted from app/routes/users.py, where it sat between two route
|
|
definitions and pulled `requests`, `logging` and the Discord bot into a
|
|
module whose subject is HTTP handlers (ARCH-003).
|
|
|
|
Every failure here is swallowed and logged on purpose: a notification that
|
|
does not reach Discord must not roll back the session it was announcing.
|
|
That is the one place in this codebase where `except Exception` is the
|
|
right answer rather than an oversight.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import requests
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
#: Either a webhook URL, or a bare Discord user id to DM instead.
|
|
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL', '')
|
|
|
|
|
|
def send_discord_notification(
|
|
player_name,
|
|
points,
|
|
date_str,
|
|
start_time_str,
|
|
end_time_str,
|
|
team_name,
|
|
coach_name,
|
|
coach_discord,
|
|
coach_discord_id,
|
|
request_id=None,
|
|
):
|
|
"""Send a Discord notification for a One on One request.
|
|
|
|
Args:
|
|
player_name: Who is asking.
|
|
points: Free-text discussion points, possibly empty.
|
|
date_str, start_time_str, end_time_str: Already formatted for display.
|
|
team_name: The player's team, or None.
|
|
coach_name: Who is being asked.
|
|
coach_discord: The coach's Discord handle, for the webhook footer.
|
|
coach_discord_id: The coach's Discord snowflake, for a direct message.
|
|
request_id: OneOnOneRequest primary key, so reactions can find it back.
|
|
"""
|
|
|
|
if coach_discord_id:
|
|
try:
|
|
from app.discord_bot import send_one_on_one_dm
|
|
|
|
send_one_on_one_dm(
|
|
coach_name=coach_name,
|
|
coach_discord_id=coach_discord_id,
|
|
player_name=player_name,
|
|
team_name=team_name,
|
|
date_str=date_str,
|
|
start_time=start_time_str,
|
|
end_time=end_time_str,
|
|
points=points,
|
|
request_id=request_id,
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send Discord DM: {e}")
|
|
|
|
if DISCORD_WEBHOOK_URL:
|
|
try:
|
|
from app.discord_bot import send_one_on_one_dm
|
|
|
|
if DISCORD_WEBHOOK_URL.isdigit() and not coach_discord_id:
|
|
send_one_on_one_dm(
|
|
coach_name=coach_name,
|
|
coach_discord_id=DISCORD_WEBHOOK_URL,
|
|
player_name=player_name,
|
|
team_name=team_name,
|
|
date_str=date_str,
|
|
start_time=start_time_str,
|
|
end_time=end_time_str,
|
|
points=points,
|
|
)
|
|
elif not DISCORD_WEBHOOK_URL.isdigit():
|
|
embed = {
|
|
"embeds": [
|
|
{
|
|
"title": "One on One Request",
|
|
"color": 3447003,
|
|
"fields": [
|
|
{"name": "Player", "value": player_name, "inline": True},
|
|
{
|
|
"name": "Team",
|
|
"value": team_name or "Unknown Team",
|
|
"inline": True,
|
|
},
|
|
{"name": "Date", "value": date_str, "inline": True},
|
|
{
|
|
"name": "Time",
|
|
"value": f"{start_time_str} - {end_time_str}",
|
|
"inline": True,
|
|
},
|
|
{
|
|
"name": "Discussion Points",
|
|
"value": points or "No specific points provided",
|
|
"inline": False,
|
|
},
|
|
],
|
|
"footer": {
|
|
"text": f"Coach: {coach_name}"
|
|
+ (f" (Discord: {coach_discord})" if coach_discord else ""),
|
|
},
|
|
}
|
|
],
|
|
}
|
|
requests.post(DISCORD_WEBHOOK_URL, json=embed, timeout=5)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to send Discord notification: {e}")
|