Compare commits
5
Commits
d7a8907953
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a616c79663 | ||
|
|
48ca62cdd0 | ||
|
|
1bef716a12 | ||
|
|
a0e31d1a2f | ||
|
|
e15b3c1293 |
+4
-2
@@ -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:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from app.models._constants import (
|
|||||||
GAME_PLATFORMS,
|
GAME_PLATFORMS,
|
||||||
PLATFORM_CODES,
|
PLATFORM_CODES,
|
||||||
TRN_URLS,
|
TRN_URLS,
|
||||||
|
EVALUATION_CRITERIA,
|
||||||
)
|
)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -37,3 +37,13 @@ tryout_coaches = db.Table(
|
|||||||
'coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
'coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tryout_managers = db.Table(
|
||||||
|
'tryout_managers',
|
||||||
|
db.Column(
|
||||||
|
'tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'), primary_key=True
|
||||||
|
),
|
||||||
|
db.Column(
|
||||||
|
'manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ Contains game lists, position mappings, platform codes, and TRN URL templates.
|
|||||||
|
|
||||||
USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout']
|
USER_TYPES = ['admin', 'manager', 'coach', 'player', 'scout']
|
||||||
|
|
||||||
|
# Ordered list of (field_name, human_label) pairs for the player evaluation
|
||||||
|
# score criteria. Kept in a single place so the evaluation forms, batch
|
||||||
|
# evaluation page, and any future reporting all stay in sync.
|
||||||
|
EVALUATION_CRITERIA = [
|
||||||
|
('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'),
|
||||||
|
]
|
||||||
|
|
||||||
ESPORT_GAMES = [
|
ESPORT_GAMES = [
|
||||||
'Valorant',
|
'Valorant',
|
||||||
'League of Legends',
|
'League of Legends',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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, tryout_managers
|
||||||
|
from app.time_utils import utc_now_naive
|
||||||
|
|
||||||
|
|
||||||
class Tryout(db.Model):
|
class Tryout(db.Model):
|
||||||
@@ -21,16 +20,17 @@ class Tryout(db.Model):
|
|||||||
max_players = db.Column(db.Integer, nullable=True)
|
max_players = db.Column(db.Integer, nullable=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)
|
||||||
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.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) # deprecated, kept for migration
|
||||||
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') # deprecated
|
||||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
||||||
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
||||||
|
managers = db.relationship('User', secondary=tryout_managers, backref='managed_tryouts_m2m')
|
||||||
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
||||||
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
||||||
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
||||||
@@ -48,3 +48,17 @@ class Tryout(db.Model):
|
|||||||
if self.end_date is not None:
|
if self.end_date is not None:
|
||||||
return self.end_date < today
|
return self.end_date < today
|
||||||
return self.date < today
|
return self.date < today
|
||||||
|
|
||||||
|
def get_managers(self):
|
||||||
|
"""Managers attached to this tryout, both legacy and many-to-many."""
|
||||||
|
manager_list = list(self.managers)
|
||||||
|
if not manager_list and self.manager:
|
||||||
|
return [self.manager]
|
||||||
|
return manager_list
|
||||||
|
|
||||||
|
def get_coaches(self):
|
||||||
|
"""Coaches attached to this tryout, both legacy and many-to-many."""
|
||||||
|
coach_list = list(self.coaches)
|
||||||
|
if not coach_list and self.coach:
|
||||||
|
return [self.coach]
|
||||||
|
return coach_list
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class Manager(User):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def can_manage_this_tryout(self, tryout):
|
def can_manage_this_tryout(self, tryout):
|
||||||
return tryout.created_by == self.id or tryout.manager_id == self.id
|
return (
|
||||||
|
tryout.created_by == self.id
|
||||||
|
or tryout.manager_id == self.id
|
||||||
|
or any(m.id == self.id for m in tryout.managers)
|
||||||
|
)
|
||||||
|
|
||||||
def can_manage_this_org_team(self, org_team):
|
def can_manage_this_org_team(self, org_team):
|
||||||
return True
|
return True
|
||||||
@@ -32,7 +36,13 @@ class Manager(User):
|
|||||||
from app.models.tryout.tryout import Tryout
|
from app.models.tryout.tryout import Tryout
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Tryout.query.filter(or_(Tryout.created_by == self.id, Tryout.manager_id == self.id))
|
Tryout.query.filter(
|
||||||
|
or_(
|
||||||
|
Tryout.created_by == self.id,
|
||||||
|
Tryout.manager_id == self.id,
|
||||||
|
Tryout.managers.any(id=self.id),
|
||||||
|
)
|
||||||
|
)
|
||||||
.order_by(Tryout.date)
|
.order_by(Tryout.date)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ 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 marshmallow import ValidationError
|
||||||
|
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import (
|
||||||
|
Admin, Coach, Manager, Player,
|
||||||
|
User, Tryout, Evaluation, TryoutRegistration,
|
||||||
|
OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA,
|
||||||
|
)
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
@@ -245,3 +253,94 @@ def players_to_evaluate(tryout_id):
|
|||||||
]
|
]
|
||||||
|
|
||||||
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
|
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
|
||||||
|
|
||||||
|
|
||||||
|
@evaluations_bp.route('/<int:tryout_id>/batch', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def batch_evaluate(tryout_id):
|
||||||
|
"""Evaluate multiple players at once in a tryout.
|
||||||
|
|
||||||
|
GET renders a single form listing every selected player with their
|
||||||
|
evaluation criteria. POST saves (creates or updates) all of them.
|
||||||
|
"""
|
||||||
|
if not current_user.can_evaluate():
|
||||||
|
flash('You do not have permission to evaluate players.', 'danger')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
tryout = Tryout.query.get_or_404(tryout_id)
|
||||||
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
|
flash('You do not have permission to evaluate players in this tryout.', 'danger')
|
||||||
|
return redirect(url_for('tryouts.list_tryouts'))
|
||||||
|
|
||||||
|
# Resolve selected player ids (query string on GET, hidden fields on POST).
|
||||||
|
player_ids = []
|
||||||
|
for raw in request.values.getlist('player_ids'):
|
||||||
|
try:
|
||||||
|
pid = int(raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if pid not in player_ids:
|
||||||
|
player_ids.append(pid)
|
||||||
|
|
||||||
|
if not player_ids:
|
||||||
|
flash('Please select at least one player to evaluate.', 'warning')
|
||||||
|
return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id))
|
||||||
|
|
||||||
|
players = []
|
||||||
|
for pid in player_ids:
|
||||||
|
player = User.query.get(pid)
|
||||||
|
if not player or not isinstance(player, Player):
|
||||||
|
continue
|
||||||
|
is_registered = TryoutRegistration.query.filter_by(
|
||||||
|
tryout_id=tryout_id, player_id=pid,
|
||||||
|
).first() is not None
|
||||||
|
if not is_registered:
|
||||||
|
continue
|
||||||
|
existing = Evaluation.query.filter_by(
|
||||||
|
tryout_id=tryout_id, player_id=pid, evaluator_id=current_user.id,
|
||||||
|
).first()
|
||||||
|
existing_scores = {
|
||||||
|
field_name: getattr(existing, field_name) if existing else None
|
||||||
|
for field_name, _ in EVALUATION_CRITERIA
|
||||||
|
}
|
||||||
|
players.append({
|
||||||
|
'player': player,
|
||||||
|
'existing': existing,
|
||||||
|
'existing_scores': existing_scores,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not players:
|
||||||
|
flash('No valid players selected for evaluation.', 'danger')
|
||||||
|
return redirect(url_for('evaluations.players_to_evaluate', tryout_id=tryout_id))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
saved = 0
|
||||||
|
for entry in players:
|
||||||
|
pid = entry['player'].id
|
||||||
|
scores = {
|
||||||
|
field_name: validate_score(request.form.get(f'{field_name}_{pid}'))
|
||||||
|
for field_name, _ in EVALUATION_CRITERIA
|
||||||
|
}
|
||||||
|
comments = request.form.get(f'comments_{pid}')
|
||||||
|
position = request.form.get(f'position_recommendation_{pid}')
|
||||||
|
|
||||||
|
existing = entry['existing']
|
||||||
|
if existing:
|
||||||
|
_apply_evaluation(existing, scores, comments, position)
|
||||||
|
else:
|
||||||
|
evaluation = Evaluation(
|
||||||
|
tryout_id=tryout_id, player_id=pid,
|
||||||
|
evaluator_id=current_user.id,
|
||||||
|
)
|
||||||
|
_apply_evaluation(evaluation, scores, comments, position)
|
||||||
|
db.session.add(evaluation)
|
||||||
|
saved += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash(f'Saved evaluations for {saved} player(s).', 'success')
|
||||||
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
|
return render_template('pages/batch_evaluate.html',
|
||||||
|
tryout=tryout, players=players,
|
||||||
|
evaluation_criteria=EVALUATION_CRITERIA,
|
||||||
|
game_positions=GAME_POSITIONS)
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
+21
-9
@@ -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,
|
||||||
@@ -52,7 +51,7 @@ def can_manage():
|
|||||||
|
|
||||||
def tryout_form_payload():
|
def tryout_form_payload():
|
||||||
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
||||||
return form_payload(list_fields=('coach_ids',), optional_blank=())
|
return form_payload(list_fields=('coach_ids', 'manager_ids'), optional_blank=())
|
||||||
|
|
||||||
|
|
||||||
def coaches_from_ids(coach_ids):
|
def coaches_from_ids(coach_ids):
|
||||||
@@ -68,6 +67,13 @@ def coaches_from_ids(coach_ids):
|
|||||||
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
||||||
|
|
||||||
|
|
||||||
|
def managers_from_ids(manager_ids):
|
||||||
|
"""The manager accounts behind these ids, filtered by role."""
|
||||||
|
if not manager_ids:
|
||||||
|
return []
|
||||||
|
return User.query.filter(User.id.in_(manager_ids), User.role == 'manager').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.
|
||||||
|
|
||||||
@@ -114,7 +120,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'])
|
||||||
@@ -161,12 +167,12 @@ def create_tryout():
|
|||||||
created_by=current_user.id,
|
created_by=current_user.id,
|
||||||
status='upcoming',
|
status='upcoming',
|
||||||
target_org_team_id=data['target_org_team_id'],
|
target_org_team_id=data['target_org_team_id'],
|
||||||
manager_id=data['manager_id'],
|
|
||||||
)
|
)
|
||||||
db.session.add(tryout)
|
db.session.add(tryout)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
|
tryout.managers = managers_from_ids(data['manager_ids'])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout created successfully!'), 'success')
|
flash(_('Tryout created successfully!'), 'success')
|
||||||
@@ -222,8 +228,14 @@ def edit_tryout(tryout_id):
|
|||||||
tryout.location = data['location']
|
tryout.location = data['location']
|
||||||
tryout.max_players = data['max_players']
|
tryout.max_players = data['max_players']
|
||||||
tryout.target_org_team_id = data['target_org_team_id']
|
tryout.target_org_team_id = data['target_org_team_id']
|
||||||
tryout.manager_id = data['manager_id']
|
|
||||||
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
# Only update staff lists when the form explicitly sends them.
|
||||||
|
# An absent checkbox group (all unchecked or JS failed) means
|
||||||
|
# \"don't change\", not \"remove everyone\".
|
||||||
|
if 'coach_ids' in request.form:
|
||||||
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
|
if 'manager_ids' in request.form:
|
||||||
|
tryout.managers = managers_from_ids(data['manager_ids'])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout updated successfully!'), 'success')
|
flash(_('Tryout updated successfully!'), 'success')
|
||||||
@@ -242,7 +254,7 @@ def view_tryout(tryout_id):
|
|||||||
if isinstance(current_user, Admin):
|
if isinstance(current_user, Admin):
|
||||||
can_view = True
|
can_view = True
|
||||||
elif isinstance(current_user, Manager):
|
elif isinstance(current_user, Manager):
|
||||||
can_view = tryout.created_by == current_user.id or tryout.manager_id == current_user.id
|
can_view = current_user.can_manage_this_tryout(tryout)
|
||||||
elif isinstance(current_user, Coach):
|
elif isinstance(current_user, Coach):
|
||||||
can_view = current_user.can_manage_this_tryout(tryout)
|
can_view = current_user.can_manage_this_tryout(tryout)
|
||||||
elif isinstance(current_user, Player):
|
elif isinstance(current_user, Player):
|
||||||
@@ -425,7 +437,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(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1270
File diff suppressed because it is too large
Load Diff
@@ -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'))
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -2037,3 +2037,30 @@ a:hover { color: var(--primary-dark); }
|
|||||||
.honeypot {
|
.honeypot {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Batch Evaluation - 2 cards wide layout */
|
||||||
|
.batch-eval-form {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-eval-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-eval-grid .card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Allow criteria rows to wrap for a 3x3 grid inside each card */
|
||||||
|
.batch-eval-grid .form-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.batch-eval-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Evaluate Players - UdeS team manager{% endblock %}
|
||||||
|
{% block page_title %}Evaluate Players{% endblock %}
|
||||||
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Evaluate</span>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}" class="form batch-eval-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
{% for entry in players %}
|
||||||
|
<input type="hidden" name="player_ids" value="{{ entry.player.id }}"/>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% set positions = game_positions.get(tryout.game, []) %}
|
||||||
|
|
||||||
|
<div class="batch-eval-grid">
|
||||||
|
{% for entry in players %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fas fa-user"></i> {{ entry.player.username }}
|
||||||
|
{% if entry.existing %}
|
||||||
|
<span class="badge badge-success">Already Evaluated</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-warning">Not Evaluated</span>
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="eval-player-info mb-4">
|
||||||
|
<div class="user-avatar avatar-lg">{{ entry.player.username[:2] | upper }}</div>
|
||||||
|
<div>
|
||||||
|
<h3>{{ entry.player.username }}</h3>
|
||||||
|
<p class="text-muted">{{ entry.player.email }} | {{ entry.player.phone or 'No phone' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set pid = entry.player.id %}
|
||||||
|
{% set existing = entry.existing %}
|
||||||
|
{% set existing_scores = entry.existing_scores %}
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
{% for field_name, label in evaluation_criteria %}
|
||||||
|
<div class="form-group col-4">
|
||||||
|
<label for="{{ field_name }}_{{ pid }}">{{ label }} (1-10)</label>
|
||||||
|
<div class="score-input">
|
||||||
|
<input type="range" id="{{ field_name }}_{{ pid }}" name="{{ field_name }}_{{ pid }}" min="1" max="10" value="{{ existing_scores[field_name] or 5 }}" data-mirror>
|
||||||
|
<span class="range-value">{{ existing_scores[field_name] or 5 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group col-12">
|
||||||
|
<label for="position_recommendation_{{ pid }}">Recommended Position</label>
|
||||||
|
{% if positions %}
|
||||||
|
<select id="position_recommendation_{{ pid }}" name="position_recommendation_{{ pid }}" class="form-select">
|
||||||
|
<option value="">-- Select Position --</option>
|
||||||
|
{% for pos in positions %}
|
||||||
|
<option value="{{ pos }}" {% if existing and existing.position_recommendation == pos %}selected{% endif %}>{{ pos }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input type="text" id="position_recommendation_{{ pid }}" name="position_recommendation_{{ pid }}" value="{{ existing.position_recommendation if existing else '' }}" placeholder="Enter position (optional)">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="comments_{{ pid }}">Comments</label>
|
||||||
|
<textarea id="comments_{{ pid }}" name="comments_{{ pid }}" rows="3" placeholder="Enter your evaluation notes...">{{ existing.comments if existing else '' }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="{{ url_for('evaluations.players_to_evaluate', tryout_id=tryout.id) }}" class="btn btn-secondary">Back</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> Save All Evaluations
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -6,54 +6,84 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-users"></i> Players in {{ tryout.title }}</h3>
|
<h3><i class="fas fa-users"></i> Select players to evaluate in {{ tryout.title }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-container">
|
<p class="text-muted mb-3">Choose the players you want to evaluate, then load all of them on a single page.</p>
|
||||||
<table class="table">
|
<form method="GET" action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}">
|
||||||
<thead>
|
<div class="table-container">
|
||||||
<tr>
|
<table class="table">
|
||||||
<th>{{ _('Player') }}</th>
|
<thead>
|
||||||
<th>{{ _('Contact') }}</th>
|
<tr>
|
||||||
<th>{{ _('Attendance') }}</th>
|
<th><input type="checkbox" id="select-all" data-action="toggle-all"></th>
|
||||||
<th>{{ _('Status') }}</th>
|
<th>Player</th>
|
||||||
<th>{{ _('Actions') }}</th>
|
<th>Contact</th>
|
||||||
</tr>
|
<th>Attendance</th>
|
||||||
</thead>
|
<th>Status</th>
|
||||||
<tbody>
|
<th>Actions</th>
|
||||||
{% for entry in players %}
|
</tr>
|
||||||
<tr>
|
</thead>
|
||||||
<td>
|
<tbody>
|
||||||
<div class="user-mini">
|
{% for entry in players %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="player_ids" value="{{ entry.player.id }}" class="player-check">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="user-mini">
|
||||||
<div class="avatar-sm">{{ entry.player.username[:2] | upper }}</div>
|
<div class="avatar-sm">{{ entry.player.username[:2] | upper }}</div>
|
||||||
<span>{{ entry.player.username }}</span>
|
<span>{{ entry.player.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ entry.player.email }}</td>
|
<td>{{ entry.player.email }}</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span>
|
<span class="badge badge-{{ entry.registration.status }}">{{ entry.registration.status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if entry.evaluated %}
|
{% if entry.evaluated %}
|
||||||
<span class="badge badge-success">Evaluated</span>
|
<span class="badge badge-success">Evaluated</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-warning">Not Evaluated</span>
|
<span class="badge badge-warning">Not Evaluated</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-primary">
|
<a href="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=entry.player.id) }}" class="btn btn-sm btn-outline">
|
||||||
<i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Evaluate{% endif %}
|
<i class="fas fa-clipboard"></i> {% if entry.evaluated %}View/Edit{% else %}Single{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center">{{ _('No players registered for this tryout.') }}</td>
|
<td colspan="6" class="text-center">No players registered for this tryout.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-secondary">Cancel</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-clipboard-check"></i> Evaluate Selected
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
function toggleAll() {
|
||||||
|
var boxes = document.querySelectorAll('.player-check');
|
||||||
|
var master = document.getElementById('select-all');
|
||||||
|
for (var i = 0; i < boxes.length; i++) {
|
||||||
|
boxes[i].checked = master.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerActions({
|
||||||
|
'toggle-all': toggleAll,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -300,6 +300,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<script nonce="{{ csp_nonce }}">
|
<script nonce="{{ csp_nonce }}">
|
||||||
function showCreateForm() {
|
function showCreateForm() {
|
||||||
document.getElementById('createTeamForm').classList.remove('hidden');
|
document.getElementById('createTeamForm').classList.remove('hidden');
|
||||||
|
|||||||
@@ -67,15 +67,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-6">
|
<div class="form-group col-6">
|
||||||
<label for="manager_id">{{ _('Assigned Manager') }}</label>
|
<label>{{ _('Assigned Managers') }}</label>
|
||||||
<select id="manager_id" name="manager_id" class="form-select">
|
<div class="checkbox-grid">
|
||||||
<option value="">{{ _('-- No manager assigned --') }}</option>
|
|
||||||
{% for manager in managers %}
|
{% for manager in managers %}
|
||||||
<option value="{{ manager.id }}" {% if tryout and tryout.manager_id == manager.id %}selected{% endif %}>
|
{% set is_checked = false %}
|
||||||
{{ manager.username }}
|
{% if tryout %}
|
||||||
</option>
|
{% for m in tryout.get_managers() %}
|
||||||
|
{% if m.id == manager.id %}{% set is_checked = true %}{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="manager_ids" value="{{ manager.id }}" {% if is_checked %}checked{% endif %}>
|
||||||
|
<span>{{ manager.username }}</span>
|
||||||
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</div>
|
||||||
|
<small class="text-muted">{{ _('Select one or more managers for this tryout.') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -70,16 +70,22 @@
|
|||||||
<span class="detail-value">{{ tryout.target_org_team.name if tryout.target_org_team else 'Not specified' }}</span>
|
<span class="detail-value">{{ tryout.target_org_team.name if tryout.target_org_team else 'Not specified' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Manager</span>
|
<span class="detail-label">Manager(s)</span>
|
||||||
<span class="detail-value">{{ tryout.manager.username if tryout.manager else 'Not assigned' }}</span>
|
<span class="detail-value">
|
||||||
|
{% set mgrs = tryout.get_managers() %}
|
||||||
|
{% if mgrs %}
|
||||||
|
{{ mgrs | map(attribute='username') | join(', ') }}
|
||||||
|
{% else %}
|
||||||
|
Not assigned
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Coaches</span>
|
<span class="detail-label">Coaches</span>
|
||||||
<span class="detail-value">
|
<span class="detail-value">
|
||||||
{% if tryout.coaches %}
|
{% set cos = tryout.get_coaches() %}
|
||||||
{{ tryout.coaches | map(attribute='username') | join(', ') }}
|
{% if cos %}
|
||||||
{% elif tryout.coach %}
|
{{ cos | map(attribute='username') | join(', ') }}
|
||||||
{{ tryout.coach.username }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
Not assigned
|
Not assigned
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -534,6 +540,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<style>
|
<style>
|
||||||
.presence-toggle-btn {
|
.presence-toggle-btn {
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -949,6 +949,7 @@ class TryoutSchema(StripMixin):
|
|||||||
target_org_team_id = fields.Integer(allow_none=True, load_default=None)
|
target_org_team_id = fields.Integer(allow_none=True, load_default=None)
|
||||||
manager_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)
|
coach_ids = fields.List(fields.Integer(), load_default=list)
|
||||||
|
manager_ids = fields.List(fields.Integer(), load_default=list)
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_span(self, data, **kwargs):
|
def validate_span(self, data, **kwargs):
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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