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

Frontend pro třídění skenů

Frontendové javascriptové ovládání skoro celé hotové,
backend bude následovat v dalším commitu.
parent 443b5823
No related branches found
No related tags found
No related merge requests found
import os
from flask import render_template, g, redirect, url_for, flash, request from flask import render_template, g, redirect, url_for, flash, request
from flask.helpers import send_file
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import flask_wtf.file import flask_wtf.file
import locale import locale
...@@ -1775,3 +1777,78 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None): ...@@ -1775,3 +1777,78 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None):
proc_task_fields=proc_task_fields, proc_task_fields=proc_task_fields,
jobs=jobs, jobs=jobs,
) )
@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):
ctx = get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.upload_solutions)
contest = ctx.contest
assert contest
sess = db.get_session()
# Získáme job a zkontrolujeme, že je to správný job a máme na něj práva
job = sess.query(db.Job).get(job_id)
if not job or job.type != db.JobType.process_scans:
flash('Dávka naskenovaných úloh nenalezena, skeny již byly pravděpodobně zpracovány nebo smazány.')
return redirect(url_for('org_contest_scans', id=id, site_id=site_id))
if not g.user.is_admin and g.user.user_id != job.user_id:
raise werkzeug.exceptions.Forbidden()
pages = sess.query(db.ScanPage).filter_by(job_id=job_id).order_by('file_nr', 'page_nr').all()
tasks = sess.query(db.Task).filter(db.Task.task_id.in_(job.in_json['task_ids'])).order_by('code').all()
pion_query = sess.query(db.Participation).filter(
db.Participation.contest == contest,
db.Participation.state == db.PartState.active
)
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()
def png_small(page: db.ScanPage) -> str:
return url_for(
'org_contest_scans_file', id=id, job_id=job_id, site_id=site_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,
file=f'p-{page.file_nr:02d}-{page.page_nr:04d}-full.png'
)
return render_template(
'org_contest_scans_process.html',
ctx=ctx,
job=job,
tasks=tasks,
pions=pions,
pages=pages,
png_small=png_small,
png_full=png_full,
)
@app.route('/org/contest/c/<int:ct_id>/scans/<int:job_id>/file/<file>')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/scans/<int:job_id>/file/<file>')
def org_contest_scans_file(ct_id: int, job_id: int, file: str, site_id: Optional[int] = None):
get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.upload_solutions)
sess = db.get_session()
# Získáme job a zkontrolujeme, že je to správný job a máme na něj práva
job = sess.query(db.Job).get(job_id)
if not job or job.type != db.JobType.process_scans:
raise werkzeug.exceptions.NotFound()
if not g.user.is_admin and g.user.user_id != job.user_id:
raise werkzeug.exceptions.Forbidden()
if os.path.basename(file) != file:
raise werkzeug.exceptions.Forbidden()
path = os.path.join(job.dir_path(), file)
if os.path.isfile(path):
return send_file(path)
else:
raise werkzeug.exceptions.NotFound()
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block head %}
<script src="{{ asset_url('js/autocomplete.js') }}" type="text/javascript"></script>
{% endblock %}
{% block title %}
Třídění skenů pro {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Třídění skenů") }}
{% endblock %}
{% block body %}
{% if job.state in [JobState.preparing, JobState.ready, JobState.running] %}
<p>Právě běží zpracování, vyčkejte prosím několik okamžiků (stránka se sama obnoví).</p>
<script>
setTimeout(function () { location.reload(1); }, 10_000);
</script>
{% elif job.state == JobState.failed %}
<p>Zpracování selhalo, více detailů naleznete v <a href="{{ url_for('org_job', id=job.job_id) }}">detailu dávky</a>.</p>
{% else %}
<p>Napravo můžete klikáním vybírat jednotlivé naskenované stránky a pomocí vrchních políček je přiřazovat jednotlivým úlohám a soutěžícím. Pokud
je nějaké řešení přes více stránek, musí na sebe navazovat číslování stránek. Až bude vše správně zatříděné, můžete aktuální stav uložit
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>
<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.');">
</form>
<style>
#sort_wrapper {
display: grid;
grid-template-columns: auto 350px;
grid-template-rows: 100px auto;
gap: 5px;
}
#sort_wrapper div.controls {
border: 1px #bbbbbb solid;
background-color: #e5e5e5;
border-radius: 5px;
padding: 5px 10px;
}
#sort_wrapper div.scan {
position: relative;
}
#sort_wrapper div.scan #scan_loader {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
background-color: black;
transition: 0.5s;
}
#sort_wrapper div.scan img {
max-width: 100%;
max-height: 100%;
}
#sort_wrapper div.pages {
max-height: 80vh;
grid-row: span 2;
overflow-y: scroll;
position: relative;
}
#sort_wrapper div.pages table {
width: 100%;
margin: 0px;
}
#sort_wrapper div.pages table thead {
position: sticky;
top: 0px;
}
#sort_wrapper div.pages table tr {
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.changed { background-color: yellow; }
#sort_wrapper div.pages table tr.active {
color: white;
background-color: black;
}
#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é.
<div id="sort_wrapper">
<div class="controls form-horizontal">
<span tabindex=1 onfocus="document.getElementById('seq_input').focus();"></span>
<div class="form-group">
<label class="col-sm-2 control-label" for="user_input">Soutěžící:</label>
<div class="col-sm-10 autocomplete">
<input tabindex=2 class="form-control" type="text" id="user_input">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="task_input">Úloha:</label>
<div class="col-sm-4 autocomplete">
<input tabindex=3 class="form-control" type="text" id="task_input">
</div>
<label class="col-sm-2 control-label" for="seq_input">Stránka:</label>
<div class="col-sm-4 autocomplete">
<input tabindex=4 class="form-control" type="number" min="1" id="seq_input">
</div>
</div>
<span tabindex=5 onfocus="document.getElementById('user_input').focus();"></span>
</div>
<div class="pages">
<table class="data">
<thead>
<th title="Číslo skenu">#
<th>Úloha
<th>Soutěžící
<th title="Stránka">St.
</thead>
<tbody id="pages_rows">
</tbody>
</table>
</div>
<div class="scan">
<div id="scan_loader"></div>
<img id="scan_img">
</div>
</div>
<script type="text/javascript">
var tasks = [
{% for task in tasks %}
{ id: {{ task.task_id }}, code: "{{ task.code }}", name: "{{ task.name }}" },
{% endfor %}
];
var users = [
{% for pion in pions %}
{ id: {{ pion.user_id }}, name: "{{ pion.user.first_name }} {{ pion.user.last_name }}" },
{% endfor %}
];
var pages = [
{% for page in pages %}
{
file_nr: {{ page.file_nr }},
page_nr: {{ page.page_nr }},
user_id: {{ page.user_id if page.user_id else 'null' }},
orig_user_id: {{ page.user_id if page.user_id else 'null' }},
task_id: {{ page.task_id if page.task_id else 'null' }},
orig_task_id: {{ page.task_id if page.task_id else 'null' }},
seq_id: {{ page.seq_id }},
orig_seq_id: {{ page.seq_id }},
img_full: "{{ png_full(page) }}",
img_small: "{{ png_small(page) }}",
},
{% endfor %}
];
var tasks_map = {};
var tasks_autocomplete = [];
var users_map = {};
var users_autocomplete = [];
// Global elements
var tbody = document.getElementById('pages_rows');
var img = document.getElementById('scan_img');
var loader = document.getElementById('scan_loader');
var rows = tbody.rows;
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');
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 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 setRow(i) {
p = pages[i];
row = rows[i];
row.innerHTML = '';
row.className = '';
row.title = '';
cellScan = row.insertCell(-1);
cellTask = row.insertCell(-1);
cellUser = row.insertCell(-1);
cellSeq = row.insertCell(-1);
cellScan.innerHTML = p.file_nr + '/' + p.page_nr;
if (p.task_id) {
task = tasks_map[p.task_id];
cellTask.innerHTML = task.code + ' ' + task.name;
}
if (p.user_id) {
user = users_map[p.user_id];
cellUser.innerHTML = user.name;
}
cellSeq.innerHTML = p.seq_id;
if (isOk(p)) {
row.classList.add('ok');
}
if (isEmpty(p)) {
row.classList.add('empty');
row.title += "Prázdná stránka, nebude se ukládat\n";
}
if (isChanged(p)) {
process_all_button.disabled = true;
window.onbeforeunload = beforeUnload;
row.classList.add('changed');
row.title += "Neuložené změny\n";
}
}
var activeRow = 0;
function selectRow(i) {
page = pages[i];
user = null;
user_input.blur();
rows[activeRow].classList.remove('active');
rows[i].classList.add('active');
activeRow = i;
loader.style.opacity = 0.7;
img.onload = function() {
loader.style.opacity = 0;
if (page.user_id) {
user_input.value = users_map[page.user_id].name;
} else {
user_input.value = '';
}
if (page.task_id) {
task = tasks_map[page.task_id];
task_input.value = task.code + ' ' + task.name;
} else {
task_input.value = '';
}
seq_input.value = page.seq_id;
}
img.src = page.img_full;
}
function refreshActiveRow() {
setRow(activeRow);
selectRow(activeRow);
}
// Activate autocomplete on two fields + prevent catching keys on third one
autocomplete(task_input, tasks_autocomplete, callback=function (id) {
pages[activeRow].task_id = id;
refreshActiveRow();
});
autocomplete(user_input, users_autocomplete, callback=function (id) {
pages[activeRow].user_id = id;
refreshActiveRow();
});
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;
refreshActiveRow();
});
// Keyboard control
function checkKey(e) {
e = e || window.event;
if (e.keyCode == '38') {
if (activeRow > 0) {
selectRow(activeRow - 1);
}
} else if (e.keyCode == '40') {
if (activeRow < pages.length - 1) {
selectRow(activeRow + 1);
}
} else if (e.key == 'e') {
user_input.focus();
} else if (e.key == 'x') {
page = pages[activeRow];
page.user_id = null;
page.task_id = null;
page.seq_id = null;
refreshActiveRow();
} else if (e.key == 'r') {
page = pages[activeRow];
page.user_id = page.orig_user_id;
page.task_id = page.orig_task_id;
page.seq_id = page.orig_seq_id;
refreshActiveRow();
} else if (e.key == 'f') {
if (activeRow > 1) {
prev = pages[activeRow-1];
page = pages[activeRow];
if (isOk(prev)) {
page.user_id = prev.user_id;
page.task_id = prev.task_id;
page.seq_id = prev.seq_id + 1;
refreshActiveRow();
} else {
alert('Nelze nastavit jako pokračování předchozí stránky, je nekompletní.');
}
}
} else {
return;
}
e.preventDefault();
}
document.onkeydown = checkKey;
// Start everything :)
for (task of tasks) {
tasks_map[task.id] = task; console.log(task);
tasks_autocomplete.push([task.id, task.code + ' ' + task.name]);
}
for (user of users) {
users_map[user.id] = user;
users_autocomplete.push([user.id, user.name]);
}
for (page of pages) { tbody.insertRow(); }
for (let i = 0; i < pages.length; i++) {
setRow(i);
rows[i].onclick = function() { selectRow(i); }
}
selectRow(0);
</script>
{% endif %}
{% endblock %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment