diff --git a/__pycache__/extensions.cpython-313.pyc b/__pycache__/extensions.cpython-313.pyc index 4610344..c654ac2 100644 Binary files a/__pycache__/extensions.cpython-313.pyc and b/__pycache__/extensions.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 71e0b93..41bfe09 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/__pycache__/seed.cpython-313.pyc b/__pycache__/seed.cpython-313.pyc index 96b0625..512ba58 100644 Binary files a/__pycache__/seed.cpython-313.pyc and b/__pycache__/seed.cpython-313.pyc differ diff --git a/app.py b/app.py index 051882c..b48276e 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,30 @@ +"""Team Tryouts Application - Flask Application Factory. + +This module provides the application factory for creating and configuring +the Flask application instance. +""" + import os from flask import Flask from extensions import db, login_manager, csrf, hash_password, check_password from sqlalchemy import text + def create_app(): + """Create and configure the Flask application. + + Initializes Flask with: + - Secret key for session security + - SQLite database configuration + - CSRF protection + - Login manager + - All route blueprints + + Handles database initialization and seeding with sample data if empty. + + Returns: + Flask: Configured Flask application instance. + """ app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'team-tryouts-secret-key-change-in-production') app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///team_tryouts.db' @@ -51,6 +72,7 @@ def create_app(): return app + if __name__ == '__main__': app = create_app() app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/extensions.py b/extensions.py index 1d000ff..4c1083d 100644 --- a/extensions.py +++ b/extensions.py @@ -3,14 +3,37 @@ from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from werkzeug.security import generate_password_hash, check_password_hash + +# Database and extension initialization db = SQLAlchemy() login_manager = LoginManager() login_manager.login_view = 'auth.login' login_manager.login_message_category = 'info' csrf = CSRFProtect() + def hash_password(password): + """ + Hash a plain text password using werkzeug's security functions. + + Args: + password (str): The plain text password to hash. + + Returns: + str: The hashed password string. + """ return generate_password_hash(password) + def check_password(password_hash, password): - return check_password_hash(password_hash, password) \ No newline at end of file + """ + Verify a password against its hash. + + Args: + password_hash (str): The stored password hash. + password (str): The plain text password to verify. + + Returns: + bool: True if the password matches the hash, False otherwise. + """ + return check_password_hash(password_hash, password) diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index 07fe449..a80efdc 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/models.py b/models.py index f2c8835..24c3401 100644 --- a/models.py +++ b/models.py @@ -1,11 +1,19 @@ +"""Database models for the Team Tryouts application. + +This module defines all SQLAlchemy models including User, Tryout, Evaluation, +Team, Match, and Contract entities with their relationships and helper methods. +""" + from extensions import db, login_manager from flask_login import UserMixin from datetime import datetime from urllib.parse import quote + +# Available user roles in the system ROLES = ['president', 'manager', 'coach', 'player', 'scout'] -# Popular E-Sports games list +# Popular E-Sports games list for player profiles ESPORT_GAMES = [ 'Valorant', 'League of Legends', @@ -13,15 +21,56 @@ ESPORT_GAMES = [ 'Apex Legends', 'Overwatch 2', 'Rainbow Six Siege', - 'Fortnite', 'Rocket League', - 'Call of Duty', - 'Dota 2', - 'Super Smash Bros.', - 'Street Fighter 6', + 'Super Smash Bros.' ] +# Game-specific positions for tryouts +GAME_POSITIONS = { + 'League of Legends': ['Top Lane', 'Jungle', 'Mid Lane', 'ADC', 'Support'], + 'Valorant': ['Controller', 'Initiator', 'Duelist', 'Sentinel'], + 'Counter-Strike 2': ['AWPer', 'Entry Fragger', 'Lurker', 'In-Game Leader', 'Support'], + 'Rainbow Six Siege': ['Entry', 'Support', 'Breacher', 'Anchor', 'Flex'], + 'Overwatch 2': ['Tank', 'Damage', 'Support'], + 'Apex Legends': [], + 'Rocket League': [], + 'Super Smash Bros.': [] +} + + +@login_manager.user_loader +def load_user(user_id): + """Load a user by ID for Flask-Login session management. + + Args: + user_id (int): The user's unique identifier. + + Returns: + User: The User object if found, None otherwise. + """ + return User.query.get(int(user_id)) + + class User(UserMixin, db.Model): + """User model representing all users in the system. + + Users can have different roles: president, manager, coach, player, or scout. + Each user has E-Sports specific fields for competitive gaming profiles. + + Attributes: + id: Unique identifier for the user. + username: Unique username for login. + password_hash: Hashed password for authentication. + role: User role determining permissions. + full_name: User's display name. + email: User's email address. + phone: Optional phone number. + team_id: Foreign key to the user's organization team. + is_active_account: Whether the account is active. + games: Comma-separated list of games the user plays. + discord_username: User's Discord handle. + league_os_profile: Link to League OS profile. + """ __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) @@ -36,8 +85,8 @@ class User(UserMixin, db.Model): # E-Sports specific fields games = db.Column(db.Text, nullable=True) # Comma-separated list of games - discord_username = db.Column(db.String(100), nullable=True) # Discord handle - league_os_profile = db.Column(db.String(255), nullable=True) # League OS profile URL or ID + discord_username = db.Column(db.String(128), nullable=True) # Discord handle + league_os_profile = db.Column(db.String(256), nullable=True) # League OS profile URL or ID evaluations_given = db.relationship('Evaluation', foreign_keys='Evaluation.evaluator_id', backref='evaluator', lazy='dynamic') evaluations_received = db.relationship('Evaluation', foreign_keys='Evaluation.player_id', backref='player', lazy='dynamic') @@ -45,31 +94,78 @@ class User(UserMixin, db.Model): team_assignments = db.relationship('TeamMember', foreign_keys='TeamMember.player_id', backref='player_ref', lazy='dynamic') def get_games_list(self): - """Return games as a list.""" + """Return the user's games as a list. + + Returns: + list: List of game names the user plays, or empty list if none. + """ if self.games: return [g.strip() for g in self.games.split(',') if g.strip()] return [] def has_role(self, *roles): + """Check if the user has one of the specified roles. + + Args: + *roles: Variable number of role names to check. + + Returns: + bool: True if user has any of the specified roles. + """ return self.role in roles def can_evaluate(self): + """Check if user can evaluate other players. + + Returns: + bool: True if user is president, manager, or coach. + """ return self.role in ['president', 'manager', 'coach'] def can_manage_users(self): + """Check if user can manage (create/delete) other users. + + Returns: + bool: True if user is president. + """ return self.role == 'president' def can_manage_tryouts(self): - return self.role in ['president', 'manager', 'coach', 'scout'] + """Check if user can manage tryouts. + + Returns: + bool: True if user is president, manager or coach. + """ + return self.role in ['president', 'manager', 'coach'] def can_manage_teams(self): + """Check if user can manage organization teams. + + Returns: + bool: True if user is president or manager. + """ return self.role in ['president', 'manager'] def can_schedule_matches(self): + """Check if user can schedule matches. + + Returns: + bool: True if user is president, manager, or coach. + """ return self.role in ['president', 'manager', 'coach'] def can_manage_this_tryout(self, tryout): - """Check if user can manage a specific tryout (president, or manager/coach in charge of it).""" + """Check if user can manage a specific tryout. + + Presidents can manage all tryouts. Managers can manage their own tryouts. + Coaches can manage tryouts targeting their coached team. + + Args: + tryout: The Tryout object to check permissions for. + + Returns: + bool: True if user has permission to manage the tryout. + """ if self.role == 'president': return True if self.role == 'manager' and tryout.created_by == self.id: @@ -81,7 +177,17 @@ class User(UserMixin, db.Model): return False def can_manage_this_org_team(self, org_team): - """Check if user can manage a specific org team (president, or manager/coach in charge of it).""" + """Check if user can manage a specific org team. + + Presidents can manage all org teams. Managers can manage all org teams. + Coaches can only manage their own coached team. + + Args: + org_team: The OrgTeam object to check permissions for. + + Returns: + bool: True if user has permission to manage the org team. + """ if self.role == 'president': return True if self.role == 'manager': @@ -91,15 +197,14 @@ class User(UserMixin, db.Model): return False def get_gamertags(self): - """Return gamertags as a dict keyed by game.""" + """Return gamertags as a dictionary keyed by game. + + Returns: + dict: Dictionary with game names as keys and gamertag info as values. + """ return {gt.game: {'gamertag': gt.gamertag, 'platform': gt.platform} for gt in self.gamertags} -@login_manager.user_loader -def load_user(user_id): - return User.query.get(int(user_id)) - - # Platform options for games that require platform specification GAME_PLATFORMS = { 'Valorant': [], # No platform needed @@ -108,12 +213,8 @@ GAME_PLATFORMS = { 'Apex Legends': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Overwatch 2': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch'], 'Rainbow Six Siege': ['Ubisoft', 'PlayStation', 'Xbox'], # Ubisoft = Uplay/Steam - 'Fortnite': ['PC', 'PlayStation', 'Xbox', 'Nintendo Switch', 'Mobile'], -'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username - 'Call of Duty': ['PC', 'PlayStation', 'Xbox'], - 'Dota 2': [], + 'Rocket League': ['Epic', 'PlayStation', 'Xbox', 'Nintendo Switch'], # Epic uses EpicID, others use username 'Super Smash Bros.': ['Nintendo Switch'], - 'Street Fighter 6': ['PC', 'PlayStation', 'Xbox'], } @@ -124,7 +225,6 @@ PLATFORM_CODES = { 'Xbox': 'xbl', 'Nintendo Switch': 'switch', 'PC': 'pc', - 'Mobile': 'mobile', 'Steam': 'steam', 'Epic': 'epic', } @@ -137,17 +237,24 @@ TRN_URLS = { 'Apex Legends': 'https://tracker.gg/apex/profile/{platform}/{username}', 'Overwatch 2': 'https://tracker.gg/overwatch/profile/{platform}/{username}', 'Rainbow Six Siege': 'https://r6.tracker.network/r6siege/profile/{platform_code}/{username}', - 'Fortnite': 'https://tracker.gg/fortnite/profile/{platform}/{username}', 'Rocket League': 'https://rocketleague.tracker.network/rocket-league/profile/{platform_code}/{username}', - 'Call of Duty': 'https://tracker.gg/call-of-duty/profile/{platform}/{username}', - 'Dota 2': 'https://tracker.gg/dota2/profile/steam/{username}', 'Super Smash Bros.': 'https://tracker.gg/smash/profile/{username}', - 'Street Fighter 6': 'https://tracker.gg/streetfighter/profile/{username}', } class UserGamertag(db.Model): - """Store gamertag per game for each user.""" + """Store gamertag per game for each user. + + Allows users to link their gaming profiles for different games with optional + platform specification for cross-platform games. + + Attributes: + id: Unique identifier. + user_id: Foreign key to the user. + game: Name of the game. + gamertag: Player's gamertag/username for the game. + platform: Platform for cross-platform games (optional). + """ __tablename__ = 'user_gamertags' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) @@ -162,7 +269,14 @@ class UserGamertag(db.Model): ) def get_trn_url(self): - """Generate the TRN URL for this gamertag.""" + """Generate the TRN (Tracker Network) URL for this gamertag. + + Builds the appropriate URL based on the game, handling platform codes + and URL encoding for special characters. + + Returns: + str: The Tracker Network URL for the gamertag, or None if game not supported. + """ if self.game not in TRN_URLS: return None @@ -182,7 +296,18 @@ class UserGamertag(db.Model): class OrgTeam(db.Model): - """Persistent organization teams (e.g., Varsity, JV) that exist across tryouts.""" + """Persistent organization teams (e.g., Varsity, JV) that exist across tryouts. + + These teams are long-term organizational structures that persist beyond + individual tryouts, unlike tryout-specific Team entities. + + Attributes: + id: Unique identifier. + name: Team name (e.g., "Varsity", "Junior Varsity"). + coach_id: Foreign key to the assigned coach. + created_by: Foreign key to the user who created the team. + created_at: Timestamp of team creation. + """ __tablename__ = 'org_teams' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False, unique=True) @@ -196,10 +321,29 @@ class OrgTeam(db.Model): class Tryout(db.Model): + """Tryout event for player evaluations and team formation. + + Represents a scheduled tryout session where players can register + and be evaluated by coaches/managers. + + Attributes: + id: Unique identifier. + title: Tryout title/name. + description: Optional description of the tryout. + game: The game this tryout is for. + date: Date of the tryout. + location: Physical or virtual location. + status: Current status (upcoming, in_progress, completed). + max_players: Maximum number of players allowed. + created_by: Foreign key to the creating manager/president. + target_org_team_id: Foreign key to target organization team. + created_at: Timestamp of creation. + """ __tablename__ = 'tryouts' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(200), nullable=False) description = db.Column(db.Text, nullable=True) + game = db.Column(db.String(50), nullable=False) date = db.Column(db.Date, nullable=False) location = db.Column(db.String(200), nullable=True) status = db.Column(db.String(20), default='upcoming') # upcoming, in_progress, completed @@ -216,6 +360,19 @@ class Tryout(db.Model): class TryoutRegistration(db.Model): + """Registration linking a player to a tryout. + + Tracks which players have registered for which tryouts and their + attendance status. + + Attributes: + id: Unique identifier. + tryout_id: Foreign key to the tryout. + player_id: Foreign key to the registered player. + registered_at: Timestamp of registration. + status: Registration status (registered, attended, no_show). + notes: Optional notes about the registration. + """ __tablename__ = 'tryout_registrations' id = db.Column(db.Integer, primary_key=True) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) @@ -226,16 +383,41 @@ class TryoutRegistration(db.Model): class Evaluation(db.Model): + """Player evaluation record. + + Contains scored evaluations from coaches/managers for players + during tryouts. + + Attributes: + id: Unique identifier. + tryout_id: Foreign key to the tryout. + player_id: Foreign key to the evaluated player. + evaluator_id: Foreign key to the evaluating coach/manager. + speed_score: Speed rating (1-10). + agility_score: Agility rating (1-10). + technique_score: Technique rating (1-10). + teamwork_score: Teamwork rating (1-10). + attitude_score: Attitude rating (1-10). + overall_score: Average of all scores. + comments: Optional evaluator comments. + position_recommendation: Recommended position. + created_at: Timestamp of creation. + updated_at: Timestamp of last update. + """ __tablename__ = 'evaluations' 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) evaluator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) - speed_score = db.Column(db.Integer, nullable=True) - agility_score = db.Column(db.Integer, nullable=True) - technique_score = db.Column(db.Integer, nullable=True) - teamwork_score = db.Column(db.Integer, nullable=True) - attitude_score = db.Column(db.Integer, nullable=True) + mecanics_score = db.Column(db.Integer, nullable=True) + cohesion_score = db.Column(db.Integer, nullable=True) + communication_score = db.Column(db.Integer, nullable=True) + gamesense_score = db.Column(db.Integer, nullable=True) + versatility_score = db.Column(db.Integer, nullable=True) + discipline_score = db.Column(db.Integer, nullable=True) + analysis_score = db.Column(db.Integer, nullable=True) + sport_ethics_score = db.Column(db.Integer, nullable=True) + mental_score = db.Column(db.Integer, nullable=True) overall_score = db.Column(db.Float, nullable=True) comments = db.Column(db.Text, nullable=True) position_recommendation = db.Column(db.String(50), nullable=True) @@ -248,7 +430,18 @@ class Evaluation(db.Model): class Team(db.Model): - """Tryout-specific teams (e.g., Alpha, Bravo within a single tryout).""" + """Tryout-specific team (e.g., Alpha, Bravo within a single tryout). + + Teams created during a tryout for match scheduling purposes. + Different from OrgTeam which is a long-term organizational entity. + + Attributes: + id: Unique identifier. + tryout_id: Foreign key to the parent tryout. + name: Team name. + created_by: Foreign key to the creator. + created_at: Timestamp of creation. + """ __tablename__ = 'teams' id = db.Column(db.Integer, primary_key=True) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) @@ -261,6 +454,15 @@ class Team(db.Model): class TeamMember(db.Model): + """Link between a player and a tryout-specific team. + + Attributes: + id: Unique identifier. + team_id: Foreign key to the team. + player_id: Foreign key to the player. + position: Player's position on the team. + added_at: Timestamp when player was added. + """ __tablename__ = 'team_members' id = db.Column(db.Integer, primary_key=True) team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=False) @@ -272,7 +474,26 @@ class TeamMember(db.Model): class Match(db.Model): - """Matches/scrimmages scheduled within tryouts.""" + """Match/scrimmage scheduled within a tryout. + + Can be team vs team or player scrim type. + + Attributes: + id: Unique identifier. + tryout_id: Foreign key to the parent tryout. + title: Match title/name. + description: Optional description. + date: Match date. + start_time: Match start time. + end_time: Match end time. + location: Match location. + status: Match status (scheduled, completed, cancelled). + match_type: Type of match (team_vs_team, player_vs_player, player_scrim). + created_by: Foreign key to the creator. + created_at: Timestamp of creation. + team1_id: Foreign key to first team (for team_vs_team). + team2_id: Foreign key to second team (for team_vs_team). + """ __tablename__ = 'matches' id = db.Column(db.Integer, primary_key=True) tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) @@ -298,12 +519,28 @@ class Match(db.Model): participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic') def get_participating_players(self): - """Return list of players participating in this match.""" + """Return list of player IDs participating in this match. + + Returns: + list: List of unique player IDs. + """ return [p.player_id for p in self.participants.all()] class PlayerDisponibility(db.Model): - """Player availability in 30-minute time blocks for scheduling matches.""" + """Player availability in 30-minute time blocks for scheduling matches. + + Allows players to specify when they're available for matches. + + Attributes: + id: Unique identifier. + player_id: Foreign key to the player. + day_of_week: Day of week (0=Monday, 6=Sunday). + start_time: Start time of availability block. + end_time: End time of availability block (always 30 min after start). + created_at: Timestamp of creation. + updated_at: Timestamp of last update. + """ __tablename__ = 'player_disponibilities' id = db.Column(db.Integer, primary_key=True) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) @@ -317,7 +554,18 @@ class PlayerDisponibility(db.Model): class MatchParticipant(db.Model): - """Players participating in player scrimmage matches.""" + """Players participating in player scrimmage matches. + + Links players to matches for player_scrim and player_vs_player match types. + + Attributes: + id: Unique identifier. + match_id: Foreign key to the match. + player_id: Foreign key to the player. + team_side: Team side (1 or 2) for player_vs_player matches. + position: Player's position for this match. + added_at: Timestamp when added. + """ __tablename__ = 'match_participants' id = db.Column(db.Integer, primary_key=True) match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False) @@ -330,7 +578,25 @@ class MatchParticipant(db.Model): class Contract(db.Model): - """Contract documents for players to sign.""" + """Contract documents for players to sign. + + Tracks contract uploads and signed documents for players. + + Attributes: + id: Unique identifier. + player_id: Foreign key to the player. + team_id: Foreign key to the player's team. + uploaded_by_id: Foreign key to the uploader. + original_filename: Original uploaded file name. + stored_filename: Stored file name on disk. + file_path: Full path to the contract file. + signed_filename: Signed file name (if signed). + signed_file_path: Full path to signed contract (if signed). + status: Contract status (pending, signed). + notes: Optional notes. + uploaded_at: Timestamp of upload. + signed_at: Timestamp of signing (if signed). + """ __tablename__ = 'contracts' id = db.Column(db.Integer, primary_key=True) player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) @@ -358,7 +624,17 @@ class Contract(db.Model): uploader = db.relationship('User', foreign_keys=[uploaded_by_id]) def can_view(self, user): - """Check if a user can view this contract.""" + """Check if a user can view this contract. + + Players can always view their own contracts. Presidents and managers + have full access. Coaches can view contracts for players on their teams. + + Args: + user: The User object requesting access. + + Returns: + bool: True if user has permission to view the contract. + """ # Player can always view their own contracts if user.id == self.player_id: return True @@ -378,6 +654,15 @@ class Contract(db.Model): return False def can_upload_signed(self, user): - """Check if a user can upload a signed contract.""" + """Check if a user can upload a signed contract. + + Only the player themselves can upload their signed contract. + + Args: + user: The User object requesting to upload. + + Returns: + bool: True if user is the player associated with the contract. + """ # Only the player can upload their signed contract - return user.id == self.player_id + return user.id == self.player_id \ No newline at end of file diff --git a/routes/__pycache__/auth.cpython-313.pyc b/routes/__pycache__/auth.cpython-313.pyc index d80c9df..b634b0a 100644 Binary files a/routes/__pycache__/auth.cpython-313.pyc and b/routes/__pycache__/auth.cpython-313.pyc differ diff --git a/routes/__pycache__/evaluations.cpython-313.pyc b/routes/__pycache__/evaluations.cpython-313.pyc index a12e75b..817648e 100644 Binary files a/routes/__pycache__/evaluations.cpython-313.pyc and b/routes/__pycache__/evaluations.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 00814b3..2e711ad 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/__pycache__/matches.cpython-313.pyc b/routes/__pycache__/matches.cpython-313.pyc index f836925..e12dd0f 100644 Binary files a/routes/__pycache__/matches.cpython-313.pyc and b/routes/__pycache__/matches.cpython-313.pyc differ diff --git a/routes/__pycache__/teams.cpython-313.pyc b/routes/__pycache__/teams.cpython-313.pyc index 3fba8f7..9457852 100644 Binary files a/routes/__pycache__/teams.cpython-313.pyc and b/routes/__pycache__/teams.cpython-313.pyc differ diff --git a/routes/__pycache__/tryouts.cpython-313.pyc b/routes/__pycache__/tryouts.cpython-313.pyc index c8536fd..3087a17 100644 Binary files a/routes/__pycache__/tryouts.cpython-313.pyc and b/routes/__pycache__/tryouts.cpython-313.pyc differ diff --git a/routes/__pycache__/users.cpython-313.pyc b/routes/__pycache__/users.cpython-313.pyc index 39bc54f..b9cca18 100644 Binary files a/routes/__pycache__/users.cpython-313.pyc and b/routes/__pycache__/users.cpython-313.pyc differ diff --git a/routes/auth.py b/routes/auth.py index 110fcf1..2e66085 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -1,3 +1,8 @@ +"""Authentication routes for user login, logout, and registration. + +This module handles user authentication including login, logout, and new user registration. +""" + from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, login_required, current_user from extensions import db, hash_password, check_password @@ -5,8 +10,20 @@ from models import User, ESPORT_GAMES auth_bp = Blueprint('auth', __name__, url_prefix='/auth') + @auth_bp.route('/login', methods=['GET', 'POST']) def login(): + """Handle user login authentication. + + GET: Render the login form. + POST: Authenticate user credentials and log them in. + + Redirects authenticated users to dashboard. Validates credentials and checks + account status before login. + + Returns: + Response: Login form or redirect to dashboard/next page. + """ if current_user.is_authenticated: return redirect(url_for('main.dashboard')) @@ -28,8 +45,20 @@ def login(): return render_template('pages/login.html') + @auth_bp.route('/register', methods=['GET', 'POST']) def register(): + """Handle new player registration. + + GET: Render the registration form with E-Sports games list. + POST: Create a new player account with provided details. + + Only players can register through this form. Validates username/email + uniqueness and password confirmation. + + Returns: + Response: Registration form or redirect to login. + """ if current_user.is_authenticated: return redirect(url_for('main.dashboard')) @@ -80,9 +109,17 @@ def register(): return render_template('pages/register.html', esport_games=ESPORT_GAMES) + @auth_bp.route('/logout') @login_required def logout(): + """Log out the current user. + + Clears the user session and redirects to the login page. + + Returns: + Response: Redirect to login page with logout message. + """ logout_user() flash('You have been logged out.', 'info') return redirect(url_for('auth.login')) \ No newline at end of file diff --git a/routes/evaluations.py b/routes/evaluations.py index 5f5ad28..d887fdc 100644 --- a/routes/evaluations.py +++ b/routes/evaluations.py @@ -1,4 +1,9 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +"""Evaluation routes for assessing player performance during tryouts. + +This module handles player evaluation creation, management, and viewing. +""" + +from flask import Blueprint, render_template, redirect, url_for, flash, request from flask_login import login_required, current_user from extensions import db from models import User, Tryout, Evaluation, TryoutRegistration @@ -6,9 +11,19 @@ from sqlalchemy import func evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations') + @evaluations_bp.route('') @login_required def list_evaluations(): + """List all evaluations accessible to the current user. + + President: All evaluations with player score summaries. + Evaluators (coach/manager): Their given evaluations. + Players: Their received evaluations. + + Returns: + Response: Rendered evaluations list template. + """ user = current_user if user.role == 'president': @@ -33,9 +48,22 @@ def list_evaluations(): return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores) + @evaluations_bp.route('//', methods=['GET', 'POST']) @login_required def evaluate_player(tryout_id, player_id): + """Evaluate a specific player in a tryout. + + GET: Render the evaluation form with any existing evaluation. + POST: Create or update the evaluation for the player. + + Args: + tryout_id: The ID of the tryout. + player_id: The ID of the player to evaluate. + + Returns: + Response: Evaluation form or redirect to tryout view. + """ if not current_user.can_evaluate(): flash('You do not have permission to evaluate players.', 'danger') return redirect(url_for('main.dashboard')) @@ -54,29 +82,41 @@ def evaluate_player(tryout_id, player_id): ).first() if request.method == 'POST': - speed = request.form.get('speed_score') - agility = request.form.get('agility_score') - technique = request.form.get('technique_score') - teamwork = request.form.get('teamwork_score') - attitude = request.form.get('attitude_score') + mecanics = request.form.get('mecanics_score') + cohesion = request.form.get('cohesion_score') + communication = request.form.get('communication_score') + gamesense = request.form.get('gamesense_score') + versatility = request.form.get('versatility_score') + discipline = request.form.get('discipline_score') + analysis = request.form.get('analysis_score') + sport_ethics = request.form.get('sport_ethics_score') + mental = request.form.get('mental_score') comments = request.form.get('comments') position = request.form.get('position_recommendation') scores = [] - if speed: scores.append(int(speed)) - if agility: scores.append(int(agility)) - if technique: scores.append(int(technique)) - if teamwork: scores.append(int(teamwork)) - if attitude: scores.append(int(attitude)) + if mecanics: scores.append(int(mecanics)) + if cohesion: scores.append(int(cohesion)) + if communication: scores.append(int(communication)) + if gamesense: scores.append(int(gamesense)) + if versatility: scores.append(int(versatility)) + if discipline: scores.append(int(discipline)) + if analysis: scores.append(int(analysis)) + if sport_ethics: scores.append(int(sport_ethics)) + if mental: scores.append(int(mental)) overall = sum(scores) / len(scores) if scores else None if existing_eval: - existing_eval.speed_score = int(speed) if speed else None - existing_eval.agility_score = int(agility) if agility else None - existing_eval.technique_score = int(technique) if technique else None - existing_eval.teamwork_score = int(teamwork) if teamwork else None - existing_eval.attitude_score = int(attitude) if attitude else None + existing_eval.mecanics_score = int(mecanics) if mecanics else None + existing_eval.cohesion_score = int(cohesion) if cohesion else None + existing_eval.communication_score = int(communication) if communication else None + existing_eval.gamesense_score = int(gamesense) if gamesense else None + existing_eval.versatility_score = int(versatility) if versatility else None + existing_eval.discipline_score = int(discipline) if discipline else None + existing_eval.analysis_score = int(analysis) if analysis else None + existing_eval.sport_ethics_score = int(sport_ethics) if sport_ethics else None + existing_eval.mental_score = int(mental) if mental else None existing_eval.overall_score = overall existing_eval.comments = comments existing_eval.position_recommendation = position @@ -86,11 +126,15 @@ def evaluate_player(tryout_id, player_id): tryout_id=tryout_id, player_id=player_id, evaluator_id=current_user.id, - speed_score=int(speed) if speed else None, - agility_score=int(agility) if agility else None, - technique_score=int(technique) if technique else None, - teamwork_score=int(teamwork) if teamwork else None, - attitude_score=int(attitude) if attitude else None, + mecanics_score=int(mecanics) if mecanics else None, + cohesion_score=int(cohesion) if cohesion else None, + communication_score=int(communication) if communication else None, + gamesense_score=int(gamesense) if gamesense else None, + versatility_score=int(versatility) if versatility else None, + discipline_score=int(discipline) if discipline else None, + analysis_score=int(analysis) if analysis else None, + sport_ethics_score=int(sport_ethics) if sport_ethics else None, + mental_score=int(mental) if mental else None, overall_score=overall, comments=comments, position_recommendation=position @@ -112,9 +156,21 @@ def evaluate_player(tryout_id, player_id): existing_eval=existing_eval, evaluators=evaluators) + @evaluations_bp.route('//players') @login_required def players_to_evaluate(tryout_id): + """List players that need evaluation in a specific tryout. + + Shows all registered players and marks which ones have already been evaluated + by the current user. + + Args: + tryout_id: The ID of the tryout. + + Returns: + Response: Rendered players-to-evaluate template. + """ if not current_user.can_evaluate(): flash('Permission denied.', 'danger') return redirect(url_for('main.dashboard')) diff --git a/routes/main.py b/routes/main.py index 7258134..aa53cc5 100644 --- a/routes/main.py +++ b/routes/main.py @@ -1,3 +1,8 @@ +"""Main dashboard routes for the Team Tryouts application. + +This module provides the main dashboard view with role-specific statistics. +""" + from flask import Blueprint, render_template, redirect, url_for, flash from flask_login import login_required, current_user from extensions import db @@ -7,13 +12,34 @@ from datetime import datetime, date main_bp = Blueprint('main', __name__) + @main_bp.route('/') def index(): + """Redirect root URL to login page. + + This is the entry point for the application when no specific route is provided. + + Returns: + Response: Redirect to login page. + """ return redirect(url_for('auth.login')) + @main_bp.route('/dashboard') @login_required def dashboard(): + """Render the main dashboard with role-specific statistics. + + Displays different statistics based on the user's role: + - President: Overview of all users, tryouts, and evaluations + - Manager: Their created tryouts and evaluations + - Coach: Their evaluations and pending evaluations + - Player: Their registrations, evaluations, and upcoming matches + - Scout: Top-rated players across all tryouts + + Returns: + Response: Rendered dashboard template with user stats. + """ user = current_user stats = {} @@ -23,9 +49,9 @@ def dashboard(): stats['total_tryouts'] = Tryout.query.count() stats['total_evaluations'] = Evaluation.query.count() stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count() - stats['upcoming_tryouts'] = Tryout.query.filter_by(status='upcoming').count() - stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(5).all() - stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(5).all() + stats['completed_tryouts'] = Tryout.query.filter_by(status='completed').count() + stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(10).all() + stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(10).all() elif user.role == 'manager': stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count() @@ -40,14 +66,11 @@ def dashboard(): registered_player_ids = [r.player_id for r in registrations] evaluated_player_ids = [e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()] stats['pending_evaluations'] = len(set(registered_player_ids) - set(evaluated_player_ids)) - stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(5).all() + stats['my_recent_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(10).all() elif user.role == 'player': stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count() - stats['my_evaluations'] = Evaluation.query.filter_by(player_id=user.id).count() - stats['average_score'] = db.session.query(func.avg(Evaluation.overall_score)).filter_by(player_id=user.id).scalar() or 0 stats['my_registrations'] = TryoutRegistration.query.filter_by(player_id=user.id).order_by(TryoutRegistration.registered_at.desc()).limit(5).all() - stats['my_eval_list'] = Evaluation.query.filter_by(player_id=user.id).order_by(Evaluation.created_at.desc()).limit(5).all() # Get next match for each tryout the player is in today = date.today() diff --git a/routes/matches.py b/routes/matches.py index 79434a2..407de22 100644 --- a/routes/matches.py +++ b/routes/matches.py @@ -1,3 +1,8 @@ +"""Match scheduling routes for managing scrimmages and matches within tryouts. + +This module handles calendar views, match creation, and player availability. +""" + from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user from extensions import db @@ -8,21 +13,35 @@ matches_bp = Blueprint('matches', __name__, url_prefix='/matches') def can_schedule_match(): - """Check if user can schedule matches (coaches and above).""" + """Check if user can schedule matches (coaches and above). + + Returns: + bool: True if user is president, manager, coach, or scout. + """ return current_user.role in ['president', 'manager', 'coach', 'scout'] @matches_bp.route('/calendar') @login_required def calendar(): - """Calendar view showing tryouts and matches.""" + """Render the calendar view showing all tryouts and matches. + + Returns: + Response: Rendered calendar template. + """ return render_template('pages/calendar.html') @matches_bp.route('/api/events') @login_required def api_events(): - """API endpoint returning calendar events.""" + """API endpoint returning calendar events for FullCalendar. + + Returns tryout events and match events with participant information. + + Returns: + Response: JSON array of calendar events. + """ events = [] # Get tryouts based on user permissions @@ -96,7 +115,14 @@ def api_events(): @matches_bp.route('/api/events/') @login_required def api_events_for_tryout(tryout_id): - """API endpoint returning calendar events for a specific tryout.""" + """API endpoint returning calendar events for a specific tryout. + + Args: + tryout_id: The ID of the tryout to get events for. + + Returns: + Response: JSON array of calendar events for the tryout. + """ tryout = Tryout.query.get_or_404(tryout_id) # Check if user can view this tryout @@ -201,7 +227,18 @@ def api_events_for_tryout(tryout_id): def get_visible_tryouts_for_user(): - """Get tryouts that the current user can see based on their role.""" + """Get tryouts that the current user can see based on their role. + + Permission hierarchy: + - President: All tryouts + - Manager: Only their created tryouts + - Coach: Tryouts targeting their org team + - Player: Tryouts they're registered for or matches they're in + - Scout: All tryouts + + Returns: + list: Query result of Tryout objects. + """ if current_user.role == 'president': return Tryout.query.order_by(Tryout.date).all() elif current_user.role == 'manager': @@ -236,7 +273,17 @@ def get_visible_tryouts_for_user(): @matches_bp.route('/create/', methods=['GET', 'POST']) @login_required def create_match(tryout_id): - """Create a new match/scrimmage within a tryout.""" + """Create a new match/scrimmage within a tryout. + + GET: Render the match creation form. + POST: Create a match with submitted details. + + Args: + tryout_id: The ID of the tryout to create the match for. + + Returns: + Response: Create form or redirect to tryout view. + """ tryout = Tryout.query.get_or_404(tryout_id) # Check if user can manage this tryout (president, manager, or coach) @@ -332,7 +379,17 @@ def create_match(tryout_id): @matches_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit_match(match_id): - """Edit an existing match.""" + """Edit an existing match. + + GET: Render the match edit form. + POST: Update match with submitted changes. + + Args: + match_id: The ID of the match to edit. + + Returns: + Response: Edit form or redirect to tryout view. + """ match = Match.query.get_or_404(match_id) tryout = match.tryout @@ -421,7 +478,14 @@ def edit_match(match_id): @matches_bp.route('//delete', methods=['POST']) @login_required def delete_match(match_id): - """Delete a match.""" + """Delete a match. + + Args: + match_id: The ID of the match to delete. + + Returns: + Response: Redirect to tryout view with status message. + """ match = Match.query.get_or_404(match_id) tryout = match.tryout @@ -438,12 +502,15 @@ def delete_match(match_id): def get_players_available_at_time(date_str, time_str): """Get list of player IDs available at a specific date and time. + Checks player disponibility records to find who is available + during the specified time block. + Args: - date_str: Date in YYYY-MM-DD format - time_str: Time in HH:MM format + date_str: Date in YYYY-MM-DD format. + time_str: Time in HH:MM format. Returns: - List of player IDs who are available at that time + list: List of available player IDs. """ try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() @@ -487,9 +554,17 @@ def get_players_available_at_time(date_str, time_str): @matches_bp.route('/api/available_players//