diff --git a/mo/web/org_score.py b/mo/web/org_score.py index c482cb7afdf7f10518dd5219f4ecaee45e05041d..c4cd28bada4a3f42255602cf0a6c2960523a6a68 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -1,4 +1,5 @@ -from flask import render_template, g +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 @@ -7,6 +8,62 @@ import werkzeug.exceptions import mo.db as db from mo.rights import Right from mo.web import app +from mo.web.table import Cell, CellLink, Column, Row, Table, cell_user_link + + +class OrderCell(Cell): + place: int + span: int + continuation: bool + + def __init__(self, place: int, span: int = 1, continuation: bool = False): + self.place = place + self.span = span + self.continuation = continuation + + def __str__(self) -> str: + if self.span == 1: + return f"{self.place}." + else: + return f"{self.place}.–{self.place + self.span - 1}." + + def to_html(self) -> str: + if self.continuation: + return "" # covered by rowspan cell above this one + elif self.span == 1: + return f"<td>{self.__str__()}" + else: + return f"<td rowspan='{self.span}'>{self.__str__()}" + + +class SolPointsCell(Cell): + url: str + exists: bool + points: Optional[int] + + def __init__(self, url: str): + self.url = url + self.exists = False + self.points = None + + def __str__(self) -> str: + if not self.exists: + return '–' + elif self.points is None: + return '?' + return str(self.points) + + def set_points(self, points: Optional[int]): + self.exists = True + self.points = points + + def to_html(self) -> str: + if not self.exists: + return '<td>–' + a = f'<td><a href="{self.url}" title="Detail řešení">' + if self.points is None: + return a + '<span class="unknown">?</span></a>' + return a + str(self.points) + '</a>' @app.route('/org/contest/r/<int:round_id>/score') @@ -16,6 +73,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): raise werkzeug.exceptions.BadRequest() if round_id is not None and contest_id is not None: raise werkzeug.exceptions.BadRequest() + format = request.args.get('format', "") sess = db.get_session() user_id_subq = sess.query(db.Participation.user_id).join( @@ -35,7 +93,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): if not contest: raise werkzeug.exceptions.NotFound() round = contest.round - rr.get_for_contest(contest) + rr = g.gatekeeper.rights_for_contest(contest) contest_subq = [contest_id] user_id_subq = user_id_subq.filter(db.Participation.contest == contest) @@ -69,65 +127,100 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): ).all() ) - class ResultOrder: - span: int - place: int - - def format(self): - if self.span == 1: - return f"{self.place}." - else: - return f"{self.place}.–{self.place + self.span - 1}." - - class Result: - def __init__(self, user: db.User, contest: db.Contest, school: db.School): - self.user = user - self.contest = contest - self.school = school - self.order = ResultOrder() - self.points: Dict[int, int] = {} - self.total_points = 0 - - results_map: Dict[int, Result] = {} + # Construct columns + columns = [ + Column(key='order', name='poradi', title='Pořadí'), + Column(key='participant', name='ucastnik', title='Účastník'), + ] + if not contest_id: + columns.append(Column(key='contest', name='oblast', title='Soutěžní oblast')) + columns.append(Column(key='school', name='skola', title='Škola')) + columns.append(Column(key='grade', name='rocnik', title='Ročník')) + for task in tasks: + title = task.code + if contest_id: + title = '<a href="{}">{}</a>'.format( + url_for('org_contest_task_submits', contest_id=contest_id, task_id=task.task_id), + task.code + ) + if rr.have_right(Right.edit_points) and round.state == db.RoundState.grading: + title += ' (<a href="{}" title="Editovat body">✎</a>)'.format( + url_for('org_contest_task_points', contest_id=contest_id, task_id=task.task_id), + ) + 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')) + + # Construct rows + rows_map: Dict[int, Row] = {} for user, pion, pant in data: - results_map[user.user_id] = Result(user, pion.contest, pant.school_place) - + school = pant.school_place + row = Row(keys={ + 'user': user, + 'participant': cell_user_link(user, user.full_name()), + 'contest': CellLink(pion.contest.place.name, url_for('org_contest', id=pion.contest_id)), + 'school': CellLink(school.name, url_for('org_place', id=school.place_id)), + 'grade': pant.grade, + 'total_points': 0, + }) + for task in tasks: + row.keys[f'task_{task.task_id}'] = SolPointsCell( + url_for('org_submit_list', contest_id=pion.contest_id, user_id=user.user_id, task_id=task.task_id) + ) + rows_map[user.user_id] = row for sol in sols: - results_map[sol.user_id].points[sol.task_id] = sol.points + rows_map[sol.user_id].keys[f'task_{sol.task_id}'].set_points(sol.points) if sol.points: - results_map[sol.user_id].total_points += sol.points + rows_map[sol.user_id].keys['total_points'] += sol.points - results = list(results_map.values()) + 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.total_points + return -result.keys['total_points'] def get_result_full_cmp(result): - return (get_result_cmp(result), result.user.last_name, result.user.first_name) + return (get_result_cmp(result), result.keys['user'].last_name, result.keys['user'].first_name) - results.sort(key=get_result_full_cmp) - last = None - for result in results: + 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: - result.order.place = 1 - result.order.span = 1 - last = result - elif get_result_cmp(last) == get_result_cmp(result): - result.order.place = last.order.place - result.order.span = 0 - last.order.span += 1 + 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: - result.order.place = last.order.place + last.order.span - result.order.span = 1 - last = result - - return render_template( - 'org_score.html', - contest=contest, round=round, tasks=tasks, - results=results, - can_edit_points=rr.have_right(Right.edit_points) and round.state == db.RoundState.grading, - db=db, # kvůli hodnotám enumů + lastOrder = row.keys['order'] + + filename = f"vysledky_{round.year}-{round.category}-{round.level}" + if contest_id: + filename += f"_oblast_{contest.place.code or contest.place.place_id}" + table = Table( + table_class="data full center", + columns=columns, + rows=rows, + filename=filename, ) + + if format == "": + return render_template( + 'org_score.html', + contest=contest, round=round, tasks=tasks, + table=table, + db=db, # kvůli hodnotám enumů + ) + else: + return table.send_as(format) diff --git a/mo/web/templates/org_score.html b/mo/web/templates/org_score.html index 7a22596cb6f705c35140fe5573575d490c1572d8..db1f4a3d0927abf4b3214a8020d4db545fc5d8f2 100644 --- a/mo/web/templates/org_score.html +++ b/mo/web/templates/org_score.html @@ -17,42 +17,6 @@ <p>Pořadí je neoficiální (seřazené podle součtu bodů s dělenými místy), testovací uživatelé jsou skryti. Rozkliknutím bodů se lze dostat na detail daného řešení.</p> -<table class="data full center"> - <thead> - <tr> - <th>Pořadí - <th>Účastník - {% if not contest %}<th>Soutěžní oblast{% endif %} - <th>Škola - {% for task in tasks %}<th> - {% if contest %} - <a href="{{ url_for('org_contest_task_submits', contest_id=contest.contest_id, task_id=task.task_id) }}">{{ task.code }}</a> - {% if can_edit_points %} - (<a title="Editovat body" href="{{ url_for('org_contest_task_points', contest_id=contest.contest_id, task_id=task.task_id) }}">✎</a>) - {% endif %} - {% else %}{{ task.code }}{% endif %} - {% endfor %} - <th>Celkové body - </tr> - </thead> - {% for result in results %} - <tr> - {% if result.order.span > 0 %}<td{% if result.order.span > 1 %} rowspan={{ result.order.span }}{% endif %}> - {{ result.order.format() }} - {% endif %} - <th>{{ result.user|user_link }} - {% if not contest %}<td><a href="{{ url_for('org_contest', id=result.contest.contest_id) }}">{{ result.contest.place.name }}</a>{% endif %} - <td><a href="{{ url_for('org_place', id=result.school.place_id) }}">{{ result.school.name }}</a> - {% for task in tasks %} - <td>{% if task.task_id in result.points %} - <a title="Detail řešení" href="{{ url_for('org_submit_list', contest_id=result.contest.contest_id, user_id=result.user.user_id, task_id=task.task_id) }}"> - {% if result.points[task.task_id] is not none %}{{ result.points[task.task_id] }}{% else %}<span class="unknown">?</span>{% endif %} - </a> - {% else %}–{% endif %} - {% endfor %} - <th>{{ result.total_points }}</th> - </tr> - {% endfor %} -</table> +{{ table.to_html() }} {% endblock %}