diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index ba4c0e614dbe3ce46ee96ef46d26b6ff2b3017c8..5459ba280cd6eab754599ae8c6087cce92a9d576 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -25,7 +25,7 @@ import mo.jobs.protocols import mo.jobs.submit from mo.rights import Right, RoundRights import mo.util -from mo.util_format import inflect_number, inflect_by_number +from mo.util_format import inflect_number, inflect_by_number, inflect_with_number from mo.web import app import mo.web.fields as mo_fields from mo.web.imports import ImportForm, generic_import_page @@ -934,28 +934,20 @@ def get_solutions_query( return query -class TaskPointsForm(FlaskForm): - submit = wtforms.SubmitField("Uložit body") - - -class TaskCreateForm(FlaskForm): - submit = wtforms.SubmitField("Založit označená řešení") +class TaskPointsEditForm(FlaskForm): + submit = wtforms.SubmitField("Uložit") @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>/task/<int:task_id>/points', methods=('GET', 'POST'), endpoint="org_contest_task_points") -@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") -@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") +@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_create = request.endpoint == "org_contest_task_create" - action_points = request.endpoint == "org_contest_task_points" - if action_create and not ctx.rights.can_create_solutions(): - raise werkzeug.exceptions.Forbidden() - if action_points and not ctx.rights.can_edit_points(): + action_edit = request.endpoint == "org_contest_task_edit" + if action_edit and not ctx.rights.can_edit_points() and not ctx.rights.can_create_solutions(): raise werkzeug.exceptions.Forbidden() sess = db.get_session() @@ -964,50 +956,51 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): rows: List[Tuple[db.Participation, db.Solution]] = q.all() rows.sort(key=lambda r: r[0].user.sort_key()) - points_form: Optional[TaskPointsForm] = None - create_form: Optional[TaskCreateForm] = None + edit_form: Optional[TaskPointsEditForm] = None - if action_create: - create_form = TaskCreateForm() - if create_form.validate_on_submit(): - new_sol_count = 0 + if action_edit: + edit_form = TaskPointsEditForm() + if edit_form.validate_on_submit(): + # Založení řešení: + new_sols: List[db.Solution] = [] + all_sols: List[db.Solution] = [] for pion, sol in rows: if sol: + all_sols.append(sol) continue # již existuje if not request.form.get(f"create_sol_{pion.user_id}"): continue # nikdo nežádá o vytvoření - sol = db.Solution(task=ctx.task, user=pion.user) - sess.add(sol) - mo.util.log( - type=db.LogType.participant, - what=pion.user_id, - details={ - 'action': 'solution-created', - 'task': task_id, - }, - ) - app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {pion.user_id} založeno") - new_sol_count += 1 + new_sols.append(sol) + all_sols.append(sol) - if new_sol_count > 0: + if len(new_sols) > 0 and not ctx.rights.can_create_solutions(): + flash("Nemáte právo k zakládání nových řešení, můžete jen editovat body", "danger") + return redirect(ctx.url_for('org_contest_task')) + elif len(new_sols) > 0: + for sol in new_sols: + sess.add(sol) + mo.util.log( + type=db.LogType.participant, + what=pion.user_id, + details={ + 'action': 'solution-created', + 'task': task_id, + }, + ) + app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {pion.user_id} založeno") 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") - return redirect(ctx.url_for('org_contest_task')) + flash(inflect_with_number(len(new_sols), "Založeno %s nové řešení", "Založena %s nová řešení", "Založeno %s nových řešení"), "success") - if action_points: - points_form = TaskPointsForm() - if points_form.validate_on_submit(): + # Zadání bodů: + if site_id is not None: + # Body lze zadat jen pro celý contest, ne pro site + if len(new_sols) == 0: + flash("Žádné změny k uložení", "info") + return redirect(ctx.url_for('org_contest_task')) count = 0 ok = True - for _, sol in rows: - if sol is None: - continue - + for sol in all_sols: points, error = mo.util.parse_points(request.form.get(f"points_{sol.user_id}"), for_task=ctx.task, for_round=ctx.round) if error: flash(f'{sol.user.first_name} {sol.user.last_name}: {error}', 'danger') @@ -1024,11 +1017,15 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): points=points, )) count += 1 - if ok: + + if count > 0 and not ctx.rights.can_edit_points(): + flash("Nemáte právo k zadávání bodů, můžete jen zakládat řešení", "danger") + return redirect(ctx.url_for('org_contest_task')) + elif ok: if count > 0: sess.commit() flash("Změněny body u " + inflect_number(count, "řešení", "řešení", "řešení"), "success") - else: + elif len(new_sols) == 0: flash("Žádné změny k uložení", "info") return redirect(ctx.url_for('org_contest_task')) @@ -1048,7 +1045,7 @@ def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): round=ctx.round, contest=ctx.contest, rows=rows, paper_counts=paper_counts, paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p), - points_form=points_form, create_form=create_form, request_form=request.form, + edit_form=edit_form, request_form=request.form, ) diff --git a/mo/web/org_score.py b/mo/web/org_score.py index 2fb1585df69b07898b70181329cb231df8212298..a8e4ca012dacfd6bce5664c010e08b4aa438c67d 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -134,7 +134,7 @@ def org_score(round_id: Optional[int] = None, ct_id: Optional[int] = None): ) if ctx.rights.can_edit_points(): title += ' <a href="{}" title="Editovat body" class="icon">✎</a>'.format( - url_for('org_contest_task_points', ct_id=local_ct_id, task_id=task.task_id), + url_for('org_contest_task_edit', ct_id=local_ct_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')) diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index d27f3268314a358d8d156efaf4816adef354bc7f..3e472539c4426bfe7778b9c2eb87f44d55d2f53c 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -4,7 +4,7 @@ {% set ct_state = contest.ct_state() %} {% set can_manage = rights.have_right(Right.manage_contest) %} {% set can_upload = rights.can_upload_feedback() %} -{% set can_edit_points = rights.can_edit_points() %} +{% set can_edit_points = not site and rights.can_edit_points() %} {% set can_create_solutions = rights.can_upload_feedback() or rights.can_upload_solutions() %} {% set can_view_statement = rights.can_view_statement() %} @@ -140,11 +140,11 @@ <td>{{ task.max_points|decimal|none_value('–') }} <td><div class="btn-group"> <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">Odevzdaná řešení</a> - {% if not site and can_edit_points %} - <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}">Zadat body</a> - {% endif %} - {% if can_create_solutions %} - <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_create', task_id=task.task_id) }}">Založit řešení</a> + {% if can_edit_points or can_create_solutions %} + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}"> + {% if can_edit_points and can_create_solutions %}Zadat body / založit řešení + {%- elif can_create_solutions %}Založit řešení{% else %}Zadat body{% endif %} + </a> {% endif %} </div> <td><div class="btn-group"> @@ -152,7 +152,7 @@ {% if can_upload %} <a class='btn btn-xs btn-default' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát ZIP</a> {% endif %} - {% if not site and can_edit_points %} + {% if can_edit_points %} <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_generic_batch_points', task_id=task.task_id) }}">Nahrát body</a> {% endif %} </div> diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index 20bbd1ac58d7511eff040d90e3335d7327df6967..1a6260d7e54435c6190deb19dfbf1ac4c9a15274 100644 --- a/mo/web/templates/org_contest_solutions.html +++ b/mo/web/templates/org_contest_solutions.html @@ -2,6 +2,10 @@ {% import "bootstrap/wtf.html" as wtf %} {% set round = contest.round %} +{% 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 %} + {% 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 %} {% endblock %} @@ -48,9 +52,6 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje <th rowspan=2>Stav účasti</th> {% for task in tasks %}<th colspan=4> <a href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">{{ task.code }}</a> - {% if rights.can_edit_points() %} - <a title="Editovat body" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}" class="icon pull-right">✎</a> - {% endif %} {% endfor %} <th rowspan=2>Body celkem </tr> @@ -121,6 +122,10 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% if rights.can_upload_feedback() %} <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> + {% endif %} + </div> {% endfor %} <td> @@ -135,7 +140,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í</a> + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}">Založit řešení hromadně</a> {% endif %} </div> {% endif %} diff --git a/mo/web/templates/org_contest_task.html b/mo/web/templates/org_contest_task.html index 96fc3f4c974a7939d619b9713194257aa161bfc2..250f095c5114b3c4408fac7d2e7b991c961bf1ee 100644 --- a/mo/web/templates/org_contest_task.html +++ b/mo/web/templates/org_contest_task.html @@ -1,12 +1,14 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% set allow_edit_points=rights.can_edit_points() %} -{% set allow_upload_solutions=rights.can_upload_solutions() %} -{% set allow_upload_feedback=rights.can_upload_feedback() %} -{% block title %}{{ "Zadávání bodů" if points_form else "Založení řešení" if create_form else "Odevzdaná řešení" }} úlohy {{ ctx.task.code }} {{ ctx.task.name }}{% endblock %} +{% 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_title = "Zadávání bodů a zakládání řešení" if edit_both else "Zadávání bodů" if edit_points else "Zakládání řešení" %} + +{% block title %}{{ edit_title if edit_form else "Odevzdaná řešení" }} úlohy {{ ctx.task.code }} {{ ctx.task.name }}{% endblock %} {% block breadcrumbs %} -{{ ctx.breadcrumbs(action="Zadávání bodů" if points_form else "Založení řešení" if create_form else None) }} +{{ ctx.breadcrumbs(action=edit_title if edit_form else None) }} {% endblock %} {% block pretitle %} @@ -21,31 +23,29 @@ {% block body %} {% include "parts/org_submit_warning.html" %} -{% set form = points_form or create_form %} -{% if form %} +{% if edit_form %} <form class="form" method="POST"> -{{ form.csrf_token }} +{{ edit_form.csrf_token }} {% endif %} {% with for_user=None, for_task=ctx.task, rows=rows %} {% include "parts/org_solution_table.html" %} {% endwith %} -{% if form %} +{% if edit_form %} <div class='btn-group'> - {{ wtf.form_field(form.submit, class="btn btn-primary" ) }} + {{ wtf.form_field(edit_form.submit, class="btn btn-primary" ) }} <a class="btn btn-default" href="{{ ctx.url_for('org_contest_task') }}">Zrušit</a> </div> </form> {% else %} <div class='btn-group'> <a class='btn btn-primary' href="{{ ctx.url_for('org_generic_batch_download') }}">Stáhnout řešení</a> - {% if allow_upload_feedback %} + {% if rights.can_upload_feedback() %} <a class='btn btn-primary' href="{{ ctx.url_for('org_generic_batch_upload') }}">Nahrát opravená řešení</a> {% endif %} - {% if allow_create_solutions %} - <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_create') }}">Založit řešení</a> - {% endif %} - {% if not ctx.site and allow_edit_points %} - <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_points') }}">Zadat body</a> + {% if edit_points or edit_create %} + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_edit') }}"> + {% if edit_both %}Zadat body / založit řešení{% elif edit_create %}Založit řešení{% else %}Zadat body{% endif %} + </a> {% endif %} </div> {% endif %} diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index 47f2affc9a603f953cb1de53be9e5968a83325f3..a44677b2d5cbf4c9abb505bba271f8f1447db4a5 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -1,8 +1,20 @@ +{% 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 %} + <p><i> -{% if create_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. +{% if edit_form %} +{% if edit_points %} +Změňte body ve sloupečku "Přidělené body". Prázdná hodnota znamená "nebodováno". +{% endif %} +{% if edit_create %} +Zaškrtnutím políček u řešení, která dosud neexistují, 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. +{% endif %} +</i></p> +<p><i> +Změny uložíte tlačítkem na spodku tabulky. {% else %} Historii všech odevzdání, oprav a bodů pro každé řešení naleznete v jeho detailu. {% if rights.can_upload_feedback() or rights.can_edit_points() %}Tamtéž můžete odevzdávat nové verze a změnit, které řešení/oprava je @@ -24,8 +36,8 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol <th>Finální oprava <th>Poznámky <th>Přidělené body - {% if not for_user and not site and rights.can_edit_points() and not points_form %} - <a title="Editovat body" href="{{ ctx.url_for('org_contest_task_points') }}" class="icon pull-right">✎</a> + {% if not for_user and edit_points and not edit_form %} + <a title="Editovat body" href="{{ ctx.url_for('org_contest_task_edit') }}" class="icon pull-right">✎</a> {% endif %} <th>Akce </tr> @@ -85,24 +97,42 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol {% if sol.note %}<span class="icon" title="Poznámka pro řešitele: {{ sol.note }}">🗩</span>{% endif %} {% if sol.org_note %} <span class="icon" title="Interní poznámka: {{ sol.org_note }}">🗩</span>{% endif %} <td> - {% if points_form %} + {% if edit_points and edit_form %} <input type="number" class="form-control" name="points_{{u.user_id}}" min=0 {% if task.max_points is not none %}max={{ task.max_points }}{% endif %} step="{{ round.points_step }}" value="{{ request_form.get("points_{}".format(u.user_id))|none_value(sol.points|decimal) }}" + oninput="this.classList.toggle('modified', this.value != '{{sol.points|decimal}}')" size="4" tabindex={{ tabindex.value }} autofocus > + {% set tabindex.value = tabindex.value + 1%} {% else %} {{ sol.points|decimal|none_value(Markup('<span class="unknown">?</span>')) }} {% endif %} - {% else %} - <td colspan="4" class="text-center"> - {% if create_form %} - <input type="checkbox" name="create_sol_{{u.user_id}}" id="create_sol_{{u.user_id}}"{% if request_form.get("create_sol_{}".format(u.user_id)) %}checked{% endif %} tabindex={{ tabindex.value }} autofocus> + {% elif edit_create and edit_form %} + <td colspan="{{ 3 if edit_points else 4 }}" class="text-center"> + <input + type="checkbox" name="create_sol_{{u.user_id}}" id="create_sol_{{u.user_id}}" + onchange="document.getElementById('points_{{u.user_id}}').disabled = !this.checked" + {% if request_form.get("create_sol_{}".format(u.user_id)) %}checked{% endif %} + tabindex={{ tabindex.value }} autofocus + > {% set tabindex.value = tabindex.value + 1%} <label for="create_sol_{{u.user_id}}">Založit řešení</label> - {% else %}–{% endif %} + {% if edit_points %} + <td><input + type="number" class="form-control" name="points_{{u.user_id}}" id="points_{{u.user_id}}" + min=0 {% if task.max_points is not none %}max={{ task.max_points }}{% endif %} + step="{{ round.points_step }}" + value="{{ request_form.get("points_{}".format(u.user_id))|none_value('') }}" + oninput="this.classList.toggle('modified', this.value != '')" + size="4" tabindex={{ tabindex.value }} autofocus + > + {% set tabindex.value = tabindex.value + 1%} + {% endif %} + {% else %} + <td colspan="4" class="text-center">– {% endif %} <td><div class="btn-group"> <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id, site_id=ctx.site_id) }}">Detail</a> @@ -110,3 +140,14 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol </tr> {% endfor %} </table> + +<script type="text/javascript"> + window.addEventListener("load", function() { + for (const ch of document.querySelectorAll("input[type=checkbox")) { + ch.onchange(); + } + for (const i of document.querySelectorAll("input[type=number]")) { + i.oninput(); + } + }); +</script> diff --git a/static/mo.css b/static/mo.css index 6f0c280b5d57ceb07ff4fddf10e60408a409c26a..7f1d5fec2eaa30ec829c9961512fb83402b6be1d 100644 --- a/static/mo.css +++ b/static/mo.css @@ -448,3 +448,10 @@ div.message .msg-date { background-color: DodgerBlue !important; color: #ffffff; } + +/* Input classes */ + +input.modified { + background-color: rgb(255, 222, 152); + border-color: orange; +}