Skip to content
Snippets Groups Projects
Commit ad91ee12 authored by Martin Mareš's avatar Martin Mareš
Browse files

Tabulka všech řešení v soutěži už také umí editovat body

Sdílíme velkou část kódu s tabulkou řešení úlohy.
parent d2da6a71
Branches
No related tags found
1 merge request!134Prázdné protokoly
from dataclasses import dataclass
from decimal import Decimal
import os
from flask import render_template, g, redirect, url_for, flash, request
......@@ -1053,44 +1054,29 @@ def get_solutions_query(
return query
class TaskPointsEditForm(FlaskForm):
submit = wtforms.SubmitField("Uložit")
@dataclass
class SolEdit:
key: str
pion: db.Participation
task: db.Task
sol: Optional[db.Solution]
gp: Optional[GenPoints] = None
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/edit', methods=('GET', 'POST'), endpoint="org_contest_task_edit")
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/edit', methods=('GET', 'POST'), endpoint="org_contest_task_edit")
def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
ctx = get_context(ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits)
assert ctx.contest and ctx.task
action_edit = request.endpoint == "org_contest_task_edit"
if action_edit and not (ctx.rights.can_edit_points() or ctx.rights.can_create_solutions()):
raise werkzeug.exceptions.Forbidden()
sess = db.get_session()
q = get_solutions_query(ctx.task, for_contest=ctx.master_contest, for_site=ctx.site)
rows: List[Tuple[db.Participation, db.Solution]] = q.all()
rows.sort(key=lambda r: r[0].user.sort_key())
edit_form: Optional[TaskPointsEditForm] = None
edit_errors: Dict[int, str] = {}
def process_edit_form() -> bool:
assert edit_form is not None
if not edit_form.validate_on_submit():
return False
def process_solution_edits(
ctx: Context,
edits: List[SolEdit],
edit_errors: Dict[str, str],
) -> bool:
"""Společná implementace editování zobecněných bodů pro org_contest_task a org_contest_solutions."""
# Parsování bodů
gen_pts_for: Dict[db.Participation, GenPoints] = {}
for pion, sol in rows:
pts = request.form.get(f"points_{pion.user_id}")
for edit in edits:
pts = request.form.get(f"points_{edit.key}")
gp = GenPoints.parse(pts)
if gp.error is not None:
edit_errors[pion.user_id] = gp.error
gen_pts_for[pion] = gp
edit_errors[edit.key] = gp.error
edit.gp = gp
if edit_errors:
return False
......@@ -1100,13 +1086,13 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
cnt_changed_sols: int = 0
to_log: List[str] = []
def update_sol(pion, sol, gp) -> Optional[str]:
def edit_sol(edit) -> Optional[str]:
try:
sact = SolAction(
task=assert_not_none(ctx.task),
user=pion.user,
sol=sol,
gp=gp,
task=edit.task,
user=edit.pion.user,
sol=edit.sol,
gp=edit.gp,
reason='web',
rights=ctx.rights,
to_log=to_log,
......@@ -1132,10 +1118,10 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
return None
# Připravíme všechny změny, ale ještě necommitujeme ani nelogujeme
for pion, sol in rows:
err = update_sol(pion, sol, gen_pts_for[pion])
for edit in edits:
err = edit_sol(edit)
if err is not None:
edit_errors[pion.user_id] = err
edit_errors[edit.key] = err
if edit_errors:
return False
......@@ -1154,12 +1140,48 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
flash(inflect_with_number(cnt_deleted_sols, "Smazáno %s řešení.", "Smazána %s řešení.", "Smazáno %s nových řešení."), "success")
if cnt_changed_sols:
flash("Změněny body u " + inflect_number(cnt_changed_sols, "řešení", "řešení", "řešení") + '.', "success")
sess = db.get_session()
sess.commit()
return True
class TaskPointsEditForm(FlaskForm):
submit = wtforms.SubmitField("Uložit změny")
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/edit', methods=('GET', 'POST'), endpoint="org_contest_task_edit")
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/edit', methods=('GET', 'POST'), endpoint="org_contest_task_edit")
def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
ctx = get_context(ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits)
assert ctx.contest and ctx.task
action_edit = request.endpoint == "org_contest_task_edit"
if action_edit and not (ctx.rights.can_edit_points() or ctx.rights.can_create_solutions()):
raise werkzeug.exceptions.Forbidden()
sess = db.get_session()
q = get_solutions_query(ctx.task, for_contest=ctx.master_contest, for_site=ctx.site)
rows: List[Tuple[db.Participation, db.Solution]] = q.all()
rows.sort(key=lambda r: r[0].user.sort_key())
edit_form: Optional[TaskPointsEditForm] = None
edit_errors: Dict[str, str] = {}
if action_edit:
edit_form = TaskPointsEditForm()
if process_edit_form():
if edit_form.validate_on_submit():
edits = [
SolEdit(
key=str(pion.user_id),
pion=pion,
task=assert_not_none(ctx.task),
sol=sol,
) for pion, sol in rows
]
if process_solution_edits(ctx, edits, edit_errors):
return redirect(ctx.url_for('org_contest_task'))
if edit_errors:
flash("Některé změny se nepodařilo provést. Důvod uvidíte po najetí myší nad políčka s červeným okrajem.", "danger")
......@@ -1186,7 +1208,7 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
class ContestSolutionsEditForm(FlaskForm):
submit = wtforms.SubmitField("Založit označená řešení")
submit = wtforms.SubmitField("Uložit změny")
@app.route('/org/contest/c/<int:ct_id>/solutions', methods=('GET', 'POST'))
......@@ -1227,16 +1249,6 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None):
joinedload(db.Solution.final_feedback_obj)
).all()
# Count papers for each task and solution
paper_counts = {}
for user_id, task_id, type, count in (
db.get_session().query(db.Paper.for_user, db.Paper.for_task, db.Paper.type, func.count(db.Paper.type))
.filter(db.Paper.for_task.in_(tasks_subq))
.group_by(db.Paper.for_user, db.Paper.for_task, db.Paper.type)
.all()
):
paper_counts[(user_id, task_id, type.name)] = count
task_sols: Dict[int, Dict[int, db.Solution]] = {}
for t in tasks:
task_sols[t.task_id] = {}
......@@ -1244,38 +1256,33 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None):
task_sols[s.task_id][s.user_id] = s
edit_form: Optional[ContestSolutionsEditForm] = None
edit_errors: Dict[str, str] = {}
if edit_action:
edit_form = ContestSolutionsEditForm()
if edit_form.validate_on_submit():
new_sol_count = 0
edits = []
for task in tasks:
for pion in pions:
if pion.user_id in task_sols[task.task_id]:
continue # již existuje
if not request.form.get(f"create_sol_{task.task_id}_{pion.user_id}"):
continue # nikdo nežádá o vytvoření
sol = db.Solution(task=task, user=pion.user)
sess.add(sol)
mo.util.log(
type=db.LogType.participant,
what=pion.user_id,
details={
'action': 'solution-created',
'task': task.task_id,
},
)
app.logger.info(f"Řešení úlohy {task.code} od účastníka {pion.user_id} založeno")
new_sol_count += 1
if new_sol_count > 0:
sess.commit()
flash(inflect_by_number(new_sol_count, "Založeno", "Založena", "Založeno") + ' '
+ inflect_number(new_sol_count, "nové řešení", "nová řešení", "nových řešení"),
"success")
else:
flash("Žádné změny k uložení", "info")
edits.append(SolEdit(
key=f'{task.task_id}_{pion.user_id}',
pion=pion,
task=task,
sol=task_sols[task.task_id].get(pion.user_id, None),
))
if process_solution_edits(ctx, edits, edit_errors):
return redirect(ctx.url_for('org_contest_solutions'))
if edit_errors:
flash("Některé změny se nepodařilo provést. Důvod uvidíte po najetí myší nad políčka s červeným okrajem.", "danger")
# Count papers for each task and solution
paper_counts = {}
for user_id, task_id, type, count in (
db.get_session().query(db.Paper.for_user, db.Paper.for_task, db.Paper.type, func.count(db.Paper.type))
.filter(db.Paper.for_task.in_(tasks_subq))
.group_by(db.Paper.for_user, db.Paper.for_task, db.Paper.type)
.all()
):
paper_counts[(user_id, task_id, type.name)] = count
return render_template(
'org_contest_solutions.html',
......@@ -1283,7 +1290,8 @@ def org_contest_solutions(ct_id: int, site_id: Optional[int] = None):
contest=ctx.contest, site=ctx.site, rights=ctx.rights,
pions=pions, tasks=tasks, tasks_sols=task_sols, paper_counts=paper_counts,
paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p),
edit_form=edit_form,
edit_form=edit_form, request_form=request.form,
edit_errors=edit_errors,
)
......
......
......@@ -5,12 +5,15 @@
{% set edit_points = not ctx.site and rights.can_edit_points() %}
{% set edit_create = rights.can_create_solutions() %}
{% set edit_both = edit_points and edit_create %}
{% set edit = edit_points or edit_create %}
{% set edit_title = "Editace řešení" if edit_form else "Odevzdaná řešení" %}
{% set edit_tip = 'Založit řešení nebo editovat body' if edit_both else 'Editovat body' if edit_points else 'Založit řešení' %}
{% block title %}
{{ "Založení řešení" if edit_form else "Tabulka řešení" }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %}
{{ edit_title }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Založení řešení" if edit_form else "Tabulka řešení") }}
{{ ctx.breadcrumbs(action=edit_title) }}
{% endblock %}
{% block pretitle %}
......@@ -26,9 +29,19 @@
<p><i>
{% if edit_form %}
Zaškrtnutím políček u řešení, která dosud neexistují, a odesláním tlačítkem pod tabulkou tato řešení založíte.
To se hodí, pokud se nechystáte do systému nahrávat soubory řešení, ale jen chcete řešení vytvořit, aby jim
bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu.
{% if edit_points %}
Zde můžete bodovat odevzdaná řešení.
{% else %}
Zde můžete zakládat a rušit odevzdaná řešení.
{% endif %}
Prázdná hodnota ve sloupečku „Přidělené body“ znamená „neobodováno“,
hodnota X znamená „řešení nebylo odevzdáno“,
P znamená „prázdný protokol, který neobsahuje řešení“.
{% if edit_create %}
Smazáním X založíte nové řešení – to se hodí, pokud se nechystáte do systému nahrávat soubory řešení,
ale jen chcete řešení vytvořit, aby jim bylo možné vyplnit body.
Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat přepsáním bodů zpět na X.
{% endif %}
{% else %}
Všechna odevzdání od účastníka k úloze můžete vidět po kliknutí na ikonku <span class="icon">🔍</span>.
Tamtéž můžete nahrávat skeny jednotlivých řešení.
......@@ -54,12 +67,15 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
{% for task in tasks %}<th colspan=4>
<a href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">{{ task.code }}</a>
{% endfor %}
{% if not edit_form %}
<th rowspan=2>Body celkem
{% endif %}
</tr>
<tr>
{% for task in tasks %}<th title="Řešení">Řeš<th title="Oprava">Opr<th title="Body">B<th title="Detail"><span class="icon">🔍</span>{% endfor %}
</tr>
</thead>
{% set tabindex = namespace(value=1) %}
{% for pion in pions %}
{% set u = pion.user %}
<tr class="state-{{ pion.state.name }}">
......@@ -67,8 +83,8 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
<td>{{ pion.state.friendly_name() }}
{% set sum_points = [] %}
{% for task in tasks %}
{% if u.user_id in tasks_sols[task.task_id] %}
{% set sol = tasks_sols[task.task_id][u.user_id] %}
{% set sol = tasks_sols[task.task_id].get(u.user_id) %}
{% if sol %}
{% if sol.final_submit_obj %}
{% set p = sol.final_submit_obj %}
{% set late = p.check_deadline(round) %}
......@@ -92,25 +108,43 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
{% else %}
<td class="sol">
{% endif %}
{% elif edit_form %}
<td>
<td>
{% else %}
<td colspan=3>
{% endif %}
{% if edit_form %}
{% set key = '{}_{}'.format(task.task_id, pion.user_id) %}
{% set def_pts = sol | sol_editable_points %}
{% set new_pts = request_form.get("points_{}".format(key)) %}
{% set err = edit_errors[key] %}
<td><input
type="text" class="form-control{% if err %} has-error{% endif %}" name="points_{{key}}"
value="{{ new_pts|none_value(def_pts) }}"
oninput="this.classList.toggle('modified', this.value != '{{ def_pts }}')"
size="4" tabindex={{ tabindex.value }} autofocus
data-sol="{% if sol %}1{% else %}0{% endif %}"
{% if not edit_create %}
disabled
{% endif %}
{% if err %}
title="{{ err }}"
{% endif %}
>
{% elif sol %}
<td class="sol">
{{ sol|sol_display_points }}
{% if sol.points is not none %}{% do sum_points.append(sol.points) %}{% endif %}
<td class="sol">
{% else %}
<td colspan=3>
{% if edit_form %}
<label>
<input type="checkbox" name="create_sol_{{task.task_id}}_{{u.user_id}}">
Založit
</label>
{% else %}–{% endif %}
<td>
{% endif %}
<a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id) }}">🔍</a>
<td{% if sol %} class="sol"{% endif %}><a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id) }}">🔍</a>
{% endfor %}
{% if not edit_form %}
<th>{{ sum_points|sum|decimal }}</th>
{% endif %}
</tr>
{% endfor %}
{% if not edit_form %}
<tfoot>
<tr><td><td>
{% for task in tasks %}
......@@ -120,15 +154,17 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
<a class='btn btn-xs btn-primary' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát</a>
{% endif %}
{% if edit_points or edit_create %}
<a class='btn btn-xs btn-primary' title="{{'Založit řešení nebo editovat body' if edit_both else 'Editovat body' if edit_points else 'Založit řešení'}}" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}">Editovat</a>
<a class='btn btn-xs btn-primary' title="{{ edit_tip }}" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}">Editovat</a>
{% endif %}
</div>
{% endfor %}
<td>
</tfoot>
{% endif %}
</table>
{% if edit_form %}
{% include "parts/org_solution_js.html" %}
<div class='btn-group'>
{{ wtf.form_field(edit_form.submit, class="btn btn-primary") }}
<a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions') }}">Zrušit</a>
......@@ -137,7 +173,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje
{% else %}
<div class='btn-group'>
{% if rights.can_create_solutions() %}
<a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}">Založit řešení hromadně</a>
<a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}" title="{{ edit_tip }}">Editovat</a>
{% endif %}
</div>
{% endif %}
......
......
......@@ -121,9 +121,10 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol
{% endif %}
<td class="text-center">
{% if edit %}
{% set key = u.user_id | string %}
{% set def_pts = sol | sol_editable_points %}
{% set new_pts = request_form.get("points_{}".format(u.user_id)) %}
{% set err = edit_errors[u.user_id] %}
{% set new_pts = request_form.get("points_{}".format(key)) %}
{% set err = edit_errors[key] %}
<input
type="text" class="form-control{% if err %} has-error{% endif %}" name="points_{{u.user_id}}"
value="{{ new_pts|none_value(def_pts) }}"
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment