Compare commits
1
Commits
48ca62cdd0
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a616c79663 |
@@ -37,3 +37,13 @@ tryout_coaches = db.Table(
|
|||||||
'coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
'coach_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tryout_managers = db.Table(
|
||||||
|
'tryout_managers',
|
||||||
|
db.Column(
|
||||||
|
'tryout_id', db.Integer, db.ForeignKey('tryouts.id', ondelete='CASCADE'), primary_key=True
|
||||||
|
),
|
||||||
|
db.Column(
|
||||||
|
'manager_id', db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""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 app.models._associations import tryout_coaches, tryout_managers
|
||||||
from app.time_utils import utc_now_naive
|
from app.time_utils import utc_now_naive
|
||||||
|
|
||||||
|
|
||||||
@@ -20,16 +20,17 @@ class Tryout(db.Model):
|
|||||||
max_players = db.Column(db.Integer, nullable=True)
|
max_players = db.Column(db.Integer, nullable=True)
|
||||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
|
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # deprecated, kept for migration
|
||||||
coach_id = db.Column(
|
coach_id = db.Column(
|
||||||
db.Integer, db.ForeignKey('users.id'), nullable=True
|
db.Integer, db.ForeignKey('users.id'), nullable=True
|
||||||
) # deprecated, kept for migration
|
) # deprecated, kept for migration
|
||||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||||
|
|
||||||
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
||||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts')
|
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts') # deprecated
|
||||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
||||||
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
||||||
|
managers = db.relationship('User', secondary=tryout_managers, backref='managed_tryouts_m2m')
|
||||||
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic')
|
||||||
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic')
|
||||||
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
teams = db.relationship('Team', backref='tryout', lazy='dynamic')
|
||||||
@@ -47,3 +48,17 @@ class Tryout(db.Model):
|
|||||||
if self.end_date is not None:
|
if self.end_date is not None:
|
||||||
return self.end_date < today
|
return self.end_date < today
|
||||||
return self.date < today
|
return self.date < today
|
||||||
|
|
||||||
|
def get_managers(self):
|
||||||
|
"""Managers attached to this tryout, both legacy and many-to-many."""
|
||||||
|
manager_list = list(self.managers)
|
||||||
|
if not manager_list and self.manager:
|
||||||
|
return [self.manager]
|
||||||
|
return manager_list
|
||||||
|
|
||||||
|
def get_coaches(self):
|
||||||
|
"""Coaches attached to this tryout, both legacy and many-to-many."""
|
||||||
|
coach_list = list(self.coaches)
|
||||||
|
if not coach_list and self.coach:
|
||||||
|
return [self.coach]
|
||||||
|
return coach_list
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class Manager(User):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def can_manage_this_tryout(self, tryout):
|
def can_manage_this_tryout(self, tryout):
|
||||||
return tryout.created_by == self.id or tryout.manager_id == self.id
|
return (
|
||||||
|
tryout.created_by == self.id
|
||||||
|
or tryout.manager_id == self.id
|
||||||
|
or any(m.id == self.id for m in tryout.managers)
|
||||||
|
)
|
||||||
|
|
||||||
def can_manage_this_org_team(self, org_team):
|
def can_manage_this_org_team(self, org_team):
|
||||||
return True
|
return True
|
||||||
@@ -32,7 +36,13 @@ class Manager(User):
|
|||||||
from app.models.tryout.tryout import Tryout
|
from app.models.tryout.tryout import Tryout
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Tryout.query.filter(or_(Tryout.created_by == self.id, Tryout.manager_id == self.id))
|
Tryout.query.filter(
|
||||||
|
or_(
|
||||||
|
Tryout.created_by == self.id,
|
||||||
|
Tryout.manager_id == self.id,
|
||||||
|
Tryout.managers.any(id=self.id),
|
||||||
|
)
|
||||||
|
)
|
||||||
.order_by(Tryout.date)
|
.order_by(Tryout.date)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-4
@@ -51,7 +51,7 @@ def can_manage():
|
|||||||
|
|
||||||
def tryout_form_payload():
|
def tryout_form_payload():
|
||||||
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
"""The tryout form, shaped for marshmallow (ARCH-005)."""
|
||||||
return form_payload(list_fields=('coach_ids',), optional_blank=())
|
return form_payload(list_fields=('coach_ids', 'manager_ids'), optional_blank=())
|
||||||
|
|
||||||
|
|
||||||
def coaches_from_ids(coach_ids):
|
def coaches_from_ids(coach_ids):
|
||||||
@@ -67,6 +67,13 @@ def coaches_from_ids(coach_ids):
|
|||||||
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
return User.query.filter(User.id.in_(coach_ids), User.role == 'coach').all()
|
||||||
|
|
||||||
|
|
||||||
|
def managers_from_ids(manager_ids):
|
||||||
|
"""The manager accounts behind these ids, filtered by role."""
|
||||||
|
if not manager_ids:
|
||||||
|
return []
|
||||||
|
return User.query.filter(User.id.in_(manager_ids), User.role == 'manager').all()
|
||||||
|
|
||||||
|
|
||||||
def _users_by_id(user_ids):
|
def _users_by_id(user_ids):
|
||||||
"""Load these users in one query, keyed by id.
|
"""Load these users in one query, keyed by id.
|
||||||
|
|
||||||
@@ -160,12 +167,12 @@ def create_tryout():
|
|||||||
created_by=current_user.id,
|
created_by=current_user.id,
|
||||||
status='upcoming',
|
status='upcoming',
|
||||||
target_org_team_id=data['target_org_team_id'],
|
target_org_team_id=data['target_org_team_id'],
|
||||||
manager_id=data['manager_id'],
|
|
||||||
)
|
)
|
||||||
db.session.add(tryout)
|
db.session.add(tryout)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
|
tryout.managers = managers_from_ids(data['manager_ids'])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout created successfully!'), 'success')
|
flash(_('Tryout created successfully!'), 'success')
|
||||||
@@ -221,8 +228,14 @@ def edit_tryout(tryout_id):
|
|||||||
tryout.location = data['location']
|
tryout.location = data['location']
|
||||||
tryout.max_players = data['max_players']
|
tryout.max_players = data['max_players']
|
||||||
tryout.target_org_team_id = data['target_org_team_id']
|
tryout.target_org_team_id = data['target_org_team_id']
|
||||||
tryout.manager_id = data['manager_id']
|
|
||||||
|
# Only update staff lists when the form explicitly sends them.
|
||||||
|
# An absent checkbox group (all unchecked or JS failed) means
|
||||||
|
# \"don't change\", not \"remove everyone\".
|
||||||
|
if 'coach_ids' in request.form:
|
||||||
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
tryout.coaches = coaches_from_ids(data['coach_ids'])
|
||||||
|
if 'manager_ids' in request.form:
|
||||||
|
tryout.managers = managers_from_ids(data['manager_ids'])
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout updated successfully!'), 'success')
|
flash(_('Tryout updated successfully!'), 'success')
|
||||||
@@ -241,7 +254,7 @@ def view_tryout(tryout_id):
|
|||||||
if isinstance(current_user, Admin):
|
if isinstance(current_user, Admin):
|
||||||
can_view = True
|
can_view = True
|
||||||
elif isinstance(current_user, Manager):
|
elif isinstance(current_user, Manager):
|
||||||
can_view = tryout.created_by == current_user.id or tryout.manager_id == current_user.id
|
can_view = current_user.can_manage_this_tryout(tryout)
|
||||||
elif isinstance(current_user, Coach):
|
elif isinstance(current_user, Coach):
|
||||||
can_view = current_user.can_manage_this_tryout(tryout)
|
can_view = current_user.can_manage_this_tryout(tryout)
|
||||||
elif isinstance(current_user, Player):
|
elif isinstance(current_user, Player):
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="form-group col-4">
|
<div class="form-group col-4">
|
||||||
<label for="{{ field_name }}_{{ pid }}">{{ label }} (1-10)</label>
|
<label for="{{ field_name }}_{{ pid }}">{{ label }} (1-10)</label>
|
||||||
<div class="score-input">
|
<div class="score-input">
|
||||||
<input type="range" id="{{ field_name }}_{{ pid }}" name="{{ field_name }}_{{ pid }}" min="1" max="10" value="{{ existing_scores[field_name] or 5 }}" oninput="this.nextElementSibling.textContent = this.value">
|
<input type="range" id="{{ field_name }}_{{ pid }}" name="{{ field_name }}_{{ pid }}" min="1" max="10" value="{{ existing_scores[field_name] or 5 }}" data-mirror>
|
||||||
<span class="range-value">{{ existing_scores[field_name] or 5 }}</span>
|
<span class="range-value">{{ existing_scores[field_name] or 5 }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><input type="checkbox" id="select-all" onclick="toggleAll(this)"></th>
|
<th><input type="checkbox" id="select-all" data-action="toggle-all"></th>
|
||||||
<th>Player</th>
|
<th>Player</th>
|
||||||
<th>Contact</th>
|
<th>Contact</th>
|
||||||
<th>Attendance</th>
|
<th>Attendance</th>
|
||||||
@@ -70,12 +70,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
{% endblock %}
|
||||||
function toggleAll(master) {
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
function toggleAll() {
|
||||||
var boxes = document.querySelectorAll('.player-check');
|
var boxes = document.querySelectorAll('.player-check');
|
||||||
|
var master = document.getElementById('select-all');
|
||||||
for (var i = 0; i < boxes.length; i++) {
|
for (var i = 0; i < boxes.length; i++) {
|
||||||
boxes[i].checked = master.checked;
|
boxes[i].checked = master.checked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerActions({
|
||||||
|
'toggle-all': toggleAll,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -300,6 +300,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<script nonce="{{ csp_nonce }}">
|
<script nonce="{{ csp_nonce }}">
|
||||||
function showCreateForm() {
|
function showCreateForm() {
|
||||||
document.getElementById('createTeamForm').classList.remove('hidden');
|
document.getElementById('createTeamForm').classList.remove('hidden');
|
||||||
|
|||||||
@@ -67,15 +67,22 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-6">
|
<div class="form-group col-6">
|
||||||
<label for="manager_id">{{ _('Assigned Manager') }}</label>
|
<label>{{ _('Assigned Managers') }}</label>
|
||||||
<select id="manager_id" name="manager_id" class="form-select">
|
<div class="checkbox-grid">
|
||||||
<option value="">{{ _('-- No manager assigned --') }}</option>
|
|
||||||
{% for manager in managers %}
|
{% for manager in managers %}
|
||||||
<option value="{{ manager.id }}" {% if tryout and tryout.manager_id == manager.id %}selected{% endif %}>
|
{% set is_checked = false %}
|
||||||
{{ manager.username }}
|
{% if tryout %}
|
||||||
</option>
|
{% for m in tryout.get_managers() %}
|
||||||
|
{% if m.id == manager.id %}{% set is_checked = true %}{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
{% endif %}
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" name="manager_ids" value="{{ manager.id }}" {% if is_checked %}checked{% endif %}>
|
||||||
|
<span>{{ manager.username }}</span>
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ _('Select one or more managers for this tryout.') }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -70,16 +70,22 @@
|
|||||||
<span class="detail-value">{{ tryout.target_org_team.name if tryout.target_org_team else 'Not specified' }}</span>
|
<span class="detail-value">{{ tryout.target_org_team.name if tryout.target_org_team else 'Not specified' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Manager</span>
|
<span class="detail-label">Manager(s)</span>
|
||||||
<span class="detail-value">{{ tryout.manager.username if tryout.manager else 'Not assigned' }}</span>
|
<span class="detail-value">
|
||||||
|
{% set mgrs = tryout.get_managers() %}
|
||||||
|
{% if mgrs %}
|
||||||
|
{{ mgrs | map(attribute='username') | join(', ') }}
|
||||||
|
{% else %}
|
||||||
|
Not assigned
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<span class="detail-label">Coaches</span>
|
<span class="detail-label">Coaches</span>
|
||||||
<span class="detail-value">
|
<span class="detail-value">
|
||||||
{% if tryout.coaches %}
|
{% set cos = tryout.get_coaches() %}
|
||||||
{{ tryout.coaches | map(attribute='username') | join(', ') }}
|
{% if cos %}
|
||||||
{% elif tryout.coach %}
|
{{ cos | map(attribute='username') | join(', ') }}
|
||||||
{{ tryout.coach.username }}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
Not assigned
|
Not assigned
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -534,6 +540,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
<style>
|
<style>
|
||||||
.presence-toggle-btn {
|
.presence-toggle-btn {
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
|
|||||||
@@ -949,6 +949,7 @@ class TryoutSchema(StripMixin):
|
|||||||
target_org_team_id = fields.Integer(allow_none=True, load_default=None)
|
target_org_team_id = fields.Integer(allow_none=True, load_default=None)
|
||||||
manager_id = fields.Integer(allow_none=True, load_default=None)
|
manager_id = fields.Integer(allow_none=True, load_default=None)
|
||||||
coach_ids = fields.List(fields.Integer(), load_default=list)
|
coach_ids = fields.List(fields.Integer(), load_default=list)
|
||||||
|
manager_ids = fields.List(fields.Integer(), load_default=list)
|
||||||
|
|
||||||
@validates_schema
|
@validates_schema
|
||||||
def validate_span(self, data, **kwargs):
|
def validate_span(self, data, **kwargs):
|
||||||
|
|||||||
Reference in New Issue
Block a user