diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index e3b3d7aecc9461bb6d9440609bc0269b5c6104b5..9e07c38b5ab73e7ad1290ab1d4e6cd6955498064 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -25,7 +25,7 @@ from mo.imports import ImportType, create_import import mo.jobs.submit from mo.rights import Right, Rights, 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 import mo.web.util @@ -991,28 +991,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() @@ -1021,50 +1013,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') @@ -1081,11 +1074,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')) @@ -1105,7 +1102,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 2297323b5d785ac615cea1f3c6a101d6b9961466..be705c733e2683345aaf9fee7fa1ebfc2f4378cd 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -201,7 +201,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_ ) if ctx.rights.can_edit_points(): title += ' <a href="{}" title="Editovat body" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span></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 26b3f235997625386874f0b5e24d2cfafce7c936..e0dbcf11020ae33710ae9810783826477bd286c1 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 offer_view_statement, offer_view_statement_reason = rights.offer_view_statement() %} {% set can_view_contestants = rights.have_right(Right.view_contestants) %} @@ -213,20 +213,20 @@ <td>{{ task.sol_count }} <td>{{ task.max_points|decimal|none_value('–') }} <td> - {% if can_view_submits %} + {% if can_view_submits %} <a class="btn btn-xs btn-success" href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">Odevzdaná řešení</a> - {% endif %} - {% 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_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 not can_edit_points %}Založit řešení{% else %}Zadat body{% endif %}</a> {% endif %} <td> - {% if can_view_submits %} + {% if can_view_submits %} <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_generic_batch_download', task_id=task.task_id) }}">Stáhnout ZIP</a> - {% endif %} + {% endif %} {% 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 %} </tr> diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index 5c902e50e3d9b4b9a4c120ccc4c57536fc55f258..a85a2bf5d64b31bc0a9ae56f434455858be61daa 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í" }} kategorie {{ round.category }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} {% endblock %} @@ -49,8 +53,11 @@ Odkazem v záhlaví se lze dostat na podrobný výpis odevzdání všech účast <th rowspan=2>Stav účasti</th> {% for task in tasks[4*i:4*(i+1)] %}<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 class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}"><span class="glyphicon glyphicon-pencil"></span> Zadat body</a> + {% if edit_points or edit_create %} + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}"> + <span class="glyphicon glyphicon-pencil"></span> + {% if edit_points %}Zadat body{% else %}Založit řešení{% endif %} + </a> {% endif %} {% endfor %} <th rowspan=2>Body celkem @@ -124,8 +131,8 @@ Odkazem v záhlaví se lze dostat na podrobný výpis odevzdání všech účast <a class='btn btn-xs btn-default' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}" title="Nahrát ZIP opravených řešení úlohy {{ task.code }}"><span class="glyphicon glyphicon-cloud-upload"></span></a> {% endif %} <td> - {% if rights.can_edit_points() %} - <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}" title="Editovat body k úloze {{ task.code }}"><span class="glyphicon glyphicon-pencil"></span></a> + {% if edit_points or edit_create %} + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_edit', task_id=task.task_id) }}" title="{% if edit_points %}Zadat body{% else %}Založit řešení{% endif %} k úloze {{ task.code }}"><span class="glyphicon glyphicon-pencil"></span></a> {% endif %} <td> <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}" title="Podrobný výpis odevzdaných řešení k úloze {{ task.code }}"><span class="glyphicon glyphicon-search"></span></a> @@ -144,7 +151,7 @@ Odkazem v záhlaví se lze dostat na podrobný výpis odevzdání všech účast {% else %} <div> {% 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 60b0b2fafae97673f3bd72457990ae7b02d459fc..beb9fc79c444d8883f3b0721bfaf8a504401b4e0 100644 --- a/mo/web/templates/org_contest_task.html +++ b/mo/web/templates/org_contest_task.html @@ -1,13 +1,13 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% set contest=ctx.contest %} -{% set allow_edit_points=rights.can_edit_points() %} -{% set allow_upload_solutions=rights.can_upload_solutions() %} -{% set allow_upload_feedback=rights.can_upload_feedback() %} +{% set edit_points = not ctx.site and rights.can_edit_points() %} +{% set edit_create = rights.can_create_solutions() %} +{% set edit_title = "Zadávání bodů" if edit_points else "Zakládání řešení" %} -{% 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 }}<br><small>{{ ctx.task.name }}</small>{% endblock %} +{% block title %}{{ edit_title if edit_form else "Odevzdaná řešení" }} úlohy {{ ctx.task.code }}<br><small>{{ ctx.task.name }}</small>{% 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 %} @@ -29,31 +29,33 @@ {% 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> - {{ 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 %} <p> <a class='btn btn-primary' href="{{ ctx.url_for('org_generic_batch_download') }}"><span class="glyphicon glyphicon-cloud-download"></span> 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') }}"><span class="glyphicon glyphicon-cloud-upload"></span> Nahrát ZIP opravených ř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') }}"><span class="glyphicon glyphicon-pencil"></span> Zadat body</a> + {% if edit_points or edit_create %} + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_edit') }}"> + <span class="glyphicon glyphicon-pencil"></span> + {% if edit_points %}Zadat body{% else %}Založit řešení{% endif %} + </a> {% endif %} </p> {% endif %} diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index a138bab10d4a385b04075e95222ee8bc62371134..d1e8c5a0deea29bb8d5d5924e40e3b80b46fc5a5 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 @@ -21,8 +33,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 class="btn btn-xs btn-default" title="Editovat body" href="{{ ctx.url_for('org_contest_task_points') }}"><span class="glyphicon glyphicon-pencil"></span></a> + {% if not for_user and edit_points and not edit_form %} + <a class="btn btn-xs btn-default" title="Editovat body" href="{{ ctx.url_for('org_contest_task_edit') }}"><span class="glyphicon glyphicon-pencil"></span></a> {% endif %} <th>Akce </tr> @@ -82,24 +94,42 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol {% if sol.note %}<span class="glyphicon glyphicon-comment" title="Poznámka pro řešitele: {{ sol.note }}"></span>{% endif %} {% if sol.org_note %} <span class="glyphicon glyphicon-comment" 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_nolocale) }}" + value="{{ request_form.get("points_{}".format(u.user_id))|none_value(sol.points|decimal_nolocale|none_value("")) }}" + oninput="this.classList.toggle('modified', this.value != '{{sol.points|decimal_nolocale|none_value("")}}')" size="4" tabindex={{ tabindex.value }} > + {% 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 }}> + {% 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 }} + > {% 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 xxx" 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 }} + > + {% 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> @@ -109,6 +139,19 @@ finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_sol </table> </div> +{% if not edit_form %} <p><i>Legenda: <span class='text-danger icon'>⚠</span> odevzdané po termínu, <span class="icon">🛈</span> nahráno někým jiným, než řešitelem, <span class="icon">🗐</span> existuje více verzí. Symboly po najetí myší zobrazí bližší informace. </i></p> +{% endif %} + +<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 301f71701c82cd47e946befe2d2845deeeb68da1..e8394dfb513908043d272da6a7de56fe807b98a0 100644 --- a/static/mo.css +++ b/static/mo.css @@ -348,3 +348,10 @@ div.message .msg-date { font-style: italic; color: #777; } + +/* Input classes */ + +input.modified { + background-color: rgb(255, 222, 152); + border-color: orange; +}