fix(audit): centraliser l'horloge UTC
CI - Security, Lint & Tests / validate (push) Failing after 16m22s

This commit is contained in:
GGThed
2026-08-17 15:21:08 -04:00
parent d7a8907953
commit e15b3c1293
25 changed files with 101 additions and 72 deletions
+4 -2
View File
@@ -65,6 +65,8 @@ from discord.ext import commands
from dotenv import load_dotenv from dotenv import load_dotenv
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from app.time_utils import utc_now_naive
load_dotenv() load_dotenv()
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN') DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
@@ -780,7 +782,7 @@ class TeamTryoutsBot(commands.Bot):
coach_obj = request.coach coach_obj = request.coach
request.status = 'approved' request.status = 'approved'
request.responded_at = datetime.utcnow() request.responded_at = utc_now_naive()
except SQLAlchemyError: except SQLAlchemyError:
db.session.rollback() db.session.rollback()
logger.exception('Could not read One on One request %s to approve it', request_id) logger.exception('Could not read One on One request %s to approve it', request_id)
@@ -875,7 +877,7 @@ class TeamTryoutsBot(commands.Bot):
try: try:
request.status = 'rejected' request.status = 'rejected'
request.responded_at = datetime.utcnow() request.responded_at = utc_now_naive()
if refusal_note: if refusal_note:
request.coach_rejection_message = refusal_note request.coach_rejection_message = refusal_note
except SQLAlchemyError: except SQLAlchemyError:
+3 -4
View File
@@ -1,8 +1,7 @@
"""Abstract base class for availability models (PlayerDisponibility + CoachAvailability).""" """Abstract base class for availability models (PlayerDisponibility + CoachAvailability)."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class BaseAvailability(db.Model): class BaseAvailability(db.Model):
@@ -13,5 +12,5 @@ class BaseAvailability(db.Model):
day_of_week = db.Column(db.Integer, nullable=False) day_of_week = db.Column(db.Integer, nullable=False)
start_time = db.Column(db.Time, nullable=False) start_time = db.Column(db.Time, nullable=False)
end_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
+2 -3
View File
@@ -1,8 +1,7 @@
"""Contract documents for players to sign.""" """Contract documents for players to sign."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class Contract(db.Model): class Contract(db.Model):
@@ -23,7 +22,7 @@ class Contract(db.Model):
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
notes = db.Column(db.Text, nullable=True) notes = db.Column(db.Text, nullable=True)
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) uploaded_at = db.Column(db.DateTime, default=utc_now_naive)
signed_at = db.Column(db.DateTime, nullable=True) signed_at = db.Column(db.DateTime, nullable=True)
player = db.relationship('User', foreign_keys=[player_id], backref='contracts') player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
+3 -4
View File
@@ -1,8 +1,7 @@
"""Player evaluation record.""" """Player evaluation record."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class Evaluation(db.Model): class Evaluation(db.Model):
@@ -25,8 +24,8 @@ class Evaluation(db.Model):
overall_score = db.Column(db.Float, nullable=True) overall_score = db.Column(db.Float, nullable=True)
comments = db.Column(db.Text, nullable=True) comments = db.Column(db.Text, nullable=True)
position_recommendation = db.Column(db.String(50), nullable=True) position_recommendation = db.Column(db.String(50), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
__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'),
+2 -3
View File
@@ -1,8 +1,7 @@
"""Abstract base class for match models (Match + TeamMatch).""" """Abstract base class for match models (Match + TeamMatch)."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class BaseMatch(db.Model): class BaseMatch(db.Model):
@@ -18,4 +17,4 @@ class BaseMatch(db.Model):
location = db.Column(db.String(200), nullable=True) location = db.Column(db.String(200), nullable=True)
status = db.Column(db.String(20), default='scheduled') status = db.Column(db.String(20), default='scheduled')
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
+2 -3
View File
@@ -1,8 +1,7 @@
"""Request from player to coach for a One on One session.""" """Request from player to coach for a One on One session."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class OneOnOneRequest(db.Model): class OneOnOneRequest(db.Model):
@@ -18,7 +17,7 @@ class OneOnOneRequest(db.Model):
end_time = db.Column(db.Time, nullable=False) end_time = db.Column(db.Time, nullable=False)
points = db.Column(db.Text, nullable=True) points = db.Column(db.Text, nullable=True)
status = db.Column(db.String(20), default='pending') status = db.Column(db.String(20), default='pending')
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
responded_at = db.Column(db.DateTime, nullable=True) responded_at = db.Column(db.DateTime, nullable=True)
discord_message_id = db.Column(db.BigInteger, nullable=True) discord_message_id = db.Column(db.BigInteger, nullable=True)
coach_rejection_message = db.Column(db.Text, nullable=True) coach_rejection_message = db.Column(db.Text, nullable=True)
+2 -3
View File
@@ -1,9 +1,8 @@
"""Persistent organisation team (e.g. Varsity, JV).""" """Persistent organisation team (e.g. Varsity, JV)."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.models._associations import org_team_coaches, org_team_managers from app.models._associations import org_team_coaches, org_team_managers
from app.time_utils import utc_now_naive
class OrgTeam(db.Model): class OrgTeam(db.Model):
@@ -13,7 +12,7 @@ class OrgTeam(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True) name = db.Column(db.String(100), nullable=False, unique=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
+2 -3
View File
@@ -1,8 +1,7 @@
"""Many-to-many junction: player to org-team.""" """Many-to-many junction: player to org-team."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class TeamPlayer(db.Model): class TeamPlayer(db.Model):
@@ -14,7 +13,7 @@ class TeamPlayer(db.Model):
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False) org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
status = db.Column(db.String(20), nullable=False, default='starter') status = db.Column(db.String(20), nullable=False, default='starter')
position = db.Column(db.String(50), nullable=True) position = db.Column(db.String(50), nullable=True)
added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=utc_now_naive)
player = db.relationship('User', foreign_keys=[player_id], backref='team_placements') player = db.relationship('User', foreign_keys=[player_id], backref='team_placements')
org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players') org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players')
+2 -3
View File
@@ -1,8 +1,7 @@
"""Abstract base class for match participant models.""" """Abstract base class for match participant models."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class BaseParticipant(db.Model): class BaseParticipant(db.Model):
@@ -11,4 +10,4 @@ class BaseParticipant(db.Model):
__abstract__ = True __abstract__ = True
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=utc_now_naive)
+3 -4
View File
@@ -1,8 +1,7 @@
"""Personal notes from coach to individual player.""" """Personal notes from coach to individual player."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class PersonalNote(db.Model): class PersonalNote(db.Model):
@@ -13,8 +12,8 @@ class PersonalNote(db.Model):
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
content = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True) match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=True)
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
+2 -3
View File
@@ -1,8 +1,7 @@
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout).""" """Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class Team(db.Model): class Team(db.Model):
@@ -13,7 +12,7 @@ class Team(db.Model):
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
name = db.Column(db.String(100), nullable=False) name = db.Column(db.String(100), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
creator = db.relationship('User', backref='created_teams') creator = db.relationship('User', backref='created_teams')
members = db.relationship('TeamMember', backref='team', lazy='dynamic') members = db.relationship('TeamMember', backref='team', lazy='dynamic')
+2 -3
View File
@@ -1,8 +1,7 @@
"""Link between a player and a tryout-specific team.""" """Link between a player and a tryout-specific team."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class TeamMember(db.Model): class TeamMember(db.Model):
@@ -13,6 +12,6 @@ class TeamMember(db.Model):
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False) team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
position = db.Column(db.String(50), nullable=True) position = db.Column(db.String(50), nullable=True)
added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=utc_now_naive)
player = db.relationship('User', overlaps="player_ref,team_assignments") player = db.relationship('User', overlaps="player_ref,team_assignments")
+3 -4
View File
@@ -1,8 +1,7 @@
"""Team improvement notes from coach.""" """Team improvement notes from coach."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class TeamNote(db.Model): class TeamNote(db.Model):
@@ -13,8 +12,8 @@ class TeamNote(db.Model):
org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False) org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=False)
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
content = db.Column(db.Text, nullable=False) content = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
team = db.relationship('OrgTeam', backref='team_notes') team = db.relationship('OrgTeam', backref='team_notes')
coach = db.relationship('User', foreign_keys=[coach_id]) coach = db.relationship('User', foreign_keys=[coach_id])
+2 -3
View File
@@ -1,9 +1,8 @@
"""Tryout event for player evaluations and team formation.""" """Tryout event for player evaluations and team formation."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.models._associations import tryout_coaches from app.models._associations import tryout_coaches
from app.time_utils import utc_now_naive
class Tryout(db.Model): class Tryout(db.Model):
@@ -25,7 +24,7 @@ class Tryout(db.Model):
coach_id = db.Column( coach_id = db.Column(
db.Integer, db.ForeignKey('users.id'), nullable=True db.Integer, db.ForeignKey('users.id'), nullable=True
) # deprecated, kept for migration ) # deprecated, kept for migration
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts') creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts') manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
+2 -3
View File
@@ -1,8 +1,7 @@
"""Registration linking a player to a tryout.""" """Registration linking a player to a tryout."""
from datetime import datetime
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class TryoutRegistration(db.Model): class TryoutRegistration(db.Model):
@@ -12,6 +11,6 @@ class TryoutRegistration(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
registered_at = db.Column(db.DateTime, default=datetime.utcnow) registered_at = db.Column(db.DateTime, default=utc_now_naive)
status = db.Column(db.String(20), default='registered') status = db.Column(db.String(20), default='registered')
notes = db.Column(db.Text, nullable=True) notes = db.Column(db.Text, nullable=True)
+2 -3
View File
@@ -1,10 +1,9 @@
"""Base User model — shared fields and polymorphic configuration.""" """Base User model — shared fields and polymorphic configuration."""
from datetime import datetime
from flask_login import UserMixin from flask_login import UserMixin
from app.extensions import db from app.extensions import db
from app.time_utils import utc_now_naive
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
@@ -25,7 +24,7 @@ class User(UserMixin, db.Model):
email = db.Column(db.String(120), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False)
phone = db.Column(db.String(20), nullable=True) phone = db.Column(db.String(20), nullable=True)
is_active_account = db.Column(db.Boolean, default=True) is_active_account = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=utc_now_naive)
failed_login_attempts = db.Column(db.Integer, default=0) failed_login_attempts = db.Column(db.Integer, default=0)
locked_until = db.Column(db.DateTime, nullable=True) locked_until = db.Column(db.DateTime, nullable=True)
+3 -2
View File
@@ -8,7 +8,7 @@ password policy enforcement and sign-up screening.
import os import os
import secrets import secrets
import time import time
from datetime import datetime, timedelta from datetime import timedelta
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
import requests import requests
@@ -22,6 +22,7 @@ from app.forms import form_gamertags
from app.i18n import LOCALE_SESSION_KEY from app.i18n import LOCALE_SESSION_KEY
from app.logging_config import log_auth_event from app.logging_config import log_auth_event
from app.models import ESPORT_GAMES, Player, User from app.models import ESPORT_GAMES, Player, User
from app.time_utils import utc_now_naive
from app.validators import LoginSchema, RegisterSchema, validate_discord_user_id from app.validators import LoginSchema, RegisterSchema, validate_discord_user_id
#: Session key holding the pending OAuth2 anti-forgery token. #: Session key holding the pending OAuth2 anti-forgery token.
@@ -294,7 +295,7 @@ def login():
) )
if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS: if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS:
minutes = cooloff_minutes(user.failed_login_attempts) minutes = cooloff_minutes(user.failed_login_attempts)
user.locked_until = datetime.utcnow() + timedelta(minutes=minutes) user.locked_until = utc_now_naive() + timedelta(minutes=minutes)
log_auth_event( log_auth_event(
'account.throttled', 'account.throttled',
username=username, username=username,
+2 -3
View File
@@ -3,8 +3,6 @@
Uses polymorphic isinstance checks instead of role-string comparisons. Uses polymorphic isinstance checks instead of role-string comparisons.
""" """
from datetime import datetime
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for from flask import Blueprint, flash, jsonify, 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
@@ -27,6 +25,7 @@ from app.pagination import paginate
from app.permissions import can_manage_org_team, coach_org_teams, visible_org_teams from app.permissions import can_manage_org_team, coach_org_teams, visible_org_teams
from app.routes.matches import default_end_time from app.routes.matches import default_end_time
from app.services.scheduling import notify_participants, zip_participants from app.services.scheduling import notify_participants, zip_participants
from app.time_utils import utc_now_naive
from app.validators import TeamMatchSchema from app.validators import TeamMatchSchema
team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches') team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches')
@@ -100,7 +99,7 @@ def list_matches():
teams=teams, teams=teams,
match_data=match_data, match_data=match_data,
pagination=matches_page, pagination=matches_page,
now=datetime.utcnow(), now=utc_now_naive(),
) )
+2 -3
View File
@@ -3,8 +3,6 @@
Uses polymorphic isinstance checks instead of role-string comparisons. Uses polymorphic isinstance checks instead of role-string comparisons.
""" """
from datetime import datetime
from flask import Blueprint, flash, jsonify, redirect, render_template, url_for from flask import Blueprint, flash, jsonify, redirect, render_template, 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
@@ -29,6 +27,7 @@ from app.models import (
User, User,
) )
from app.permissions import visible_org_teams from app.permissions import visible_org_teams
from app.time_utils import utc_now_naive
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
teams_bp = Blueprint('teams', __name__, url_prefix='/teams') teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
@@ -82,7 +81,7 @@ def my_teams():
from app.models import TeamMatch, TeamMatchParticipant from app.models import TeamMatch, TeamMatchParticipant
player_teams = current_user.get_org_teams() player_teams = current_user.get_org_teams()
now = datetime.utcnow() now = utc_now_naive()
team_data = [] team_data = []
for org_team in player_teams: for org_team in player_teams:
+3 -4
View File
@@ -4,8 +4,6 @@ This module handles CRUD operations for tryouts and player registrations.
Uses polymorphic isinstance checks instead of role-string comparisons. Uses polymorphic isinstance checks instead of role-string comparisons.
""" """
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
@@ -33,6 +31,7 @@ from app.models import (
TryoutRegistration, TryoutRegistration,
User, User,
) )
from app.time_utils import utc_now_naive
from app.validators import ( from app.validators import (
PlayerSelectionSchema, PlayerSelectionSchema,
TryoutRegistrationStatusSchema, TryoutRegistrationStatusSchema,
@@ -114,7 +113,7 @@ def list_tryouts():
Delegates to the polymorphic User subclass's get_visible_tryouts() method. Delegates to the polymorphic User subclass's get_visible_tryouts() method.
""" """
tryouts = current_user.get_visible_tryouts() tryouts = current_user.get_visible_tryouts()
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow()) return render_template('pages/tryouts.html', tryouts=tryouts, now=utc_now_naive())
@tryouts_bp.route('/create', methods=['GET', 'POST']) @tryouts_bp.route('/create', methods=['GET', 'POST'])
@@ -425,7 +424,7 @@ def view_tryout(tryout_id):
matches=matches, matches=matches,
match_data=match_data, match_data=match_data,
game_positions=GAME_POSITIONS, game_positions=GAME_POSITIONS,
now=datetime.utcnow(), now=utc_now_naive(),
) )
+2 -2
View File
@@ -2,7 +2,6 @@
import os import os
import uuid import uuid
from datetime import datetime
from flask import flash, redirect, render_template, request, send_file, url_for from flask import flash, redirect, render_template, request, send_file, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
@@ -20,6 +19,7 @@ from app.routes.users._shared import (
) )
from app.routes.users.blueprint import users_bp from app.routes.users.blueprint import users_bp
from app.storage import CONTRACTS_DIR, document_path from app.storage import CONTRACTS_DIR, document_path
from app.time_utils import utc_now_naive
from app.validators import UploadContractSchema from app.validators import UploadContractSchema
@@ -172,7 +172,7 @@ def upload_signed_contract(contract_id):
contract.signed_filename = signed_filename contract.signed_filename = signed_filename
contract.signed_file_path = signed_path contract.signed_file_path = signed_path
contract.status = 'signed' contract.status = 'signed'
contract.signed_at = datetime.utcnow() contract.signed_at = utc_now_naive()
db.session.commit() db.session.commit()
flash(_('Signed contract uploaded successfully!'), 'success') flash(_('Signed contract uploaded successfully!'), 'success')
return redirect(url_for('users.list_contracts')) return redirect(url_for('users.list_contracts'))
+3 -4
View File
@@ -1,7 +1,5 @@
"""One-on-one sessions between a player and their coach.""" """One-on-one sessions between a player and their coach."""
from datetime import datetime
from flask import flash, redirect, render_template, request, url_for from flask import 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
@@ -12,6 +10,7 @@ from app.forms import flash_validation_errors, form_payload
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
from app.routes.users.blueprint import users_bp from app.routes.users.blueprint import users_bp
from app.services.notifications import send_discord_notification from app.services.notifications import send_discord_notification
from app.time_utils import utc_now_naive
from app.validators import OneOnOneRejectionSchema, OneOnOneRequestSchema from app.validators import OneOnOneRejectionSchema, OneOnOneRequestSchema
@@ -178,7 +177,7 @@ def accept_one_on_one(request_id):
player = request_obj.player player = request_obj.player
request_obj.status = 'approved' request_obj.status = 'approved'
request_obj.responded_at = datetime.utcnow() request_obj.responded_at = utc_now_naive()
db.session.commit() db.session.commit()
# Notify player via Discord (same message as if approved through Discord reactions) # Notify player via Discord (same message as if approved through Discord reactions)
@@ -235,7 +234,7 @@ def reject_one_on_one(request_id):
player = request_obj.player player = request_obj.player
request_obj.status = 'rejected' request_obj.status = 'rejected'
request_obj.responded_at = datetime.utcnow() request_obj.responded_at = utc_now_naive()
if rejection_reason: if rejection_reason:
request_obj.coach_rejection_message = rejection_reason request_obj.coach_rejection_message = rejection_reason
db.session.commit() db.session.commit()
+19
View File
@@ -0,0 +1,19 @@
"""Time helpers with explicit storage semantics.
The deployed schema currently stores timestamps in ``DateTime`` columns
without timezone information. Until the real PostgreSQL schema is restored
and migrated, application timestamps must therefore remain naive values.
They are nevertheless generated from an aware UTC clock so the convention is
explicit and does not rely on the deprecated :meth:`datetime.utcnow` API.
"""
from datetime import UTC, datetime
def utc_now_naive() -> datetime:
"""Return the current UTC instant without ``tzinfo`` for legacy columns.
Replace this compatibility boundary with aware UTC values when the
corresponding columns are migrated to timezone-aware types.
"""
return datetime.now(UTC).replace(tzinfo=None)
-3
View File
@@ -21,9 +21,6 @@ filterwarnings = [
"default", "default",
# discord.py imports audioop, removed from the stdlib in 3.13. # discord.py imports audioop, removed from the stdlib in 3.13.
"ignore:'audioop' is deprecated:DeprecationWarning", "ignore:'audioop' is deprecated:DeprecationWarning",
# Every model uses datetime.utcnow as a column default. Tracked as
# DB-009; the warning would otherwise drown the run.
"ignore:datetime.datetime.utcnow:DeprecationWarning",
] ]
[tool.coverage.report] [tool.coverage.report]
+29
View File
@@ -0,0 +1,29 @@
"""Regression tests for the application's timestamp convention."""
import ast
from datetime import UTC, datetime
from pathlib import Path
from app.time_utils import utc_now_naive
def test_utc_now_naive_is_an_explicit_utc_value():
before = datetime.now(UTC).replace(tzinfo=None)
actual = utc_now_naive()
after = datetime.now(UTC).replace(tzinfo=None)
assert actual.tzinfo is None
assert before <= actual <= after
def test_application_does_not_call_deprecated_utcnow():
app_root = Path(__file__).parents[1] / 'app'
offenders = []
for path in app_root.rglob('*.py'):
tree = ast.parse(path.read_text(encoding='utf-8'), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Attribute) and node.attr == 'utcnow':
offenders.append(f'{path.relative_to(app_root)}:{node.lineno}')
assert offenders == []