Merge branch 'korekturovatko-full' into korekturovatko

This commit is contained in:
Jonas Havelka 2025-01-22 20:28:31 +01:00
commit 7676b0ef60
9 changed files with 362 additions and 221 deletions

View file

@ -5,4 +5,5 @@ from . import views
urlpatterns = [
path('<int:pdf_id>/stav', org_required(views.korektury_stav_view), name='korektury_api_pdf_stav'),
path('oprava/stav', org_required(views.oprava_stav_view), name='korektury_api_oprava_stav'),
path('<int:pdf_id>/opravy_a_komentare', org_required(views.opravy_a_komentare_view), name='korektury_api_opravy_a_komentare'),
]

View file

@ -1,5 +1,6 @@
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.html import linebreaks
from django.views.decorators.csrf import csrf_exempt
from rest_framework import serializers
@ -32,3 +33,33 @@ def oprava_stav_view(request, **kwargs):
op.status = status
op.save()
return JsonResponse({'status': op.status})
class KomentarSerializer(serializers.ModelSerializer):
class Meta:
model = Komentar
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["autor"] = str(instance.autor)
ret["text"] = linebreaks(ret["text"], autoescape=True) # Autora není třeba escapovat, ten se vkládá jako text.
return ret
class OpravaSerializer(serializers.ModelSerializer):
class Meta:
model = Oprava
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["komentare"] = [KomentarSerializer(komentar).data for komentar in instance.komentar_set.all()]
return ret
# komentar_set = serializers.ListField(child=KomentarSerializer())
def opravy_a_komentare_view(request, pdf_id: int, **kwargs):
q = request.POST
opravy = Oprava.objects.filter(pdf=pdf_id).all()
# Serializovat list je prý security vulnerability, tedy je přidán slovník pro bezpečnost
return JsonResponse({"context": [OpravaSerializer(oprava).data for oprava in opravy]})

View file

@ -1,5 +1,6 @@
.textzanaseni { display:none; }
.textzastarale { display:none; }
#prekomentar, #preoprava, #prepointer { display: none; }
body {
&[data-status="pridavani"] {
@ -79,6 +80,7 @@ img{background:white;}
padding: 3px;
border: 2px solid rgb(var(--rgb));
border-radius: 10px;
position: absolute;
&:hover {
border-width:3px;
@ -96,6 +98,8 @@ img{background:white;}
}
}
button img { pointer-events: none; }
.corr-header {
overflow: auto;
}

View file

@ -1,129 +1,46 @@
const W_SKIP = 10;
const H_SKIP = 5;
const POINTER_MIN_H = 30;
function place_comments_one_div(img_id, comments)
{
var img = document.getElementById(img_id);
if( img == null ) {
return;
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);
}
var par = img.parentNode;
var w = img.clientWidth;
var h = img.clientHeight;
var w_skip = 10;
var h_skip = 5;
var pointer_min_h = 30;
var bott_max = 0;
var comments_sorted = comments.sort(function (a,b) {
return a[2] - b[2];
//pokus o hezci kladeni poiteru, ale nic moc
if( a[3] < b[3] ) {
return (a[2] + pointer_min_h)- b[2];
} else {
return (a[2] - pointer_min_h)- b[2];
}
});
for (c in comments_sorted) {
var id = comments_sorted[c][0];
var x = comments_sorted[c][1];
var y = comments_sorted[c][2];
var el = document.getElementById(id);
var elp = document.getElementById(id + "-pointer");
if( el == null || elp == null ) {
continue;
}
par.appendChild(elp);
par.appendChild(el);
var delta_y = (y > bott_max) ? 0: bott_max - y + h_skip;
elp.style.left = x;
elp.style.top = y ;
elp.style.width = w - x + w_skip;
elp.style.height = pointer_min_h + delta_y;
el.pointer = elp;
el.img_id = img_id;
el.x = x
el.y = y
el.style.position = 'absolute';
el.style.left = w + w_skip;
el.style.top = y + delta_y;
var bott = el.offsetTop + el.offsetHeight;
bott_max = ( bott_max > bott ) ? bott_max : bott;
}
if( par.offsetHeight < bott_max ) {
par.style.height = bott_max;
}
if (par.offsetHeight < bott_max) par.style.height = bott_max;
}
function place_comments() {
for (var i=0; i < comments.length-1; i++) {
place_comments_one_div(comments[i][0], comments[i][1])
for (let [img_id, opravy] of Object.entries(comments)) {
place_comments_one_div(img_id, opravy)
}
}
// show comment form, when clicked to image
function img_click(element, 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;
}
var dx, dy;
var par = element.parentNode;
if( ev.pageX != null ) {
dx = ev.pageX - par.offsetLeft;
dy = ev.pageY - par.offsetTop;
} else { //IE
dx = ev.offsetX;
dy = ev.offsetY;
}
var img_id = element.id;
return show_form(img_id, dx, dy, '', '', '');
}
// hide or show text of correction
function toggle_visibility(oprava){
oprava.dataset.opravazobrazit = oprava.dataset.opravazobrazit !== 'true';
for (var i=0;i<comments.length-1;i++){
place_comments_one_div(comments[i][0], comments[i][1])
}
}
// show comment form, when 'comment' button pressed
function comment(op)
{
return show_form(op.img_id, op.x, op.y, op.dataset.opid, "", "comment");
}
// show comment form when 'update-comment' button pressed
function update_comment(oid,ktid)
{
var op = document.getElementById(oid);
var text = document.getElementById(ktid).textContent;
return show_form(op.img_id, op.x, op.y, ktid.substring(2), text, 'update-comment');
}
function oprava_onmouseover(op)
{
var pointer = op.pointer;
pointer.dataset.highlight = "true";
}
function oprava_onmouseout(op)
{
var pointer = op.pointer;
pointer.dataset.highlight = "false";
}

View file

@ -38,7 +38,7 @@
//fill up comment form and show him
function show_form(img_id, dx, dy, id, text, action) {
var img = document.getElementById(img_id);
const img = document.getElementById("img-" + img_id);
if (commform._div.style.display !== 'none' && commform._text.value !== "" && !confirm("Zavřít předchozí okénko přidávání korektury / editace komentáře?")) return 1;

View file

@ -1,23 +1,86 @@
{% load static %}
<div class='comment' id='k{{k.id}}'>
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='corr-header'>
<div class='author'>{{k.autor}}</div>
<div class="float-right">
<!-- Komentar !-->
<form action='' method='POST'>
{% csrf_token %}
<input type='hidden' name='id' value='{{k.id}}'>
<button style="display: none" type='submit' name='action' value='del-comment' title='Smaž komentář'
onclick='return confirm("Opravdu smazat komentář?")'>
<img src="{% static "korektury/imgs/delete.png" %}"/>
</button>
</form>
<!-- /Komentar !-->
<button type='button' onclick="update_comment('op{{o.id}}','kt{{k.id}}');" title='Uprav komentář'>
<img src="{% static "korektury/imgs/edit.png"%}"/>
<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 id='kt{{k.id}}'>{{k.text|linebreaks}}</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
</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; #}
/**
*
* @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;};
set_text(text) {
this.#text.innerHTML=text;
};
// show comment form when 'update-comment' button pressed
#update_comment() {
return show_form(this.oprava.img_id, this.oprava.x, this.oprava.y, this.id, this.#text.textContent, 'update-comment');
}
#delete_comment() {
if (confirm('Opravdu smazat komentář?')) {
throw {name : 'NotImplementedError', message: '(Webaři jsou) too lazy to implement'};
}
}
}
</script>

View file

@ -1,63 +1,145 @@
{% load static %}
<div
id='op{{o.id}}-pointer'
<div id='prepointer' {# id='op{{o.id}}-pointer' #}
class='pointer'
data-highlight="false"
data-opravastatus="{{o.status}}"
>
</div>
<div name='op{{o.id}}' id='op{{o.id}}'
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"
data-opid="{{o.id}}"
onmouseover='oprava_onmouseover(this)'
onmouseout='oprava_onmouseout(this)'
{# data-opravastatus='{{o.status}}' #}
data-opravazobrazit='true'
>
<div class='corr-body'>
{% for k in o.komentare %}
{% include "korektury/korekturovatko/__komentar.html" %}
<hr>
{% endfor %}
{# {% for k in o.komentare %} {% include "korektury/korekturovatko/__komentar.html" %} <hr> {% endfor %} #}
</div>
<div class='corr-header'>
<span class='float-right'>
<span class='corr-buttons'>
<form action='' method='POST'>
{% csrf_token %}
<input type='hidden' name='id' value='{{o.id}}'>
<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>
<button style="display: none" type='submit' name='action' value='del' title='Smaž opravu'>
<img src="{% static "korektury/imgs/delete.png"%}"/>
</button>
<button type='submit' name='action' value='{{ o.STATUS.K_OPRAVE }}' title='Označ jako neopravené'>
<img src="{% static "korektury/imgs/undo.png"%}"/>
</button>
<button type='submit' name='action' value='{{ o.STATUS.OPRAVENO }}' title='Označ jako opravené'>
<img src="{% static "korektury/imgs/check.png"%}"/>
</button>
<button type='submit' name='action' value='{{ o.STATUS.NENI_CHYBA }}' title='Označ, že se nebude měnit'>
<img src="{% static "korektury/imgs/cross.png" %}"/>
</button>
<button type='submit' name='action' value='{{ o.STATUS.K_ZANESENI }}' title='Označ jako připraveno k zanesení'>
<img src="{% static "korektury/imgs/tex.png" %}"/>
</button>
</form>
<button type='button' class='notcomment' 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='comment' title='Komentovat'>
<img src='{% static "korektury/imgs/comment.png" %}' alt='💭'/>
</button>
</span>
<button type='button' value="notcomment" title='Korekturu nelze komentovat, protože už je uzavřená'>
<img src="{% static "korektury/imgs/comment-gr.png" %}"/>
</button>
<button type='button' value="comment" onclick='comment(this.parentElement.parentElement.parentElement.parentElement);' title='Komentovat'>
<img src="{% static "korektury/imgs/comment.png" %}"/>
</button>
</span>
<button type='button' onclick='toggle_visibility(this.parentElement.parentElement.parentElement);' title='Skrýt/Zobrazit'>
<img class='toggle-button' src="{% static "korektury/imgs/hide.png" %}"/>
</button>
<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;
htmlElement; pointer;
id; x; y; img_id; zobrazit = true; {# oprava_data; #}
constructor(oprava_data) {
this.htmlElement = preoprava.cloneNode(true);
this.pointer = prepointer.cloneNode(true);
this.#komentare = this.htmlElement.getElementsByClassName('corr-body')[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('comment')[0].addEventListener('click', _ => this.#comment())
this.htmlElement.getElementsByClassName('del')[0].addEventListener('click', _ => this.#delete());
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']);
return this;
};
set_status(status) {
this.htmlElement.dataset.opravastatus=status;
this.pointer.dataset.opravastatus=status;
};
add_komentar_htmlElement(htmlElement) {
this.#komentare.appendChild(htmlElement);
this.#komentare.appendChild(document.createElement('hr'));
}
// hide or show text of correction
#toggle_visibility(){
this.zobrazit = !this.zobrazit;
this.htmlElement.dataset.opravazobrazit = String(this.zobrazit);
place_comments()
}
// show comment form, when 'comment' button pressed
#comment() { return show_form(this.img_id, this.x, this.y, this.id, "", "comment"); }
#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']));
})
.catch(error => {alert('Něco se nepovedlo:' + error);});
}
#delete() {
if (confirm('Opravdu smazat korekturu?')) {
throw {name : 'NotImplementedError', message: '(Webaři jsou) too lazy to implement'};
}
}
}
</script>

View file

@ -0,0 +1,50 @@
{% for i in img_indexes %}
<div class='imgdiv'>
<img
id='img-{{i}}'
width='1021' height='1448'
src='/media/korektury/img/{{img_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
class="strana"
/>
</div>
<hr/>
{% endfor %}
<script>
// Mapování stránka -> korektury
const comments = {
{% for s in opravy_strany %}
{{s.strana}}: []{% 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;
return show_form(img_id, dx, dy, '', '', '');
});
}
</script>

View file

@ -1,47 +1,40 @@
{% include "korektury/korekturovatko/__edit_komentar.html" %}
{% for i in img_indexes %}
<div class='imgdiv'>
<img
width='1021' height='1448'
onclick='img_click(this,event)' id='img-{{i}}'
src='/media/korektury/img/{{img_prefix}}-{{i}}.png'
alt='Strana {{ i|add:1 }}'
/>
</div>
{% include "korektury/korekturovatko/__stranky.html" %}
<hr/>
{% endfor %}
{% for o in opravy %}
{% include "korektury/korekturovatko/__oprava.html" %}
{% endfor %}
{# {% for o in opravy %} {% include "korektury/korekturovatko/__oprava.html" %} {% endfor %} #}
{% include "korektury/korekturovatko/__oprava.html" %}
{% include "korektury/korekturovatko/__komentar.html" %}
<script>
var comments = [
{% for s in opravy_strany %}
["img-{{s.strana}}", [{% for o in s.op_id %}["op{{o.id}}",{{o.x}},{{o.y}}],{% endfor %}[]]],
{% endfor %}
[]]
</script>
<script>
function zmenStavKorektury(event) {
event.preventDefault();
const oprava = event.target.parentElement.parentElement.parentElement.parentElement;
const data = new FormData(event.target);
data.append("action", event.submitter.value);
fetch("{% url 'korektury_api_oprava_stav' %}", {method: "POST", body: data})
/**
*
* @param {RequestInit} data
* @param {Boolean} catchError
*/
function update_all(data={}, catchError=true) { // FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval GET.
fetch('{% url "korektury_api_opravy_a_komentare" pdf.id %}', data)
.then(response => {
if (!response.ok) {alert("Něco se nepovedlo:" + response.statusText);}
else response.json().then(data => {oprava.dataset.opravastatus = data["status"]; oprava.previousElementSibling.dataset.opravastatus = data["status"]});
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);
}
}
place_comments();
});
})
.catch(error => {alert("Něco se nepovedlo:" + error);});
.catch(error => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
for (const form of document.querySelectorAll(".corr-buttons form")) form.addEventListener('submit', async event => { zmenStavKorektury(event); });
update_all();
</script>
<form id='CSRF_form' style='display: none'>{% csrf_token %}</form>
<script>
const CSRF_FORM = document.getElementById('CSRF_form');
</script>