Skip to content
Snippets Groups Projects
Commit 1ef925fe authored by Jiří Setnička's avatar Jiří Setnička Committed by Martin Mareš
Browse files

Třídění skenů - doladění UI + ukládání změn třídění do databáze

parent d40dd4d2
No related branches found
No related tags found
No related merge requests found
This commit is part of merge request !94. Comments created here will be created in the context of that merge request.
......@@ -3,13 +3,13 @@ from flask import render_template, g, redirect, url_for, flash, request
from flask.helpers import send_file
from flask_wtf import FlaskForm
import flask_wtf.file
import json
import locale
from markupsafe import Markup
from sqlalchemy import func, and_, select
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.orm.query import Query
from sqlalchemy.dialects.postgresql import insert as pgsql_insert
import sqlalchemy.sql.schema
from typing import Any, List, Tuple, Optional, Dict
import urllib.parse
import werkzeug.exceptions
......@@ -1751,7 +1751,7 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None):
in_file_names=[f.stream.name for f in files],
)
flash('Zpracování skenů zahájeno. Vyčkejte chvíli, než budou připraveny k roztřídění, poté je roztřiďte', 'success')
return redirect(url_for('org_contest_scans_process', id=id, site_id=site_id, job_id=job_id))
return redirect(ctx.url_for('org_contest_scans_process', job_id=job_id))
jobs_query = sess.query(db.Job).filter_by(type=db.JobType.process_scans)
if not g.user.is_admin:
......@@ -1779,6 +1779,12 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None):
)
class ScanProcessForm(FlaskForm):
data = wtforms.HiddenField()
save = wtforms.SubmitField()
process_all = wtforms.SubmitField()
@app.route('/org/contest/c/<int:ct_id>/scans/<int:job_id>', methods=('GET', 'POST', 'PUT'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/scans/<int:job_id>', methods=('GET', 'POST', 'PUT'))
def org_contest_scans_process(ct_id: int, job_id: int, site_id: Optional[int] = None):
......@@ -1804,17 +1810,108 @@ def org_contest_scans_process(ct_id: int, job_id: int, site_id: Optional[int] =
)
if site_id is not None:
pion_query = pion_query.filter_by(place_id=site_id)
pions = pion_query.options(joinedload(db.Participation.user)).all()
pions: List[db.Participation] = pion_query.options(joinedload(db.Participation.user)).all()
# Vytvoříme si mapu stránek načtených z databáze, mapu uživatelů a úloh
p_map: Dict[Tuple[int, int], db.ScanPage] = {(p.file_nr, p.page_nr): p for p in pages}
pion_map: Dict[int, db.Participation] = {pion.user_id: pion for pion in pions}
task_map: Dict[int, db.Task] = {task.task_id: task for task in tasks}
print(pion_map)
# Uložení stránek z GUI
self_url = ctx.url_for('org_contest_scans_process', job_id=job_id)
process_form = ScanProcessForm()
if process_form.validate_on_submit() and process_form.save.data:
data: List = json.loads(process_form.data.data)
if len(data) == 0:
flash('Žádné změny k uložení', 'info')
return redirect(self_url)
count = 0
for p in data:
if 'file_nr' not in p or 'page_nr' not in p:
flash('Schází file_nr a page_nr, nelze zpracovat!', 'danger')
return redirect(self_url)
index = (p['file_nr'], p['page_nr'])
if 'user_id' not in p or 'task_id' not in p or 'seq_id' not in p:
flash(f'Schází user_id, task_id nebo seq_id u skenu {index}, nelze zpracovat!', 'danger')
return redirect(self_url)
user_id = p['user_id']
task_id = p['task_id']
seq_id = p['seq_id']
if seq_id is None:
seq_id = 0
if index not in p_map:
flash(f'Stránka {index} není v databázi, nebyla uložena', 'danger')
continue
if user_id is not None and user_id not in pion_map:
flash(f'Neexistující uživatel {user_id} u stránky {index}', 'danger')
continue
if task_id is not None and task_id not in task_map:
flash(f'Neexistující úloha {task_id} u stránky {index}', 'danger')
continue
if seq_id < -4:
flash(f'Schází číslo stránky u stránky {index}', 'danger')
continue
pp = p_map[index]
if pp.user_id != user_id or pp.task_id != task_id or pp.seq_id != seq_id:
pp.user_id = user_id
pp.task_id = task_id
pp.seq_id = seq_id
sess.add(pp)
count += 1
# neukládáme do databázového logu, zpracování skenů je jen dočasné
# a finální uložení založí nová řešení
sess.commit()
flash('Uloženy změny u ' + inflect_number(count, 'skenu', 'skenů', 'skenů'), 'success')
return redirect(self_url)
# Kontrola stavu
warnings: List[str] = []
errors: List[str] = []
seq_map: Dict[Tuple[int, int, int], db.ScanPage] = {}
sol_map: Dict[Tuple[int, int], bool] = {}
for p in pages:
if not p.is_ok() and not p.is_empty():
errors.append(f'Sken {p.file_nr}/{p.page_nr} není označen jako prázdný ani není správně určen!')
if p.is_ok():
index = (p.task_id, p.user_id, p.seq_id)
if index in seq_map:
pp = seq_map[index]
t = task_map[p.task_id]
u = pion_map[p.user_id].user
errors.append(
f'Duplicita - Sken {p.file_nr}/{p.page_nr} je označený jako {p.seq_id}. stránka řešení'
+f' úlohy {t.code} {t.name} soutěžícího {u.full_name()} ale'
+f' sken {pp.file_nr}/{pp.page_nr} je označený úplně stejně!'
)
else:
seq_map[index] = p
sol_map[(p.task_id, p.user_id)] = True
for task in tasks:
for pion in pions:
index = (task.task_id, pion.user_id)
if index not in sol_map:
warnings.append(f'Mezi skeny není řešení úlohy {task.code} {task.name} od {pion.user.full_name()}')
if process_form.validate_on_submit() and process_form.process_all.data:
if len(errors) > 0:
flash('Nelze zpracovat, dokud kontrola vrací chyby. Nejdříve je opravte.')
return redirect(self_url)
print("PROCESS")
def png_small(page: db.ScanPage) -> str:
return url_for(
'org_contest_scans_file', id=id, job_id=job_id, site_id=site_id,
return ctx.url_for(
'org_contest_scans_file', job_id=job_id,
file=f'p-{page.file_nr:02d}-{page.page_nr:04d}-small.png'
)
def png_full(page: db.ScanPage) -> str:
return url_for(
'org_contest_scans_file', id=id, job_id=job_id, site_id=site_id,
return ctx.url_for(
'org_contest_scans_file', job_id=job_id,
file=f'p-{page.file_nr:02d}-{page.page_nr:04d}-full.png'
)
......@@ -1825,6 +1922,9 @@ def org_contest_scans_process(ct_id: int, job_id: int, site_id: Optional[int] =
tasks=tasks,
pions=pions,
pages=pages,
process_form=process_form,
errors=errors,
warnings=warnings,
png_small=png_small,
png_full=png_full,
)
......
......
......@@ -28,11 +28,45 @@ je nějaké řešení přes více stránek, musí na sebe navazovat číslován
tlačítkem <b>[Uložit]</b>. Poté můžete celou dávku odeslat ke zpracování pomocí <b>[Ukončit a zpracovat]</b> (řešení se uloží k řešitelům,
tuto akci nelze vzít zpět).
<p><b>TODO: Zatím není backend pro tlačítka, ale bude</b>
{% if errors or warnings %}
<div class="collapsible">
{% set error_count = errors | count %}
{% set warning_count = warnings | count %}
<input type="checkbox" class="toggle" id="messages-toggle" {% if errors %}checked{% endif %}>
<label for="messages-toggle" class="toggle">
Kontrola třídění (
{%- if warning_count > 0 %}{{ warning_count }} varování{% endif -%}
{%- if warning_count > 0 and error_count > 0 %}, {% endif %}
{%- if error_count > 0 %}<span class="error">{{ error_count|inflected('chyba', 'chyby', 'chyb') }}</span>{% endif -%}
)
</label>
<div class="collapsible-inner">
<div class="alert alert-warning">
<ul>
{% for msg in warnings %}
<li>Varování: {{ msg }}
{% endfor %}
{% for msg in errors %}
<li class="error"><b>Chyba: {{ msg }}</b>
{% endfor %}
</ul></div>
</div>
</div>
</div>
{% else %}
<p><span class="text-success">Skeny zkontrolovány, nenalezeny žádné chyby ani varování.</span></p>
{% endif %}
<form method="post" class="btn-group pull-right">
<input class="btn btn-primary" type="submit" name="savel" value="Uložit" id="save-button">
<input class="btn btn-primary" type="submit" name="process-all" value="Ukončit a zpracovat" id="process-all-button" onclick="return confirm('Opravud ukončit a zpracovat? Nelze vzít zpět.');">
<input class="btn btn-primary" type="submit" name="save" value="Uložit" id="save-button" onclick="saveData()">
{{ process_form.csrf_token }}
<input type="hidden" name="data" id="save-data">
{% if errors %}
<input class="btn btn-primary" type="submit" value="Ukončit a zpracovat" title="Kontrola ukázala chyby, nejdříve je opravte a uložte pomocí [Uložit]" disabled>
{% else %}
<input class="btn btn-primary" type="submit" name="process_all" value="Ukončit a zpracovat" id="process-all-button" onclick="return confirm('Opravud ukončit a zpracovat? Nelze vzít zpět.');">
{% endif %}
</form>
<style>
......@@ -81,7 +115,8 @@ tuto akci nelze vzít zpět).
cursor: pointer;
}
#sort_wrapper div.pages table tr.ok { background-color: lightgreen; }
#sort_wrapper div.pages table tr.empty { background-color: #ff7777; }
#sort_wrapper div.pages table tr.empty { background-color: #aabbff; }
#sort_wrapper div.pages table tr.error { background-color: #ff7777; }
#sort_wrapper div.pages table tr.changed { background-color: yellow; }
#sort_wrapper div.pages table tr.active {
color: white;
......@@ -90,9 +125,9 @@ tuto akci nelze vzít zpět).
#sort_wrapper div.pages table tr.active.changed { color: yellow; }
</style>
<p><b>Ovládání klávesnicí:</b> <code></code> a <code></code> – posun ve skenech, <code>e</code> – přepnutí se na editaci,
<code>[esc]</code> – vyskočení z editačního políčka při editaci, <code>r</code> – reset (vrácení) změn u konkrétního skenu,
<code>x</code> – nastavit stránku jako prázdnou, <code>f</code> – nastavit stránku jako pokračování minulé.
<p><b>Ovládání klávesnicí:</b> <b><code></code></b> a <b><code></code></b> – posun ve skenech, <b><code>e</code></b> – přepnutí se na editaci,
<b><code>[esc]</code></b> – vyskočení z editačního políčka při editaci, <b><code>r</code></b> – reset (vrácení) změn u konkrétního skenu,
<b><code>x</code></b> – nastavit stránku jako prázdnou, <b><code>f</code></b> – nastavit stránku jako pokračování minulé.
<div id="sort_wrapper">
<div class="controls form-horizontal">
......@@ -177,16 +212,46 @@ var user_input = document.getElementById('user_input');
var task_input = document.getElementById('task_input');
var seq_input = document.getElementById('seq_input');
var process_all_button = document.getElementById('process-all-button');
var save_data_field = document.getElementById('save-data');
PAGE_FIX = -1;
PAGE_EMPTY = -2;
PAGE_CONTINUE = -3;
PAGE_UFO = -4;
function isChanged(p) { return (p.orig_user_id != p.user_id || p.orig_task_id != p.task_id || p.orig_seq_id != p.seq_id); }
function isOk(p) { return (p.user_id && p.task_id); }
function isEmpty(p) { return (p.user_id == null && p.task_id == null); }
function isOk(p) { return (p.user_id && p.task_id && p.seq_id >= 0); }
function isEmpty(p) { return (p.user_id == null && p.task_id == null && p.seq_id == PAGE_EMPTY); }
function isError(p) { return (!isEmpty(p) && !isOk(p)); }
function anyChanged(p) { return pages.some(isChanged); }
function beforeUnload(e) {
e.preventDefault();
return e.returnValue = "Byly provedeny editace, opuštěním stránky je ztratíte. Skutečně opustit stránku?";
}
function refreshButtons() {
if (anyChanged()) {
if (process_all_button) {
process_all_button.disabled = true;
process_all_button.title = "Neuložené změny, nejdříve je uložte vedlejším tlačítkem";
}
window.onbeforeunload = beforeUnload;
} else {
if (process_all_button) {
process_all_button.disabled = false;
process_all_button.title = '';
}
window.onbeforeunload = null;
}
}
function saveData() {
data = pages.filter(isChanged).map(p => ({file_nr: p.file_nr, page_nr: p.page_nr, user_id: p.user_id, task_id: p.task_id, seq_id: p.seq_id}));
save_data_field.value = JSON.stringify(data);
window.onbeforeunload = null;
}
function setRow(i) {
p = pages[i];
row = rows[i];
......@@ -208,7 +273,18 @@ function setRow(i) {
user = users_map[p.user_id];
cellUser.innerHTML = user.name;
}
switch (p.seq_id) {
case PAGE_EMPTY:
cellSeq.innerHTML = '';
break
case PAGE_FIX:
case PAGE_CONTINUE:
case PAGE_UFO:
cellSeq.innerHTML = '?';
break;
default:
cellSeq.innerHTML = p.seq_id;
}
if (isOk(p)) {
row.classList.add('ok');
......@@ -217,13 +293,16 @@ function setRow(i) {
row.classList.add('empty');
row.title += "Prázdná stránka, nebude se ukládat\n";
}
if (isError(p)) {
row.classList.add('error');
row.title += "Neprázdná nerozpoznaná stránka, potřebuje opravit\n";
}
if (isChanged(p)) {
process_all_button.disabled = true;
window.onbeforeunload = beforeUnload;
row.classList.add('changed');
row.title += "Neuložené změny\n";
}
refreshButtons();
}
var activeRow = 0;
......@@ -252,8 +331,11 @@ function selectRow(i) {
} else {
task_input.value = '';
}
if (page.seq_id >= 0) {
seq_input.value = page.seq_id;
} else {
seq_input.value = '';
}
}
img.src = page.img_full;
}
......@@ -265,21 +347,24 @@ function refreshActiveRow() {
// Activate autocomplete on two fields + prevent catching keys on third one
autocomplete(task_input, tasks_autocomplete, callback=function (id) {
pages[activeRow].task_id = id;
pages[activeRow].task_id = parseInt(id);
refreshActiveRow();
});
autocomplete(user_input, users_autocomplete, callback=function (id) {
pages[activeRow].user_id = id;
pages[activeRow].user_id = parseInt(id);
refreshActiveRow();
});
seq_input.addEventListener("focus", closeAllAutocomplete);
seq_input.addEventListener("keydown", function(e) {
if (e.keyCode == 27) { // escape
this.blur();
}
e.stopPropagation();
});
seq_input.addEventListener("input", function(e) {
pages[activeRow].seq_id = seq_input.value;
seq_input.addEventListener("blur", function(e) {
x = parseInt(seq_input.value);
if (Number.isNaN(x)) x = 0
pages[activeRow].seq_id = x;
refreshActiveRow();
});
......@@ -289,10 +374,12 @@ function checkKey(e) {
if (e.keyCode == '38') {
if (activeRow > 0) {
selectRow(activeRow - 1);
rows[activeRow].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else if (e.keyCode == '40') {
if (activeRow < pages.length - 1) {
selectRow(activeRow + 1);
rows[activeRow].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else if (e.key == 'e') {
user_input.focus();
......@@ -300,7 +387,7 @@ function checkKey(e) {
page = pages[activeRow];
page.user_id = null;
page.task_id = null;
page.seq_id = null;
page.seq_id = PAGE_EMPTY;
refreshActiveRow();
} else if (e.key == 'r') {
page = pages[activeRow];
......@@ -331,7 +418,7 @@ document.onkeydown = checkKey;
// Start everything :)
for (task of tasks) {
tasks_map[task.id] = task; console.log(task);
tasks_map[task.id] = task;
tasks_autocomplete.push([task.id, task.code + ' ' + task.name]);
}
for (user of users) {
......
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment