From 2fb4aab0162d6a951a810a0a2f1e882c2c3d09fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ji=C5=99=C3=AD=20Setni=C4=8Dka?= <setnicka@seznam.cz>
Date: Sun, 14 Nov 2021 13:57:24 +0100
Subject: [PATCH] =?UTF-8?q?Slou=C4=8Den=C3=AD=20zakl=C3=A1d=C3=A1n=C3=AD?=
 =?UTF-8?q?=20=C5=99e=C5=A1en=C3=AD=20a=20editace=20bod=C5=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Endpointy /org/contest/c/123/task/456/{points,create} sloučeny do .../edit,
obdobně pro soutěžní místa.

Zakládat řešení povolujeme i na úrovni soutěžního místa, zadávat body jen na
úrovni soutěže. Počítá to s právy jen pro zakládání řešení (dozor), jen pro
editování bodů (momentálně nikdo) nebo s oběma (třeba opravovatel).

Issue #251.
---
 mo/web/org_contest.py                         | 97 +++++++++----------
 mo/web/org_score.py                           |  2 +-
 mo/web/templates/org_contest.html             | 16 +--
 mo/web/templates/org_contest_solutions.html   | 17 +++-
 mo/web/templates/org_contest_task.html        | 28 +++---
 .../templates/parts/org_solution_table.html   | 65 ++++++++++---
 static/mo.css                                 |  7 ++
 7 files changed, 144 insertions(+), 88 deletions(-)

diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index e3b3d7ae..9e07c38b 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 2297323b..be705c73 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 26b3f235..e0dbcf11 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 5c902e50..a85a2bf5 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 60b0b2fa..beb9fc79 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 a138bab1..d1e8c5a0 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 301f7170..e8394dfb 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;
+}
-- 
GitLab