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

Ukládání a zveřejňování snapshotů výsledkových listin

Issue #209
parent 56c37de1
Branches
No related tags found
1 merge request!110Zveřejňování výsledkovky účastníkům
from flask import render_template, request
import decimal
from flask import g, render_template, request
from flask.helpers import flash, url_for
from typing import List, Optional, Union
from typing import Iterable, List, Optional, Tuple, Union
from flask_wtf.form import FlaskForm
import json
import werkzeug.exceptions
from werkzeug.utils import redirect
import wtforms
......@@ -14,6 +16,7 @@ from mo.web import app
from mo.web.org_contest import get_context
from mo.web.table import Cell, CellInput, CellLink, Column, Row, Table, OrderCell, cell_pion_link
from mo.util_format import format_decimal, inflect_number
from mo.web.user import scoretable_construct
class SolPointsCell(Cell):
......@@ -59,6 +62,11 @@ class ScoreEditForm(FlaskForm):
submit = wtforms.SubmitField("Uložit zjednoznačnění")
class ScoreSnapshotForm(FlaskForm):
note = wtforms.TextField("Poznámka k verzi (pro organizátory)")
submit_snapshot = wtforms.SubmitField("Uložit současnou verzi")
@app.route('/org/contest/r/<int:round_id>/score')
@app.route('/org/contest/r/<int:round_id>/score/edit', methods=('GET', 'POST'), endpoint="org_score_edit")
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/score')
......@@ -87,6 +95,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
messages = score.get_messages()
edit_form: Optional[ScoreEditForm] = None
snapshot_form: Optional[ScoreSnapshotForm] = None
if is_edit:
edit_form = ScoreEditForm()
......@@ -229,15 +238,186 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
group_rounds = round.get_group_rounds(True)
group_rounds.sort(key=lambda r: r.round_code())
snapshots_count = db.get_count(sess.query(db.ScoreTable).filter_by(contest_id=ct_id))
if ctx.rights.have_right(Right.manage_contest):
snapshot_form = ScoreSnapshotForm()
if format == "":
return render_template(
'org_score.html',
ctx=ctx,
contest=contest, round=round, tasks=tasks,
tasks=tasks,
table=table, messages=messages,
group_rounds=group_rounds,
round_id=round_id, ct_id=ct_id,
edit_form=edit_form,
snapshots_count=snapshots_count,
edit_form=edit_form, snapshot_form=snapshot_form,
)
else:
return table.send_as(format)
class SetFinalScoretableForm(FlaskForm):
scoretable_id = wtforms.IntegerField()
submit_set_final = wtforms.SubmitField("Zveřejnit")
submit_hide = wtforms.SubmitField("Skrýt")
class OrderKeyEncoder(json.JSONEncoder):
def encode(self, o):
if isinstance(o, Iterable) and (not isinstance(o, str)):
return '[' + ', '.join(map(self.encode, o)) + ']'
if isinstance(o, decimal.Decimal):
return f'{o.normalize():f}' # using normalize() gets rid of trailing 0s, using ':f' prevents scientific notation
return super().encode(o)
@app.route('/org/contest/c/<int:ct_id>/score/snapshots', methods=('GET', 'POST'))
def org_score_snapshots(ct_id: int):
ctx = get_context(ct_id=ct_id)
assert ctx.contest
if not ctx.rights.have_right(Right.view_contestants):
raise werkzeug.exceptions.Forbidden()
sess = db.get_session()
scoretables = sess.query(db.ScoreTable).filter_by(contest_id=ct_id).all()
snapshot_form: Optional[ScoreSnapshotForm] = None
set_final_form: Optional[SetFinalScoretableForm] = None
if ctx.rights.have_right(Right.manage_contest):
snapshot_form = ScoreSnapshotForm()
if snapshot_form.validate_on_submit() and snapshot_form.submit_snapshot.data:
score = Score(ctx.round.master, ctx.contest)
tasks = score.get_tasks()
results = score.get_sorted_results()
snapshot_tasks: List[Tuple[str, str]] = [
(task.code, task.name) for task in tasks
]
snapshot_rows = []
for result in results:
snapshot_row = {
'order': result.order.__dict__,
'winner': result.winner,
'successful': result.successful,
'name': result.user.full_name(),
'school': result.pant.school_place.name or "?",
'grade': result.pant.grade,
'tasks': [],
'total_points': format_decimal(result.get_total_points()),
'birth_year': result.pant.birth_year,
'order_key': json.dumps(result._order_key, cls=OrderKeyEncoder)
}
sols = result.get_sols_map()
for task in tasks:
sol = sols.get(task.task_id)
snapshot_row['tasks'].append(format_decimal(sol.points) if sol else None)
snapshot_rows.append(snapshot_row)
score_table = db.ScoreTable(
contest_id=ct_id,
user=g.user,
score_mode=ctx.round.score_mode,
note=snapshot_form.note.data,
tasks=snapshot_tasks,
rows=snapshot_rows,
)
sess.add(score_table)
sess.flush()
mo.util.log(
type=db.LogType.contest,
what=ctx.contest.contest_id,
details={
'action': 'score-snapshot-created',
'scoretable_id': score_table.scoretable_id,
},
)
sess.commit()
app.logger.info(f"Nový snapshot výsledkové listiny #{score_table.scoretable_id} pro soutěž #{ctx.contest.contest_id} vytvořen")
flash("Současný stav výsledkové listiny uložen, nyní můžete tuto verzi zveřejnit.", "success")
return redirect(ctx.url_for('org_score_snapshots'))
set_final_form = SetFinalScoretableForm()
if set_final_form.validate_on_submit():
found = False
scoretable_id = set_final_form.scoretable_id.data
for scoretable in scoretables:
if scoretable.scoretable_id == scoretable_id:
found = True
break
if found and set_final_form.submit_set_final:
ctx.contest.scoretable_id = scoretable_id
mo.util.log(
type=db.LogType.contest,
what=ctx.contest.contest_id,
details={
'action': 'score-publish',
'scoretable_id': scoretable_id,
},
)
sess.commit()
app.logger.info(f"Zveřejněna výsledková listina #{scoretable_id} pro soutěž #{ctx.contest.contest_id}")
flash("Výsledková listina zveřejněna.", "success")
elif set_final_form.submit_hide.data:
ctx.contest.scoretable_id = None
mo.util.log(
type=db.LogType.contest,
what=ctx.contest.contest_id,
details={
'action': 'score-hide',
},
)
sess.commit()
app.logger.info(f"Skryta výsledková listina pro soutěž #{ctx.contest.contest_id}")
flash("Výsledková listina skryta.", "success")
else:
flash("Neznámé ID výsledkové listiny.", "danger")
return redirect(ctx.url_for('org_score_snapshots'))
return render_template(
'org_score_snapshots.html',
ctx=ctx,
scoretables=scoretables,
set_final_form=set_final_form
)
@app.route('/org/contest/c/<int:ct_id>/score/<int:scoretable_id>')
def org_score_snapshot(ct_id: int, scoretable_id: int):
ctx = get_context(ct_id=ct_id)
assert ctx.contest
if not ctx.rights.have_right(Right.view_contestants):
raise werkzeug.exceptions.Forbidden()
format = request.args.get('format', "")
sess = db.get_session()
scoretable = sess.query(db.ScoreTable).get(scoretable_id)
if not scoretable or scoretable.contest_id != ct_id:
raise werkzeug.exceptions.NotFound()
columns, table_rows = scoretable_construct(scoretable, format != "")
# columns.append(Column(key='order_key', name='order_key', title='Třídící klíč'))
filename = f"vysledky_{ctx.round.year}-{ctx.round.category}-{ctx.round.level}_oblast_{ctx.contest.place.code or ctx.contest.place.place_id}"
table = Table(
table_class="data full center",
columns=columns,
rows=table_rows,
filename=filename,
)
if format == "":
return render_template(
'org_score_snapshot.html',
ctx=ctx,
table=table,
scoretable=scoretable,
)
else:
return table.send_as(format)
......@@ -46,6 +46,11 @@
{% else %}
{% endif %}
{% if state in [RoundState.grading, RoundState.closed] %}
<tr><td>Oficiální výsledková listina<td>
{% if contest.scoretable %}<a href="{{ ctx.url_for('org_score_snapshot', scoretable_id=contest.scoretable_id) }}">Zveřejněna</a>
{% else %}<i>zatím není</i>{% endif %}
{% endif %}
</table>
<div class="btn-group">
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% set round = ctx.round %}
{% set contest = ctx.contest %}
{% block title %}
{{ round.round_code() }}: Výsledky pro {{ round.name|lower }} kategorie {{ round.category }}{% if contest %} {{ contest.place.name_locative() }}{% endif %}
......@@ -64,6 +66,16 @@ Rozkliknutím bodů se lze dostat na detail daného řešení.</p>
{% endif %}
{% endif %}
{% if contest and (snapshots_count or snapshot_form) %}
<p>
{% if snapshots_count %}
K této výsledkové listině {{ snapshots_count|inflected_by('existuje', 'existují', 'existují')}}
<strong><a href="{{ ctx.url_for('org_score_snapshots') }}">{{ snapshots_count|inflected('uložená verze', 'uložené verze', 'uložených verzí') }}</a></strong>,
{% if contest.scoretable_id %}jedna z nich je{% else %}žádná z nich není{% endif %} vydána jako oficiální výsledková listina.
{% endif %}
{% if snapshot_form %}Uložit současnou verzi výsledkové listiny můžete formulářem na spodku stránky.{% endif %}
{% endif %}
{% if edit_form %}
<p><strong>Zjednoznačnění pořadí:</strong> U soutěžících na sdílených pozicích vyplňte číslo do políčka na konci řádku. Třídí se vzestupně od nejmenšího, prázdné políčko se považuje za nulu.</p>
<form method="POST" class="form form-horizontal" action="">
......@@ -82,4 +94,17 @@ Rozkliknutím bodů se lze dostat na detail daného řešení.</p>
<a class="btn btn-default pull-right" href="{{ ctx.url_for('org_score_edit') }}">Zjednoznačnit pořadí</a><br>
{% endif %}
{% if snapshot_form %}
<div class="form-frame" style="margin-top: 20px; padding-top: 0px;">
<h4>Uložit současnou verzi výsledkové listiny</h4>
<p>Uložené verze výsledkové listiny odpovídají stavu k určitému datu. Jedna z nich může být označena jako oficiální a zveřejněna.</p>
<form method="post" action="{{ ctx.url_for('org_score_snapshots') }}" class="form form-inline">
{{ snapshot_form.csrf_token }}
{{ wtf.form_field(snapshot_form.note) }}
<input type="submit" name="submit_snapshot" class="btn btn-primary" value="Uložit současnou verzi">
</form>
</div>
</div>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}
{{ ctx.round.round_code() }}: Uložená verze výsledkové listiny pro {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }} {{ ctx.contest.place.name_locative() }}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Uložená verze výsledkové listiny") }}
{% endblock %}
{% block pretitle %}
<div class="btn-group pull-right">
<a class="btn btn-default" href="{{ ctx.url_for('org_score') }}">Aktuální výsledky</a>
</div>
{% endblock %}
{% block body %}
<p>Výsledková listina odpovídající stavu k {{ scoretable.created_at|timeformat }}. Lze ji
zveřejnit jako oficiální výsledkovou listinu v přehledu všech uložených verzí výsledkových listin pro tuto soutěž.</p>
<table class='data'>
<tr><td>Vygenerováno:<th>{{ scoretable.created_at|timeformat }}
<tr><td>Autor:<td>{{ scoretable.user|user_link }}
<tr><td>Mód výsledkové listiny:<td>{{ scoretable.score_mode.friendly_name() }}
<tr><td>Oficiální výsledková listina:<th>{{ "ano" if scoretable.scoretable_id == ctx.contest.scoretable_id else "ne" }}
{% if scoretable.note %}<tr><td>Poznámka:<td>{{ scoretable.note }}{% endif %}
</table>
{{ table.to_html() }}
{% endblock %}
{% extends "base.html" %}
{% block title %}
{{ ctx.round.round_code() }}: Uložené výsledkové listiny {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }} {{ ctx.contest.place.name_locative() }}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Uložené výsledkové listiny") }}
{% endblock %}
{% block pretitle %}
<div class="btn-group pull-right">
<a class="btn btn-default" href="{{ ctx.url_for('org_score') }}">Aktuální výsledky</a>
</div>
{% endblock %}
{% block body %}
<p>Uložené verze výsledkové listiny odpovídají stavu k určitému datu. Jedna z nich může být označena jako oficiální a zveřejněna.</p>
{% if scoretables %}
<table class="data full">
<thead>
<tr>
<th>Datum</th><th>Autor</th><th>Poznámka</th><th>Akce</th>
</tr>
</thead>
{% for scoretable in scoretables %}
<tr {% if ctx.contest.scoretable_id == scoretable.scoretable_id %}class="active"{% endif %}>
<td>{{ scoretable.created_at|timeformat }}
<td>{{ scoretable.user|user_link }}
<td>{{ scoretable.note }}
<td>{% if ctx.contest.scoretable_id == scoretable.scoretable_id %}<strong>Zveřejněná verze</strong><br>{% endif %}
<div class="btn-group">
<a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_score_snapshot', scoretable_id=scoretable.scoretable_id) }}">Zobrazit</a>
{% if set_final_form %}
<form method="POST" class="btn-group">
{{ set_final_form.csrf_token }}
{% if ctx.contest.scoretable_id == scoretable.scoretable_id %}
<input type="submit" name="submit_hide" class="btn btn-xs btn-danger" value="Zrušit zveřejnění">
{% else %}
<input type="hidden" name="scoretable_id" value="{{ scoretable.scoretable_id }}">
<input type="submit" name="submit_set_final" class="btn btn-xs btn-primary" value="Zveřejnit tuto verzi">
{% endif %}
</form>
{% endif %}
</div>
</tr>
{% endfor %}
</table>
{% else %}
<i>Žádné uložené verze dosud neexistují.</i>
{% endif %}
{% endblock %}
......@@ -21,6 +21,7 @@ from mo.util_format import time_and_timedelta
from mo.web import app
import mo.web.fields as mo_fields
import mo.web.org_round
from mo.web.table import Column, Row, OrderCell, Table
import mo.web.util
......@@ -492,3 +493,41 @@ def user_paper(contest_id: int, paper_id: int):
raise werkzeug.exceptions.Forbidden()
return mo.web.util.send_task_paper(paper)
def scoretable_construct(scoretable: db.ScoreTable, is_export: bool = False) -> Tuple[List[Column], List[Row]]:
"""Pro konstrukci výsledkovky zobrazované soutěžícím. Využito i při zobrazení
uložených snapshotů výsledkovky v org_score.py.
"""
columns = [
Column(key='order', name='poradi', title='Pořadí'),
Column(key='name', name='ucastnik', title='Účastník'),
Column(key='school', name='skola', title='Škola'),
Column(key='grade', name='rocnik', title='Ročník')
]
if is_export:
columns.insert(1, Column(key='status', name='stav'))
for (code, name) in scoretable.tasks:
columns.append(Column(key=f'task_{code}', name=code, title=code))
columns.append(Column(key='total_points', name='celkove_body', title='Celkové body'))
table_rows = []
for row in scoretable.rows:
order_cell = OrderCell(place=row['order']['place'], span=row['order']['span'], continuation=row['order']['continuation'])
row['order'] = order_cell
html_attr = {}
if row['winner']:
row['status'] = 'vítěz'
html_attr = {"class": "winner", "title": "Vítěz"}
elif row['successful']:
row['status'] = 'úspěšný'
html_attr = {"class": "successful", "title": "Úspěšný řešitel"}
for ((code, _), points) in zip(scoretable.tasks, row['tasks']):
row[f'task_{code}'] = points or ''
table_rows.append(Row(keys=row, html_attr=html_attr))
return (columns, table_rows)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment