Skip to content
Snippets Groups Projects
Commit b596d6e9 authored by Jiří Setnička's avatar Jiří Setnička
Browse files

Výsledkovka: Zobrazení výsledkovky na webu používá nový backend z mo.score

Navenek vypadá pořád skoro stejně, ale již umí zjednoznačňovat podle
pravidel MO.

Navíc přibylo zvýraznění vítězů a úspěšných řešitelů.
parent 7bf04ae4
Branches
No related tags found
1 merge request!33Výsledkové listiny - backend modul & pravidla MO
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)
......@@ -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 %}
......@@ -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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment