bug fix:
Manager ne pouvait pas voir les tryouts. probleme avec discord bot
This commit is contained in:
+43
-28
@@ -114,21 +114,36 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
"""Check if a channel is a DM channel."""
|
"""Check if a channel is a DM channel."""
|
||||||
return hasattr(channel, 'recipient') or hasattr(channel, 'recipients')
|
return hasattr(channel, 'recipient') or hasattr(channel, 'recipients')
|
||||||
|
|
||||||
async def on_reaction_add(self, reaction, user):
|
async def on_raw_reaction_add(self, payload):
|
||||||
"""Handle when a reaction is added to a message."""
|
"""Handle when a reaction is added to a message (works even after bot restart)."""
|
||||||
if user.bot:
|
# Ignore bot's own reactions
|
||||||
|
if payload.user_id == self.user.id:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.is_dm_channel(reaction.message.channel):
|
# Check if this is a pending request we're tracking
|
||||||
|
if payload.message_id not in self.pending_requests:
|
||||||
return
|
return
|
||||||
|
|
||||||
message_id = reaction.message.id
|
# Fetch the channel and check if it's a DM
|
||||||
|
try:
|
||||||
if message_id not in self.pending_requests:
|
channel = await self.fetch_channel(payload.channel_id)
|
||||||
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
request_info = self.pending_requests[message_id]
|
if not self.is_dm_channel(channel):
|
||||||
emoji_str = str(reaction.emoji)
|
return
|
||||||
|
|
||||||
|
# Fetch the user who reacted
|
||||||
|
try:
|
||||||
|
user = await self.fetch_user(payload.user_id)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
if user is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
request_info = self.pending_requests[payload.message_id]
|
||||||
|
emoji_str = str(payload.emoji)
|
||||||
|
|
||||||
handler_type = request_info.get('type')
|
handler_type = request_info.get('type')
|
||||||
request_id = request_info.get('id')
|
request_id = request_info.get('id')
|
||||||
@@ -137,28 +152,28 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
if handler_type == 'one_on_one':
|
if handler_type == 'one_on_one':
|
||||||
if self.flask_app:
|
if self.flask_app:
|
||||||
with self.flask_app.app_context():
|
with self.flask_app.app_context():
|
||||||
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
|
await self.handle_one_on_one_approve(user, payload.message_id, request_id, channel)
|
||||||
else:
|
else:
|
||||||
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
|
await self.handle_one_on_one_approve(user, payload.message_id, request_id, channel)
|
||||||
elif handler_type == 'schedule_addition':
|
elif handler_type == 'schedule_addition':
|
||||||
if self.flask_app:
|
if self.flask_app:
|
||||||
with self.flask_app.app_context():
|
with self.flask_app.app_context():
|
||||||
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
|
await self.handle_attendance_confirm(user, payload.message_id, request_id, channel)
|
||||||
else:
|
else:
|
||||||
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
|
await self.handle_attendance_confirm(user, payload.message_id, request_id, channel)
|
||||||
elif emoji_str == CROSS_EMOJI:
|
elif emoji_str == CROSS_EMOJI:
|
||||||
if handler_type == 'one_on_one':
|
if handler_type == 'one_on_one':
|
||||||
if self.flask_app:
|
if self.flask_app:
|
||||||
with self.flask_app.app_context():
|
with self.flask_app.app_context():
|
||||||
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
|
await self.handle_one_on_one_reject(user, payload.message_id, request_id, channel)
|
||||||
else:
|
else:
|
||||||
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
|
await self.handle_one_on_one_reject(user, payload.message_id, request_id, channel)
|
||||||
elif handler_type == 'schedule_addition':
|
elif handler_type == 'schedule_addition':
|
||||||
if self.flask_app:
|
if self.flask_app:
|
||||||
with self.flask_app.app_context():
|
with self.flask_app.app_context():
|
||||||
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
|
await self.handle_attendance_decline(user, payload.message_id, request_id, channel)
|
||||||
else:
|
else:
|
||||||
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
|
await self.handle_attendance_decline(user, payload.message_id, request_id, channel)
|
||||||
|
|
||||||
async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str,
|
async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str,
|
||||||
team_name: str, date_str: str, start_time: str, end_time: str,
|
team_name: str, date_str: str, start_time: str, end_time: str,
|
||||||
@@ -259,7 +274,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
logger.error(f"Error sending schedule notification: {e}")
|
logger.error(f"Error sending schedule notification: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message):
|
async def handle_one_on_one_approve(self, coach, message_id, request_id, channel):
|
||||||
"""Handle coach approving a One on One request."""
|
"""Handle coach approving a One on One request."""
|
||||||
try:
|
try:
|
||||||
from app.models import OneOnOneRequest
|
from app.models import OneOnOneRequest
|
||||||
@@ -270,7 +285,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if request.coach.discord_user_id != str(coach.id):
|
if request.coach.discord_user_id != str(coach.id):
|
||||||
await original_message.channel.send("⚠️ You are not the intended recipient.")
|
await channel.send("⚠️ You are not the intended recipient.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Re-attach to current session (object may be detached across app contexts)
|
# Re-attach to current session (object may be detached across app contexts)
|
||||||
@@ -285,7 +300,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
request.responded_at = datetime.utcnow()
|
request.responded_at = datetime.utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
await original_message.channel.send(
|
await channel.send(
|
||||||
f"✅ You have **approved** the One on One session with {player_full_name}."
|
f"✅ You have **approved** the One on One session with {player_full_name}."
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -303,7 +318,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}")
|
logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
async def handle_one_on_one_reject(self, coach, message_id, request_id, original_message):
|
async def handle_one_on_one_reject(self, coach, message_id, request_id, channel):
|
||||||
"""Handle coach rejecting a One on One request."""
|
"""Handle coach rejecting a One on One request."""
|
||||||
try:
|
try:
|
||||||
from app.models import OneOnOneRequest
|
from app.models import OneOnOneRequest
|
||||||
@@ -314,7 +329,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if request.coach.discord_user_id != str(coach.id):
|
if request.coach.discord_user_id != str(coach.id):
|
||||||
await original_message.channel.send("⚠️ You are not the intended recipient.")
|
await channel.send("⚠️ You are not the intended recipient.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Re-attach to current session (object may be detached across app contexts)
|
# Re-attach to current session (object may be detached across app contexts)
|
||||||
@@ -326,7 +341,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
|
|
||||||
refusal_note = None
|
refusal_note = None
|
||||||
try:
|
try:
|
||||||
async for reply in original_message.channel.history(limit=20):
|
async for reply in channel.history(limit=20):
|
||||||
if reply.author.id == coach.id and reply.reference and reply.reference.message_id == message_id:
|
if reply.author.id == coach.id and reply.reference and reply.reference.message_id == message_id:
|
||||||
refusal_note = reply.content
|
refusal_note = reply.content
|
||||||
break
|
break
|
||||||
@@ -345,7 +360,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
else:
|
else:
|
||||||
rejection_msg += "\n\nℹ️ The player has been notified that you are not available."
|
rejection_msg += "\n\nℹ️ The player has been notified that you are not available."
|
||||||
|
|
||||||
await original_message.channel.send(rejection_msg)
|
await channel.send(rejection_msg)
|
||||||
if player_discord_id:
|
if player_discord_id:
|
||||||
await self.notify_player_about_one_on_one_direct(
|
await self.notify_player_about_one_on_one_direct(
|
||||||
player_discord_id=player_discord_id,
|
player_discord_id=player_discord_id,
|
||||||
@@ -360,7 +375,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}")
|
logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
async def handle_attendance_confirm(self, player, message_id, reference_id, original_message):
|
async def handle_attendance_confirm(self, player, message_id, reference_id, channel):
|
||||||
"""Handle player confirming attendance for a match/tryout."""
|
"""Handle player confirming attendance for a match/tryout."""
|
||||||
try:
|
try:
|
||||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
||||||
@@ -382,13 +397,13 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
await original_message.channel.send("✅ Your attendance has been confirmed!")
|
await channel.send("✅ Your attendance has been confirmed!")
|
||||||
del self.pending_requests[message_id]
|
del self.pending_requests[message_id]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}")
|
logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
async def handle_attendance_decline(self, player, message_id, reference_id, original_message):
|
async def handle_attendance_decline(self, player, message_id, reference_id, channel):
|
||||||
"""Handle player declining attendance for a match/tryout."""
|
"""Handle player declining attendance for a match/tryout."""
|
||||||
try:
|
try:
|
||||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
||||||
@@ -410,7 +425,7 @@ class TeamTryoutsBot(commands.Bot):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
await original_message.channel.send("❌ Your attendance has been declined.")
|
await channel.send("❌ Your attendance has been declined.")
|
||||||
del self.pending_requests[message_id]
|
del self.pending_requests[message_id]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -15,4 +15,11 @@ org_team_managers = db.Table('org_team_managers',
|
|||||||
primary_key=True),
|
primary_key=True),
|
||||||
db.Column('manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
db.Column('manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||||
primary_key=True),
|
primary_key=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tryout_coaches = db.Table('tryout_coaches',
|
||||||
|
db.Column('tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True),
|
||||||
|
db.Column('coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Tryout event for player evaluations and team formation."""
|
"""Tryout event for player evaluations and team formation."""
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.models._associations import tryout_coaches
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
@@ -18,12 +19,13 @@ class Tryout(db.Model):
|
|||||||
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)
|
||||||
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
coach_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # deprecated, kept for migration
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
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')
|
||||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='coached_tryouts')
|
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
||||||
|
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
||||||
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')
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ class Coach(User):
|
|||||||
).first() is not None
|
).first() is not None
|
||||||
if is_coach_of_target:
|
if is_coach_of_target:
|
||||||
return True
|
return True
|
||||||
|
# Check many-to-many coaches relationship
|
||||||
|
if any(c.id == self.id for c in tryout.coaches):
|
||||||
|
return True
|
||||||
|
# Backward compat: check deprecated coach_id
|
||||||
if tryout.coach_id == self.id:
|
if tryout.coach_id == self.id:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -38,9 +42,13 @@ class Coach(User):
|
|||||||
|
|
||||||
def get_visible_tryouts(self):
|
def get_visible_tryouts(self):
|
||||||
from app.models.tryout.tryout import Tryout
|
from app.models.tryout.tryout import Tryout
|
||||||
|
from app.models._associations import tryout_coaches
|
||||||
team_ids = [t.id for t in self.coached_org_teams.all()]
|
team_ids = [t.id for t in self.coached_org_teams.all()]
|
||||||
conditions = []
|
conditions = []
|
||||||
if team_ids:
|
if team_ids:
|
||||||
conditions.append(Tryout.target_org_team_id.in_(team_ids))
|
conditions.append(Tryout.target_org_team_id.in_(team_ids))
|
||||||
|
# Check many-to-many coaches
|
||||||
|
conditions.append(Tryout.coaches.any(id=self.id))
|
||||||
|
# Backward compat: check deprecated coach_id
|
||||||
conditions.append(Tryout.coach_id == self.id)
|
conditions.append(Tryout.coach_id == self.id)
|
||||||
return Tryout.query.filter(db.or_(*conditions)).order_by(Tryout.date).all()
|
return Tryout.query.filter(db.or_(*conditions)).order_by(Tryout.date).all()
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ class Manager(User):
|
|||||||
|
|
||||||
def get_visible_tryouts(self):
|
def get_visible_tryouts(self):
|
||||||
from app.models.tryout.tryout import Tryout
|
from app.models.tryout.tryout import Tryout
|
||||||
return Tryout.query.filter_by(created_by=self.id).order_by(Tryout.date).all()
|
from sqlalchemy import or_
|
||||||
|
return Tryout.query.filter(
|
||||||
|
or_(Tryout.created_by == self.id, Tryout.manager_id == self.id)
|
||||||
|
).order_by(Tryout.date).all()
|
||||||
|
|||||||
+19
-6
@@ -56,7 +56,7 @@ def create_tryout():
|
|||||||
max_players = request.form.get('max_players')
|
max_players = request.form.get('max_players')
|
||||||
target_org_team_id = request.form.get('target_org_team_id')
|
target_org_team_id = request.form.get('target_org_team_id')
|
||||||
manager_id = request.form.get('manager_id')
|
manager_id = request.form.get('manager_id')
|
||||||
coach_id = request.form.get('coach_id')
|
coach_ids = request.form.getlist('coach_ids')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
@@ -86,9 +86,15 @@ def create_tryout():
|
|||||||
created_by=current_user.id, status='upcoming',
|
created_by=current_user.id, status='upcoming',
|
||||||
target_org_team_id=int(target_org_team_id) if target_org_team_id else None,
|
target_org_team_id=int(target_org_team_id) if target_org_team_id else None,
|
||||||
manager_id=int(manager_id) if manager_id else None,
|
manager_id=int(manager_id) if manager_id else None,
|
||||||
coach_id=int(coach_id) if coach_id else None,
|
|
||||||
)
|
)
|
||||||
db.session.add(tryout)
|
db.session.add(tryout)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
# Assign coaches via many-to-many
|
||||||
|
if coach_ids:
|
||||||
|
coach_users = User.query.filter(User.id.in_([int(c) for c in coach_ids])).all()
|
||||||
|
tryout.coaches = coach_users
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Tryout created successfully!', 'success')
|
flash('Tryout created successfully!', 'success')
|
||||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
@@ -125,7 +131,7 @@ def edit_tryout(tryout_id):
|
|||||||
max_players = request.form.get('max_players')
|
max_players = request.form.get('max_players')
|
||||||
target_org_team_id = request.form.get('target_org_team_id')
|
target_org_team_id = request.form.get('target_org_team_id')
|
||||||
manager_id = request.form.get('manager_id')
|
manager_id = request.form.get('manager_id')
|
||||||
coach_id = request.form.get('coach_id')
|
coach_ids = request.form.getlist('coach_ids')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||||
@@ -156,7 +162,14 @@ def edit_tryout(tryout_id):
|
|||||||
tryout.max_players = int(max_players) if max_players else None
|
tryout.max_players = int(max_players) if max_players else None
|
||||||
tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None
|
tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None
|
||||||
tryout.manager_id = int(manager_id) if manager_id else None
|
tryout.manager_id = int(manager_id) if manager_id else None
|
||||||
tryout.coach_id = int(coach_id) if coach_id else None
|
|
||||||
|
# Update coaches via many-to-many
|
||||||
|
if coach_ids:
|
||||||
|
coach_users = User.query.filter(User.id.in_([int(c) for c in coach_ids])).all()
|
||||||
|
tryout.coaches = coach_users
|
||||||
|
else:
|
||||||
|
tryout.coaches = []
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Tryout updated successfully!', 'success')
|
flash('Tryout updated successfully!', 'success')
|
||||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
@@ -174,8 +187,8 @@ def view_tryout(tryout_id):
|
|||||||
can_view = False
|
can_view = False
|
||||||
if isinstance(current_user, Admin):
|
if isinstance(current_user, Admin):
|
||||||
can_view = True
|
can_view = True
|
||||||
elif isinstance(current_user, Manager) and tryout.created_by == current_user.id:
|
elif isinstance(current_user, Manager):
|
||||||
can_view = True
|
can_view = tryout.created_by == current_user.id or tryout.manager_id == current_user.id
|
||||||
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):
|
||||||
|
|||||||
@@ -280,6 +280,22 @@ for (let h = 8; h <= 22; h++) {
|
|||||||
const COACH_DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
const COACH_DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
|
||||||
let coachSelectedSlots = {};
|
let coachSelectedSlots = {};
|
||||||
|
|
||||||
|
// Drag state for coach grid
|
||||||
|
let coachDragMode = false;
|
||||||
|
let coachDragAction = null;
|
||||||
|
|
||||||
|
function applyCoachSlotAction(dayOfWeek, timeStr, element) {
|
||||||
|
if (!coachSelectedSlots[dayOfWeek]) coachSelectedSlots[dayOfWeek] = [];
|
||||||
|
const index = coachSelectedSlots[dayOfWeek].indexOf(timeStr);
|
||||||
|
if (coachDragAction === 'select' && index === -1) {
|
||||||
|
coachSelectedSlots[dayOfWeek].push(timeStr);
|
||||||
|
element.classList.add('selected');
|
||||||
|
} else if (coachDragAction === 'deselect' && index > -1) {
|
||||||
|
coachSelectedSlots[dayOfWeek].splice(index, 1);
|
||||||
|
element.classList.remove('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function loadCoachAvailability() {
|
function loadCoachAvailability() {
|
||||||
{% for av in existing_availability %}
|
{% for av in existing_availability %}
|
||||||
if (!coachSelectedSlots[{{ av.day_of_week }}]) coachSelectedSlots[{{ av.day_of_week }}] = [];
|
if (!coachSelectedSlots[{{ av.day_of_week }}]) coachSelectedSlots[{{ av.day_of_week }}] = [];
|
||||||
@@ -296,25 +312,40 @@ function renderCoachGrid() {
|
|||||||
COACH_TIME_SLOTS.forEach(slot => {
|
COACH_TIME_SLOTS.forEach(slot => {
|
||||||
const isSelected = coachSelectedSlots[dayIndex] && coachSelectedSlots[dayIndex].includes(slot.time);
|
const isSelected = coachSelectedSlots[dayIndex] && coachSelectedSlots[dayIndex].includes(slot.time);
|
||||||
const cssClass = isSelected ? 'time-slot selected' : 'time-slot';
|
const cssClass = isSelected ? 'time-slot selected' : 'time-slot';
|
||||||
html += '<div class="' + cssClass + '" data-day="' + dayIndex + '" data-time="' + slot.time + '" onclick="toggleCoachSlot(' + dayIndex + ', \'' + slot.time + '\', this)">' + slot.display + '</div>';
|
html += '<div class="' + cssClass + '" data-day="' + dayIndex + '" data-time="' + slot.time + '">' + slot.display + '</div>';
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
});
|
});
|
||||||
grid.innerHTML = html;
|
grid.innerHTML = html;
|
||||||
}
|
|
||||||
|
|
||||||
function toggleCoachSlot(dayOfWeek, timeStr, element) {
|
// Drag event listeners
|
||||||
if (!coachSelectedSlots[dayOfWeek]) coachSelectedSlots[dayOfWeek] = [];
|
grid.addEventListener('mousedown', function(e) {
|
||||||
const index = coachSelectedSlots[dayOfWeek].indexOf(timeStr);
|
const slot = e.target.closest('.time-slot');
|
||||||
if (index === -1) {
|
if (!slot) return;
|
||||||
coachSelectedSlots[dayOfWeek].push(timeStr);
|
e.preventDefault();
|
||||||
element.classList.add('selected');
|
coachDragMode = true;
|
||||||
} else {
|
const day = parseInt(slot.dataset.day);
|
||||||
coachSelectedSlots[dayOfWeek].splice(index, 1);
|
const time = slot.dataset.time;
|
||||||
element.classList.remove('selected');
|
if (!coachSelectedSlots[day]) coachSelectedSlots[day] = [];
|
||||||
}
|
const isSelected = coachSelectedSlots[day].indexOf(time) > -1;
|
||||||
clearTimeout(window._coachSaveTimeout);
|
coachDragAction = isSelected ? 'deselect' : 'select';
|
||||||
window._coachSaveTimeout = setTimeout(saveCoachAvailability, 1000);
|
applyCoachSlotAction(day, time, slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.addEventListener('mousemove', function(e) {
|
||||||
|
if (!coachDragMode) return;
|
||||||
|
const slot = e.target.closest('.time-slot');
|
||||||
|
if (!slot) return;
|
||||||
|
applyCoachSlotAction(parseInt(slot.dataset.day), slot.dataset.time, slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', function() {
|
||||||
|
if (coachDragMode) {
|
||||||
|
coachDragMode = false;
|
||||||
|
coachDragAction = null;
|
||||||
|
saveCoachAvailability();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveCoachAvailability() {
|
function saveCoachAvailability() {
|
||||||
@@ -400,9 +431,27 @@ var DAYS = [
|
|||||||
// Store selected slots: {day: [time, time, ...]}
|
// Store selected slots: {day: [time, time, ...]}
|
||||||
var selectedSlots = {};
|
var selectedSlots = {};
|
||||||
|
|
||||||
|
// Drag state
|
||||||
|
var dispDragMode = false;
|
||||||
|
var dispDragAction = null; // 'select' or 'deselect'
|
||||||
|
|
||||||
|
function applySlotAction(block, action) {
|
||||||
|
var day = parseInt(block.dataset.day);
|
||||||
|
var time = block.dataset.time;
|
||||||
|
if (!selectedSlots[day]) selectedSlots[day] = [];
|
||||||
|
var index = selectedSlots[day].indexOf(time);
|
||||||
|
if (action === 'select' && index === -1) {
|
||||||
|
selectedSlots[day].push(time);
|
||||||
|
block.classList.add('selected');
|
||||||
|
} else if (action === 'deselect' && index > -1) {
|
||||||
|
selectedSlots[day].splice(index, 1);
|
||||||
|
block.classList.remove('selected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderDisponibilityGrid() {
|
function renderDisponibilityGrid() {
|
||||||
var grid = document.getElementById('disponibilities-grid');
|
var grid = document.getElementById('disponibilities-grid');
|
||||||
grid.innerHTML = '<div style="margin-bottom: 10px;"><strong>Click time blocks to select your available hours</strong></div>';
|
grid.innerHTML = '<div style="margin-bottom: 10px;"><strong>Click or click-and-drag to select your available hours</strong></div>';
|
||||||
|
|
||||||
var container = document.createElement('div');
|
var container = document.createElement('div');
|
||||||
container.className = 'disponibility-grid';
|
container.className = 'disponibility-grid';
|
||||||
@@ -425,9 +474,6 @@ function renderDisponibilityGrid() {
|
|||||||
block.dataset.day = day.value;
|
block.dataset.day = day.value;
|
||||||
block.dataset.time = slot.time;
|
block.dataset.time = slot.time;
|
||||||
block.textContent = slot.display;
|
block.textContent = slot.display;
|
||||||
block.onclick = function() {
|
|
||||||
toggleSlot(day.value, slot.time, block);
|
|
||||||
};
|
|
||||||
timeBlocks.appendChild(block);
|
timeBlocks.appendChild(block);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -435,23 +481,39 @@ function renderDisponibilityGrid() {
|
|||||||
container.appendChild(dayRow);
|
container.appendChild(dayRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drag event listeners on the container
|
||||||
|
container.addEventListener('mousedown', function(e) {
|
||||||
|
var block = e.target.closest('.disponibility-time-block');
|
||||||
|
if (!block) return;
|
||||||
|
e.preventDefault();
|
||||||
|
dispDragMode = true;
|
||||||
|
var day = parseInt(block.dataset.day);
|
||||||
|
var time = block.dataset.time;
|
||||||
|
if (!selectedSlots[day]) selectedSlots[day] = [];
|
||||||
|
var isSelected = selectedSlots[day].indexOf(time) > -1;
|
||||||
|
dispDragAction = isSelected ? 'deselect' : 'select';
|
||||||
|
applySlotAction(block, dispDragAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', function(e) {
|
||||||
|
if (!dispDragMode) return;
|
||||||
|
var block = e.target.closest('.disponibility-time-block');
|
||||||
|
if (!block) return;
|
||||||
|
applySlotAction(block, dispDragAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', function() {
|
||||||
|
if (dispDragMode) {
|
||||||
|
dispDragMode = false;
|
||||||
|
dispDragAction = null;
|
||||||
|
saveDisponibilities();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
grid.appendChild(container);
|
grid.appendChild(container);
|
||||||
loadMyDisponibilities();
|
loadMyDisponibilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSlot(day, time, element) {
|
|
||||||
if (!selectedSlots[day]) selectedSlots[day] = [];
|
|
||||||
|
|
||||||
var index = selectedSlots[day].indexOf(time);
|
|
||||||
if (index > -1) {
|
|
||||||
selectedSlots[day].splice(index, 1);
|
|
||||||
element.classList.remove('selected');
|
|
||||||
} else {
|
|
||||||
selectedSlots[day].push(time);
|
|
||||||
element.classList.add('selected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadMyDisponibilities() {
|
function loadMyDisponibilities() {
|
||||||
fetch('{{ url_for("users.get_my_disponibilities") }}')
|
fetch('{{ url_for("users.get_my_disponibilities") }}')
|
||||||
.then(function(response) { return response.json(); })
|
.then(function(response) { return response.json(); })
|
||||||
|
|||||||
@@ -80,15 +80,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-12">
|
<div class="form-group col-12">
|
||||||
<label for="coach_id">Assigned Coach</label>
|
<label>Assigned Coaches</label>
|
||||||
<select id="coach_id" name="coach_id" class="form-select">
|
<div class="checkbox-grid">
|
||||||
<option value="">-- No coach assigned --</option>
|
|
||||||
{% for coach in coaches %}
|
{% for coach in coaches %}
|
||||||
<option value="{{ coach.id }}" {% if tryout and tryout.coach_id == coach.id %}selected{% endif %}>
|
{% set is_checked = false %}
|
||||||
{{ coach.username }}
|
{% if tryout %}
|
||||||
</option>
|
{% for c in tryout.coaches %}
|
||||||
|
{% if c.id == coach.id %}{% set is_checked = true %}{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if tryout.coach_id == coach.id %}{% set is_checked = true %}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="coach_ids" value="{{ coach.id }}" {% if is_checked %}checked{% endif %}>
|
||||||
|
<span>{{ coach.username }}</span>
|
||||||
|
</label>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</div>
|
||||||
|
<small class="text-muted">Select one or more coaches for this tryout.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="user-mini">
|
<div class="user-mini">
|
||||||
<div class="avatar-sm">{{ u.username[:2] | upper }}</div>
|
<div class="avatar-sm">{{ u.username[:2] | upper }}</div>
|
||||||
<span>{{ u.username }}</span>
|
<a href="{{ url_for('users.view_user', user_id=u.id) }}">{{ u.username }}</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ u.username }}</td>
|
<td>{{ u.username }}</td>
|
||||||
|
|||||||
@@ -74,8 +74,16 @@
|
|||||||
<span class="detail-value">{{ tryout.manager.username if tryout.manager else 'Not assigned' }}</span>
|
<span class="detail-value">{{ tryout.manager.username if tryout.manager else 'Not assigned' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Coach</span>
|
<span class="detail-label">Coaches</span>
|
||||||
<span class="detail-value">{{ tryout.coach.username if tryout.coach else 'Not assigned' }}</span>
|
<span class="detail-value">
|
||||||
|
{% if tryout.coaches %}
|
||||||
|
{{ tryout.coaches | map(attribute='username') | join(', ') }}
|
||||||
|
{% elif tryout.coach %}
|
||||||
|
{{ tryout.coach.username }}
|
||||||
|
{% else %}
|
||||||
|
Not assigned
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Registered Players</span>
|
<span class="detail-label">Registered Players</span>
|
||||||
|
|||||||
@@ -34,6 +34,16 @@
|
|||||||
<span class="detail-value">{{ profile_user.discord_username }}</span>
|
<span class="detail-value">{{ profile_user.discord_username }}</span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if profile_user.league_os_profile %}
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label"><i class="fas fa-link"></i> League OS</span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<a href="{{ profile_user.league_os_profile }}" target="_blank" rel="noopener noreferrer" class="trn-link">
|
||||||
|
<i class="fas fa-external-link-alt"></i> {{ profile_user.league_os_profile }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% set gamertags = profile_user.gamertags %}
|
{% set gamertags = profile_user.gamertags %}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Migration: Create tryout_coaches association table and migrate existing data.
|
||||||
|
|
||||||
|
Run this script to create the many-to-many relationship between tryouts and coaches.
|
||||||
|
Usage: python migrations/add_tryout_coaches.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Check if table already exists
|
||||||
|
result = db.session.execute(text(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.tables "
|
||||||
|
"WHERE table_name = 'tryout_coaches'"
|
||||||
|
))
|
||||||
|
exists = result.scalar() > 0
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
print("Table 'tryout_coaches' already exists. Skipping creation.")
|
||||||
|
else:
|
||||||
|
db.session.execute(text("""
|
||||||
|
CREATE TABLE tryout_coaches (
|
||||||
|
tryout_id INTEGER NOT NULL,
|
||||||
|
coach_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (tryout_id, coach_id),
|
||||||
|
FOREIGN KEY (tryout_id) REFERENCES tryouts (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (coach_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
db.session.commit()
|
||||||
|
print("Created 'tryout_coaches' association table.")
|
||||||
|
|
||||||
|
# Migrate existing coach_id data into the new table
|
||||||
|
result = db.session.execute(text(
|
||||||
|
"SELECT COUNT(*) FROM tryouts WHERE coach_id IS NOT NULL"
|
||||||
|
))
|
||||||
|
count = result.scalar()
|
||||||
|
|
||||||
|
if count > 0:
|
||||||
|
# Check how many already migrated
|
||||||
|
migrated = db.session.execute(text(
|
||||||
|
"SELECT COUNT(*) FROM tryout_coaches"
|
||||||
|
)).scalar()
|
||||||
|
|
||||||
|
if migrated == 0:
|
||||||
|
db.session.execute(text("""
|
||||||
|
INSERT INTO tryout_coaches (tryout_id, coach_id)
|
||||||
|
SELECT id, coach_id FROM tryouts WHERE coach_id IS NOT NULL
|
||||||
|
"""))
|
||||||
|
db.session.commit()
|
||||||
|
print(f"Migrated {count} existing coach assignments to tryout_coaches.")
|
||||||
|
else:
|
||||||
|
print(f"Skipping data migration — {migrated} rows already exist in tryout_coaches.")
|
||||||
|
else:
|
||||||
|
print("No existing coach assignments to migrate.")
|
||||||
|
|
||||||
|
print("Migration complete.")
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Migration: Add end_date column to tryouts table.
|
|
||||||
|
|
||||||
Run this script to add the end_date column to the tryouts table.
|
|
||||||
Usage: python migrations/add_tryout_end_date.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from app.app import create_app
|
|
||||||
from app.extensions import db
|
|
||||||
from sqlalchemy import text
|
|
||||||
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
# Check if column already exists
|
|
||||||
result = db.session.execute(text(
|
|
||||||
"SELECT COUNT(*) FROM information_schema.columns "
|
|
||||||
"WHERE table_name = 'tryouts' AND column_name = 'end_date'"
|
|
||||||
))
|
|
||||||
exists = result.scalar() > 0
|
|
||||||
|
|
||||||
if exists:
|
|
||||||
print("Column 'end_date' already exists in 'tryouts' table. Skipping.")
|
|
||||||
else:
|
|
||||||
db.session.execute(text(
|
|
||||||
"ALTER TABLE tryouts ADD COLUMN end_date DATE NULL"
|
|
||||||
))
|
|
||||||
# Backfill: set end_date = date for existing tryouts
|
|
||||||
db.session.execute(text(
|
|
||||||
"UPDATE tryouts SET end_date = date WHERE end_date IS NULL"
|
|
||||||
))
|
|
||||||
db.session.commit()
|
|
||||||
print("Successfully added 'end_date' column to 'tryouts' table and backfilled existing rows.")
|
|
||||||
|
|
||||||
print("Migration complete.")
|
|
||||||
Reference in New Issue
Block a user