fix(audit): centraliser l'horloge UTC
CI - Security, Lint & Tests / validate (push) Failing after 16m22s
CI - Security, Lint & Tests / validate (push) Failing after 16m22s
This commit is contained in:
+4
-2
@@ -65,6 +65,8 @@ from discord.ext import commands
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
load_dotenv()
|
||||
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
|
||||
|
||||
@@ -780,7 +782,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
coach_obj = request.coach
|
||||
|
||||
request.status = 'approved'
|
||||
request.responded_at = datetime.utcnow()
|
||||
request.responded_at = utc_now_naive()
|
||||
except SQLAlchemyError:
|
||||
db.session.rollback()
|
||||
logger.exception('Could not read One on One request %s to approve it', request_id)
|
||||
@@ -875,7 +877,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
|
||||
try:
|
||||
request.status = 'rejected'
|
||||
request.responded_at = datetime.utcnow()
|
||||
request.responded_at = utc_now_naive()
|
||||
if refusal_note:
|
||||
request.coach_rejection_message = refusal_note
|
||||
except SQLAlchemyError:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Abstract base class for availability models (PlayerDisponibility + CoachAvailability)."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class BaseAvailability(db.Model):
|
||||
@@ -13,5 +12,5 @@ class BaseAvailability(db.Model):
|
||||
day_of_week = db.Column(db.Integer, nullable=False)
|
||||
start_time = db.Column(db.Time, nullable=False)
|
||||
end_time = db.Column(db.Time, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Contract documents for players to sign."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class Contract(db.Model):
|
||||
@@ -23,7 +22,7 @@ class Contract(db.Model):
|
||||
|
||||
status = db.Column(db.String(20), default='pending')
|
||||
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)
|
||||
|
||||
player = db.relationship('User', foreign_keys=[player_id], backref='contracts')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Player evaluation record."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class Evaluation(db.Model):
|
||||
@@ -25,8 +24,8 @@ class Evaluation(db.Model):
|
||||
overall_score = db.Column(db.Float, nullable=True)
|
||||
comments = db.Column(db.Text, nullable=True)
|
||||
position_recommendation = db.Column(db.String(50), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('tryout_id', 'player_id', 'evaluator_id', name='unique_evaluation'),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Abstract base class for match models (Match + TeamMatch)."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class BaseMatch(db.Model):
|
||||
@@ -18,4 +17,4 @@ class BaseMatch(db.Model):
|
||||
location = db.Column(db.String(200), nullable=True)
|
||||
status = db.Column(db.String(20), default='scheduled')
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Request from player to coach for a One on One session."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class OneOnOneRequest(db.Model):
|
||||
@@ -18,7 +17,7 @@ class OneOnOneRequest(db.Model):
|
||||
end_time = db.Column(db.Time, nullable=False)
|
||||
points = db.Column(db.Text, nullable=True)
|
||||
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)
|
||||
discord_message_id = db.Column(db.BigInteger, nullable=True)
|
||||
coach_rejection_message = db.Column(db.Text, nullable=True)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Persistent organisation team (e.g. Varsity, JV)."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.models._associations import org_team_coaches, org_team_managers
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class OrgTeam(db.Model):
|
||||
@@ -13,7 +12,7 @@ class OrgTeam(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
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)
|
||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Many-to-many junction: player to org-team."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
status = db.Column(db.String(20), nullable=False, default='starter')
|
||||
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')
|
||||
org_team = db.relationship('OrgTeam', foreign_keys=[org_team_id], backref='team_players')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Abstract base class for match participant models."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class BaseParticipant(db.Model):
|
||||
@@ -11,4 +10,4 @@ class BaseParticipant(db.Model):
|
||||
__abstract__ = True
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Personal notes from coach to individual player."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class PersonalNote(db.Model):
|
||||
@@ -13,8 +12,8 @@ class PersonalNote(db.Model):
|
||||
player_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)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
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)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Tryout-specific team (e.g. Alpha, Bravo within a single tryout)."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class Team(db.Model):
|
||||
@@ -13,7 +12,7 @@ class Team(db.Model):
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False)
|
||||
name = db.Column(db.String(100), 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')
|
||||
members = db.relationship('TeamMember', backref='team', lazy='dynamic')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Link between a player and a tryout-specific team."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class TeamMember(db.Model):
|
||||
@@ -13,6 +12,6 @@ class TeamMember(db.Model):
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False)
|
||||
player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
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")
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Team improvement notes from coach."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
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)
|
||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
content = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||
updated_at = db.Column(db.DateTime, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
team = db.relationship('OrgTeam', backref='team_notes')
|
||||
coach = db.relationship('User', foreign_keys=[coach_id])
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""Tryout event for player evaluations and team formation."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.models._associations import tryout_coaches
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class Tryout(db.Model):
|
||||
@@ -25,7 +24,7 @@ class Tryout(db.Model):
|
||||
coach_id = db.Column(
|
||||
db.Integer, db.ForeignKey('users.id'), nullable=True
|
||||
) # 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')
|
||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Registration linking a player to a tryout."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class TryoutRegistration(db.Model):
|
||||
@@ -12,6 +11,6 @@ class TryoutRegistration(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.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')
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Base User model — shared fields and polymorphic configuration."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import UserMixin
|
||||
|
||||
from app.extensions import db
|
||||
from app.time_utils import utc_now_naive
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
@@ -25,7 +24,7 @@ class User(UserMixin, db.Model):
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
phone = db.Column(db.String(20), nullable=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)
|
||||
locked_until = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
+3
-2
@@ -8,7 +8,7 @@ password policy enforcement and sign-up screening.
|
||||
import os
|
||||
import secrets
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlencode, urlparse
|
||||
|
||||
import requests
|
||||
@@ -22,6 +22,7 @@ from app.forms import form_gamertags
|
||||
from app.i18n import LOCALE_SESSION_KEY
|
||||
from app.logging_config import log_auth_event
|
||||
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
|
||||
|
||||
#: Session key holding the pending OAuth2 anti-forgery token.
|
||||
@@ -294,7 +295,7 @@ def login():
|
||||
)
|
||||
if user.failed_login_attempts >= MAX_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(
|
||||
'account.throttled',
|
||||
username=username,
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
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_babel import gettext as _
|
||||
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.routes.matches import default_end_time
|
||||
from app.services.scheduling import notify_participants, zip_participants
|
||||
from app.time_utils import utc_now_naive
|
||||
from app.validators import TeamMatchSchema
|
||||
|
||||
team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches')
|
||||
@@ -100,7 +99,7 @@ def list_matches():
|
||||
teams=teams,
|
||||
match_data=match_data,
|
||||
pagination=matches_page,
|
||||
now=datetime.utcnow(),
|
||||
now=utc_now_naive(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+2
-3
@@ -3,8 +3,6 @@
|
||||
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_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
@@ -29,6 +27,7 @@ from app.models import (
|
||||
User,
|
||||
)
|
||||
from app.permissions import visible_org_teams
|
||||
from app.time_utils import utc_now_naive
|
||||
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
|
||||
|
||||
teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||
@@ -82,7 +81,7 @@ def my_teams():
|
||||
from app.models import TeamMatch, TeamMatchParticipant
|
||||
|
||||
player_teams = current_user.get_org_teams()
|
||||
now = datetime.utcnow()
|
||||
now = utc_now_naive()
|
||||
team_data = []
|
||||
|
||||
for org_team in player_teams:
|
||||
|
||||
@@ -4,8 +4,6 @@ This module handles CRUD operations for tryouts and player registrations.
|
||||
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_babel import gettext as _
|
||||
from flask_login import current_user, login_required
|
||||
@@ -33,6 +31,7 @@ from app.models import (
|
||||
TryoutRegistration,
|
||||
User,
|
||||
)
|
||||
from app.time_utils import utc_now_naive
|
||||
from app.validators import (
|
||||
PlayerSelectionSchema,
|
||||
TryoutRegistrationStatusSchema,
|
||||
@@ -114,7 +113,7 @@ def list_tryouts():
|
||||
Delegates to the polymorphic User subclass's get_visible_tryouts() method.
|
||||
"""
|
||||
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'])
|
||||
@@ -425,7 +424,7 @@ def view_tryout(tryout_id):
|
||||
matches=matches,
|
||||
match_data=match_data,
|
||||
game_positions=GAME_POSITIONS,
|
||||
now=datetime.utcnow(),
|
||||
now=utc_now_naive(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from flask import flash, redirect, render_template, request, send_file, url_for
|
||||
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.storage import CONTRACTS_DIR, document_path
|
||||
from app.time_utils import utc_now_naive
|
||||
from app.validators import UploadContractSchema
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ def upload_signed_contract(contract_id):
|
||||
contract.signed_filename = signed_filename
|
||||
contract.signed_file_path = signed_path
|
||||
contract.status = 'signed'
|
||||
contract.signed_at = datetime.utcnow()
|
||||
contract.signed_at = utc_now_naive()
|
||||
db.session.commit()
|
||||
flash(_('Signed contract uploaded successfully!'), 'success')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""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_babel import gettext as _
|
||||
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.routes.users.blueprint import users_bp
|
||||
from app.services.notifications import send_discord_notification
|
||||
from app.time_utils import utc_now_naive
|
||||
from app.validators import OneOnOneRejectionSchema, OneOnOneRequestSchema
|
||||
|
||||
|
||||
@@ -178,7 +177,7 @@ def accept_one_on_one(request_id):
|
||||
|
||||
player = request_obj.player
|
||||
request_obj.status = 'approved'
|
||||
request_obj.responded_at = datetime.utcnow()
|
||||
request_obj.responded_at = utc_now_naive()
|
||||
db.session.commit()
|
||||
|
||||
# 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
|
||||
|
||||
request_obj.status = 'rejected'
|
||||
request_obj.responded_at = datetime.utcnow()
|
||||
request_obj.responded_at = utc_now_naive()
|
||||
if rejection_reason:
|
||||
request_obj.coach_rejection_message = rejection_reason
|
||||
db.session.commit()
|
||||
|
||||
@@ -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)
|
||||
@@ -21,9 +21,6 @@ filterwarnings = [
|
||||
"default",
|
||||
# discord.py imports audioop, removed from the stdlib in 3.13.
|
||||
"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]
|
||||
|
||||
@@ -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 == []
|
||||
Reference in New Issue
Block a user