diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..6b5d54a Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 2f07596..2a51998 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/app.py b/app.py index 61f6a94..736775d 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,7 @@ def create_app(): from routes.users import users_bp from routes.main import main_bp from routes.teams import teams_bp + from routes.matches import matches_bp app.register_blueprint(auth_bp) app.register_blueprint(tryouts_bp) @@ -27,6 +28,7 @@ def create_app(): app.register_blueprint(users_bp) app.register_blueprint(main_bp) app.register_blueprint(teams_bp) + app.register_blueprint(matches_bp) with app.app_context(): import models diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index 91d116b..926066b 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/models.py b/models.py index 6eb2296..93228a7 100644 --- a/models.py +++ b/models.py @@ -60,10 +60,13 @@ class User(UserMixin, db.Model): return self.role == 'president' def can_manage_tryouts(self): - return self.role in ['president', 'manager'] + return self.role in ['president', 'manager', 'coach', 'scout'] def can_manage_teams(self): return self.role in ['president', 'manager'] + + def can_schedule_matches(self): + 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).""" @@ -181,3 +184,45 @@ class TeamMember(db.Model): added_at = db.Column(db.DateTime, default=datetime.utcnow) player = db.relationship('User', overlaps="player_ref,team_assignments") # Many-to-one, no dynamic loader + + +class Match(db.Model): + """Matches/scrimmages scheduled within tryouts.""" + __tablename__ = 'matches' + id = db.Column(db.Integer, primary_key=True) + tryout_id = db.Column(db.Integer, db.ForeignKey('tryouts.id'), nullable=False) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text, nullable=True) + date = db.Column(db.Date, nullable=False) + start_time = db.Column(db.Time, nullable=True) + end_time = db.Column(db.Time, nullable=True) + location = db.Column(db.String(200), nullable=True) + status = db.Column(db.String(20), default='scheduled') # scheduled, completed, cancelled + match_type = db.Column(db.String(20), nullable=False) # team_vs_team, player_scrim + created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # For team vs team matches + team1_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) + team2_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) + + creator = db.relationship('User', backref='created_matches') + tryout = db.relationship('Tryout', backref='matches') + team1 = db.relationship('Team', foreign_keys=[team1_id], backref='matches_as_team1') + team2 = db.relationship('Team', foreign_keys=[team2_id], backref='matches_as_team2') + participants = db.relationship('MatchParticipant', backref='match', lazy='dynamic') + + def get_participating_players(self): + """Return list of players participating in this match.""" + return [p.player_id for p in self.participants.all()] + + +class MatchParticipant(db.Model): + """Players participating in player scrimmage matches.""" + __tablename__ = 'match_participants' + id = db.Column(db.Integer, primary_key=True) + match_id = db.Column(db.Integer, db.ForeignKey('matches.id'), nullable=False) + player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + added_at = db.Column(db.DateTime, default=datetime.utcnow) + + player = db.relationship('User') diff --git a/routes/__pycache__/matches.cpython-313.pyc b/routes/__pycache__/matches.cpython-313.pyc new file mode 100644 index 0000000..83accc4 Binary files /dev/null and b/routes/__pycache__/matches.cpython-313.pyc differ diff --git a/routes/__pycache__/tryouts.cpython-313.pyc b/routes/__pycache__/tryouts.cpython-313.pyc index 86171b9..ecbf1a6 100644 Binary files a/routes/__pycache__/tryouts.cpython-313.pyc and b/routes/__pycache__/tryouts.cpython-313.pyc differ diff --git a/routes/matches.py b/routes/matches.py new file mode 100644 index 0000000..480f9d7 --- /dev/null +++ b/routes/matches.py @@ -0,0 +1,340 @@ +from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify +from flask_login import login_required, current_user +from extensions import db +from models import User, Tryout, Match, MatchParticipant, Team, TeamMember, OrgTeam +from datetime import datetime, time + +matches_bp = Blueprint('matches', __name__, url_prefix='/matches') + + +def can_schedule_match(): + """Check if user can schedule matches (coaches and above).""" + return current_user.role in ['president', 'manager', 'coach', 'scout'] + + +@matches_bp.route('/calendar') +@login_required +def calendar(): + """Calendar view showing tryouts and matches.""" + return render_template('pages/calendar.html') + + +@matches_bp.route('/api/events') +@login_required +def api_events(): + """API endpoint returning calendar events.""" + events = [] + + # Get tryouts based on user permissions + tryouts = get_visible_tryouts_for_user() + + for tryout in tryouts: + events.append({ + 'id': f'tryout_{tryout.id}', + 'title': tryout.title, + 'date': tryout.date.strftime('%Y-%m-%d'), + 'type': 'tryout', + 'color': '#3b82f6', # Blue for tryouts + 'extendedProps': { + 'location': tryout.location or 'TBD', + 'status': tryout.status, + 'description': tryout.description or '', + 'tryout_id': tryout.id + } + }) + + # Add matches for this tryout + for match in tryout.matches: + match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b' + + # Build match description with participants + match_desc = match.description or '' + participants_str = '' + if match.match_type == 'team_vs_team': + teams = [] + if match.team1: + teams.append(match.team1.name) + if match.team2: + teams.append(match.team2.name) + participants_str = f"{' vs '.join(teams)}" + match_desc = participants_str + (f"
{match.description}" if match.description else '') + else: + # Player scrim - show all participants + player_names = [p.player.full_name for p in match.participants.all()] + participants_str = ', '.join(player_names) if player_names else 'No players' + match_desc = participants_str + (f"
{match.description}" if match.description else '') + + # Include time for calendar display + start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None + end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None + + events.append({ + 'id': f'match_{match.id}', + 'title': match.title + ' (' + participants_str + ')', + 'date': match.date.strftime('%Y-%m-%d'), + 'type': 'match', + 'color': match_color, + 'extendedProps': { + 'location': match.location or tryout.location or 'TBD', + 'status': match.status, + 'description': match_desc, + 'match_type': match.match_type, + 'tryout_id': tryout.id, + 'match_id': match.id, + 'start_time': start_time_str, + 'end_time': end_time_str, + 'participants': participants_str + } + }) + + return jsonify(events) + + +@matches_bp.route('/api/events/') +@login_required +def api_events_for_tryout(tryout_id): + """API endpoint returning calendar events for a specific tryout.""" + tryout = Tryout.query.get_or_404(tryout_id) + + # Check if user can view this tryout + if not current_user.can_manage_this_tryout(tryout): + # For players, check if they're registered or participating in the match + if current_user.role == 'player': + # Check if player is registered for this tryout + is_registered = TryoutRegistration.query.filter_by( + tryout_id=tryout_id, player_id=current_user.id + ).first() is not None + + # Check if player is participating in any matches for this tryout + match_participation = MatchParticipant.query.filter( + MatchParticipant.match_id == MatchParticipant.match_id + ).join(Match).filter(Match.tryout_id == tryout_id).all() + + player_in_match = any(mp.player_id == current_user.id for mp in match_participation) + + if not is_registered and not player_in_match: + return jsonify([]) + + events = [] + + for match in tryout.matches: + match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b' + + # Build participant string + participants_str = '' + if match.match_type == 'team_vs_team': + teams = [] + if match.team1: + teams.append(match.team1.name) + if match.team2: + teams.append(match.team2.name) + participants_str = f"{' vs '.join(teams)}" + else: + player_names = [p.player.full_name for p in match.participants.all()] + participants_str = ', '.join(player_names) if player_names else 'No players' + + events.append({ + 'id': f'match_{match.id}', + 'title': match.title + ' (' + participants_str + ')', + 'date': match.date.strftime('%Y-%m-%d'), + 'type': 'match', + 'color': match_color, + 'extendedProps': { + 'location': match.location or tryout.location or 'TBD', + 'status': match.status, + 'match_type': match.match_type, + 'tryout_id': tryout.id, + 'match_id': match.id + } + }) + + return jsonify(events) + + +def get_visible_tryouts_for_user(): + """Get tryouts that the current user can see based on their role.""" + if current_user.role == 'president': + return Tryout.query.order_by(Tryout.date).all() + elif current_user.role == 'manager': + return Tryout.query.filter_by(created_by=current_user.id).order_by(Tryout.date).all() + elif current_user.role == 'coach': + org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first() + if org_team: + return Tryout.query.filter_by(target_org_team_id=org_team.id).order_by(Tryout.date).all() + return [] + elif current_user.role == 'player': + # Get tryouts player is registered for + player_tryout_ids = [r.tryout_id for r in current_user.tryout_registrations.all()] + tryouts = Tryout.query.filter(Tryout.id.in_(player_tryout_ids)).order_by(Tryout.date).all() if player_tryout_ids else [] + + # Also include matches where player is participating + player_matches = Match.query.join(MatchParticipant).filter( + MatchParticipant.player_id == current_user.id + ).all() + + player_match_tryout_ids = list(set(m.tryout_id for m in player_matches)) + additional_tryouts = Tryout.query.filter( + Tryout.id.in_(player_match_tryout_ids) + ).order_by(Tryout.date).all() if player_match_tryout_ids else [] + + # Combine and deduplicate + all_tryouts = tryouts + [t for t in additional_tryouts if t.id not in player_tryout_ids] + return all_tryouts + else: # scout + return Tryout.query.order_by(Tryout.date).all() + + +@matches_bp.route('/create/', methods=['GET', 'POST']) +@login_required +def create_match(tryout_id): + """Create a new match/scrimmage within a tryout.""" + tryout = Tryout.query.get_or_404(tryout_id) + + # Check if user can manage this tryout (president, manager, or coach) + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to schedule matches for this tryout.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + teams = Team.query.filter_by(tryout_id=tryout_id).all() + all_players = User.query.filter_by(role='player').order_by(User.full_name).all() + + if request.method == 'POST': + title = request.form.get('title') + description = request.form.get('description') + date_str = request.form.get('date') + start_time_str = request.form.get('start_time') + end_time_str = request.form.get('end_time') + location = request.form.get('location') + match_type = request.form.get('match_type') + + try: + date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date + except (ValueError, TypeError): + flash('Invalid date format.', 'danger') + return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) + + start_time = None + end_time = None + try: + if start_time_str: + start_time = datetime.strptime(start_time_str, '%H:%M').time() + if end_time_str: + end_time = datetime.strptime(end_time_str, '%H:%M').time() + except ValueError: + flash('Invalid time format.', 'danger') + return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) + + match = Match( + tryout_id=tryout_id, + title=title, + description=description, + date=date_obj, + start_time=start_time, + end_time=end_time, + location=location, + match_type=match_type, + created_by=current_user.id + ) + db.session.add(match) + db.session.flush() # Get match.id before commit + + # Handle team vs team matches + if match_type == 'team_vs_team': + team1_id = request.form.get('team1_id') + team2_id = request.form.get('team2_id') + match.team1_id = int(team1_id) if team1_id else None + match.team2_id = int(team2_id) if team2_id else None + + # Handle player scrim matches + elif match_type == 'player_scrim': + player_ids = request.form.getlist('player_ids') + for pid in player_ids: + participant = MatchParticipant(match_id=match.id, player_id=int(pid)) + db.session.add(participant) + + db.session.commit() + flash('Match scheduled successfully!', 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + return render_template('pages/create_match.html', tryout=tryout, teams=teams, all_players=all_players) + + +@matches_bp.route('//edit', methods=['GET', 'POST']) +@login_required +def edit_match(match_id): + """Edit an existing match.""" + match = Match.query.get_or_404(match_id) + tryout = match.tryout + + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to edit this match.', 'danger') + return redirect(url_for('matches.calendar')) + + teams = Team.query.filter_by(tryout_id=tryout.id).all() + all_players = User.query.filter_by(role='player').order_by(User.full_name).all() + current_player_ids = [p.player_id for p in match.participants.all()] + + if request.method == 'POST': + match.title = request.form.get('title') + match.description = request.form.get('description') + date_str = request.form.get('date') + start_time_str = request.form.get('start_time') + end_time_str = request.form.get('end_time') + location = request.form.get('location') + status = request.form.get('status') + + try: + match.date = datetime.strptime(date_str, '%Y-%m-%d').date() + except (ValueError, TypeError): + flash('Invalid date format.', 'danger') + return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) + + try: + if start_time_str: + match.start_time = datetime.strptime(start_time_str, '%H:%M').time() + if end_time_str: + match.end_time = datetime.strptime(end_time_str, '%H:%M').time() + except ValueError: + pass + + match.location = location + if status in ['scheduled', 'completed', 'cancelled']: + match.status = status + + # Handle team vs team matches + if match.match_type == 'team_vs_team': + team1_id = request.form.get('team1_id') + team2_id = request.form.get('team2_id') + match.team1_id = int(team1_id) if team1_id else None + match.team2_id = int(team2_id) if team2_id else None + + # Handle player scrim matches - update participants + elif match.match_type == 'player_scrim': + MatchParticipant.query.filter_by(match_id=match.id).delete() + player_ids = request.form.getlist('player_ids') + for pid in player_ids: + participant = MatchParticipant(match_id=match.id, player_id=int(pid)) + db.session.add(participant) + + db.session.commit() + flash('Match updated successfully!', 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) + + return render_template('pages/edit_match.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids) + + +@matches_bp.route('//delete', methods=['POST']) +@login_required +def delete_match(match_id): + """Delete a match.""" + match = Match.query.get_or_404(match_id) + tryout = match.tryout + + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to delete this match.', 'danger') + return redirect(url_for('matches.calendar')) + + db.session.delete(match) + db.session.commit() + flash('Match deleted successfully.', 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) \ No newline at end of file diff --git a/routes/tryouts.py b/routes/tryouts.py index b7911a9..1142606 100644 --- a/routes/tryouts.py +++ b/routes/tryouts.py @@ -1,7 +1,7 @@ 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, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam +from models import User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant from datetime import datetime tryouts_bp = Blueprint('tryouts', __name__, url_prefix='/tryouts') @@ -148,6 +148,22 @@ def view_tryout(tryout_id): # Get all players (for manager registration dropdown) all_players = User.query.filter_by(role='player').order_by(User.full_name).all() + # Get matches for this tryout with participant info + matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date).all() + match_data = [] + for match in matches: + if match.match_type == 'team_vs_team': + participants = { + 'team1': match.team1.name if match.team1 else 'TBD', + 'team2': match.team2.name if match.team2 else 'TBD' + } + else: + participants = [p.player.full_name for p in match.participants.all()] + match_data.append({ + 'match': match, + 'participants': participants + }) + return render_template('pages/view_tryout.html', tryout=tryout, registered_players=registered_players, @@ -158,6 +174,8 @@ def view_tryout(tryout_id): team_data=team_data, can_edit=can_edit, all_players=all_players, + matches=matches, + match_data=match_data, now=datetime.utcnow()) @tryouts_bp.route('//register', methods=['POST']) diff --git a/static/css/style.css b/static/css/style.css index 089f508..a978463 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -358,6 +358,8 @@ a:hover { color: var(--primary-dark); } .badge-upcoming { background: var(--info-light); color: var(--info); } .badge-in_progress { background: var(--warning-light); color: var(--warning); } .badge-completed { background: var(--success-light); color: var(--success); } +.badge-scheduled { background: var(--info-light); color: var(--info); } +.badge-cancelled { background: var(--danger-light); color: var(--danger); } .badge-president { background: #eef2ff; color: #4338ca; } .badge-manager { background: #ecfdf5; color: #059669; } .badge-coach { background: #fffbeb; color: #d97706; } @@ -888,7 +890,65 @@ a:hover { color: var(--primary-dark); } .mr-2 { margin-right: 8px; } .mx-2 { margin-left: 8px; margin-right: 8px; } -/* Responsive */ +/* Calendar Styles */ +#calendar { + min-height: 600px; +} + +.fc { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.fc-toolbar { + margin-bottom: 16px; +} + +.fc-toolbar-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--gray-900); +} + +.fc-button { + background: var(--primary) !important; + border: none !important; + border-radius: var(--radius-sm) !important; + padding: 6px 12px !important; + font-size: 0.85rem !important; +} + +.fc-button:hover { + background: var(--primary-dark) !important; +} + +.fc-button-primary:not(:disabled).fc-button-active, +.fc-button-primary:not(:disabled):active { + background: var(--primary-dark) !important; +} + +.fc-daygrid-day { + transition: var(--transition); +} + +.fc-daygrid-day:hover { + background: var(--gray-50); +} + +.fc-event { + border-radius: var(--radius-sm); + padding: 2px 4px; + font-size: 0.8rem; +} + +.fc-event-title { + font-weight: 600; +} + +.btn-group { + display: flex; + gap: 4px; +} + @media (max-width: 768px) { .sidebar { width: var(--sidebar-collapsed); @@ -910,4 +970,4 @@ a:hover { color: var(--primary-dark); } .content { padding: 16px; } .top-bar { padding: 12px 16px; } .flash-messages { padding: 0 16px; } -} \ No newline at end of file +} diff --git a/templates/layouts/base.html b/templates/layouts/base.html index 50963cd..136598f 100644 --- a/templates/layouts/base.html +++ b/templates/layouts/base.html @@ -39,6 +39,12 @@ Tryouts +
  • + + + Calendar + +
  • {% if current_user.can_evaluate() %}
  • @@ -130,5 +136,6 @@ {% endif %} + {% block scripts %}{% endblock %} \ No newline at end of file diff --git a/templates/pages/calendar.html b/templates/pages/calendar.html new file mode 100644 index 0000000..289892c --- /dev/null +++ b/templates/pages/calendar.html @@ -0,0 +1,145 @@ +{% extends "layouts/base.html" %} +{% block title %}Calendar - TryoutPro{% endblock %} +{% block page_title %}Calendar{% endblock %} +{% block breadcrumb %}Home / Calendar{% endblock %} + +{% block content %} +
    +
    +

    Schedule

    +
    +
    + + + + +
    +
    +
    +
    +
    +
    +
    + + + +{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/create_match.html b/templates/pages/create_match.html new file mode 100644 index 0000000..0c6d45d --- /dev/null +++ b/templates/pages/create_match.html @@ -0,0 +1,134 @@ +{% extends "layouts/base.html" %} +{% block title %}Schedule Match - {{ tryout.title }} - TryoutPro{% endblock %} +{% block page_title %}Schedule Match{% endblock %} +{% block breadcrumb %}Home /
    Tryouts / {{ tryout.title }} / Schedule Match{% endblock %} + +{% block content %} +
    +
    +

    Schedule Match for {{ tryout.title }}

    +
    +
    +
    + + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + + +
    +
    +

    Select Teams

    + +
    +
    + + +
    +
    + + +
    +
    +
    + + + + +
    + + + Cancel + +
    +
    +
    +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/pages/edit_match.html b/templates/pages/edit_match.html new file mode 100644 index 0000000..8fba2a6 --- /dev/null +++ b/templates/pages/edit_match.html @@ -0,0 +1,108 @@ +{% extends "layouts/base.html" %} +{% block title %}Edit Match - TryoutPro{% endblock %} +{% block page_title %}Edit Match{% endblock %} +{% block breadcrumb %}Home / Tryouts / {{ tryout.title }} / Edit Match{% endblock %} + +{% block content %} +
    +
    +

    Edit Match

    +
    +
    +
    + + +
    + + +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + + {% if match.match_type == 'team_vs_team' %} +
    +

    Teams

    + +
    +
    + + +
    +
    + + +
    +
    + {% else %} +
    +

    Players

    + +
    + +
    + {% for player in all_players %} + + {% endfor %} +
    +
    + {% endif %} + +
    + + + Cancel + +
    +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/templates/pages/view_tryout.html b/templates/pages/view_tryout.html index eef9458..abf3e1e 100644 --- a/templates/pages/view_tryout.html +++ b/templates/pages/view_tryout.html @@ -6,9 +6,14 @@ {% block content %}
    -
    +

    Tryout Details

    + {% if can_edit %} + + Schedule Match + + {% endif %} {% if can_edit %}
    @@ -226,6 +231,80 @@
    + {% if matches %} +
    +
    +

    Scheduled Matches

    + {% if can_edit %} + + Schedule Match + + {% endif %} +
    +
    + +
    + + +
    + + + + + + + + + + {% if can_edit %} + + {% endif %} + + + + {% for item in match_data %} + {% set m = item.match %} + + + + + + + + {% if can_edit %} + + {% endif %} + + {% endfor %} + +
    MatchTypeDateParticipantsTimeStatusActions
    {{ m.title }} + + {{ 'Team vs Team' if m.match_type == 'team_vs_team' else 'Player Scrim' }} + + {{ m.date.strftime('%m/%d/%Y') }} + {% if m.match_type == 'team_vs_team' %} + {{ item.participants.team1 }} vs {{ item.participants.team2 }} + {% else %} + {{ item.participants | join(', ') }} + {% endif %} + + {% if m.start_time and m.end_time %} + {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} + {% else %} + TBD + {% endif %} + + {{ m.status }} + + + Edit + +
    +
    +
    +
    + {% endif %} +

    Evaluation Summary

    @@ -274,5 +353,29 @@ -{% endblock %} \ No newline at end of file +{% endblock %}