From 42e12f74a77ab04e02d8067d66ff6d75587726af Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Thu, 18 Feb 2021 23:44:57 +0100
Subject: [PATCH] =?UTF-8?q?UI=20pro=20d=C3=A1vkov=C3=A9=20bodov=C3=A1n?=
 =?UTF-8?q?=C3=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 mo/web/org_contest.py                         | 64 +++++++++++++++++++
 mo/web/org_round.py                           | 31 +++++----
 .../templates/org_generic_batch_points.html   | 26 ++++++++
 3 files changed, 107 insertions(+), 14 deletions(-)
 create mode 100644 mo/web/templates/org_generic_batch_points.html

diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index e9fd7c04..839b3435 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -615,6 +615,7 @@ def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optio
         user=user,
         task=task,
         site=site,
+        # XXX: Potřebujeme tohle všechno? Nechceme spíš vracet rr a nechat každého, ať na něm volá metody?
         allow_view=allow_view,
         allow_upload_solutions=rr.can_upload_solutions(round),
         allow_upload_feedback=rr.can_upload_feedback(round),
@@ -1153,6 +1154,69 @@ def org_contest_task_upload(contest_id: int, task_id: int, site_id: Optional[int
                                 can_upload_feedback=sc.allow_upload_feedback)
 
 
+class BatchPointsForm(FlaskForm):
+    file = flask_wtf.file.FileField("Soubor")
+    fmt = wtforms.SelectField(
+        "Formát souboru",
+        choices=FileFormat.choices(), coerce=FileFormat.coerce,
+        default=FileFormat.cs_csv,
+    )
+    submit = wtforms.SubmitField('Nahrát body')
+    get_template = wtforms.SubmitField('Stáhnout šablonu')
+
+
+def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: db.Task):
+    """Společná funkce pro download/upload bodů do soutěží a kol."""
+
+    form = BatchPointsForm()
+    errs = []
+    if form.validate_on_submit():
+        fmt = form.fmt.data
+        imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task)
+        if form.submit.data:
+            if form.file.data is not None:
+                file = form.file.data.stream
+                import_tmp = mo.util.link_to_dir(file.name, mo.util.data_dir('imports'), suffix='.csv')
+
+                if imp.run(import_tmp):
+                    if imp.cnt_rows == 0:
+                        flash('Soubor neobsahoval žádné řádky s daty', 'danger')
+                    else:
+                        flash(f'Importováno ({imp.cnt_rows} řádků, změněny body u {imp.cnt_set_points} řešení)', 'success')
+                        if contest is not None:
+                            return redirect(url_for('org_contest', id=contest.contest_id))
+                        else:
+                            return redirect(url_for('org_round', id=round.round_id))
+                else:
+                    errs = imp.errors
+            else:
+                flash('Vyberte si prosím soubor', 'danger')
+        elif form.get_template.data:
+            out = imp.get_template()
+            resp = app.make_response(out)
+            resp.content_type = fmt.get_content_type()
+            resp.headers.add('Content-Disposition', 'attachment; filename=OSMO-' + imp.template_basename + '.' + fmt.get_extension())
+            return resp
+
+    return render_template(
+        'org_generic_batch_points.html',
+        round=round, contest=contest, task=task,
+        form=form,
+        errs=errs,
+    )
+
+
+@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
+def org_contest_task_batch_points(contest_id: int, task_id: int):
+    sc = get_solution_context(contest_id, None, task_id, None)
+    assert sc.task is not None
+
+    if not sc.allow_edit_points:
+        raise werkzeug.exceptions.Forbidden()
+
+    return generic_batch_points(round=sc.round, contest=sc.contest, task=sc.task)
+
+
 @app.route('/org/contest/c/<int:contest_id>/user/<int:user_id>')
 def org_contest_user(contest_id: int, user_id: int):
     sc = get_solution_context(contest_id, user_id, None, None)
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index 63b268b2..7e86f508 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -1,4 +1,3 @@
-import datetime
 from flask import render_template, g, redirect, url_for, flash, request
 import locale
 from flask_wtf.form import FlaskForm
@@ -17,7 +16,7 @@ from mo.rights import Right, Rights
 import mo.util
 from mo.web import app
 from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, get_contestants_query, make_contestant_table, \
-    generic_import, generic_batch_download, generic_batch_upload
+    generic_import, generic_batch_download, generic_batch_upload, generic_batch_points
 
 
 def get_round(id: int) -> db.Round:
@@ -38,6 +37,13 @@ def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tup
     return round, rr
 
 
+def get_task(round: db.Round, task_id: int) -> db.Task:
+    task = db.get_session().query(db.Task).get(task_id)
+    if not task or task.round_id != round.round_id:
+        raise werkzeug.exceptions.NotFound()
+    return task
+
+
 @app.route('/org/contest/')
 def org_rounds():
     sess = db.get_session()
@@ -271,30 +277,27 @@ def org_round_task_edit(id: int, task_id: int):
 
 @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
 def org_round_task_download(round_id: int, task_id: int):
-    sess = db.get_session()
     round, rr = get_round_rr(round_id, Right.view_submits, False)
-
-    task = sess.query(db.Task).get(task_id)
-    if not task or task.round_id != round_id:
-        raise werkzeug.exceptions.NotFound()
-
+    task = get_task(round, task_id)
     return generic_batch_download(round=round, contest=None, site=None, task=task)
 
 
 @app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
 def org_round_task_upload(round_id: int, task_id: int):
-    sess = db.get_session()
     round, rr = get_round_rr(round_id, Right.view_submits, False)
-
-    task = sess.query(db.Task).get(task_id)
-    if not task or task.round_id != round_id:
-        raise werkzeug.exceptions.NotFound()
-
+    task = get_task(round, task_id)
     return generic_batch_upload(round=round, contest=None, site=None, task=task,
                                 can_upload_solutions=rr.can_upload_solutions(round),
                                 can_upload_feedback=rr.can_upload_feedback(round))
 
 
+@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
+def org_round_task_batch_points(round_id: int, task_id: int):
+    round, rr = get_round_rr(round_id, Right.edit_points, True)
+    task = get_task(round, task_id)
+    return generic_batch_points(round=round, contest=None, task=task)
+
+
 @app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
 def org_round_list(id: int):
     round, rr = get_round_rr(id, Right.manage_contest, True)
diff --git a/mo/web/templates/org_generic_batch_points.html b/mo/web/templates/org_generic_batch_points.html
new file mode 100644
index 00000000..26246028
--- /dev/null
+++ b/mo/web/templates/org_generic_batch_points.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Dávkové bodování úlohy {{ task.code }} {{ task.name }}{% endblock %}
+{% block breadcrumbs %}
+{{ contest_breadcrumbs(round=round, contest=contest, task=task, action="Dávkové bodování") }}
+{% endblock %}
+
+{% block body %}
+
+{% if errs %}
+<h3>Chyby při importu</h3>
+
+<pre><div class="alert alert-danger" role="alert">{{ "" -}}
+{% for e in errs %}
+{{ e }}
+{% endfor %}
+</div></pre>
+{% endif %}
+
+<p>Zde si můžete stáhnout bodovací formulář v zadaném formátu a pak ho nahrát zpět
+s vyplněnými body.
+
+{{ wtf.quick_form(form, form_type='simple', button_map={'submit': 'primary'}) }}
+
+{% endblock %}
-- 
GitLab