diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7028253 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.env +instance/ +documents/ +__pycache__/ +*.pyc \ No newline at end of file diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index f4abcd0..71e0b93 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 2a8e047..96b0625 100644 Binary files a/__pycache__/seed.cpython-313.pyc and b/__pycache__/seed.cpython-313.pyc differ diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index f3d14f2..07fe449 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/models.py b/models.py index 5ef6c63..f2c8835 100644 --- a/models.py +++ b/models.py @@ -327,3 +327,57 @@ class MatchParticipant(db.Model): added_at = db.Column(db.DateTime, default=datetime.utcnow) player = db.relationship('User') + + +class Contract(db.Model): + """Contract documents for players to sign.""" + __tablename__ = 'contracts' + id = db.Column(db.Integer, primary_key=True) + player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True) + uploaded_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + + # File information + original_filename = db.Column(db.String(255), nullable=False) + stored_filename = db.Column(db.String(255), nullable=False) + file_path = db.Column(db.String(500), nullable=False) + + # Signed contract information + signed_filename = db.Column(db.String(255), nullable=True) + signed_file_path = db.Column(db.String(500), nullable=True) + + # Status and metadata + status = db.Column(db.String(20), default='pending') # pending, signed + notes = db.Column(db.Text, nullable=True) + uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) + signed_at = db.Column(db.DateTime, nullable=True) + + # Relationships + player = db.relationship('User', foreign_keys=[player_id], backref='contracts') + team = db.relationship('OrgTeam', foreign_keys=[team_id]) + uploader = db.relationship('User', foreign_keys=[uploaded_by_id]) + + def can_view(self, user): + """Check if a user can view this contract.""" + # Player can always view their own contracts + if user.id == self.player_id: + return True + # President can view all contracts + if user.role == 'president': + return True + # Manager can view contracts for players on their teams + if user.role == 'manager': + player = User.query.get(self.player_id) + if player and player.team_id: + return True + # Coach can view contracts for players on their team + if user.role == 'coach': + org_team = OrgTeam.query.filter_by(coach_id=user.id).first() + if org_team and (not self.team_id or self.team_id == org_team.id): + return True + return False + + def can_upload_signed(self, user): + """Check if a user can upload a signed contract.""" + # Only the player can upload their signed contract + return user.id == self.player_id diff --git a/routes/__pycache__/users.cpython-313.pyc b/routes/__pycache__/users.cpython-313.pyc index 34d7a03..39bc54f 100644 Binary files a/routes/__pycache__/users.cpython-313.pyc and b/routes/__pycache__/users.cpython-313.pyc differ diff --git a/routes/users.py b/routes/users.py index c21e8ad..d87a96e 100644 --- a/routes/users.py +++ b/routes/users.py @@ -1,7 +1,9 @@ -from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +import os +from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file from flask_login import login_required, current_user from extensions import db, hash_password -from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS +from models import User, ROLES, ESPORT_GAMES, PlayerDisponibility, UserGamertag, GAME_PLATFORMS, Contract, OrgTeam +from werkzeug.utils import secure_filename from datetime import datetime, timedelta users_bp = Blueprint('users', __name__, url_prefix='/users') @@ -147,7 +149,11 @@ def create_user(): @users_bp.route('/profile') @login_required def profile(): - return render_template('pages/profile.html', user=current_user) + # Get contracts ordered by uploaded_at desc for the current user + contracts = None + if current_user.role == 'player': + contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all() + return render_template('pages/profile.html', user=current_user, contracts=contracts) @users_bp.route('/profile/edit', methods=['GET', 'POST']) @login_required @@ -378,4 +384,196 @@ def delete_disponibility(disponibility_id): db.session.delete(disponibility) db.session.commit() - return jsonify({'success': True}) \ No newline at end of file + return jsonify({'success': True}) + + +# Contract Dropbox Functions + +def can_manage_player_contract(user, player_id): + """Check if a user can upload contracts for a specific player.""" + # President can manage all contracts + if user.role == 'president': + return True + + # Manager can upload contracts for any player + if user.role == 'manager': + return True + + # Coach can upload contracts only for players on their team + if user.role == 'coach': + org_team = OrgTeam.query.filter_by(coach_id=user.id).first() + if org_team: + player = User.query.get(player_id) + if player and player.team_id == org_team.id: + return True + return False + + +@users_bp.route('/contracts') +@login_required +def list_contracts(): + """View contracts for the current user (player) or players they manage.""" + contracts = None + + if current_user.role == 'player': + # Players see their own contracts + contracts = Contract.query.filter_by(player_id=current_user.id).order_by(Contract.uploaded_at.desc()).all() + elif current_user.role in ['president', 'manager', 'coach']: + # Superiors see contracts for players on their teams + players = [] + if current_user.role == 'coach': + org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() + if org_team: + players = User.query.filter_by(role='player', team_id=org_team.id).all() + else: # president or manager + players = User.query.filter_by(role='player').all() + + if players: + player_ids = [p.id for p in players] + contracts = Contract.query.filter(Contract.player_id.in_(player_ids)).order_by(Contract.uploaded_at.desc()).all() + + return render_template('pages/contracts.html', contracts=contracts, players=players if current_user.role in ['president', 'manager', 'coach'] else None) + + +@users_bp.route('/contracts/upload', methods=['GET', 'POST']) +@login_required +def upload_contract(): + """Upload a contract for a player.""" + if current_user.role not in ['president', 'manager', 'coach']: + flash('Only presidents, managers, and coaches can upload contracts.', 'danger') + return redirect(url_for('users.list_contracts')) + + # Get players this user can manage + if current_user.role == 'coach': + org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() + players = User.query.filter_by(role='player', team_id=org_team.id).all() if org_team else [] + else: + players = User.query.filter_by(role='player').all() + + if request.method == 'POST': + player_id = request.form.get('player_id', type=int) + notes = request.form.get('notes', '').strip() + + if not can_manage_player_contract(current_user, player_id): + flash('You do not have permission to upload a contract for this player.', 'danger') + return redirect(url_for('users.upload_contract')) + + if 'contract_file' not in request.files: + flash('No file selected.', 'danger') + return redirect(url_for('users.upload_contract')) + + file = request.files['contract_file'] + if file.filename == '': + flash('No file selected.', 'danger') + return redirect(url_for('users.upload_contract')) + + # Create upload directory + upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés') + os.makedirs(upload_dir, exist_ok=True) + + # Get player info for folder structure + player = User.query.get_or_404(player_id) + team = OrgTeam.query.get(player.team_id) if player.team_id else None + + # Create team folder if team exists + if team: + team_folder = os.path.join(upload_dir, secure_filename(team.name)) + os.makedirs(team_folder, exist_ok=True) + final_dir = team_folder + else: + final_dir = upload_dir + + # Generate unique filename + original_filename = secure_filename(file.filename) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + stored_filename = f"{secure_filename(player.full_name)}_{timestamp}_{original_filename}" + stored_filename = stored_filename.replace(' ', '_') + + # Save file + file_path = os.path.join(final_dir, stored_filename) + file.save(file_path) + + # Create database record + contract = Contract( + player_id=player_id, + team_id=player.team_id, + uploaded_by_id=current_user.id, + original_filename=original_filename, + stored_filename=stored_filename, + file_path=file_path, + notes=notes if notes else None + ) + db.session.add(contract) + db.session.commit() + + flash(f'Contract uploaded successfully for {player.full_name}!', 'success') + return redirect(url_for('users.list_contracts')) + + return render_template('pages/upload_contract.html', players=players) + + +@users_bp.route('/contracts//upload_signed', methods=['POST']) +@login_required +def upload_signed_contract(contract_id): + """Upload a signed contract.""" + contract = Contract.query.get_or_404(contract_id) + + if not contract.can_upload_signed(current_user): + flash('Only the player can upload their signed contract.', 'danger') + return redirect(url_for('users.list_contracts')) + + if 'signed_file' not in request.files: + flash('No file selected.', 'danger') + return redirect(url_for('users.list_contracts')) + + file = request.files['signed_file'] + if file.filename == '': + flash('No file selected.', 'danger') + return redirect(url_for('users.list_contracts')) + + # Generate unique filename for signed contract + original_filename = secure_filename(file.filename) + signed_filename = f"signed_{contract.stored_filename}" + + # Save file + file.save(contract.file_path.replace(contract.stored_filename, signed_filename)) + + # Update contract record + contract.signed_filename = signed_filename + contract.signed_file_path = contract.file_path.replace(contract.stored_filename, signed_filename) + contract.status = 'signed' + contract.signed_at = datetime.utcnow() + db.session.commit() + + flash('Signed contract uploaded successfully!', 'success') + return redirect(url_for('users.list_contracts')) + + +@users_bp.route('/contracts//download') +@login_required +def download_contract(contract_id): + """Download a contract file.""" + contract = Contract.query.get_or_404(contract_id) + + if not contract.can_view(current_user): + flash('You do not have permission to download this contract.', 'danger') + return redirect(url_for('users.list_contracts')) + + return send_file(contract.file_path, as_attachment=True, download_name=contract.original_filename) + + +@users_bp.route('/contracts//download_signed') +@login_required +def download_signed_contract(contract_id): + """Download a signed contract file.""" + contract = Contract.query.get_or_404(contract_id) + + if not contract.can_view(current_user): + flash('You do not have permission to download this contract.', 'danger') + return redirect(url_for('users.list_contracts')) + + if not contract.signed_file_path: + flash('No signed contract available.', 'danger') + return redirect(url_for('users.list_contracts')) + + return send_file(contract.signed_file_path, as_attachment=True, download_name=contract.signed_filename) diff --git a/templates/layouts/base.html b/templates/layouts/base.html index 1e2a8cb..1106c27 100644 --- a/templates/layouts/base.html +++ b/templates/layouts/base.html @@ -67,6 +67,12 @@ {% endif %} +
  • + + + Contracts + +
  • diff --git a/templates/pages/contracts.html b/templates/pages/contracts.html new file mode 100644 index 0000000..56af425 --- /dev/null +++ b/templates/pages/contracts.html @@ -0,0 +1,120 @@ +{% extends "layouts/base.html" %} +{% block title %}Contracts - TryoutPro{% endblock %} +{% block page_title %}Contracts{% endblock %} +{% block breadcrumb %}Home / Profile / Contracts{% endblock %} + +{% block header_actions %} +{% if current_user.role in ['president', 'manager', 'coach'] %} + + Upload Contract + +{% endif %} +{% endblock %} + +{% block content %} +
    +
    +

    Contract Dropbox

    +
    +
    + {% if contracts %} +
    + + + + + + + + + + + + + {% for contract in contracts %} + + + + + + + + + {% endfor %} + +
    PlayerTeamContractStatusUploadedActions
    {{ contract.player.full_name }}{{ contract.team.name if contract.team else 'N/A' }}{{ contract.original_filename }} + {% if contract.status == 'signed' %} + Signed + {% else %} + Pending Signature + {% endif %} + {{ contract.uploaded_at.strftime('%Y-%m-%d %H:%M') }} +
    + + Download + + {% if contract.signed_file_path %} + + Signed + + {% endif %} + {% if current_user.role == 'player' and contract.status == 'pending' %} + + {% endif %} +
    +
    +
    + {% else %} +
    + +

    No contracts found

    + {% if current_user.role == 'player' %} +

    No contracts have been uploaded for you yet. Contact your coach or manager.

    + {% else %} +

    No contracts have been uploaded yet. Upload a contract using the button above.

    + {% endif %} +
    + {% endif %} +
    +
    + +{% if current_user.role == 'player' %} + + +{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/profile.html b/templates/pages/profile.html index 1457d0e..f344ef0 100644 --- a/templates/pages/profile.html +++ b/templates/pages/profile.html @@ -5,6 +5,9 @@ {% block header_actions %}
    + + Contracts + Edit Profile @@ -151,5 +154,41 @@ {% endif %}
    + + + {% if user.role == 'player' %} +
    +
    +

    My Contracts

    +
    +
    + {% if contracts %} +
    +
    + Contracts Pending + + {% set pending = contracts | selectattr('status', 'equalto', 'pending') | list %} + {{ pending | length }} + +
    +
    + Contracts Signed + + {% set signed = contracts | selectattr('status', 'equalto', 'signed') | list %} + {{ signed | length }} + +
    +
    + + {% else %} +

    No contracts have been uploaded for you yet.

    + {% endif %} +
    +
    + {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/upload_contract.html b/templates/pages/upload_contract.html new file mode 100644 index 0000000..7b66e2f --- /dev/null +++ b/templates/pages/upload_contract.html @@ -0,0 +1,45 @@ +{% extends "layouts/base.html" %} +{% block title %}Upload Contract - TryoutPro{% endblock %} +{% block page_title %}Upload Contract{% endblock %} +{% block breadcrumb %}Home / Contracts / Upload{% endblock %} + +{% block content %} +
    +
    +

    Upload Contract for Player

    +
    +
    +
    + + +
    + + +
    + +
    + + + Accepted formats: PDF, DOC, DOCX, JPG, PNG +
    + +
    + + +
    + +
    + Cancel + +
    +
    +
    +
    +{% endblock %} \ No newline at end of file