diff --git a/mo/web/org_score.py b/mo/web/org_score.py index c20df96183a37c63973996017c08d92ac09d6b99..4925de52983b4623069b642da1d8207bc0d6bc4a 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -1,13 +1,12 @@ from flask import render_template, request, g from flask.helpers import url_for -from sqlalchemy import and_ -from sqlalchemy.orm import joinedload -from typing import List, Tuple, Optional, Dict +from typing import Optional import werkzeug.exceptions import mo import mo.db as db from mo.rights import Right +from mo.score import Score from mo.web import app from mo.web.table import Cell, CellLink, Column, Row, Table, cell_pion_link @@ -42,10 +41,10 @@ class SolPointsCell(Cell): user: db.User sol: Optional[db.Solution] - def __init__(self, contest_id: int, user: db.User): + def __init__(self, contest_id: int, user: db.User, sol: db.Solution): self.contest_id = contest_id self.user = user - self.sol = None + self.sol = sol def __str__(self) -> str: if not self.sol: @@ -54,9 +53,6 @@ class SolPointsCell(Cell): return '?' return str(self.sol.points) - def set_sol(self, sol: db.Solution): - self.sol = sol - def to_html(self) -> str: if not self.sol: return '<td>–' @@ -85,66 +81,35 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): format = request.args.get('format', "") sess = db.get_session() - user_id_subq = sess.query(db.Participation.user_id).join( - db.User, db.Participation.user_id == db.User.user_id - ).filter( - db.User.is_test == False, - db.Participation.state.notin_([db.PartState.disqualified, db.PartState.refused, db.PartState.absent]), - ) - if round_id: contest = None round = sess.query(db.Round).get(round_id) if not round: raise werkzeug.exceptions.NotFound() rr = g.gatekeeper.rights_for_round(round, True) - contest_subq = sess.query(db.Contest.contest_id).filter_by(round=round) - user_id_subq = user_id_subq.filter(db.Participation.contest_id.in_(contest_subq)) else: contest = sess.query(db.Contest).get(contest_id) if not contest: raise werkzeug.exceptions.NotFound() round = contest.round rr = g.gatekeeper.rights_for_contest(contest) - contest_subq = [contest_id] - user_id_subq = user_id_subq.filter(db.Participation.contest == contest) if not rr.have_right(Right.view_submits): raise werkzeug.exceptions.Forbidden() - tasks_subq = sess.query(db.Task.task_id).filter_by(round=round) - tasks = (sess.query(db.Task) - .filter_by(round=round) - .order_by(db.Task.code) - .all()) - - sols: List[db.Solution] = sess.query(db.Solution).filter( - db.Solution.user_id.in_(user_id_subq), - db.Solution.task_id.in_(tasks_subq), - ).all() - - data: List[Tuple[db.User, db.Participation, db.Participant]] = ( - sess.query(db.User, db.Participation, db.Participant) - .select_from(db.Participation) - .join(db.User, isouter=True) - .join(db.Participant, and_( - db.Participant.user_id == db.Participation.user_id, - db.Participant.year == round.year - ), isouter=True).filter( - db.Participation.user_id.in_(user_id_subq), - db.Participation.contest_id.in_(contest_subq) - ).options( - joinedload(db.Participant.school_place), - joinedload(db.Participation.contest).joinedload(db.Contest.place), - ).all() - ) + score = Score(round, contest) + tasks = score.get_tasks() + results = score.get_sorted_results() + messages = score.get_messages() # Construct columns is_export = (format != "") - columns = [ - Column(key='order', name='poradi', title='Pořadí'), - Column(key='participant', name='ucastnik', title='Účastník'), - ] + columns = [] + columns.append(Column(key='order', name='poradi', title='Pořadí')) + if is_export: + columns.append(Column(key='winner', name='vitez')) + columns.append(Column(key='successful', name='uspesny_resitel')) + columns.append(Column(key='participant', name='ucastnik', title='Účastník')) if is_export: columns.append(Column(key='email', name='email')) if not contest_id: @@ -168,12 +133,17 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): ) columns.append(Column(key=f'task_{task.task_id}', name=task.code, title=title)) columns.append(Column(key='total_points', name='celkove_body', title='Celkové body')) + # columns.append(Column(key='order_key', name='order_key', title='Třídící klíč')) # Construct rows - rows_map: Dict[int, Row] = {} - for user, pion, pant in data: + table_rows = [] + for result in results: + user, pant, pion = result.user, result.pant, result.pion school = pant.school_place row = Row(keys={ + 'order': OrderCell(result.order.place, result.order.span, result.order.continuation), + 'winner': 'ano' if result.winner else '', + 'successful': 'ano' if result.successful else '', 'user': user, 'email': user.email, 'participant': cell_pion_link(user, pion.contest_id, user.full_name()), @@ -181,49 +151,20 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): 'pion_place': pion.place.name, 'school': CellLink(school.name, url_for('org_place', id=school.place_id)), 'grade': pant.grade, - 'total_points': 0, + 'total_points': result.get_total_points(), 'birth_year': pant.birth_year, + 'order_key': result._order_key, }) + sols = result.get_sols_map() for task in tasks: - row.keys[f'task_{task.task_id}'] = SolPointsCell(contest_id=pion.contest_id, user=user) - rows_map[user.user_id] = row - for sol in sols: - rows_map[sol.user_id].keys[f'task_{sol.task_id}'].set_sol(sol) - if sol.points: - rows_map[sol.user_id].keys['total_points'] += sol.points - - rows = list(rows_map.values()) - - # FIXME: Pokud to chceme přetavit do finální výsledkovky, tak musíme - # předefinovat následující funkci (a udělat to konfigurovatelné, protože - # různé olympiády i různé ročníky to mohou mít různě?) - def get_result_cmp(result): - return -result.keys['total_points'] - - def get_result_full_cmp(result): - return (get_result_cmp(result), result.keys['user'].last_name, result.keys['user'].first_name) - - rows.sort(key=get_result_full_cmp) - - # Spočítáme pořadí - v prvním kroku prolinkujeme opakující se OrderCell na první, - # ve druhém kroku je pak správně rozkopírujeme s nastaveným continuation na True - last: Row = None - for row in rows: - if last is None: - row.keys['order'] = OrderCell(1) - last = row - elif get_result_cmp(last) == get_result_cmp(row): - row.keys['order'] = last.keys['order'] - last.keys['order'].span += 1 - else: - row.keys['order'] = OrderCell(last.keys['order'].place + last.keys['order'].span) - last = row - lastOrder: OrderCell = None - for row in rows: - if row.keys['order'] == lastOrder: - row.keys['order'] = OrderCell(lastOrder.place, lastOrder.span, True) - else: - lastOrder = row.keys['order'] + row.keys[f'task_{task.task_id}'] = SolPointsCell( + contest_id=pion.contest_id, user=user, sol=sols.get(task.task_id) + ) + if result.winner: + row.html_attr = {"class": "winner", "title": "Vítěz"} + elif result.successful: + row.html_attr = {"class": "successful", "title": "Úspěšný řešitel"} + table_rows.append(row) filename = f"vysledky_{round.year}-{round.category}-{round.level}" if contest_id: @@ -231,7 +172,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): table = Table( table_class="data full center", columns=columns, - rows=rows, + rows=table_rows, filename=filename, ) @@ -239,7 +180,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): return render_template( 'org_score.html', contest=contest, round=round, tasks=tasks, - table=table, + table=table, messages=messages, ) else: return table.send_as(format) diff --git a/mo/web/templates/org_score.html b/mo/web/templates/org_score.html index 27bd42a5f09e75739766a69b8ef7237a71d4b153..15ae4bebd5e42a2953787808516afefd50b9be27 100644 --- a/mo/web/templates/org_score.html +++ b/mo/web/templates/org_score.html @@ -15,12 +15,44 @@ {% endif %} </div> -<p>Toto je polotovar výsledkové listiny určený k ručnímu dodělání. -Pořadí je neoficiální (seřazené podle součtu bodů s dělenými místy). +{% if messages %} +<div class="collapsible"> + {% set error_count = messages | selectattr(0, "equalto", "error") | list | count %} + + <input type="checkbox" class="toggle" id="messages-toggle"> + <label for="messages-toggle" class="toggle"> + Log vytváření výsledkové listiny ({{ messages|length|inflected('zpráva', 'zprávy', 'zpráv') }} + {%- if error_count > 0 %}, <span class="error">{{ error_count|inflected('chyba', 'chyby', 'chyb') }}</span>{% endif %}) + </label> + <div class="collapsible-inner"> + <div class="alert alert-warning"> + <ul> + {% for (type, msg) in messages %} + {% if type == "error" %}<li class="error"><b>Chyba: {{ msg }}</b> + {% elif type == "warning" %}<li>Varování: {{ msg }} + {% else %}<li class="text-info">Info: {{ msg }}{% endif %} + {% endfor %} + </ul></div> + </div> + </div> +</div> +{% endif %} + +<p>Mód této výsledkové listiny je <b>{{ round.score_mode.friendly_name() }}</b>. Diskvalifikovaní, odmítnuvší a nepřítomní účastníci jsou skryti, stejně tak testovací uživatelé. Export pod tabulkou obsahuje sloupce navíc. Rozkliknutím bodů se lze dostat na detail daného řešení.</p> +{% if round.score_winner_limit is not none or round.score_successful_limit is not none %} +<p> +{% if round.score_winner_limit is not none %} +<b>Vítězi</b> se stávají účastníci s alespoň <b>{{ round.score_winner_limit|inflected("bodem", "body", "body") }}</b>. +{% endif %} +{% if round.score_successful_limit is not none %} +<b>Úspěšnými řešiteli</b> se stávají účastníci s alespoň <b>{{ round.score_successful_limit|inflected("bodem", "body", "body") }}</b>. +{% endif %} +{% endif %} + {{ table.to_html() }} {% endblock %} diff --git a/static/mo.css b/static/mo.css index 34c5a998fd9f0b2048d4d3fa2b56ae29159e6aab..1eda5be8253ae8b24cc5c04f31fada18a84d6bd6 100644 --- a/static/mo.css +++ b/static/mo.css @@ -130,6 +130,11 @@ table tr.state-refused, table tr.state-refused a:not(.btn) { color: #888; } +table tbody tr.winner { background-color: #fe5; } +table tbody tr.winner:hover { background-color: #dc3; } +table tbody tr.successful { background-color: #9e9; } +table tbody tr.successful:hover { background-color: #7c7; } + nav#main-menu { display: flex; flex-wrap: wrap;