Skip to content
Snippets Groups Projects
Select Git revision
  • 9cf17b66d253914b3df4429d6ba86eefbac4304f
  • devel default
  • master
  • fo
  • jirka/typing
  • fo-base
  • mj/submit-images
  • jk/issue-96
  • jk/issue-196
  • honza/add-contestant
  • honza/mr7
  • honza/mrf
  • honza/mrd
  • honza/mra
  • honza/mr6
  • honza/submit-images
  • honza/kolo-vs-soutez
  • jh-stress-test-wip
  • shorten-schools
19 results

mo.css

Blame
    • Jiří Setnička's avatar
      9cf17b66
      Vytváření řešení ze stránky úlohy · 9cf17b66
      Jiří Setnička authored
      Vytvořen nový endpoint org_contest_task_create. Vše v pozadí obsluhuje
      stejná univerzální tabulka, jen na správných místech vypisuje správné
      formuláře ve správném módu (má teď mód zobrazení, zakládání řešení a
      bodování).
      
      Přidány odkazy z různých míst.
      9cf17b66
      History
      Vytváření řešení ze stránky úlohy
      Jiří Setnička authored
      Vytvořen nový endpoint org_contest_task_create. Vše v pozadí obsluhuje
      stejná univerzální tabulka, jen na správných místech vypisuje správné
      formuláře ve správném módu (má teď mód zobrazení, zakládání řešení a
      bodování).
      
      Přidány odkazy z různých míst.
    org_contest_scans_process.html 12.88 KiB
    {% extends "base.html" %}
    {% import "bootstrap/wtf.html" as wtf %}
    
    {% block head %}
    	<script src="{{ asset_url('js/autocomplete.js') }}" type="text/javascript"></script>
    {% endblock %}
    
    {% set for_solutions = scans_type == 'solution' %}
    
    {% set scans_title = 'odevzdaných řešení' if for_solutions else 'oprav' %}
    {% block title %}
    Třídění skenů {{ scans_title }} pro {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }}
    {% endblock %}
    {% block breadcrumbs %}
    {{ ctx.breadcrumbs(action="Třídění skenů " + scans_title) }}
    {% endblock %}
    {% block body %}
    
    <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í' if for_solutions else 'opravy' }} se uloží k soutěžícím, tuto akci nelze vzít zpět).
    
    {% 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="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>
    	#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: #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;
    		background-color: black;
    	}
    	#sort_wrapper div.pages table tr.active.changed { color: yellow; }
    </style>
    
    <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">
    		<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" id="rows_scroller">
    		<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 }},
    		human_nr: "{{ page.human_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');
    var save_data_field = document.getElementById('save-data');
    var rows_scroller = document.getElementById('rows_scroller');
    
    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 && 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];
    
    	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.human_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;
    	}
    	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');
    	}
    	if (isEmpty(p)) {
    		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)) {
    		row.classList.add('changed');
    		row.title += "Neuložené změny\n";
    	}
    	refreshButtons();
    }
    
    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 = '';
    		}
    		if (page.seq_id >= 0) {
    			seq_input.value = page.seq_id;
    		} else {
    			seq_input.value = '';
    		}
    	}
    	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 = parseInt(id);
    	refreshActiveRow();
    });
    autocomplete(user_input, users_autocomplete, callback=function (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("blur", function(e) {
    	x = parseInt(seq_input.value);
    	if (Number.isNaN(x)) x = 0
    	pages[activeRow].seq_id = x;
    	refreshActiveRow();
    });
    
    function scrollToRow(i) {
    	var top = rows[i].offsetTop;
    	var bottom = top + rows[i].offsetHeight;
    
    	if (rows_scroller.scrollTop > top - 45) {
    		rows_scroller.scrollTo({ top: top - 45, behavior: 'smooth'});
    	} else if (rows_scroller.scrollTop + rows_scroller.offsetHeight < bottom + 30) {
    		rows_scroller.scrollTo({ top: bottom - rows_scroller.offsetHeight + 30, behavior: 'smooth' });
    	}
    }
    
    function prevPage() {
    	if (activeRow > 0) {
    		selectRow(activeRow - 1);
    		scrollToRow(activeRow);
    	}
    }
    
    function nextPage() {
    	if (activeRow < pages.length - 1) {
    		selectRow(activeRow + 1);
    		scrollToRow(activeRow);
    	}
    }
    
    // Keyboard control
    function checkKey(e) {
    	e = e || window.event;
    	if (e.keyCode == '38') {
    		prevPage();
    	} else if (e.keyCode == '40') {
    		nextPage();
    	} 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 = PAGE_EMPTY;
    		refreshActiveRow();
    		nextPage();
    	} 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();
    				nextPage();
    			} 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;
    	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);
    scrollToRow(0);
    </script>
    
    {% endblock %}