Compare commits

..

5 commits

Author SHA1 Message Date
LEdoian
a44fcce1c8 Exif je zběsilej standard 2025-05-19 03:18:15 +02:00
LEdoian
2cbaee10aa fixup! fixup! fixup! WIP sortítko galerií 2025-05-19 02:05:11 +02:00
LEdoian
1d157e7e68 fixup! fixup! WIP sortítko galerií 2025-05-19 02:02:50 +02:00
LEdoian
d999c46e5e fixup! WIP sortítko galerií 2025-05-19 02:01:04 +02:00
LEdoian
88695313c1 WIP sortítko galerií
potřebuji otestovat na testwebu :-)
2025-05-19 01:54:27 +02:00
25 changed files with 801 additions and 1059 deletions

View file

@ -91,7 +91,7 @@ TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte.
Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?"
Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, ""
Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``); nezapomenout vygenerovat locale ``cs_CZ.UTF-8``" Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)"
openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config."
Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu."

View file

@ -100,3 +100,41 @@ class Galerie(models.Model):
class Meta: class Meta:
verbose_name = 'Galerie' verbose_name = 'Galerie'
verbose_name_plural = 'Galerie' verbose_name_plural = 'Galerie'
# TODO: patří to spíš sem, nebo do nějakých utils?
def setrid_galerii_podle_exifu(self):
"""Setřídí galerii podle data v EXIF tazích.
Velmi experimentální, zatím pro použití jen webařem v shellu (důvod:
EXIF se trochu blbě parsuje, není jasné, jestli se použít DateTime,
DateTimeOriginal nebo DateTimeDigitized, EXIF nemusí mít informace o
časové zóně a tenhle kód časovou zónu ignoruje, vůbec nezachovává
pořadí)."""
from PIL import Image, ExifTags, UnidentifiedImageError
from datetime import datetime
EXIF_DATETIME_FMT = "%Y:%m:%d %H:%M:%S"
self.obrazek_set.update(poradi=None)
for obrazek in self.obrazek_set.all():
try:
obr = Image.open(obrazek.obrazek_velky)
except UnidentifiedImageError as e:
raise ValueError from e
exif = obr.getexif() # Pokud tam není, tak si Pillow podle zdrojáků vyhaluzí prázdný.
ifd_exif = {}
if ExifTags.IFD.Exif in exif: ifd_exif = exif.get_ifd(ExifTags.IFD.Exif)
date_str = (False # zarovnání
or exif.get(ExifTags.Base.DateTimeOriginal, None)
or ifd_exif.get(ExifTags.Base.DateTimeOriginal, None)
or exif.get(ExifTags.Base.DateTime, None)
or ifd_exif.get(ExifTags.Base.DateTime, None)
or exif.get(ExifTags.Base.DateTimeDigitized, None)
or ifd_exif.get(ExifTags.Base.DateTimeDigitized, None)
)
if date_str is None: continue
dt = datetime.strptime(date_str, EXIF_DATETIME_FMT)
poradi = int(dt.strftime("1%d%H%M%S"))
obrazek.poradi = poradi
obrazek.save()

View file

@ -1,19 +1,19 @@
.textzanaseni { display:none; } .textzanaseni { display:none; }
.textzastarale { display:none; } .textzastarale { display:none; }
#prekomentar, #prekorektura, #prepointer { display: none; } #prekomentar, #preoprava, #prepointer { display: none; }
body { body {
&[data-stav_pdf="pridavani"] { &[data-status="pridavani"] {
background: #f3f3f3; background: #f3f3f3;
} }
&[data-stav_pdf="zanaseni"] { &[data-status="zanaseni"] {
background: yellow; background: yellow;
.textzanaseni { display: unset; } .textzanaseni { display: unset; }
} }
&[data-stav_pdf="zastarale"] { &[data-status="zastarale"] {
background: red; background: red;
.textzastarale { display: unset; } .textzastarale { display: unset; }
@ -28,25 +28,25 @@ body {
img{background:white;} img{background:white;}
/* Barvy korektur */ /* Barvy korektur */
[data-stav_korektury="k_oprave"] { [data-opravastatus="k_oprave"] {
--rgb: 255, 0, 0; --rgb: 255, 0, 0;
[value="k_oprave"] { display: none } [value="k_oprave"] { display: none }
.komentovat_disabled { display: none } .komentovat_disabled { display: none }
} }
[data-stav_korektury="opraveno"] { [data-opravastatus="opraveno"] {
--rgb: 0, 0, 255; --rgb: 0, 0, 255;
[value="opraveno"] { display: none } [value="opraveno"] { display: none }
.komentovat { display: none } .komentovat { display: none }
} }
[data-stav_korektury="neni_chyba"] { [data-opravastatus="neni_chyba"] {
--rgb: 128, 128, 128; --rgb: 128, 128, 128;
[value="neni_chyba"] { display: none } [value="neni_chyba"] { display: none }
.komentovat { display: none } .komentovat { display: none }
} }
[data-stav_korektury="k_zaneseni"] { [data-opravastatus="k_zaneseni"] {
--rgb: 0, 255, 0; --rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none } [value="k_zaneseni"] { display: none }
@ -54,16 +54,10 @@ img{background:white;}
} }
/* Skrývání korektur */ /* Skrývání korektur */
[data-korektura_sbalena="true"] { [data-opravazobrazit="false"] {
.korektura-telo { display: none; } .corr-body { display: none; }
.korektura-tlacitka { display: none; } .corr-buttons { display: none; }
.sbal-rozbal-img { transform: rotate(180deg); } .toggle-button { transform: rotate(180deg); }
}
/* Skrývání komentářů */
[data-komentar_sbalen="true"] {
.sbal-rozbal-img { transform: rotate(180deg); }
.uprav-komentar { display: none; }
.komtext { display: none; }
} }
@ -78,14 +72,14 @@ img{background:white;}
--alpha: 0.35; --alpha: 0.35;
/* Zvýraznění čáry při najetí na korekturu */ /* Zvýraznění čáry při najetí na korekturu */
&[data-hover="true"] { &[data-highlight="true"] {
border-width: 3px; border-width: 3px;
--alpha: 1; --alpha: 1;
} }
} }
/* Korektura samotná */ /* Korektura samotná */
.korektura { .oprava {
margin: 1px; margin: 1px;
background-color: white; background-color: white;
width: 300px; width: 300px;
@ -112,11 +106,11 @@ img{background:white;}
button img { pointer-events: none; } button img { pointer-events: none; }
.hlavicka-komentare { .corr-header {
overflow: auto; overflow: auto;
} }
.autor { .author {
font-weight: bold; font-weight: bold;
float: left; float: left;
margin-top: 3px; margin-top: 3px;
@ -139,7 +133,7 @@ form {
} }
/* Přidávání korektury / úprava komentáře */ /* Přidávání korektury / úprava komentáře */
#korekturovaci-formular-div { #commform-div {
position: absolute; position: absolute;
background-color: white; background-color: white;
padding: 3px; padding: 3px;
@ -154,8 +148,8 @@ form {
margin: 2px; margin: 2px;
padding: 2px; padding: 2px;
&[data-vybran="false"] { background: unset !important; } &[data-selected="false"] { background: unset !important; }
/*&[data-vybran="true"] { border-color: unset !important; }*/ /*&[data-selected="true"] { border-color: unset !important; }*/
} }
/* Šipky na posouvání korektur */ /* Šipky na posouvání korektur */

View file

@ -0,0 +1,46 @@
const W_SKIP = 10;
const H_SKIP = 5;
const POINTER_MIN_H = 30;
function place_comments_one_div(img_id, comments)
{
const img = document.getElementById("img-"+img_id);
if( img == null ) return;
const comments_sorted = comments.sort((a, b) => a.y - b.y);
const par = img.parentNode;
const w = img.clientWidth;
let bott_max = 0;
for (const oprava of comments_sorted) {
const x = oprava.x;
const y = oprava.y;
const htmlElement = oprava.htmlElement;
const pointer = oprava.pointer;
par.appendChild(pointer);
par.appendChild(htmlElement);
const delta_y = (y > bott_max) ? 0: bott_max - y + H_SKIP;
pointer.style.left = x;
pointer.style.top = y;
pointer.style.width = w - x + W_SKIP;
pointer.style.height = POINTER_MIN_H + delta_y;
htmlElement.style.left = w + W_SKIP;
htmlElement.style.top = y + delta_y;
bott_max = Math.max(bott_max, htmlElement.offsetTop + htmlElement.offsetHeight + H_SKIP); // FIXME nemám páru, proč +H_SKIP funguje, ale opravuje to bug, že nově vytvořené korektury za sebou neměly mezeru
}
if (par.offsetHeight < bott_max) par.style.height = bott_max;
}
function place_comments() {
for (let [img_id, opravy] of Object.entries(comments)) {
place_comments_one_div(img_id, opravy)
}
}

View file

@ -0,0 +1,76 @@
{% load static %}
<div id="korektury-sipky">
<button type='button' id="predchozi-korektura" title='Předchozí korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-oprave" title='Předchozí korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-zaneseni" title='Předchozí korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<br>
<button type='button' id="dalsi-korektura" title='Další korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-oprave" title='Další korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-zaneseni" title='Další korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id='korektury-aktualizace'
title='Aktualizuj korektury
Nemusíš mačkat, pokud ti stačí, že se korektury aktualizují samy každé 2 minuty a při každém přidání korektury/komentáře.'
>
<img class='toggle-button' src='{% static "korektury/imgs/reload.svg" %}' alt='↻' style="width: 15px"/>
</button>
</div>
<script>
const predchozi_k = document.getElementById('predchozi-korektura');
const dalsi_k = document.getElementById('dalsi-korektura');
const predchozi_k_o = document.getElementById('predchozi-korektura-k-oprave');
const dalsi_k_o = document.getElementById('dalsi-korektura-k-oprave');
const predchozi_k_z = document.getElementById('predchozi-korektura-k-zaneseni');
const dalsi_k_z = document.getElementById('dalsi-korektura-k-zaneseni');
function dalsi_nebo_predchozi_korektura(dalsi=true, stav=null) {
let predchozi = null;
for (let [_, opravy] of Object.entries(comments)) {
for (const oprava of opravy) {
if (stav == null || oprava.status === stav) {
const y = oprava.htmlElement.getBoundingClientRect().y;
if (y >= -1) {
if (dalsi) {
if (y > 1) {
oprava.htmlElement.scrollIntoView();
return;
}
} else {
if (predchozi !== null) predchozi.htmlElement.scrollIntoView(); else alert("Výše už není žádná taková korektura.");
return;
}
}
predchozi = oprava;
}
}
}
if (!dalsi && predchozi !== null) {
predchozi.htmlElement.scrollIntoView();
return;
}
alert("Žádná další korektura.");
}
predchozi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false) });
dalsi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true) });
predchozi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_oprave") });
dalsi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_oprave") });
predchozi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_zaneseni") });
dalsi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_zaneseni") });
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
document.getElementById("korektury-aktualizace").addEventListener("click", _ => update_all({}, false));
</script>

View file

@ -0,0 +1,111 @@
<div id="commform-div" style="display: none">
<input size="24" name="au" value="{{user.first_name}} {{user.last_name}}" readonly/>
<button type="button" id="commform-submit">Oprav!</button>
<button type="button" id="commform-close">Zavřít</button>
<br/>
<textarea id="commform-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<div id="commform-tagy-info">Úprava tagů celé korektury:</div>
<div id="commform-tagy">
{% for tag in tagy %}
<button type="button" class="korektury-tag" value="{{tag.id}}" data-selected="false" style="background: {{ tag.barva }}; border-color: {{ tag.barva }};">{{tag.nazev}}</button>
{% endfor %}
</div>
</div>
<script>
class _CommForm {
constructor() {
this.div = document.getElementById('commform-div');
this.text = document.getElementById('commform-text');
this.submit_button = document.getElementById('commform-submit');
const close_button = document.getElementById('commform-close');
this.tagy = document.getElementById('commform-tagy');
this.tagy_info = document.getElementById('commform-tagy-info');
// ctrl-enter submits form
this.text.addEventListener("keydown", ev => {
if (ev.code === "Enter" && ev.ctrlKey) this.submit();
});
close_button.addEventListener("click", _ => { this.close(); });
this.submit_button.addEventListener("click", _ => { this.submit(); });
for (const tag of this.tagy.getElementsByTagName("button")) tag.addEventListener("click", event => { this.toggle_tag(event); });
this.reset_tags_every_open = true;
}
toggle_tag(event) {
const button = event.target;
button.dataset.selected = String(button.dataset.selected === "false");
}
reset_tags() { for (const tag of this.tagy.getElementsByTagName("button")) tag.dataset.selected = "false"; }
// schová commform
close() { this.div.style.display = 'none'; }
// zobrazí commform (bez vyplňování)
_show(img_id, x, y) {
this.submit_button.disabled = false;
this.div.style.display = 'block';
this.div.style.left = x;
this.div.style.top = y;
const img = document.getElementById("img-" + img_id);
img.parentNode.appendChild(commform.div);
this.text.focus();
}
// fill up comment form and show him
show(img_id, x, y, text, oprava_id=-1, komentar_id=-1) {
if (this.div.style.display !== 'none' && this.text.value !== "" && !confirm("Zavřít předchozí okénko přidávání korektury / editace komentáře?")) return;
// set hidden values
this.x = x;
this.y = y;
this.imgID = img_id;
this.oprava_id = oprava_id;
this.komentar_id = komentar_id;
this.text.value = text;
// show form
if (oprava_id === -1 && komentar_id === -1) {
if (this.reset_tags_every_open) this.reset_tags();
this.tagy_info.style.display = 'none';
} else {
const oprava = opravy[oprava_id];
this.tagy_info.style.display = 'unset';
for (const tag of this.tagy.getElementsByTagName("button"))
tag.dataset.selected = String(oprava.tagy.has(parseInt(tag.value)));
}
this._show(img_id, x, y);
}
submit() {
this.submit_button.disabled = true;
const data = new FormData(CSRF_FORM);
data.append('x', this.x);
data.append('y', this.y);
data.append('img_id', this.imgID);
data.append('oprava_id', this.oprava_id);
data.append('komentar_id', this.komentar_id);
const tagy = [];
for (const tag of this.tagy.getElementsByTagName("button")) {
if (tag.dataset.selected !== "false") tagy.push(tag.value);
}
data.append('tagy', String(tagy));
data.append('text', this.text.value);
update_all({method: 'POST', body: data}, true, () => {this.close(); this.submit_button.disabled = false;});
}
}
const commform = new _CommForm();
</script>

View file

@ -0,0 +1,106 @@
{% load static %}
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='corr-header'>
<div class='author'>{# {{k.autor}} #}</div>
<div class='float-right'>
<button type='button' style='display: none' class='del-comment' title='Smaž komentář'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='del'/>
</button>
<button type='button' class='update-comment' title='Uprav komentář'>
<img src='{% static "korektury/imgs/edit.png"%}' alt='edit'/>
</button>
</div>
</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
<hr>
</div>
<script>
const prekomentar = document.getElementById('prekomentar');
const komentare = {};
class Komentar {
static update_or_create(komentar_data, oprava) {
const id = komentar_data['id'];
if (id in komentare) komentare[id].update(komentar_data);
else new Komentar(komentar_data, oprava);
}
#autor; #text;
htmlElement;
id; oprava; {# komentar_data; #}
autor;
/**
*
* @param komentar_data
* @param {Oprava} oprava
*/
constructor(komentar_data, oprava) {
this.htmlElement = prekomentar.cloneNode(true);
this.#autor = this.htmlElement.getElementsByClassName('author')[0];
this.#text = this.htmlElement.getElementsByClassName('komtext')[0];
this.id = komentar_data['id'];
this.htmlElement.id = 'k' + this.id;
this.oprava = oprava;
this.oprava.add_komentar_htmlElement(this.htmlElement);
this.update(komentar_data);
this.htmlElement.getElementsByClassName('update-comment')[0].addEventListener('click', _ => this.#update_comment());
this.htmlElement.getElementsByClassName('del-comment')[0].addEventListener('click', _ => this.#delete_comment());
komentare[this.id] = this;
}
update(komentar_data) {
{# this.komentar_data = komentar_data; #}
this.set_autor(komentar_data['autor']);
this.set_text(komentar_data['text']);
};
set_autor(autor) {
this.#autor.textContent=autor;
this.autor = autor;
};
set_text(text) {
this.#text.innerHTML=text;
};
// show comment form when 'update-comment' button pressed
#update_comment() {
return commform.show(this.oprava.img_id, this.oprava.x, this.oprava.y, this.#text.textContent, this.oprava.id, this.id);
}
#delete_comment() {
if (confirm('Opravdu smazat komentář?')) {
const data = new FormData(CSRF_FORM);
data.append('komentar_id', this.id);
fetch('{% url "korektury_api_komentar_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.smaz_pouze_na_strance();
place_comments();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
smaz_pouze_na_strance() {
delete komentare[this.id];
this.htmlElement.remove();
}
}
</script>

View file

@ -0,0 +1,189 @@
{% load static %}
<div id='prepointer' {# id='op{{o.id}}-pointer' #}
class='pointer'
data-highlight='false'
{# data-opravastatus='{{o.status}}' #}
></div>
<div id='preoprava' {# name='op{{o.id}}' id='op{{o.id}}' #}
class='oprava'
{# data-opravastatus='{{o.status}}' #}
data-opravazobrazit='true'
>
<div class='corr-tagy'>
{# {% for tag in o.tagy %} <span style="background:{{ tag.barva }}>{{ tag.text }}<span/> #}
</div>
<div class='corr-body'>
{# {% for k in o.komentare %} {% include "korektury/korekturovatko/__komentar.html" %} {% endfor %} #}
</div>
<div class='corr-header'>
<span class='float-right'>
<span class='corr-buttons'>
<button type='button' style='display: none' class='del' title='Smaž opravu'>
<img src='{% static "korektury/imgs/delete.png"%}' alt='🗑️'/>
</button>
<button type='button' class='action' value='k_oprave' title='Označ jako neopravené'>
<img src='{% static "korektury/imgs/undo.png"%}' alt='↪'/>
</button>
<button type='button' class='action' value='opraveno' title='Označ jako opravené'>
<img src='{% static "korektury/imgs/check.png"%}' alt='✔️'/>
</button>
<button type='button' class='action' value='neni_chyba' title='Označ, že se nebude měnit'>
<img src='{% static "korektury/imgs/cross.png" %}' alt='❌'/>
</button>
<button type='button' class='action' value='k_zaneseni' title='Označ jako připraveno k zanesení'>
<img src='{% static "korektury/imgs/tex.png" %}' alt='TeX'/>
</button>
<a href='{% url "admin:korektury_oprava_change" -1 %}' class='edit' title='Uprav korekturu jako takovou.' style="text-decoration: none;"> {# FIXME Udělat z toho tlačítko? #}
<img src='{% static "korektury/imgs/edit.png"%}' alt='✏️' style="opacity: 0.5;"/> {# FIXME Odlišit jinak než pomocí opacity? #}
</a>
<button type='button' class='komentovat_disabled' title='Korekturu nelze komentovat, protože už je uzavřená' disabled=''>
<img src='{% static "korektury/imgs/comment-gr.png" %}' alt='💭'/>
</button>
<button type='button' class='komentovat' title='Komentovat'>
<img src='{% static "korektury/imgs/comment.png" %}' alt='💭'/>
</button>
</span>
<button type='button' class='toggle-vis' title='Skrýt/Zobrazit'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</span>
</div>
</div>
<script>
const preoprava = document.getElementById('preoprava');
const prepointer = document.getElementById('prepointer');
const opravy = {};
class Oprava {
static update_or_create(oprava_data) {
const id = oprava_data['id'];
if (id in opravy) return opravy[id].update(oprava_data);
else return new Oprava(oprava_data);
}
#komentare; #tagy;
htmlElement; pointer;
id; x; y; img_id; status; zobrazit = true; {# oprava_data; #}
tagy;
constructor(oprava_data) {
this.htmlElement = preoprava.cloneNode(true);
this.pointer = prepointer.cloneNode(true);
this.#komentare = this.htmlElement.getElementsByClassName('corr-body')[0];
this.#tagy = this.htmlElement.getElementsByClassName('corr-tagy')[0];
this.id = oprava_data['id'];
this.htmlElement.id = 'op' + this.id;
this.pointer.id = 'op' + this.id + '-pointer';
this.x = oprava_data['x'];
this.y = oprava_data['y'];
this.img_id = oprava_data['strana'];
this.update(oprava_data);
this.htmlElement.getElementsByClassName('toggle-vis')[0].addEventListener('click', _ => this.#toggle_visibility());
for (const button of this.htmlElement.getElementsByClassName('action'))
button.addEventListener('click', async event => this.#zmenStavKorektury(event));
this.htmlElement.getElementsByClassName('komentovat')[0].addEventListener('click', _ => this.#comment())
this.htmlElement.getElementsByClassName('del')[0].addEventListener('click', _ => this.#delete());
const odkaz_editace = this.htmlElement.getElementsByClassName('edit')[0];
odkaz_editace.href = odkaz_editace.href.replace("-1", this.id);
odkaz_editace.onclick = ev => { if (!confirm("Editace korektury je velmi pokročilá featura umožňující přesouvat korekturu nebo přidávat informované orgy, opravdu chceš pokračovat do adminu?")) ev.preventDefault(); };
this.htmlElement.addEventListener('mouseover', _ => this.pointer.dataset.highlight = 'true');
this.htmlElement.addEventListener('mouseout', _ => this.pointer.dataset.highlight = 'false');
opravy[this.id] = this;
if (this.img_id in comments) comments[this.img_id].push(this); else alert("Někdo korekturoval stranu, která neexistuje. Dejte vědět webařům :)");
}
update(oprava_data) {
{# this.oprava_data = oprava_data; #}
this.set_status(oprava_data['status']);
this.#tagy.innerHTML = "";
this.tagy = new Set();
for (const tag of oprava_data["tagy"]) {
this.tagy.add(tag["id"]);
const span = document.createElement("span");
span.innerHTML = tag["nazev"];
span.classList.add("korektury-tag");
span.style.backgroundColor = tag["barva"];
this.#tagy.appendChild(span);
}
return this;
};
set_status(status) {
this.status = status;
this.htmlElement.dataset.opravastatus=status;
this.pointer.dataset.opravastatus=status;
};
add_komentar_htmlElement(htmlElement) { this.#komentare.appendChild(htmlElement); }
// hide or show text of correction
toggle_visibility() {
this.zobrazit = !this.zobrazit;
this.htmlElement.dataset.opravazobrazit = String(this.zobrazit);
}
#toggle_visibility(){
this.toggle_visibility();
place_comments()
}
// show comment form, when 'comment' button pressed
#comment() { commform.show(this.img_id, this.x, this.y, "", this.id); }
#zmenStavKorektury(event) {
const data = new FormData(CSRF_FORM);
data.append('id', this.id);
data.append('action', event.target.value);
fetch('{% url "korektury_api_oprava_stav" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
this.set_status(data['status']);
updatuj_pocty_stavu();
});
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
#delete() {
if (confirm('Opravdu smazat korekturu?')) {
const data = new FormData(CSRF_FORM);
data.append('oprava_id', this.id);
fetch('{% url "korektury_api_oprava_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.#smaz_pouze_na_strance()
updatuj_pocty_stavu();
updatuj_pocty_zasluh();
place_comments();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
#smaz_pouze_na_strance() {
comments[this.img_id].splice(comments[this.img_id].indexOf(this), 1);
delete opravy[this.id];
for (const komentar of Object.values(komentare)) if (komentar.oprava === this) komentar.smaz_pouze_na_strance();
this.htmlElement.remove();
this.pointer.remove();
}
}
</script>

View file

@ -0,0 +1,54 @@
{% for i in img_indexes %}
<div class='imgdiv'>
<img
id='img-{{i}}'
width='1021' height='1448'
src='/media/korektury/img/{{korekturovanepdf.get_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
class="strana"
/>
</div>
<hr/>
{% endfor %}
<script>
// Mapování stránka -> korektury
/**
* @type {Object.<number, Array<Oprava>>}
*/
const comments = {
{% for s in img_indexes %}
{{s}}: []{% if not forloop.last %},{% endif %}
{% endfor %}
};
// show comment form, when clicked to image
for (const image of document.getElementsByClassName('strana')) {
image.addEventListener('click', ev => {
switch (document.body.dataset.status) {
case 'zanaseni':
if (!confirm('Právě jsou zanášeny korektury, opravdu chcete přidat novou?'))
return;
break;
case 'zastarale':
if (!confirm('Toto PDF je již zastaralé, opravdu chcete vytvořit korekturu?'))
return;
break;
}
let dx, dy;
const par = image.parentNode;
if (ev.pageX != null) {
dx = ev.pageX - par.offsetLeft;
dy = ev.pageY - par.offsetTop;
} else { //IE a další
dx = ev.offsetX;
dy = ev.offsetY;
}
const img_id = image.id.substring(4);
commform.show(img_id, dx, dy, '');
console.log("Pro přesun korektur: strana = " + img_id + ", x = " + dx + ", y = " + dy);
});
}
</script>

View file

@ -0,0 +1,57 @@
{% include "korektury/korekturovatko/__edit_komentar.html" %}
{% include "korektury/korekturovatko/__stranky.html" %}
{# {% for o in opravy %} {% include "korektury/korekturovatko/__oprava.html" %} {% endfor %} #}
{% include "korektury/korekturovatko/__oprava.html" %}
{% include "korektury/korekturovatko/__komentar.html" %}
{% include "korektury/korekturovatko/__dalsi_korektura.html" %}
<script>
/**
*
* @param {RequestInit} data
* @param {Boolean} catchError
* @param pri_uspechu Akce, která se má provést při úspěchu (speciálně zavřít formulář)
*/
function update_all(data={}, catchError=true, pri_uspechu=null) { // FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
fetch('{% url "korektury_api_opravy_a_komentare" korekturovanepdf.id %}', data)
.then(response => {
if (!response.ok && catchError) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
for (const oprava_data of data["context"]) {
const oprava = Oprava.update_or_create(oprava_data);
for (const komentar_data of oprava_data["komentare"]) {
Komentar.update_or_create(komentar_data, oprava);
}
}
updatuj_pocty_stavu();
updatuj_pocty_zasluh();
place_comments();
if (pri_uspechu) pri_uspechu();
});
})
.catch(error => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
window.addEventListener("load", _ => {
update_all({}, true, _ => {
if (location.hash !== "") { // Po rozházení korektur sescrollujeme na kotvu v URL
const h = location.hash.substring(1);
location.hash = "HACK";
location.hash = h;
}
});
});
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
setInterval(() => update_all({}, false), 120000); // Každý dvě minuty fetchni korektury
</script>
<form id='CSRF_form' style='display: none'>{% csrf_token %}</form>
<script>
const CSRF_FORM = document.getElementById('CSRF_form');
</script>

View file

@ -0,0 +1,83 @@
Zobrazit:
<input type="checkbox"
id="k_oprave_checkbox"
name="k_oprave_checkbox"
onchange="toggle_corrections('k_oprave')" checked>
<label for="k_oprave_checkbox">K opravě (<span id="k_oprave_pocet"></span>)</label>
<input type="checkbox"
id="opraveno_checkbox"
name="opraveno_checkbox"
onchange="toggle_corrections('opraveno')" checked>
<label for="opraveno_checkbox">Opraveno (<span id="opraveno_pocet"></span>)</label>
<input type="checkbox"
id="neni_chyba_checkbox"
name="neni_chyba_checkbox"
onchange="toggle_corrections('neni_chyba')" checked>
<label for="neni_chyba_checkbox">Není chyba (<span id="neni_chyba_pocet"></span>)</label>
<input type="checkbox"
id="k_zaneseni_checkbox"
name="k_zaneseni_checkbox"
onchange="toggle_corrections('k_zaneseni')" checked>
<label for="k_zaneseni_checkbox">K zanesení (<span id="k_zaneseni_pocet"></span>)</label>
<button type="button" id="sbal-korektury">Sbal korektury</button>
<button type="button" id="rozbal-korektury">Rozbal korektury</button>
<hr/>
<script>
const spany_s_pocty_stavu = {
'k_oprave': document.getElementById('k_oprave_pocet'),
'opraveno': document.getElementById('opraveno_pocet'),
'neni_chyba': document.getElementById('neni_chyba_pocet'),
'k_zaneseni': document.getElementById('k_zaneseni_pocet'),
}
function toggle_corrections(aclass)
{
const stylesheets = document.styleSheets;
let ssheet = null;
for (let i=0; i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (let i=0; i<ssheet.cssRules.length; i++){
const rule = ssheet.cssRules[i];
if (rule.selectorText === '[data-opravastatus="'+aclass+'"]'){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
place_comments();
}
function updatuj_pocty_stavu() {
const pocty_stavu = {};
for (const stav of Object.keys(spany_s_pocty_stavu)) pocty_stavu[stav] = 0;
for (const oprava of Object.values(opravy)) {
if (!(oprava.status in pocty_stavu)) pocty_stavu[oprava.status] = 0;
pocty_stavu[oprava.status] += 1;
}
for (let [stav, pocet] of Object.entries(pocty_stavu)) spany_s_pocty_stavu[stav].innerText = pocet;
}
document.getElementById("sbal-korektury").addEventListener("click", () => {
for (const oprava of Object.values(opravy))
if (oprava.zobrazit) oprava.toggle_visibility();
place_comments();
})
document.getElementById("rozbal-korektury").addEventListener("click", () => {
for (const oprava of Object.values(opravy))
if (!oprava.zobrazit) oprava.toggle_visibility();
place_comments();
})
</script>

View file

@ -1,5 +1,5 @@
{# Template starající se o formulář na změnu stavu PDF (včetně jeho odeslání) #} <h4>Změnit stav PDF:</h4>
<b>Změnit stav PDF:</b> <i>Aktuální: {{korekturovanepdf.status}}</i>
<br> <br>
<form method="post" id="PDFSTAV_FORM"> <form method="post" id="PDFSTAV_FORM">
{% csrf_token %} {% csrf_token %}
@ -13,24 +13,19 @@
</form> </form>
<script> <script>
/**
* Formulář měnící stav korekturovaného PDF
* @type {HTMLFormElement}
*/
const pdfstav_form = document.getElementById('PDFSTAV_FORM'); const pdfstav_form = document.getElementById('PDFSTAV_FORM');
/** /**
* Fetchne stav korekturovaného PDF a změní ho na dané stránce. *
* FIXME: nemění, který radio-button je vybrán. * @param {RequestInit} data
* @param {RequestInit} data FormData a jiné náležitosti (method: POST) posílané při změně stavu korekturovaného PDF * @param {Boolean} catchError
* @param {Boolean} catchError jestli padat hlasitě (pokud se aktualizuje automaticky a spadne to např. na nepřítomnost sítě, pak není třeba informovat uživatele)
*/ */
function fetchStav(data, catchError=true) { function fetchStav(data, catchError=true) {
fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data
) )
.then(response => { .then(response => {
if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);} if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);}
else response.json().then(data => document.body.dataset.stav_pdf = data["status"]); else response.json().then(data => document.body.dataset.status = data["status"]);
}) })
.catch(error => {if (catchError) alert("Něco se nepovedlo:" + error);}); .catch(error => {if (catchError) alert("Něco se nepovedlo:" + error);});
} }

View file

@ -1,66 +0,0 @@
{# Část korekturovátka, která obsahuje všechno okolo korektur #}
{% include "korektury/korekturovatko/moduly/schovani_korektur.html" %}
{% include "korektury/korekturovatko/moduly/edit_komentar.html" %}
{% include "korektury/korekturovatko/moduly/stranky_pdfka.html" %}
{# {% for k in korektury %} {% include "korektury/korekturovatko/korektura.html" %} {% endfor %} #}
{% include "korektury/korekturovatko/moduly/korektura.html" %}
{% include "korektury/korekturovatko/moduly/komentar.html" %}
{% include "korektury/korekturovatko/moduly/dalsi_korektura.html" %}
<script>
/**
* Fetchne korektury a komentáře a na základě toho aktualizuje všechno
* (korektury, komentáře, zásluhy, počty korektur v daných stavech, umístění korektur)
* @param {RequestInit} data FormData a jiné náležitosti (method: POST) posílané při přidání/úpravě korektury/komentáře
* @param {Boolean} catchError jestli padat hlasitě (pokud se aktualizuje automaticky a spadne to např. na nepřítomnost sítě, pak není třeba informovat uživatele)
* @param {(() => *)?} pri_uspechu akce, která se má provést při úspěchu (speciálně zavřít formulář)
*/
function aktualizuj_vse(data={}, catchError=true, pri_uspechu=null) { // FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
fetch('{% url "korektury_api_opravy_a_komentare" korekturovanepdf.id %}', data)
.then(response => {
if (!response.ok && catchError) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
for (const korektura_data of data["context"]) {
const korektura = Korektura.aktualizuj_nebo_vytvor(korektura_data);
for (const komentar_data of korektura_data["komentare"]) {
Komentar.aktualizuj_nebo_vytvor(komentar_data, korektura);
}
}
aktualizuj_pocty_stavu();
aktualizuj_pocty_zasluh();
umisti_korektury();
if (pri_uspechu) pri_uspechu();
});
})
.catch(error => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
window.addEventListener("load", _ => {
aktualizuj_vse({}, true, () => {
if (location.hash !== "") { // Po rozházení korektur sescrollujeme na kotvu v URL
const h = location.hash.substring(1);
location.hash = "HACK";
location.hash = h;
}
});
});
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
setInterval(() => aktualizuj_vse({}, false), 120000); // Každý dvě minuty fetchni korektury
</script>
{# Formulář, který mouhou použít tlačítka bez svého formuláře k vytvoření POST requestu, viz CSRF_FORM níže #}
<form id='CSRF_form' style='display: none'>{% csrf_token %}</form>
<script>
/**
* Formulář, který mouhou použít tlačítka bez svého formuláře k vytvoření POST requestu
* @type {HTMLFormElement}
*/
const CSRF_FORM = document.getElementById('CSRF_form');
</script>

View file

@ -1,14 +1,14 @@
{# Okolí samotného hlavni_cast_korekturovatka.html, tedy „povinné HTML věci“, informace o korekturovaném PDF a starání se o stav PDF #}
{% load static %} {% load static %}
<html lang='cs'> <html>
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}?version=3" /> <link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}?version=2" />
<link href="{% static 'css/rozliseni.css' %}?version=3" rel="stylesheet"> <link href="{% static 'css/rozliseni.css' %}?version=2" rel="stylesheet">
<script src="{% static "korektury/opraf.js"%}?version=2"></script>
<title>Korektury {{korekturovanepdf.nazev}}</title> <title>Korektury {{korekturovanepdf.nazev}}</title>
</head> </head>
<body class="{{ LOCAL_TEST_PROD }}web" data-stav_pdf="{{ korekturovanepdf.status }}"> <body class="{{ LOCAL_TEST_PROD }}web" data-status="{{ korekturovanepdf.status }}">
<h1>Korektury {{korekturovanepdf.nazev}}</h1> <h1>Korektury {{korekturovanepdf.nazev}}</h1>
@ -27,9 +27,11 @@
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> | <a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<hr /> <hr />
{% include "korektury/korekturovatko/hlavni_cast_korekturovatka.html" %} {% include "korektury/korekturovatko/_schovani_korektur.html" %}
{% include "korektury/korekturovatko/_main.html" %}
{% include "korektury/korekturovatko/zmena_stavu_pdf.html" %} {% include "korektury/korekturovatko/_zmena_stavu.html" %}
<hr/> <hr/>
<p> <p>
@ -37,14 +39,9 @@
<hr> <hr>
<script> <script>
/**
* HTML prvek, kam se zapíší (pomocí .innerHTML) počty korektur jednotlivých autorů
* @type {HTMLElement}
*/
const span_s_pocty_autoru = document.getElementById("pocty_autoru") const span_s_pocty_autoru = document.getElementById("pocty_autoru")
/** Aktualizuje, kolik který autor má komentářů u daného korekturovaného PDF. */ function updatuj_pocty_zasluh() {
function aktualizuj_pocty_zasluh() {
const pocty_autoru = {}; const pocty_autoru = {};
for (let komentar of Object.values(komentare)) { for (let komentar of Object.values(komentare)) {
if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0; if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0;

View file

@ -1,77 +0,0 @@
{# Template starající se o tlačítka v levém dolním rohu, především skákající na další/předchozí korekturu. #}
{% load static %}
<div id="korektury-sipky">
<button type='button' id="predchozi-korektura" title='Předchozí korektura'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-oprave" title='Předchozí korektura k opravě'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-zaneseni" title='Předchozí korektura k zaneseni'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<br>
<button type='button' id="dalsi-korektura" title='Další korektura'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-oprave" title='Další korektura k opravě'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-zaneseni" title='Další korektura k zaneseni'>
<img src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id='korektury-aktualizace'
title='Aktualizuj korektury
Nemusíš mačkat, pokud ti stačí, že se korektury aktualizují samy každé 2 minuty a při každém přidání korektury/komentáře.'
>
<img src='{% static "korektury/imgs/reload.svg" %}' alt='↻' style="width: 15px"/>
</button>
</div>
<script>
document.getElementById('predchozi-korektura').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false) });
document.getElementById('dalsi-korektura').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true) });
document.getElementById('predchozi-korektura-k-oprave').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_oprave") });
document.getElementById('dalsi-korektura-k-oprave').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_oprave") });
document.getElementById('predchozi-korektura-k-zaneseni').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_zaneseni") });
document.getElementById('dalsi-korektura-k-zaneseni').addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_zaneseni") });
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
document.getElementById("korektury-aktualizace").addEventListener("click", _ => aktualizuj_vse({}, false));
/**
* Sescrolluje na další nebo předchozí (vůči hornímu okraji okna) korekturu (v daném stavu).
* V případě neexistence takové korektury vyhodí alert.
* @param {boolean} dalsi reprezentuje, zda chceme další nebo předchozí korekturu
* @param {?string} stav pokud je nenullový, tak ignoruje korektury v jiném stavu
*/
function dalsi_nebo_predchozi_korektura(dalsi=true, stav=null) {
let predchozi = null;
for (const strana of setrizene_strany) {
// strana.setrid_korektury(); // Nemělo by být potřeba, protože se volá vždy, když se renderují korektury.
for (const korektura of strana.korektury) {
if (stav == null || korektura.stav === stav) {
const y = korektura.htmlElement.getBoundingClientRect().y;
if (y >= -1) {
if (dalsi) {
if (y > 1) {
korektura.htmlElement.scrollIntoView();
return;
}
} else {
if (predchozi !== null) predchozi.htmlElement.scrollIntoView(); else alert("Výše už není žádná taková korektura.");
return;
}
}
predchozi = korektura;
}
}
}
if (!dalsi && predchozi !== null) {
predchozi.htmlElement.scrollIntoView();
return;
}
alert("Žádná další korektura.");
}
</script>

View file

@ -1,166 +0,0 @@
{# Template starající se o editační/přidávací formulář. #}
<div id="korekturovaci-formular-div" style="display: none">
<input size="24" name="au" value="{{user.osoba}}" readonly/>
<button type="button" id="korekturovaci-formular-odesli">Oprav!</button>
<button type="button" id="korekturovaci-formular-zavri">Zavřít</button>
<br/>
<textarea id="korekturovaci-formular-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<div id="korekturovaci-formular-tagy-info">Úprava tagů celé korektury:</div>
<div id="korekturovaci-formular-tagy">
{% for tag in tagy %}
<button type="button" class="korektury-tag" value="{{tag.id}}" data-vybran="false" style="background: {{ tag.barva }}; border-color: {{ tag.barva }};">{{tag.nazev}}</button>
{% endfor %}
</div>
</div>
<script>
/** V podstatě singleton (viz korekturovaci_formular) starající se o editační/přidávací formulář. */
class _KorekturovaciFormular {
/**
* <div> obsahující celý formulář.
* @type {HTMLElement}
*/
div;
/**
* Políčko, kam uživatel vyplňuje text.
* @type {HTMLElement}
*/
text;
/**
* Tlačítko odeslat. Často ho chceme disablenout.
* @type {HTMLElement}
*/
odesilaci_button;
/**
* <div> obsahující všechny tagy, pomocí tagy.getElementsByTagName("button") umíme dělat operace nad všemi tagy.
* @type {HTMLElement}
*/
tagy;
/**
* Text upozorňující na to, že tagy nepřidáváme, ale editujeme. (Tj. chceme ho schovat, když vytváříme novou korekturu.)
* @type {HTMLElement}
*/
tagy_info;
/**
* zda při přidávání nové korektury mají být všechny tagy odvybrané, nebo mají kopírovat předchozí nastavení
* @type {boolean}
*/
pri_otevreni_odvyber_tagy;
constructor() {
this.div = document.getElementById('korekturovaci-formular-div');
this.text = document.getElementById('korekturovaci-formular-text');
this.odesilaci_button = document.getElementById('korekturovaci-formular-odesli');
const zaviraci_button = document.getElementById('korekturovaci-formular-zavri');
this.tagy = document.getElementById('korekturovaci-formular-tagy');
this.tagy_info = document.getElementById('korekturovaci-formular-tagy-info');
// ctrl-enter odešle formulář
this.text.addEventListener("keydown", ev => {
if (ev.code === "Enter" && ev.ctrlKey) this.odesli_formular();
});
zaviraci_button.addEventListener("click", _ => { this.schovej(); });
this.odesilaci_button.addEventListener("click", _ => { this.odesli_formular(); });
for (const tag of this.tagy.getElementsByTagName("button")) tag.addEventListener("click", event => { this.vyber_nebo_odvyber_tag(event); });
this.pri_otevreni_odvyber_tagy = true;
}
/**
* Přepne tag na vybraný/nevybraný (v závislosti na tom, zda byl nevybrán/vybrán)
* @param {MouseEvent} event vyvolaný kliknutím na daný tag (musí mít za event.target daný tag)
*/
vyber_nebo_odvyber_tag(event) {
const button = event.target;
button.dataset.vybran = String(button.dataset.vybran === "false");
}
/** Nastaví všechny tagy na nevybrané. */
odvyber_tagy() { for (const tag of this.tagy.getElementsByTagName("button")) tag.dataset.vybran = "false"; }
/** Schová (zavře) korekturovací formulář */
schovej() { this.div.style.display = 'none'; }
/**
* Zobrazí/otevře korekturovací formulář (bez toho, aby v něm cokoliv měnil).
* @param {Strana} strana (na které straně se má zobrazit)
* @param {number} x
* @param {number} y
*/
_zobraz(strana, x, y) {
this.odesilaci_button.disabled = false;
this.div.style.display = 'block';
this.div.style.left = x;
this.div.style.top = y;
strana.htmlElement_div.appendChild(korekturovaci_formular.div);
this.text.focus();
}
/**
* Předvyplní správně korekturovací formulář a zobrazí/otevře ho
* @param {Strana} strana (na které straně se má zobrazit)
* @param {Number} x
* @param {Number} y
* @param {string} text (text k předvyplněný, místo null chceš psáť "")
* @param {Number} komentar_id (!= -1 znamená úprava komentáře, -1 znamená přidávání korektury/komentáře)
* @param {Number} korektura_id (v případě komentar_id != -1 znamená: -1 je nová korektura, ne-1 je nový komentář)
*/
zobraz(strana, x, y, text, korektura_id=-1, komentar_id=-1) {
if (this.div.style.display !== 'none' && this.text.value !== "" && !confirm("Zavřít předchozí okénko přidávání korektury / editace komentáře?")) return;
// set hidden values
this.x = x;
this.y = y;
this.strana = strana;
this.korektura_id = korektura_id;
this.komentar_id = komentar_id;
this.text.value = text;
// show form
if (korektura_id === -1 && komentar_id === -1) {
if (this.pri_otevreni_odvyber_tagy) this.odvyber_tagy();
this.tagy_info.style.display = 'none';
} else {
const korektura = korektury[korektura_id];
this.tagy_info.style.display = 'unset';
for (const tag of this.tagy.getElementsByTagName("button"))
tag.dataset.vybran = String(korektura.tagy.has(parseInt(tag.value)));
}
this._zobraz(strana, x, y);
}
/** Shrábne data a pošle daný požadavek, čímž kromě vyřízení dané věci aktualizuje korektury+komentáře. */
odesli_formular() {
this.odesilaci_button.disabled = true;
const data = new FormData(CSRF_FORM);
data.append('x', this.x);
data.append('y', this.y);
data.append('img_id', this.strana.id);
data.append('oprava_id', this.korektura_id);
data.append('komentar_id', this.komentar_id);
const tagy = [];
for (const tag of this.tagy.getElementsByTagName("button")) {
if (tag.dataset.vybran !== "false") tagy.push(tag.value);
}
data.append('tagy', String(tagy));
data.append('text', this.text.value);
aktualizuj_vse({method: 'POST', body: data}, true, () => {this.schovej(); this.odesilaci_button.disabled = false;});
}
}
/**
* Objekt starající se o editační/přidávací formulář (jeho předvyplňování, zobrazování a posílání).
* @type {_KorekturovaciFormular}
*/
const korekturovaci_formular = new _KorekturovaciFormular();
</script>

View file

@ -1,168 +0,0 @@
{# Template starající se o jeden každý komentář u korektury. #}
{% load static %}
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='hlavicka-komentare'>
<div class='autor'>{# {{k.autor}} #}</div>
<div class='float-right'>
<button type='button' style='display: none' class="smaz-komentar" title='Smaž komentář'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='del'/>
</button>
<button type='button' class="uprav-komentar" title='Uprav komentář'>
<img src='{% static "korektury/imgs/edit.png" %}' alt='edit'/>
</button>
<button type='button' class='sbal-rozbal' title='Skrýt/Zobrazit'>
<img class='sbal-rozbal-img' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</div>
</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
<hr>
</div>
<script>
/**
* Prototyp komentáře, ze kterého se vygeneruje každý komentář (resp. jeho HTML reprezentace) v dokumentu
* @type {HTMLElement}
*/
const prekomentar = document.getElementById('prekomentar');
/**
* Mapování ID |-> komentář
* @type {Object.<Number, Komentar>}
*/
const komentare = {};
/** Třída reprezentující jeden komentář (a starající se o vytvoření a updatování jeho HTML reprezentace) */
class Komentar {
/**
* Z dat aktualizuje (v případě, že korektura s daným ID existuje) nebo vytvoří Komentar
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
* @param {Korektura} korektura ke které se komentář má připojit
*/
static aktualizuj_nebo_vytvor(komentar_data, korektura) {
const id = komentar_data['id'];
if (id in komentare) komentare[id].aktualizuj(komentar_data);
else new Komentar(komentar_data, korektura);
}
/**
* <div> se jménem autora komentáře
* @type {HTMLElement}
*/
#autor;
/**
* <div> obsahující text komentáře
* @type {HTMLElement}
*/
#text;
/**
* <div> reprezentující celý komentář
* @type {HTMLElement}
*/
htmlElement;
/** @type {Number} */
id;
/** @type{Korektura} */
korektura;
/** @type{string} */
autor;
/** @type {boolean} */
sbalen = false;
/**
* Vytvoří HTML reprezentaci, připojí komentář pod korekturu, nastaví event-listenery, uloží si data
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
* @param {Korektura} korektura korektura ke které se komentář má připojit
*/
constructor(komentar_data, korektura) {
this.htmlElement = prekomentar.cloneNode(true);
this.#autor = this.htmlElement.getElementsByClassName('autor')[0];
this.#text = this.htmlElement.getElementsByClassName('komtext')[0];
this.id = komentar_data['id'];
this.htmlElement.id = 'k' + this.id;
this.korektura = korektura;
this.korektura.pridej_htmlElement_komentare(this.htmlElement);
this.aktualizuj(komentar_data);
this.htmlElement.getElementsByClassName('sbal-rozbal')[0].addEventListener('click', _ => this.#sbal_nebo_rozbal());
this.htmlElement.getElementsByClassName('uprav-komentar')[0].addEventListener('click', _ => this.#uprav_komentar());
this.htmlElement.getElementsByClassName('smaz-komentar')[0].addEventListener('click', _ => this.#smaz_komentar());
komentare[this.id] = this;
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci komentáře
* @param {Object.<string, ?>} komentar_data „Slovník“ obsahující data daného komentáře
*/
aktualizuj(komentar_data) {
this.set_autor(komentar_data['autor']);
this.set_text(komentar_data['text']);
};
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci autora komentáře
* @param {String} autor
*/
set_autor(autor) {
this.#autor.textContent=autor;
this.autor = autor;
};
/**
* @param {String} text
*/
set_text(text) {
this.#text.innerHTML=text;
};
/** Sbalí/rozbalí (podle toho, zda byl rozbalený/sbalený) komentář, ale nezmění pozice korektur (je třeba později zavolat umisti_korektury()) */
sbal_nebo_rozbal() {
this.sbalen = !this.sbalen;
this.htmlElement.dataset.komentar_sbalen = String(this.sbalen);
}
/** Doplněk sbal_nebo_rozbal, který i přeskládá korektury. */
#sbal_nebo_rozbal(){
this.sbal_nebo_rozbal();
umisti_korektury();
}
/** Ukáže formulář na editaci komentáře (když je zmáčknuto „uprav-komentar“) */
#uprav_komentar() {
return korekturovaci_formular.zobraz(this.korektura.strana, this.korektura.x, this.korektura.y, this.#text.textContent, this.korektura.id, this.id);
}
/** Smaže komentář (když je zmáčknuto „smaz-komentar“) */
#smaz_komentar() {
if (confirm('Opravdu smazat komentář?')) {
const data = new FormData(CSRF_FORM);
data.append('komentar_id', this.id);
fetch('{% url "korektury_api_komentar_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.smaz_pouze_na_strance();
umisti_korektury();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
/** Smaže div komentáře (ne databázový záznam!), používá se, když je smazán komentář nebo jeho nadřazená korektura */
smaz_pouze_na_strance() {
delete komentare[this.id];
this.htmlElement.remove();
}
}
</script>

View file

@ -1,271 +0,0 @@
{% load static %}
<div id='prepointer' {# id='kor{{k.id}}-pointer' #}
class='pointer'
data-hover='false'
{# data-stav_korektury='{{k.status}}' #}
></div>
<div id='prekorektura' {# name='kor{{k.id}}' id='kor{{k.id}}' #}
class='korektura'
{# data-stav_korektury='{{k.status}}' #}
data-korektura_sbalena='false'
>
<div class="korektura-tagy">
{# {% for tag in k.tagy %} <span style="background:{{ tag.barva }}>{{ tag.text }}<span/> #}
</div>
<div class='korektura-telo'>
{# {% for k in k.komentare %} {% include "korektury/korekturovatko/komentar.html" %} {% endfor %} #}
</div>
<div class='hlavicka-komentare'>
<span class='float-right'>
<span class='korektura-tlacitka'>
<button type='button' style='display: none' class="smaz-korekturu" title='Smaž korekturu'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='🗑️'/>
</button>
<button type='button' class='action' value='k_oprave' title='Označ jako neopravené'>
<img src='{% static "korektury/imgs/undo.png" %}' alt='↪'/>
</button>
<button type='button' class='action' value='opraveno' title='Označ jako opravené'>
<img src='{% static "korektury/imgs/check.png" %}' alt='✔️'/>
</button>
<button type='button' class='action' value='neni_chyba' title='Označ, že se nebude měnit'>
<img src='{% static "korektury/imgs/cross.png" %}' alt='❌'/>
</button>
<button type='button' class='action' value='k_zaneseni' title='Označ jako připraveno k zanesení'>
<img src='{% static "korektury/imgs/tex.png" %}' alt='TeX'/>
</button>
<a href='{% url "admin:korektury_oprava_change" -1 %}' class='edit' title='Uprav korekturu jako takovou.' style="text-decoration: none;"> {# FIXME Udělat z toho tlačítko? #}
<img src='{% static "korektury/imgs/edit.png" %}' alt='✏️' style="opacity: 0.5;"/> {# FIXME Odlišit jinak než pomocí opacity? #}
</a>
<button type='button' class='komentovat_disabled' title='Korekturu nelze komentovat, protože už je uzavřená' disabled=''>
<img src='{% static "korektury/imgs/comment-gr.png" %}' alt='💭'/>
</button>
<button type='button' class='komentovat' title='Komentovat'>
<img src='{% static "korektury/imgs/comment.png" %}' alt='💭'/>
</button>
</span>
<button type='button' class='sbal-rozbal' title='Skrýt/Zobrazit'>
<img class='sbal-rozbal-img' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
</span>
</div>
</div>
<script>
/**
* Prototyp korektury, ze kterého se vygeneruje každý komentář (resp. jeho HTML reprezentace) v dokumentu
* @type {HTMLElement}
*/
const prekorektura = document.getElementById('prekorektura');
/**
* Prototyp pointeru (té lomené čáry od korektury)
* @type {HTMLElement}
*/
const prepointer = document.getElementById('prepointer');
/**
* Mapování ID |-> korektura
* @type {Object.<Number, Korektura>}
*/
const korektury = {};
/** Třída reprezentující jednu korekturu (a starající se o vytvoření a updatování její HTML reprezentace) */
class Korektura {
/**
* Z dat aktualizuje (v případě, že korektura s daným ID existuje) nebo vytvoří Korekturu
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
* @returns {Korektura} vytvořená/aktualizovaná Korektura (pro použití při vytváření/aktualizaci komentářů)
*/
static aktualizuj_nebo_vytvor(korektura_data) {
const id = korektura_data['id'];
if (id in korektury) return korektury[id].aktualizuj(korektura_data);
else return new Korektura(korektura_data);
}
/**
* <div> obsahující <div>y komentářů
* @type {HTMLElement}
*/
#komentare;
/**
* <div> obsahující tagy
* @type {HTMLElement}
*/
#tagy;
/**
* <div> reprezentující celý korekturu
* @type {HTMLElement}
*/
htmlElement;
/**
* <div> reprezentující pointer (tu lomenou čáru od korektury)
* @type {HTMLElement}
*/
pointer;
/** @type {Number} */
id;
/** @type {Number} */
x;
/** @type {Number} */
y;
/** @type {Strana} */
strana;
/** @type {string} */
stav;
/** @type {boolean} */
sbalena = false;
/** @type Set<Number> */
tagy;
/**
* Vytvoří HTML reprezentaci, připojí korekturu pod stranu (ale neumístí ji), nastaví event-listenery, uloží si data
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
*/
constructor(korektura_data) {
this.htmlElement = prekorektura.cloneNode(true);
this.pointer = prepointer.cloneNode(true);
this.#komentare = this.htmlElement.getElementsByClassName('korektura-telo')[0];
this.#tagy = this.htmlElement.getElementsByClassName('korektura-tagy')[0];
this.id = korektura_data['id'];
this.htmlElement.id = 'kor' + this.id;
this.pointer.id = 'kor' + this.id + '-pointer';
this.x = korektura_data['x'];
this.y = korektura_data['y'];
this.aktualizuj(korektura_data);
this.htmlElement.getElementsByClassName('sbal-rozbal')[0].addEventListener('click', _ => this.#sbal_nebo_rozbal());
for (const button of this.htmlElement.getElementsByClassName('action'))
button.addEventListener('click', async event => this.#zmen_stav_korektury(event));
this.htmlElement.getElementsByClassName('komentovat')[0].addEventListener('click', _ => this.#komentuj())
this.htmlElement.getElementsByClassName('smaz-korekturu')[0].addEventListener('click', _ => this.#smaz_korekturu());
const odkaz_editace = this.htmlElement.getElementsByClassName('edit')[0];
odkaz_editace.href = odkaz_editace.href.replace("-1", this.id);
odkaz_editace.onclick = ev => { if (!confirm("Editace korektury je velmi pokročilá featura umožňující přesouvat korekturu nebo přidávat informované orgy, opravdu chceš pokračovat do adminu?")) ev.preventDefault(); };
this.htmlElement.addEventListener('mouseover', _ => this.pointer.dataset.hover = 'true');
this.htmlElement.addEventListener('mouseout', _ => this.pointer.dataset.hover = 'false');
const cislo_strany = korektura_data['strana'];
if (cislo_strany in strany) {
this.strana = strany[cislo_strany];
this.strana.korektury.push(this);
} else alert("Někdo korekturoval stranu, která neexistuje. Dejte vědět webařům :)");
korektury[this.id] = this;
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci korektury
* @param {Object.<string, ?>} korektura_data „Slovník“ obsahující data dané korektury
* @returns {Korektura} pro jednodušší implementaci aktualizuj_nebo_vytvor vracíme this
*/
aktualizuj(korektura_data) {
this.set_stav(korektura_data['status']);
this.set_tagy(korektura_data["tagy"]);
return this;
};
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci tagů korektury
* @param {Object.<string, ?>[]} tagy
*/
set_tagy(tagy) {
this.#tagy.innerHTML = "";
this.tagy = new Set();
for (const tag of tagy) {
this.tagy.add(tag["id"]);
const span = document.createElement("span");
span.innerHTML = tag["nazev"];
span.classList.add("korektury-tag");
span.style.backgroundColor = tag["barva"];
this.#tagy.appendChild(span);
}
}
/**
* Aktualizuje/nastaví JS data i HTML reprezentaci stavu korektury
* @param {String} stav
*/
set_stav(stav) {
this.stav = stav;
this.htmlElement.dataset.stav_korektury=stav;
this.pointer.dataset.stav_korektury=stav;
};
/**
* Přidá HTML reprezentaci komentáře pod tuto korekturu
* @param {HTMLElement} htmlElement přidávaný komentář (jako HTML prvek)
*/
pridej_htmlElement_komentare(htmlElement) { this.#komentare.appendChild(htmlElement); }
/** Sbalí/rozbalí (podle toho, zda byla rozbalená/sbalená) korekturu, ale nezmění pozice korektur (je třeba později zavolat umisti_korektury()) */
sbal_nebo_rozbal() {
this.sbalena = !this.sbalena;
this.htmlElement.dataset.korektura_sbalena = String(this.sbalena);
}
/** Doplněk sbal_nebo_rozbal, který i přeskládá korektury. */
#sbal_nebo_rozbal(){
this.sbal_nebo_rozbal();
umisti_korektury();
}
/** Ukaž komentovací formulář (když je zmáčknuto komentovat) */
#komentuj() { korekturovaci_formular.zobraz(this.strana, this.x, this.y, "", this.id); }
/**
* Změní stav (když je zmáčknuto tlačítko daného stavu)
* @param {MouseEvent} event který vyvolal danou změnu (event.target.value musí být chtěný stav)
*/
#zmen_stav_korektury(event) {
const data = new FormData(CSRF_FORM);
data.append('id', this.id);
data.append('action', event.target.value);
fetch('{% url "korektury_api_oprava_stav" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
else response.json().then(data => {
this.set_stav(data['status']);
aktualizuj_pocty_stavu();
});
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
/** Smaže korekturu (když je zmáčknuto „smaz-korekturu“) */
#smaz_korekturu() {
if (confirm('Opravdu smazat korekturu?')) {
const data = new FormData(CSRF_FORM);
data.append('oprava_id', this.id);
fetch('{% url "korektury_api_oprava_smaz" %}', {method: 'POST', body: data})
.then(response => {
if (!response.ok) {alert('Něco se nepovedlo:' + response.statusText);}
this.#smaz_pouze_na_strance()
aktualizuj_pocty_stavu();
aktualizuj_pocty_zasluh();
umisti_korektury();
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
}
/** Smaže div korektury (včetně všech komentářů; ne databázový záznam!) */
#smaz_pouze_na_strance() {
this.strana.korektury.splice(this.strana.korektury.indexOf(this), 1);
delete korektury[this.id];
for (const komentar of Object.values(komentare)) if (komentar.korektura === this) komentar.smaz_pouze_na_strance();
this.htmlElement.remove();
this.pointer.remove();
}
}
</script>

View file

@ -1,86 +0,0 @@
{# Template starající se o tlačítkovou lištu nahoře, tj. hlavně o hromadné schovávání korektur. #}
Zobrazit:
<input type="checkbox" id="k_oprave_checkbox" checked>
<label for="k_oprave_checkbox">K opravě (<span id="k_oprave_pocet"></span>)</label>
<input type="checkbox" id="opraveno_checkbox" checked>
<label for="opraveno_checkbox">Opraveno (<span id="opraveno_pocet"></span>)</label>
<input type="checkbox" id="neni_chyba_checkbox" checked>
<label for="neni_chyba_checkbox">Není chyba (<span id="neni_chyba_pocet"></span>)</label>
<input type="checkbox" id="k_zaneseni_checkbox" checked>
<label for="k_zaneseni_checkbox">K zanesení (<span id="k_zaneseni_pocet"></span>)</label>
<button type="button" id="sbal-korektury">Sbal korektury</button>
<button type="button" id="rozbal-korektury">Rozbal korektury</button>
<hr/>
<script>
document.getElementById('k_oprave_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('k_oprave'));
document.getElementById('opraveno_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('opraveno'));
document.getElementById('neni_chyba_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('neni_chyba'));
document.getElementById('k_zaneseni_checkbox').addEventListener('change', () => skryj_nebo_zobraz_korektury('k_zaneseni'));
document.getElementById("sbal-korektury").addEventListener("click", () => {
for (const korektura of Object.values(korektury))
if (!korektura.sbalena) korektura.sbal_nebo_rozbal();
umisti_korektury();
})
document.getElementById("rozbal-korektury").addEventListener("click", () => {
for (const korektura of Object.values(korektury))
if (korektura.sbalena) korektura.sbal_nebo_rozbal();
umisti_korektury();
})
/**
* Změní CSS tak, aby se korektury příslušného stavu nezobrazovali/zobrazovali (v závislosti na tom, jestli byly zobrazené/nezobrazené)
* @param {string} aclass stav korektur, které mají být skryty/zobrazeny
*/
function skryj_nebo_zobraz_korektury(aclass)
{
const stylesheets = document.styleSheets;
let ssheet = null;
for (let i=0; i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (let i=0; i<ssheet.cssRules.length; i++){
const rule = ssheet.cssRules[i];
if (rule.selectorText === '[data-stav_korektury="'+aclass+'"]'){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
umisti_korektury();
}
/**
* Mapování stav korektur |-> span, kde se píše, kolik je korektur toho stavu.
* Používané v následující funcki
* @type {Object.<string, HTMLElement>}
*/
const spany_s_pocty_stavu_korektur = {
'k_oprave': document.getElementById('k_oprave_pocet'),
'opraveno': document.getElementById('opraveno_pocet'),
'neni_chyba': document.getElementById('neni_chyba_pocet'),
'k_zaneseni': document.getElementById('k_zaneseni_pocet'),
}
/** Aktualizuje počty korektur jednotlivých stavů */
function aktualizuj_pocty_stavu() {
const pocty_stavu_korektur = {};
for (const stav_korektury of Object.keys(spany_s_pocty_stavu_korektur)) pocty_stavu_korektur[stav_korektury] = 0;
for (const korektura of Object.values(korektury)) {
if (!(korektura.stav in pocty_stavu_korektur)) pocty_stavu_korektur[korektura.stav] = 0;
pocty_stavu_korektur[korektura.stav] += 1;
}
for (let [stav, pocet] of Object.entries(pocty_stavu_korektur)) spany_s_pocty_stavu_korektur[stav].innerText = pocet;
}
</script>

View file

@ -1,148 +0,0 @@
{# Template starající se o zobrazení PDF stran a o umístění korektur na ně. (O samotné korektury se stará `./korektura.html`.) #}
{% for i in indexy_stran %}
<div class='imgdiv'>
<img
id='img-{{i}}'
width='1021' height='1448'
src='/media/korektury/img/{{korekturovanepdf.get_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
class="strana"
/>
</div>
<hr/>
{% endfor %}
<script>
// Pro umisťování korektur
const HORIZONTALNI_MEZERA = 10;
const VERTIKALNI_MEZERA = 5;
const MINIMALNI_VYSKA_POINTERU = 30;
/**
* Mapování index_strany |-> strana
* @type {Object.<int, Strana>}
*/
const strany = {};
/** Třída spravující jednu stranu PDF a umisťující na ni příslušné korektury. */
class Strana {
/**
* <img> příslušící straně
* @type {HTMLElement}
*/
htmlElement_img;
/**
* <div> obalující stranu, do něj se umisťují korektury
* @type {HTMLElement}
*/
htmlElement_div;
/**
* Index strany (používá se při ukládání korektury (a načítání <img>))
* @type {Number}
*/
id;
/**
* Korektury na příslušné straně (BÚNO setříděné podle vertikálního umístění)
* @type {Korektura[]}
*/
korektury;
/**
* Uloží si data (včetně pointrů na správné části HTML DOMu) a nastaví event-listener
* @param {HTMLElement} htmlElement_img
*/
constructor(htmlElement_img) {
this.htmlElement_img = htmlElement_img;
this.htmlElement_div = this.htmlElement_img.parentNode;
this.id = parseInt(this.htmlElement_img.id.substring(4));
this.korektury = []
this.htmlElement_img.addEventListener('click', event => this.#korekturuj(event));
strany[this.id] = this;
}
/**
* Otevře korekturovací formulář pro přidání korektury v daném místě
* @param {MouseEvent} event
*/
#korekturuj(event) {
switch (document.body.dataset.stav_pdf) {
case 'zanaseni':
if (!confirm('Právě jsou zanášeny korektury, opravdu chcete přidat novou?')) return;
break;
case 'zastarale':
if (!confirm('Toto PDF je již zastaralé, opravdu chcete vytvořit korekturu?')) return;
break;
}
let dx, dy;
if (event.pageX != null) {
dx = event.pageX - this.htmlElement_div.offsetLeft;
dy = event.pageY - this.htmlElement_div.offsetTop;
} else { //IE a další
dx = event.offsetX;
dy = event.offsetY;
}
korekturovaci_formular.zobraz(this, dx, dy, '');
console.log("Pro přesun korektur: strana = " + this.id + ", x = " + dx + ", y = " + dy);
}
/** Setřídí seznam korektur příslušný dané straně */
setrid_korektury() { this.korektury.sort((a, b) => a.y - b.y); }
/** Zobrazí korektury a jejich pointry (a umístí je správně pod sebe) na dané straně */
umisti_korektury() {
this.setrid_korektury()
const w = this.htmlElement_img.clientWidth;
let spodek_posledni_korektury = 0;
for (const korektura of this.korektury) {
const x = korektura.x;
const y = korektura.y;
const pointer = korektura.pointer;
this.htmlElement_div.appendChild(pointer);
this.htmlElement_div.appendChild(korektura.htmlElement);
const delta_y = (y > spodek_posledni_korektury) ? 0: spodek_posledni_korektury - y + VERTIKALNI_MEZERA;
pointer.style.left = x;
pointer.style.top = y;
pointer.style.width = w - x + HORIZONTALNI_MEZERA;
pointer.style.height = MINIMALNI_VYSKA_POINTERU + delta_y;
korektura.htmlElement.style.left = w + HORIZONTALNI_MEZERA;
korektura.htmlElement.style.top = y + delta_y;
spodek_posledni_korektury = Math.max(
spodek_posledni_korektury,
korektura.htmlElement.offsetTop + korektura.htmlElement.offsetHeight + VERTIKALNI_MEZERA
); // FIXME nemám páru, proč +VERTIKALNI_MEZERA funguje, ale opravuje to bug, že nově vytvořené korektury za sebou neměly mezeru
}
this.htmlElement_div.style.height = "unset";
if (this.htmlElement_div.offsetHeight < spodek_posledni_korektury)
this.htmlElement_div.style.height = spodek_posledni_korektury;
}
}
// Vytvoření objektu Strana pro každou stranu
for (const strana_img of document.getElementsByClassName('strana'))
new Strana(strana_img);
/**
* Seznam stran setřízený podle toho, jak jdou po sobě (aby se dali korektury prohledávat od první na HTML stránce po poslední)
* @type {Strana[]}
*/
const setrizene_strany = Object.values(strany);
setrizene_strany.sort((a, b) => a.htmlElement_img.offsetTop - b.htmlElement_img.offsetTop);
/** Zobrazí korektury a jejich pointry (a umístí je správně pod sebe) na všech stranách */
function umisti_korektury() { for (const strana of Object.values(strany)) strana.umisti_korektury(); }
</script>

View file

@ -14,7 +14,7 @@ def send_email_notification_komentar(oprava: Oprava, autor: Organizator, request
# parametry e-mailu # parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk) #odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk})) odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#kor{oprava.id}-pointer" odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz' from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev) subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = [] texty = []

View file

@ -49,11 +49,11 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
class KorekturyView(generic.DetailView): class KorekturyView(generic.DetailView):
model = KorekturovanePDF model = KorekturovanePDF
pk_url_kwarg = "pdf" pk_url_kwarg = "pdf"
template_name = 'korektury/korekturovatko/html_obal.html' template_name = 'korektury/korekturovatko/htmlstrana.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['indexy_stran'] = range(self.object.stran) context['img_indexes'] = range(self.object.stran)
context['tagy'] = KorekturaTag.objects.all() context['tagy'] = KorekturaTag.objects.all()
return context return context

View file

@ -8,7 +8,3 @@
color: #aaa; color: #aaa;
} }
} }
.hodnoceni.zvyraznene {
background-color: var(--svetla-oranzova);
}

View file

@ -1,13 +1,11 @@
{% extends "odevzdavatko/base.html" %} {% extends "base.html" %}
{% load static %} {% load static %}
{% load deadliny %} {% load deadliny %}
{% load mail %} {% load mail %}
{% load jmena %} {% load jmena %}
{% load orgove %}
{# Přišlo mi to hezčí, než psát všude if. #} {# Přišlo mi to hezčí, než psát všude if. #}
{% block custom_css %} {% block custom_css %}
{{ block.super }}
{% if object.resitele.count == 1 %} {% if object.resitele.count == 1 %}
<style>.teamovaCast {display: none}</style> <style>.teamovaCast {display: none}</style>
{% endif %} {% endif %}
@ -113,7 +111,7 @@
<tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑‍🤝‍🧑</th><th class="teamovaCast">{# ❤ #}🧑‍🤝‍🧑∑</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr> <tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑‍🤝‍🧑</th><th class="teamovaCast">{# ❤ #}🧑‍🤝‍🧑∑</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr>
{% for subform in form %} {% for subform in form %}
<tbody> <tbody>
<tr class="hodnoceni{% if subform.problem.initial|ma_opravovatele:user %} zvyraznene{% endif %}"> <tr class="hodnoceni">
<td>{{ subform.problem }}</td> <td>{{ subform.problem }}</td>
<td class="bodovani">{{ subform.body }}</td> <td class="bodovani">{{ subform.body }}</td>
<td class="bodovani">{{ subform.body_celkem }}</td> <td class="bodovani">{{ subform.body_celkem }}</td>

View file

@ -12,22 +12,6 @@ POZOR! Kolize jmen! Dva řešitelé mají stejné makro!
{% autoescape off %} {% autoescape off %}
{% load tex %} {% load tex %}
\ExplSyntaxOn
\char_set_catcode_other:n{32}% Odsud dál do \char_set_catcode_space:n{32} nesmí být za žádnou cenu jiná mezera (tj. i zlom řádku) než ty mezi jménem a příjmením
\prop_const_from_keyval:Nn\g_tituly%
{%
{% for r in resitele %}{{r|sloz}}={% if r.titul == '' %}{}{% else %}{\titul{{r.titul|sloz}}}{% endif %},%
{% endfor %}}%
\char_set_catcode_space:n{32}
\DeclareDocumentCommand\Titul{mO{#1}}{%
\prop_if_in:NnTF\g_tituly{#1}%
{\prop_item:Nn\g_tituly{#1}}%
{\ClassError{mam}{Titul pro #1 nenalezen!}{}}%
#2%
}
\ExplSyntaxOff
{% for r in resitele %} {% for r in resitele %}
{% if r.titul == '' %} {% if r.titul == '' %}
{% spaceless %} {% spaceless %}