Compare commits

..

56 commits

Author SHA1 Message Date
7676b0ef60 Merge branch 'korekturovatko-full' into korekturovatko 2025-01-22 20:28:31 +01:00
95b46541c0 Uhlazení JavaScriptu… 2025-01-22 20:28:24 +01:00
0af99d4f3e Aktualizace všech komentářů jako funkce (a aktualizace, ne vytvoření) 2025-01-22 20:23:12 +01:00
f369110cd3 Stránky PDF zvlášť 2025-01-22 19:36:44 +01:00
c54e11f25a Uhlazení JavaScriptu 2025-01-22 19:31:42 +01:00
e205ca52d3 Korektury načtené z API (místo v templatu)… 2025-01-22 18:38:28 +01:00
44a8649d0e Vytváření korektur z jednoho prototypu 2025-01-22 15:39:34 +01:00
a4175f836e Zvýrazňování čar (pointrů) pomocí atributu místo třídy 2025-01-22 11:25:19 +01:00
2c627b3d60 Když už jsem u toho, tak event.keyCode -> event.code 2025-01-21 13:01:57 +01:00
5f904b5c66 Vytažení commform konstant z funkce showform (budu potřebovat pro submit) 2025-01-21 12:58:23 +01:00
94ca903cec Chybějící const 2025-01-21 12:01:47 +01:00
50936e2b50 Prázdné okno editace komentáře klidně zavřít 2025-01-21 11:53:12 +01:00
4a35a63f31 Zbavení se deprecated věci 2025-01-21 10:49:14 +01:00
4006ecd6b8 A ještě jedno uhlazení CSS korekturovátka 2025-01-21 10:24:43 +01:00
ce2d183446 Ještě jedno uhlazení CSS korekturovátka 2025-01-21 10:23:48 +01:00
1ff69f943e Uhlazení CSS korekturovátka 2025-01-21 10:15:42 +01:00
050ebba03b Nepoužívaná css 2025-01-21 10:06:11 +01:00
443a226943 box -> oprava 2025-01-21 10:05:27 +01:00
7da9aa5fe3 Upozorňovat při zavírání okénka editace komentáře 2025-01-21 09:40:42 +01:00
e257f31ea5 x,y uložené v opravě (nemusíme tahat z čáry (pointru)) 2025-01-21 09:28:46 +01:00
87466ce2b6 box -> oprava 2025-01-21 09:23:20 +01:00
291f39990d Vyčištění IDček v html opravy 2025-01-21 09:12:36 +01:00
b637aed7ab Zapomenutý switch podle document.body.class místo podle atributu 2025-01-21 08:22:34 +01:00
3e2469fc45 Tady je těch prázných stringů nějak moc 2025-01-21 08:20:34 +01:00
d5c57da921 Čára (pointer) jako atribut opravy místo grepování "opid-pointer" 2025-01-21 08:17:12 +01:00
370dd7d841 Už nemáme text v opravě, takže nemá smysl ho editovat 2025-01-21 08:12:42 +01:00
d818ce251b Box_edit za pomoci objektu opravy místo id 2025-01-21 08:10:13 +01:00
4121de260e Ha, tohle jsem chtěl tady 2025-01-21 07:48:05 +01:00
a8b7788d35 Merge branch 'master' into korekturovatko 2025-01-21 07:40:42 +01:00
9aa3a5154d Přesnější query (aby nebral i jiné formuláře) 2025-01-21 07:39:55 +01:00
7a4c8239f6 Čára (pointer) opravy nemá vůbec interagovat s klikáním 2025-01-21 07:37:36 +01:00
2deccfada4 Oprava URL 2025-01-20 23:48:54 +01:00
d2e199e509 Dynamický update stavu opravy 2025-01-20 23:40:06 +01:00
9b0fe3d32f Tohle tu nějak zůstalo 2025-01-20 22:35:46 +01:00
af41ca5784 Čistka scrollování 2025-01-20 22:11:53 +01:00
ad2d1e676c Zobrazení tlačítek podle atributu (ne v podle stavu v templatech) 2025-01-20 22:11:02 +01:00
00e0ed0a50 Skrývání korektur pomocí atributu 2025-01-20 21:45:37 +01:00
5563eb681c Status korektury jako atribut a ne třída 2025-01-20 21:06:33 +01:00
3b74772949 Přeházení JavaScriptových věcí 2025-01-20 19:12:27 +01:00
ee69bf4c4f Drobný úklid 2025-01-20 18:38:47 +01:00
dd86fc1fcb Dynamický update stavu 2024-12-12 13:52:55 +01:00
341ae7ce45 Rozlišení lokálního/testovacího/produkčního webu v korekturovátku 2024-12-12 12:51:32 +01:00
7906c87733 Zanášení a zastaralé pomocí atributu místo třídy 2024-12-12 12:50:56 +01:00
daf24ff981 Rozstřílení korekturovátka (html) 2024-12-12 12:33:33 +01:00
0dde05f102 Odstranění textu a autora z opravy (přesun do prvního komentáře) 2024-12-12 11:50:17 +01:00
3528a44d3c Úprava chování korektury (schovává text korektury!!!) 2024-12-12 11:24:17 +01:00
524593b7ef Merge branch 'master' into korekturovatko 2024-12-10 19:03:22 +01:00
22e88daf02 Korektury api init 2024-12-03 20:10:41 +01:00
04508206bb Funkce na posílání e-mailu rozhodně nemá být uvnitř View 2024-12-03 19:41:12 +01:00
8d09fd5389 Status opravy řešit přes enum 2024-12-03 19:10:36 +01:00
9df34c22e0 Moderní přístup k choices (umožňuje např. vytáhnout seznam všech hodnot) 2024-12-03 19:01:33 +01:00
d3d5484d0e Form se nikde nepoužívá 2024-12-03 18:37:23 +01:00
62160e8440 PDF už dostáváme v URL, není to potřeba shánět znovu 2024-12-03 18:33:26 +01:00
1e6e6118a7 Nepoužívat náhodné stringy, když už máme nějaké definované… 2024-12-03 18:20:25 +01:00
c1440687aa Nepotřebné importy… 2024-12-03 17:51:33 +01:00
69e870f958 Podle mě funguje… 2024-12-03 17:50:48 +01:00
46 changed files with 956 additions and 1026 deletions

View file

7
korektury/api/apps.py Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'korektury.api'
label = 'korektury_api' # Protože jedno api už máme.

View file

9
korektury/api/urls.py Normal file
View file

@ -0,0 +1,9 @@
from django.urls import path
from personalni.utils import org_required
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'),
]

65
korektury/api/views.py Normal file
View file

@ -0,0 +1,65 @@
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
from korektury.utils import send_email_notification_komentar
from korektury.models import Oprava, KorekturovanePDF, Komentar
from personalni.models import Organizator, Osoba
def korektury_stav_view(request, pdf_id: int, **kwargs):
q = request.POST
pdf = get_object_or_404(KorekturovanePDF, id=pdf_id)
status = q.get('state')
if status is not None:
assert status in KorekturovanePDF.STATUS.values
pdf.status = status
pdf.save()
return JsonResponse({'status': pdf.status})
def oprava_stav_view(request, **kwargs):
q = request.POST
op_id_str = q.get('id')
assert op_id_str is not None
op_id = int(op_id_str)
op = get_object_or_404(Oprava, id=op_id)
status = q.get('action')
if status is not None:
assert status in Oprava.STATUS.values
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,14 +0,0 @@
from django import forms
class OpravaForm(forms.Form):
""" formulář k přidání opravy (:class:`korektury.models.Oprava`) """
text = forms.CharField(max_length=256)
autor = forms.CharField(max_length=20)
x = forms.IntegerField()
y = forms.IntegerField()
scroll = forms.CharField(max_length=256)
pdf = forms.CharField(max_length=256)
img_id = forms.CharField(max_length=256)
id = forms.CharField(max_length=256)
action = forms.CharField(max_length=256)

View file

@ -0,0 +1,45 @@
# Generated by Django 4.2.16 on 2024-12-12 10:25
from django.db import migrations
import datetime
from django.utils import timezone
def oprava2komentar(apps, schema_editor):
Oprava = apps.get_model('korektury', 'Oprava')
Komentar = apps.get_model('korektury', 'Komentar')
for o in Oprava.objects.all():
Komentar.objects.create(oprava=o, text=o.text, autor=o.autor, cas=timezone.make_aware(datetime.datetime.fromtimestamp(0)))
def komentar2oprava(apps, schema_editor):
Oprava = apps.get_model('korektury', 'Oprava')
Komentar = apps.get_model('korektury', 'Komentar')
for o in Oprava.objects.all():
k = Komentar.objects.filter(oprava=o).first()
o.text = k.text
o.autor = k.autor
o.save()
k.delete()
class Migration(migrations.Migration):
dependencies = [
('korektury', '0024_vic_orgu_k_pdf'),
]
operations = [
migrations.RunPython(oprava2komentar, komentar2oprava),
migrations.RemoveField(
model_name='oprava',
name='autor',
),
migrations.RemoveField(
model_name='oprava',
name='text',
),
]

View file

@ -52,16 +52,13 @@ class KorekturovanePDF(models.Model):
stran = models.IntegerField(u'počet stran', help_text='Počet stran PDF', stran = models.IntegerField(u'počet stran', help_text='Počet stran PDF',
default=0) default=0)
STATUS_PRIDAVANI = 'pridavani'
STATUS_ZANASENI = 'zanaseni' class STATUS(models.TextChoices):
STATUS_ZASTARALE = 'zastarale' PRIDAVANI = 'pridavani', 'Přidávání korektur'
STATUS_CHOICES = ( ZANASENI = 'zanaseni', 'Korektury jsou zanášeny'
(STATUS_PRIDAVANI, u'Přidávání korektur'), ZASTARALE = 'zastarale', 'Stará verze, nekorigovat'
(STATUS_ZANASENI, u'Korektury jsou zanášeny'),
(STATUS_ZASTARALE, u'Stará verze, nekorigovat'), status = models.CharField(u'stav PDF',max_length=16, choices=STATUS.choices, blank=False, default = STATUS.PRIDAVANI)
)
status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False,
default = STATUS_PRIDAVANI)
poslat_mail = models.BooleanField('Poslat mail o novém PDF', default=True, poslat_mail = models.BooleanField('Poslat mail o novém PDF', default=True,
help_text='Určuje, zda se má o nově nahraném PDF poslat e-mail do mam-org. Při upravování existujícího souboru už nemá žádný vliv.', help_text='Určuje, zda se má o nově nahraném PDF poslat e-mail do mam-org. Při upravování existujícího souboru už nemá žádný vliv.',
@ -152,32 +149,13 @@ class Oprava(models.Model):
x = models.IntegerField(u'x-ová souřadnice bugu') x = models.IntegerField(u'x-ová souřadnice bugu')
y = models.IntegerField(u'y-ová souřadnice bugu') y = models.IntegerField(u'y-ová souřadnice bugu')
STATUS_K_OPRAVE = 'k_oprave' class STATUS(models.TextChoices):
STATUS_OPRAVENO = 'opraveno' K_OPRAVE = 'k_oprave', 'K opravě'
STATUS_NENI_CHYBA = 'neni_chyba' OPRAVENO = 'opraveno', 'Opraveno'
STATUS_K_ZANESENI = 'k_zaneseni' NENI_CHYBA = 'neni_chyba', 'Není chyba'
STATUS_CHOICES = ( K_ZANESENI = 'k_zaneseni', 'K zanesení do TeXu'
(STATUS_K_OPRAVE, u'K opravě'),
(STATUS_OPRAVENO, u'Opraveno'),
(STATUS_NENI_CHYBA, u'Není chyba'),
(STATUS_K_ZANESENI, u'K zanesení do TeXu'),
)
status = models.CharField(u'stav opravy',max_length=16, choices=STATUS_CHOICES, blank=False,
default = STATUS_K_OPRAVE)
autor = models.ForeignKey(Organizator, blank = True,
help_text='Autor opravy',
null = True, on_delete=models.SET_NULL)
text = models.TextField(u'text opravy',blank = True, help_text='Text opravy')
# def __init__(self,dictionary):
# for k,v in dictionary.items():
# setattr(self,k,v)
def __str__(self):
return '{} od {}: {}'.format(self.status,self.autor,self.text)
status = models.CharField(u'stav opravy',max_length=16, choices=STATUS.choices, blank=False, default = STATUS.K_OPRAVE)
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 B

After

Width:  |  Height:  |  Size: 307 B

View file

@ -1,136 +1,159 @@
body, .textzanaseni { display:none; }
.adding{ .textzastarale { display:none; }
background: #f3f3f3; #prekomentar, #preoprava, #prepointer { display: none; }
color: black;
} body {
.comitting &[data-status="pridavani"] {
{ background: #f3f3f3;
background: yellow; }
}
.deprecated { &[data-status="zanaseni"] {
background: red; background: yellow;
.textzanaseni { display: unset; }
}
&[data-status="zastarale"] {
background: red;
.textzastarale { display: unset; }
}
} }
img{background:white;} img{background:white;}
/* Barvy korektur */ /* Barvy korektur */
.k_oprave { [data-opravastatus="k_oprave"] {
--rgb: 255, 0, 0; --rgb: 255, 0, 0;
[value="k_oprave"] { display: none }
[value="notcomment"] { display: none }
} }
.opraveno { [data-opravastatus="opraveno"] {
--rgb: 0, 0, 255; --rgb: 0, 0, 255;
[value="opraveno"] { display: none }
[value="comment"] { display: none }
} }
.neni_chyba { [data-opravastatus="neni_chyba"] {
--rgb: 128, 128, 128; --rgb: 128, 128, 128;
[value="neni_chyba"] { display: none }
[value="comment"] { display: none }
} }
.k_zaneseni { [data-opravastatus="k_zaneseni"] {
--rgb: 0, 255, 0; --rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none }
[value="notcomment"] { display: none }
} }
.pointer-hi, /* Skrývání korektur */
[data-opravazobrazit="false"] {
.corr-body { display: none; }
.corr-buttons { display: none; }
.toggle-button { transform: rotate(180deg); }
}
/* Čára od textu k místu korektury */
.pointer{ .pointer{
position:absolute; position:absolute;
/*border-bottom-left-radius: 10px; */ border-bottom-left-radius: 10px;
border-left: 2px solid yellow; border-left: 1px solid rgb(var(--rgb),var(--alpha));
border-bottom: 2px solid yellow; border-bottom: 1px solid rgb(var(--rgb),var(--alpha));
border-color: rgb(var(--rgb),var(--alpha)); pointer-events: none;
--alpha: 0.35;
/* Zvýraznění čáry při najetí na korekturu */
&[data-highlight="true"] {
border-width: 3px;
--alpha: 1;
}
} }
.pointer { /* Korektura samotná */
border-width: 1px; .oprava {
--alpha: 0.35;
}
.pointer-hi {
border-width: 3px;
--alpha: 1;
}
.box:hover{
border-width:3px;
margin: 0px;
}
.box {
margin: 1px; margin: 1px;
background-color: white; background-color: white;
width:300px; width: 300px;
/*position:absolute;*/
padding: 3px; padding: 3px;
border: 2px solid black; border: 2px solid rgb(var(--rgb));
border-radius: 10px; border-radius: 10px;
border-color: rgb(var(--rgb)); position: absolute;
&:hover {
border-width:3px;
margin: 0;
}
button, img {
border: 1px solid white;
background-color:transparent;
margin:0;
padding: 1px;
&:hover {
border: 1px solid black;
}
}
button img { pointer-events: none; }
.corr-header {
overflow: auto;
}
.author {
font-weight: bold;
float: left;
margin-top: 3px;
}
.float-right{
float:right;
}
} }
form { form {
display:inline; display:inline;
} }
.float-right{ /* Zobrazované PDF */
float:right;
}
.imgdiv { .imgdiv {
position:relative; position:relative;
left:0px; left:0;
top:0px; top:0;
} }
/* Přidávání korektury / úprava komentáře */
#commform-div { #commform-div {
display: none;
position: absolute; position: absolute;
background-color: white; background-color: white;
border: 1px solid;
padding: 3px; padding: 3px;
/*
width: 310;
height: 220;
*/
z-index: 10; z-index: 10;
border: 4px solid red; border: 4px solid red;
border-radius: 10px; border-radius: 10px;
background-color: white;
opacity: 80%; opacity: 80%;
} }
.close-button{
background-color: yellow;
}
.box button, /**** ROZLIŠENÍ MEZI LOKÁLNÍM, TESTOVACÍM A PRODUKČNÍM WEBEM ****/
.box img, body.localweb, body.testweb, body.suprodweb {
.box-done button, &:before, &:after {
.box-done img, content: "";
.box-ready button, position: fixed;
.box-ready img, width: 20px;
.box-wontfix button, height: 100%;
.box-wontfix img{ top: 0;
border: 1px solid white; z-index: -1000;
background-color:transparent; }
margin:0;
padding: 1px; &:before { left: 0; }
} &:after { right: 0; }
.box button:hover,
.box img:hover,
.box-done img:hover,
.box-done button:hover,
.box-ready img:hover,
.box-ready button:hover,
.box-wontfix img:hover,
.box-wontfix button:hover{
border: 1px solid black;
}
.comment hr {
height: 0px;
}
.corr-header {
overflow: auto;
}
.author {
font-weight: bold;
float: left;
margin-top: 3px;
} }
body.localweb { &:before, &:after { background: greenyellow; } }
body.testweb { &:before, &:after { background: darkorange; } }
body.suprodweb { &:before, &:after { background: red; } }
/****************************************************************/

View file

@ -1,283 +1,46 @@
const W_SKIP = 10;
const H_SKIP = 5;
const POINTER_MIN_H = 30;
function place_comments_one_div(img_id, comments) function place_comments_one_div(img_id, comments)
{ {
var img = document.getElementById(img_id); const img = document.getElementById("img-"+img_id);
if( img == null ) { if( img == null ) return;
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; if (par.offsetHeight < bott_max) par.style.height = bott_max;
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];
}
});
//console.log("w:" + w);
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;
elp.img_id = img_id;
el.img_id = img_id;
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;
//console.log( "par.w:" + par.style.width);
}
if( par.offsetHeight < bott_max ) {
//par.style.height = bott_max;
//alert("preteklo to:"+ par.offsetHeight +",mx:" + bott_max );
par.style.height = bott_max;
}
} }
function place_comments() { function place_comments() {
for (var i=0; i < comments.length-1; i++) { for (let [img_id, opravy] of Object.entries(comments)) {
place_comments_one_div(comments[i][0], comments[i][1]) place_comments_one_div(img_id, opravy)
} }
} }
// ctrl-enter submits form
function textarea_onkey(ev)
{
//console.log("ev:" + ev.keyCode + "," + ev.ctrlKey);
if( (ev.keyCode == 13 || ev.keyCode == 10 ) && ev.ctrlKey ) {
var form = document.getElementById('commform');
if( form ) {
save_scroll(form);
//form.action ='';
form.submit();
}
return true;
}
return false;
}
//hide comment form
function close_commform() {
var formdiv = document.getElementById('commform-div');
if( formdiv == null ) {
alert("form null");
return true;
}
formdiv.style.display = 'none';
return false;
}
// show comment form, when clicked to image
function img_click(element, ev) {
var body_class = document.body.className;
switch(body_class){
case "comitting":
if (!confirm("Právě jsou zanášeny korektury, opravdu chcete přidat novou?"))
return;
break;
case "deprecated":
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;
if( element.img_id != null ) {
// click was to '-pointer'
img_id = element.img_id;
}
return show_form(img_id, dx, dy, '', '', '', '');
}
// hide or show text of correction
function toggle_visibility(oid){
var buttondiv = document.getElementById(oid+'-buttons')
var text = document.getElementById(oid+'-body');
if (text.style.display == 'none'){
text.style.display = 'block';
buttondiv.style.display = 'inline-block';
}else {
text.style.display = 'none';
buttondiv.style.display = 'none';
}
for (var i=0;i<comments.length-1;i++){
place_comments_one_div(comments[i][0], comments[i][1])
}
}
// show comment form, when 'edit' or 'comment' button pressed
function box_edit(oid, action)
{
var divpointer = document.getElementById(oid + '-pointer');
var text;
if (action == 'update') {
var text_el = document.getElementById(oid + '-text');
text = text_el.textContent; // FIXME původně tu bylo innerHTML.unescapeHTML()
} else {
text = '';
}
var dx = parseInt(divpointer.style.left);
var dy = parseInt(divpointer.style.top);
var divbox = document.getElementById(oid);
//alert('not yet 2:' + text + text_el); // + divpointer.style.top "x" + divpo );
id = oid.substring(2);
return show_form(divbox.img_id, dx, dy, id, text, action);
}
// show comment form when 'update-comment' button pressed
function update_comment(oid,ktid)
{
var divpointer = document.getElementById(oid + '-pointer');
var dx = parseInt(divpointer.style.left);
var dy = parseInt(divpointer.style.top);
var divbox = document.getElementById(oid);
var text = document.getElementById(ktid).textContent; // FIXME původně tu bylo innerHTML.unescapeHTML()
return show_form(divbox.img_id, dx, dy, ktid.substring(2), text, 'update-comment');
}
//fill up comment form and show him
function show_form(img_id, dx, dy, id, text, action) {
var form = document.getElementById('commform');
var formdiv = document.getElementById('commform-div');
var textarea = document.getElementById('commform-text');
var inputX = document.getElementById('commform-x');
var inputY = document.getElementById('commform-y');
var inputImgId = document.getElementById('commform-img-id');
var inputId = document.getElementById('commform-id');
var inputAction = document.getElementById('commform-action');
var img = document.getElementById(img_id);
if( formdiv == null || textarea == null ) {
alert("form null");
return 1;
}
//form.action = "#" + img_id;
// set hidden values
inputX.value = dx;
inputY.value = dy;
inputImgId.value = img_id;
inputId.value = id;
inputAction.value = action;
textarea.value = text;
//textarea.value = "dxy:"+ dx + "x" + dy + "\n" + 'id:' + img_id;
// show form
formdiv.style.display = 'block';
formdiv.style.left = dx;
formdiv.style.top = dy;
img.parentNode.appendChild(formdiv);
textarea.focus();
return true;
}
function box_onmouseover(box)
{
var id = box.id;
var pointer = document.getElementById(box.id + '-pointer');
pointer.classList.remove('pointer');
pointer.classList.add('pointer-hi');
}
function box_onmouseout(box)
{
var id = box.id;
var pointer = document.getElementById(box.id + '-pointer');
pointer.classList.remove('pointer-hi');
pointer.classList.add('pointer');
}
function save_scroll(form)
{
//alert('save_scroll:' + document.body.scrollTop);
form.scroll.value = document.body.scrollTop;
//alert('save_scroll:' + form.scroll.value);
return true;
}
function toggle_corrections(aclass)
{
var stylesheets = document.styleSheets;
var ssheet = null;
for (var i=0;i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (var i=0;i<ssheet.cssRules.length;i++){
var rule = ssheet.cssRules[i];
if (rule.selectorText === '.'+aclass){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
place_comments();
}
String.prototype.unescapeHTML = function () {
return(
this.replace(/&amp;/g,'&').
replace(/&gt;/g,'>').
replace(/&lt;/g,'<').
replace(/&quot;/g,'"')
);
};

View file

@ -0,0 +1,64 @@
<div id="commform-div" style="display: none">
<form action='' id="commform" method="POST">
{% csrf_token %}
<input size="24" name="au" value="{{user.first_name}} {{user.last_name}}" readonly/>
<input type=submit value="Oprav!"/>
<button type="button" onclick="close_commform()">Zavřít</button>
<br/>
<textarea id="commform-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<input type="hidden" size="3" id="commform-x" name="x"/>
<input type="hidden" size="3" id="commform-y" name="y"/>
<input type="hidden" size="3" id="commform-img-id" name="img-id"/>
<input type="hidden" size="3" id="commform-id" name="id"/>
<input type="hidden" size="3" id="commform-action" name="action"/>
</form>
</div>
<script>
const commform = document.getElementById('commform');
commform._div = document.getElementById('commform-div');
commform._text = document.getElementById('commform-text');
commform._x = document.getElementById('commform-x');
commform._y = document.getElementById('commform-y');
commform._imgID = document.getElementById('commform-img-id');
commform._id = document.getElementById('commform-id');
commform._action = document.getElementById('commform-action');
// ctrl-enter submits form
commform._text.addEventListener("keydown", ev => {
if (ev.code === "Enter" && ev.ctrlKey) commform.submit();
});
//hide comment form
function close_commform() {
commform._div.style.display = 'none';
return false;
}
//fill up comment form and show him
function show_form(img_id, dx, dy, id, text, action) {
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;
// set hidden values
commform._x.value = dx;
commform._y.value = dy;
commform._imgID.value = img_id;
commform._id.value = id;
commform._action.value = action;
commform._text.value = text;
// show form
commform._div.style.display = 'block';
commform._div.style.left = dx;
commform._div.style.top = dy;
img.parentNode.appendChild(commform._div);
commform._text.focus();
return true;
}
</script>

View file

@ -0,0 +1,86 @@
{% 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>
</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

@ -0,0 +1,145 @@
{% 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-body'>
{# {% 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'>
<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 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' 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

@ -0,0 +1,40 @@
{% 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" %}
<script>
/**
*
* @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 && 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 => {if (catchError) alert('Něco se nepovedlo:' + error);});
}
update_all();
</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,51 @@
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ě ({{k_oprave_cnt}})</label>
<input type="checkbox"
id="opraveno_checkbox"
name="opraveno_checkbox"
onchange="toggle_corrections('opraveno')" checked>
<label for="opraveno_checkbox">Opraveno ({{opraveno_cnt}})</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 ({{neni_chyba_cnt}})</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í ({{k_zaneseni_cnt}})</label>
<hr/>
<script>
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();
}
</script>

View file

@ -0,0 +1,44 @@
<h4>Změnit stav PDF:</h4>
<i>Aktuální: {{pdf.status}}</i>
<br>
<form method="post" id="PDFSTAV_FORM">
{% csrf_token %}
<input type="radio" name="state" value="{{ pdf.STATUS.PRIDAVANI }}" {% if pdf.status == pdf.STATUS.PRIDAVANI %} checked {% endif %}>Přidávání korektur
<br>
<input type="radio" name="state" value="{{ pdf.STATUS.ZANASENI }}" {% if pdf.status == pdf.STATUS.ZANASENI %} checked {% endif %}>Zanášení korektur
<br>
<input type="radio" name="state" value="{{ pdf.STATUS.ZASTARALE }}" {% if pdf.status == pdf.STATUS.ZASTARALE %} checked {% endif %}>Zastaralé, nekorigovat
<br>
<input type='submit' value='Změnit stav PDF'/>
</form>
<script>
const pdfstav_form = document.getElementById('PDFSTAV_FORM');
/**
*
* @param {RequestInit} data
* @param {Boolean} catchError
*/
function fetchStav(data, catchError=true) {
fetch("{% url 'korektury_api_pdf_stav' pdf.id %}", data
)
.then(response => {
if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);}
else response.json().then(data => document.body.dataset.status = data["status"]);
})
.catch(error => {if (catchError) alert("Něco se nepovedlo:" + error);});
}
pdfstav_form.addEventListener('submit', async event => {
event.preventDefault();
const data = new FormData(pdfstav_form);
fetchStav({method: "POST", body: data});
});
// FIXME není mi jasné, zda v {} nemá být `cache: "no-store"`, aby prohlížeč necachoval get.
setInterval(() => fetchStav({}, false), 30000); // Každou půl minutu fetchni stav
</script>

View file

@ -0,0 +1,44 @@
{% load static %}
<html>
<head>
<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"%}" />
<link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet">
<script src="{% static "korektury/opraf.js"%}"></script>
<title>Korektury {{pdf.nazev}}</title>
</head>
<body class="{{ LOCAL_TEST_PROD }}web" data-status="{{ pdf.status }}" onload='place_comments()'>
<h1>Korektury {{pdf.nazev}}</h1>
<h2 class="textzanaseni"> Probíhá zanášení korektur, zvažte, zda chcete přidávat nové </h2>
<h2 class="textzastarale"> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2>
<i>{{pdf.komentar}}</i>
<br>
<i>Klikni na chybu, napiš komentář</i> |
<a href="{{pdf.pdf.url}}">stáhnout PDF (bez korektur)</a> |
<a href="../">seznam souborů</a> |
<a href="/admin/korektury/korekturovanepdf/">Spravovat PDF</a> |
<a href="../help">nápověda</a> |
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
<a href="/">hlavní stránka</a> |
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<hr />
{% include "korektury/korekturovatko/_schovani_korektur.html" %}
{% include "korektury/korekturovatko/_main.html" %}
{% include "korektury/korekturovatko/_zmena_stavu.html" %}
<hr/>
<p>
Děkujeme opravovatelům:
{% for z in zasluhy %}
{{z.autor}} ({{z.pocet}}){% if not forloop.last %},{% endif %}
{% endfor %}</p>
<hr>
</body>
</html>

View file

@ -1,244 +0,0 @@
{% load static %}
<html>
<head>
<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"%}" />
<link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet">
<script src="{% static "korektury/opraf.js"%}"></script>
<title>Korektury {{pdf.nazev}}</title>
</head>
<body class="{{ LOCAL_TEST_PROD }}web{% if pdf.status == 'zanaseni'%} comitting{% elif pdf.status == 'zastarale' %} deprecated{% endif %}" onload='place_comments()'>
<h1>Korektury {{pdf.nazev}}</h1>
{% if pdf.status == 'zanaseni' %} <h2> Probíhá zanášení korektur, zvažte, zda chcete přidávat nové </h2> {% endif %}
{% if pdf.status == 'zastarale' %} <h2> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2> {% endif %}
<i>{{pdf.komentar}}</i>
<br>
<i>Klikni na chybu, napiš komentář</i> |
<a href="{{pdf.pdf.url}}">stáhnout PDF (bez korektur)</a> |
<a href="../">seznam souborů</a> |
<a href="/admin/korektury/korekturovanepdf/">Spravovat PDF</a> |
<a href="../help">nápověda</a> |
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
<a href="/">hlavní stránka</a> |
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<hr />
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ě ({{k_oprave_cnt}})</label>
<input type="checkbox"
id="opraveno_checkbox"
name="opraveno_checkbox"
onchange="toggle_corrections('opraveno')" checked>
<label for="opraveno_checkbox">Opraveno ({{opraveno_cnt}})</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 ({{neni_chyba_cnt}})</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í ({{k_zaneseni_cnt}})</label>
<hr/>
<div id="commform-div">
<!-- Pridat korekturu / komentar !-->
<form action='' onsubmit='save_scroll(this)' id="commform" method="POST">
{% csrf_token %}
<input size="24" name="au" value="{{user.first_name}} {{user.last_name}}" readonly/>
<input type=submit value="Oprav!"/>
<button type="button" onclick="close_commform()">Zavřít</button>
<br/>
<textarea onkeypress="textarea_onkey(event);" id="commform-text" cols=40 rows=10 name="txt"></textarea>
<br/>
<input type="hidden" size="3" name="pdf" value='{{pdf.id}}'/>
<input type="hidden" size="3" id="commform-x" name="x"/>
<input type="hidden" size="3" id="commform-y" name="y"/>
<input type="hidden" size="3" id="commform-img-id" name="img-id"/>
<input type="hidden" size="3" id="commform-id" name="id"/>
<input type="hidden" size="3" id="commform-action" name="action"/>
<input type="hidden" size="3" id="commform-action" name="scroll"/>
</form>
<!-- /Pridat korekturu / komentar !-->
</div>
{% 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'/>
</div>
<hr/>
{% endfor %}
<h4>Změnit stav PDF:</h4>
<i>Aktuální: {{pdf.status}}</i>
<br>
<!-- Zmenit stav PDF !-->
<form method="post">
{% csrf_token %}
<input type='hidden' name='action' value='set-state'/>
<input type='hidden' name='pdf' value='{{pdf.id}}'/>
<input type="radio" name="state" value="adding" {% if pdf.status == 'pridavani' %} checked {% endif %}>Přidávání korektur
<br>
<input type="radio" name="state" value="comitting" {% if pdf.status == 'zanaseni' %} checked {% endif %}>Zanášení korektur
<br>
<input type="radio" name="state" value="deprecated" {% if pdf.status == 'zastarale' %} checked {% endif %}>Zastaralé, nekorigovat
<br>
<input type='submit' value='Změnit stav PDF'/>
</form>
<!-- /Zmenit stav PDF !-->
<hr/>
<p>
Děkujeme opravovatelům:
{% for z in zasluhy %}
{{z.autor}} ({{z.pocet}}){% if not forloop.last %},{% endif %}
{% endfor %}</p>
<hr>
{% for o in opravy %}
<div onclick='img_click(this,event)'
id='op{{o.id}}-pointer'
class='pointer {{o.status}}'>
</div>
<div name='op{{o.id}}' id='op{{o.id}}'
class='box {{o.status}}'
onmouseover='box_onmouseover(this)'
onmouseout='box_onmouseout(this)'>
<div class='corr-header'>
<span class='author' id='op{{o.id}}-autor'>{{o.autor}}</span>
<span class='float-right'>
<span id='op{{o.id}}-buttons'>
<!-- Existujici korektura !-->
<form action='' onsubmit='save_scroll(this)' method='POST'>
{% csrf_token %}
<input type='hidden' name="au" value="{{o.autor}}"/>
<input type='hidden' name='pdf' value='{{pdf.id}}'>
<input type='hidden' name='id' value='{{o.id}}'>
<input type='hidden' name='scroll'>
{% if o.komentare %}
<button name='action' value='del' type='button'
title="Opravu nelze smazat &ndash; už ji někdo okomentoval">
<img src="{% static "korektury/imgs/delete-gr.png"%}"/>
</button>
{% else %}
<button type='submit' name='action' value='del' title='Smaž opravu'>
<img src="{% static "korektury/imgs/delete.png"%}"/>
</button>
{% endif %}
{% if o.status != 'k_oprave' %}
<button type='submit' name='action' value='undone' title='Označ jako neopravené'>
<img src="{% static "korektury/imgs/undo.png"%}"/>
</button>
{% endif %}
{% if o.status != 'opraveno' %}
<button type='submit' name='action' value='done' title='Označ jako opravené'>
<img src="{% static "korektury/imgs/check.png"%}"/>
</button>
{% endif %}
{% if o.status != 'neni_chyba' %}
<button type='submit' name='action' value='wontfix' title='Označ, že se nebude měnit'>
<img src="{% static "korektury/imgs/cross.png" %}"/>
</button>
{% endif %}
{% if o.status != 'k_zaneseni' %}
<button type='submit' name='action' value='ready' title='Označ jako připraveno k zanesení'>
<img src="{% static "korektury/imgs/tex.png" %}"/>
</button>
{% endif %}
</form>
<!-- /Existujici korektura !-->
{% if o.komentare %}
<button type='button' title="Korekturu nelze upravit &ndash; už ji někdo okomentoval">
<img src="{% static "korektury/imgs/edit-gr.png" %}"/>
</button>
{% else %}
<button type='button' onclick='box_edit("op{{o.id}}","update");' title='Oprav opravu'>
<img src="{% static "korektury/imgs/edit.png" %}"/>
</button>
{% endif %}
{% if o.status == 'opraveno' or o.status == 'neni_chyba' %}
<button type='button' title='Korekturu nelze komentovat, protože už je uzavřená'>
<img src="{% static "korektury/imgs/comment-gr.png" %}"/>
</button>
{% else %}
<button type='button' onclick='box_edit("op{{o.id}}", "comment");' title='Komentovat'>
<img src="{% static "korektury/imgs/comment.png" %}"/>
</button>
{% endif %}
</span>
<button type='button' onclick='toggle_visibility("op{{o.id}}");' title='Skrýt/Zobrazit'>
<img src="{% static "korektury/imgs/hide.png" %}"/>
</button>
</span>
</div>
<div class='corr-body' id='op{{o.id}}-body'>
<div id='op{{o.id}}-text'>{{o.text|linebreaks}}</div>
{% for k in o.komentare %}
<hr>
<div class='comment' id='k{{k.id}}'>
<div class='corr-header'>
<div class='author'>{{k.autor}}</div>
<div class="float-right">
<!-- Komentar !-->
<form action='' onsubmit='save_scroll(this)' method='POST'>
{% csrf_token %}
<input type='hidden' name='pdf' value='{{pdf.id}}'>
<input type='hidden' name='id' value='{{k.id}}'>
<input type='hidden' name='scroll'>
{% if forloop.last %}
<button type='submit' name='action' value='del-comment' title='Smaž komentář'
onclick='return confirm("Opravdu smazat komentář?")'>
<img src="{% static "korektury/imgs/delete.png" %}"/>
</button>
{% else %}
<button name='action' value='del-comment' type='button'
title="Komentář nelze smazat &ndash; existuje novější">
<img src="{% static "korektury/imgs/delete-gr.png"%}"/>
</button>
{% endif %}
</form>
<!-- /Komentar !-->
{% if forloop.last %}
<button type='button' onclick="update_comment('op{{o.id}}','kt{{k.id}}');" title='Uprav komentář'>
<img src="{% static "korektury/imgs/edit.png"%}"/>
</button>
{% else %}
<button type='button' title="Komentář nelze upravit &ndash; existuje novější">
<img src="{% static "korektury/imgs/edit-gr.png" %}"/>
</button>
{% endif %}
</div>
</div>
<div id='kt{{k.id}}'>{{k.text|linebreaks}}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
<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 %}
[]]
{% if scroll %}
window.scrollTo(0,{{scroll}});
{% endif %}
</script>
</body>
</html>

View file

@ -1,4 +1,6 @@
from django.urls import path from django.urls import path
from django.urls import include
from personalni.utils import org_required from personalni.utils import org_required
from . import views from . import views
@ -7,4 +9,6 @@ urlpatterns = [
path('korektury/neseskupene/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury_neseskupene_list'), path('korektury/neseskupene/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury_neseskupene_list'),
path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'), path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'),
path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'), path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'),
path('korektury/api/', include('korektury.api.urls')),
] ]

53
korektury/utils.py Normal file
View file

@ -0,0 +1,53 @@
from django.core.mail import EmailMessage
from django.http import HttpRequest
from django.urls import reverse
from korektury.models import Komentar, Oprava
from personalni.models import Organizator
def send_email_notification_komentar(oprava: Oprava, autor: Organizator, request: HttpRequest):
''' Rozesle e-mail pri pridani komentare / opravy,
ktery obsahuje text vlakna opravy.
'''
# parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = []
for kom in Komentar.objects.filter(oprava=oprava):
texty.append((kom.autor.osoba.plne_jmeno(),kom.text))
optext = "\n\n\n".join([": ".join(t) for t in texty])
text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\
\nodkaz do korekturovátka: {}\n\
\nVaše korekturovátko\n".format(optext, odkaz)
# Prijemci e-mailu
emails = set()
# nalezeni e-mailu na autory komentaru
for komentar in oprava.komentar_set.all():
email_komentujiciho = komentar.autor.osoba.email
if email_komentujiciho:
emails.add(email_komentujiciho)
# zodpovedni orgove
for org in oprava.pdf.orgove.all():
email_zobpovedny = org.osoba.email
if email_zobpovedny:
emails.add(email_zobpovedny)
# odstran e-mail autora opravy
email = autor.osoba.email
if email:
emails.discard(email)
EmailMessage(
subject=subject,
body=text,
from_email=from_email,
to=list(emails),
).send()

View file

@ -2,24 +2,18 @@ from django.shortcuts import get_object_or_404, render
from django.views import generic from django.views import generic
from django.conf import settings from django.conf import settings
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.core.mail import EmailMessage
from django.db.models import Count,Q from django.db.models import Count,Q
from .utils import send_email_notification_komentar
from .models import Oprava,Komentar,KorekturovanePDF, Organizator from .models import Oprava,Komentar,KorekturovanePDF, Organizator
from .forms import OpravaForm
import subprocess
import shutil
import os
class KorekturyListView(generic.ListView): class KorekturyListView(generic.ListView):
model = KorekturovanePDF model = KorekturovanePDF
# Nefunguje, filtry se vubec nepouziji
queryset = KorekturovanePDF.objects.annotate( queryset = KorekturovanePDF.objects.annotate(
k_oprave_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_oprave')), k_oprave_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.K_OPRAVE)),
opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='opraveno')), opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.OPRAVENO)),
neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='neni_chyba')), neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.NENI_CHYBA)),
k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_zaneseni')), k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.K_ZANESENI)),
) )
template_name = 'korektury/seznam.html' template_name = 'korektury/seznam.html'
@ -58,13 +52,15 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
### Korektury ### Korektury
class KorekturyView(generic.TemplateView): class KorekturyView(generic.TemplateView):
model = Oprava model = Oprava
template_name = 'korektury/opraf.html' template_name = 'korektury/korekturovatko/htmlstrana.html'
form_class = OpravaForm
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.pdf_id = self.kwargs["pdf"]
self.pdf = get_object_or_404(KorekturovanePDF, id=self.pdf_id)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
q = request.POST q = request.POST
scroll = q.get('scroll')
# prirazeni autora podle prihlaseni # prirazeni autora podle prihlaseni
autor_user = request.user autor_user = request.user
@ -73,50 +69,27 @@ class KorekturyView(generic.TemplateView):
if not autor: if not autor:
return HttpResponseForbidden() return HttpResponseForbidden()
if not scroll:
scroll = 0
action = q.get('action') action = q.get('action')
if (action == ''): # Přidej if (action == ''): # Přidej
x = int(q.get('x')) x = int(q.get('x'))
y = int(q.get('y')) y = int(q.get('y'))
text = q.get('txt') text = q.get('txt')
strana = int(q.get('img-id')[4:]) strana = int(q.get('img-id')[4:])
pdf = KorekturovanePDF.objects.get(id=q.get('pdf')) op = Oprava(x=x,y=y, strana=strana, pdf=self.pdf)
op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf)
op.save() op.save()
self.send_email_notification_komentar(op,autor) kom = Komentar(oprava=op,autor=autor,text=text)
kom.save()
send_email_notification_komentar(op, autor, request)
elif (action == 'del'): elif (action == 'del'):
id = int(q.get('id')) id = int(q.get('id'))
op = Oprava.objects.get(id=id) op = Oprava.objects.get(id=id)
for k in Komentar.objects.filter(oprava=op):
k.delete()
op.delete() op.delete()
elif (action == 'update'): elif action in Oprava.STATUS.values:
id = int(q.get('id')) id = int(q.get('id'))
op = Oprava.objects.get(id=id) op = Oprava.objects.get(id=id)
text = q.get('txt') op.status = action
op.autor = autor
op.text = text
op.save()
elif (action == 'undone'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_K_OPRAVE
op.save()
elif (action == 'done'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_OPRAVENO
op.save()
elif (action == 'ready'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_K_ZANESENI
op.save()
elif (action == 'wontfix'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_NENI_CHYBA
op.save() op.save()
elif (action == 'comment'): elif (action == 'comment'):
id = int(q.get('id')) id = int(q.get('id'))
@ -124,7 +97,7 @@ class KorekturyView(generic.TemplateView):
text = q.get('txt') text = q.get('txt')
kom = Komentar(oprava=op,autor=autor,text=text) kom = Komentar(oprava=op,autor=autor,text=text)
kom.save() kom.save()
self.send_email_notification_komentar(op,autor) send_email_notification_komentar(op, autor, request)
elif (action == 'update-comment'): elif (action == 'update-comment'):
id = int(q.get('id')) id = int(q.get('id'))
kom = Komentar.objects.get(id=id) kom = Komentar.objects.get(id=id)
@ -137,85 +110,23 @@ class KorekturyView(generic.TemplateView):
kom = Komentar.objects.get(id=id) kom = Komentar.objects.get(id=id)
kom.delete() kom.delete()
elif (action == 'set-state'): elif (action == 'set-state'):
pdf = KorekturovanePDF.objects.get(id=q.get('pdf')) status = q.get('state')
if (q.get('state') == 'adding'): assert status in KorekturovanePDF.STATUS.values
pdf.status = pdf.STATUS_PRIDAVANI self.pdf.status = status
elif (q.get('state') == 'comitting'): self.pdf.save()
pdf.status = pdf.STATUS_ZANASENI
elif (q.get('state') == 'deprecated'):
pdf.status = pdf.STATUS_ZASTARALE
pdf.save()
context = self.get_context_data() context = self.get_context_data()
context['scroll'] = scroll
context['autor'] = autor context['autor'] = autor
return render(request, 'korektury/opraf.html',context) return render(request, 'korektury/korekturovatko/htmlstrana.html', context)
def send_email_notification_komentar(self, oprava, autor):
''' Rozesle e-mail pri pridani komentare / opravy,
ktery obsahuje text vlakna opravy.
'''
# parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
from django.urls import reverse
odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = [(oprava.autor.osoba.plne_jmeno(),oprava.text)]
for kom in Komentar.objects.filter(oprava=oprava):
texty.append((kom.autor.osoba.plne_jmeno(),kom.text))
optext = "\n\n\n".join([": ".join(t) for t in texty])
text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\
\nodkaz do korekturovátka: {}\n\
\nVaše korekturovátko\n".format(optext, odkaz)
# Prijemci e-mailu
emails = set()
# e-mail autora korektury
email = oprava.autor.osoba.email
if email:
emails.add(email)
# nalezeni e-mailu na autory komentaru
for komentar in oprava.komentar_set.all():
email_komentujiciho = komentar.autor.osoba.email
if email_komentujiciho:
emails.add(email_komentujiciho)
# zodpovedni orgove
for org in oprava.pdf.orgove.all():
email_zobpovedny = org.osoba.email
if email_zobpovedny:
emails.add(email_zobpovedny)
# odstran e-mail autora opravy
email = autor.osoba.email
if email:
emails.discard(email)
EmailMessage(
subject=subject,
body=text,
from_email=from_email,
to=list(emails),
).send()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
pdf = get_object_or_404(KorekturovanePDF, id=self.kwargs['pdf']) context['pdf'] = self.pdf
context['pdf'] = pdf context['img_prefix'] = self.pdf.get_prefix()
context['img_prefix'] = pdf.get_prefix()
context['img_path'] = settings.KOREKTURY_IMG_DIR context['img_path'] = settings.KOREKTURY_IMG_DIR
context['img_indexes'] = range(pdf.stran) context['img_indexes'] = range(self.pdf.stran)
context['form_oprava'] = OpravaForm() opravy = Oprava.objects.filter(pdf=self.pdf_id)
opravy = Oprava.objects.filter(pdf=self.kwargs['pdf'])
zasluhy = {} zasluhy = {}
for o in opravy: for o in opravy:
if o.autor in zasluhy:
zasluhy[o.autor]+=1
else:
zasluhy[o.autor]=1
o.komentare = o.komentar_set.all() o.komentare = o.komentar_set.all()
for k in o.komentare: for k in o.komentare:
if k.autor in zasluhy: if k.autor in zasluhy:
@ -241,6 +152,3 @@ class KorekturyView(generic.TemplateView):
context['zasluhy'] = zasluhy context['zasluhy'] = zasluhy
return context return context
def form_valid(self,form):
return super().form_valid(form)

View file

@ -95,7 +95,7 @@ function safe_checkout_branch {
echo >&2 "Změna v $SCRIPT, prosím pullni manuálně" echo >&2 "Změna v $SCRIPT, prosím pullni manuálně"
exit 1 exit 1
fi fi
git checkout "$BRANCH" -- git checkout "$BRANCH"
git pull git pull
git clean -f git clean -f
} }

View file

@ -134,6 +134,7 @@ INSTALLED_APPS = (
'tvorba', 'tvorba',
'galerie', 'galerie',
'korektury', 'korektury',
'korektury.api',
'prednasky', 'prednasky',
'header_fotky', 'header_fotky',
'various', 'various',

View file

@ -1,6 +1,6 @@
# Tento soubor slouží pouze pro shell a podobné. Nikde neimportovat v kódu! # Tento soubor slouží pouze pro shell a podobné. Nikde neimportovat v kódu!
print("Naimportoval jsi `mamweb.vsechno`. Pevně věřím, že to nebylo nikde v kódu. Díky.") print("Naimportoval jsi `seminar.models`. Pevně věřím, že to nebylo nikde v kódu. Díky.")
from galerie.models import * from galerie.models import *
from header_fotky.models import * from header_fotky.models import *

View file

@ -65,9 +65,6 @@ class Reseni(SeminarModelBase):
def absolute_url(self): def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url() return "https://" + str(get_current_site(None)) + self.verejne_url()
def resitel_url(self):
return f'https://{get_current_site(None)}{reverse_lazy("odevzdavatko_resitel_reseni", args=[self.id])}'
# má OneToOneField s: # má OneToOneField s:
# Konfera # Konfera

View file

@ -191,7 +191,7 @@ Sloupce:
</ul> </ul>
</li> </li>
<li>Pokud nemáš důvod, deadline neměň. Sloupeček s deadlinem znamená, do kterého deadlinu se započítají body (nemusí se shodovat s deadlinem řešení).</li> <li>Pokud nemáš důvod, deadline neměň. Sloupeček s deadlinem znamená, do kterého deadlinu se započítají body (nemusí se shodovat s deadlinem řešení).</li>
<li>Poslední sloupec je na zpětnou vazbu řešiteli, tedy (na rozdíl od Neveřejné poznámky, která je určena pro synchronizaci orgů) ji uvidí řešitelé. Změníte-li u nějakého hodnocení toto políčko, řešitel bude upozorněn emailem, pokud si tuto možnost nevypl ve svém profilu. Pohled řešitele si můžete prohlédnout <a href="{% url 'odevzdavatko_resitel_reseni' reseni.id %}">zde</a>. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.</li> <li>Poslední sloupec je na zpětnou vazbu řešiteli, tedy (na rozdíl od Neveřejné poznámky, která je určena pro synchronizaci orgů) ji uvidí řešitelé. Zatím jen pasivně (nechodí e-mail). Pohled řešitele si můžete prohlédnout <a href="{% url 'odevzdavatko_resitel_reseni' reseni.id %}">zde</a>. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.</li>
</ol> </ol>
Další poznámky Další poznámky

View file

@ -222,17 +222,6 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
ctx["problem_id"] = self.kwargs['problem'] ctx["problem_id"] = self.kwargs['problem']
return ctx return ctx
HODNOCENI_INITIAL_DATA = [
"problem",
"body",
"body_celkem",
"body_neprepocitane",
"body_neprepocitane_celkem",
"body_max",
"body_neprepocitane_max",
"deadline_body",
"feedback",
]
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView(DetailView): class DetailReseniView(DetailView):
""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """ """ Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
@ -243,7 +232,18 @@ class DetailReseniView(DetailView):
self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk']) self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk'])
result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in Hodnoceni.objects.filter(reseni=self.reseni): for hodn in Hodnoceni.objects.filter(reseni=self.reseni):
result.append({attr: getattr(hodn, attr) for attr in HODNOCENI_INITIAL_DATA}) seznam_atributu = [
"problem",
"body",
"body_celkem",
"body_neprepocitane",
"body_neprepocitane_celkem",
"body_max",
"body_neprepocitane_max",
"deadline_body",
"feedback",
]
result.append({attr: getattr(hodn, attr) for attr in seznam_atributu})
return result return result
def get_context_data(self, **kw): def get_context_data(self, **kw):
@ -291,9 +291,9 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
reseni = get_object_or_404(Reseni, pk=pk) reseni = get_object_or_404(Reseni, pk=pk)
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
formset = f.OhodnoceniReseniFormSet(request.POST, initial=[ # FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
{k: getattr(h, k) for k in HODNOCENI_INITIAL_DATA} for h in Hodnoceni.objects.filter(reseni=reseni) # Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm
]) formset = f.OhodnoceniReseniFormSet(request.POST)
poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni) poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni)
# TODO: Napsat validaci formuláře a formsetu # TODO: Napsat validaci formuláře a formsetu
if not (formset.is_valid() and poznamka_form.is_valid()): if not (formset.is_valid() and poznamka_form.is_valid()):
@ -309,9 +309,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
qs.delete() qs.delete()
# Vyrobíme nová podle formsetu # Vyrobíme nová podle formsetu
notifikace = False
for form in formset: for form in formset:
notifikace |= 'feedback' in form.changed_data
data_for_hodnoceni = form.cleaned_data data_for_hodnoceni = form.cleaned_data
data_for_body = data_for_hodnoceni.copy() data_for_body = data_for_hodnoceni.copy()
del(data_for_hodnoceni["body_celkem"]) del(data_for_hodnoceni["body_celkem"])
@ -322,44 +320,16 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
**form.cleaned_data, **form.cleaned_data,
) )
logger.info(f"Creating Hodnoceni: {hodnoceni}") logger.info(f"Creating Hodnoceni: {hodnoceni}")
# FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak
zmeny_bodu = [it for it in form.changed_data if it.startswith("body")] zmeny_bodu = [it for it in form.changed_data if it.startswith("body")]
if len(zmeny_bodu) != 0: if len(zmeny_bodu) == 1:
body_nastaveny: None | tuple[str, object] = None hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]])
def nastav_body(jake, na_kolik): # > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno
nonlocal body_nastaveny if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2:
if body_nastaveny is not None: # 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.") logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.")
hodnoceni.body = -0.1 hodnoceni.body = -0.1
else:
body_nastaveny = (jake, na_kolik)
hodnoceni.__setattr__(jake, na_kolik)
for key, value in data_for_body.items():
if key.startswith("body") and value is not None:
nastav_body(key, value)
# Něco se změnilo, ale nic není nastavené = něco bylo smazáno
if body_nastaveny is None:
hodnoceni.body = None
hodnoceni.save() hodnoceni.save()
adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True)
if notifikace and adresati:
email = EmailMessage(
subject='Změna hodnocení odevzdaného řešení',
body=f"""Milá řešitelko, milý řešiteli,
došlo ke změně zpětné vazby k Tebou odevzdanému řešení. Zobrazit si ji můžeš na {reseni.resitel_url()}.
Tvoji organizátoři M&M
---
Nechceš-li tato upozornění dostávat, můžeš si to nastavit ve svém profilu.""",
from_email='odevzdavatko@mam.mff.cuni.cz',
bcc=adresati,
)
email.send()
return redirect(success_url) return redirect(success_url)

View file

@ -71,8 +71,6 @@ class UdajeForm(forms.Form):
zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False, initial=True) zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False, initial=True)
spam = forms.BooleanField(label='Souhlasím se zasíláním propagačních materiálů od MFF UK', required=False) spam = forms.BooleanField(label='Souhlasím se zasíláním propagačních materiálů od MFF UK', required=False)
upozornovat_na_opravy_reseni = forms.BooleanField(label='Chci dostávat emailová upozornění na změnu zpětné vazby k mým řešením', required=False, initial=True)
def clean_prezdivka_resitele(self): def clean_prezdivka_resitele(self):
prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele') prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele')
if prezdivka_resitele == '': if prezdivka_resitele == '':

View file

@ -1,22 +0,0 @@
# Generated by Django 4.2.16 on 2024-12-03 19:08
from django.db import migrations, models
def vypnuti_upozorneni_na_opravy_reseni(apps, schema_editor):
Resitel = apps.get_model('personalni', 'Resitel')
Resitel.objects.update(upozorneni=False)
class Migration(migrations.Migration):
dependencies = [
('personalni', '0017_odstrel_treenode_post'),
]
operations = [
migrations.AddField(
model_name='resitel',
name='upozorneni',
field=models.BooleanField(default=True, verbose_name='zasílat upozornění na změnu zpětné vazby k řešení emailem'),
),
migrations.RunPython(vypnuti_upozorneni_na_opravy_reseni),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.2.18 on 2025-01-14 19:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0018_resitel_upozorneni'),
]
operations = [
migrations.RenameField(
model_name='resitel',
old_name='upozorneni',
new_name='upozornovat_na_opravy_reseni',
),
]

View file

@ -250,8 +250,6 @@ class Resitel(SeminarModelBase):
poznamka = models.TextField('neveřejná poznámka', blank=True, poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešiteli (plain text)') help_text='Neveřejná poznámka k řešiteli (plain text)')
upozornovat_na_opravy_reseni = models.BooleanField('zasílat upozornění na změnu zpětné vazby k řešení emailem', default=True)
def export_row(self): def export_row(self):
"Slovnik pro pouziti v AESOP exportu" "Slovnik pro pouziti v AESOP exportu"

View file

@ -1,34 +0,0 @@
.seznam {
display: flex;
flex-direction: column;
gap: 0.3em;
}
.hint {
border: 1px solid #ccc;
padding: 0.3em 1em;
border-radius: 5px;
margin-bottom: 1em;
}
.osoba {
display: flex;
justify-content: space-between;
gap: 0.5em;
.uno {
flex: 2;
}
.dos {
flex: 2;
}
.tres {
flex: 1;
}
.grey {
opacity: 0.5;
}
}

View file

@ -1,28 +0,0 @@
{% extends "base.html" %}
{% block custom_css %}
{% load static %}
<link href="{% static 'personalni/jak_se_dozvedeli.css' %}" rel="stylesheet">
{% endblock %}
{% block content %}
<div class="seznam">
<div class="osoba hint">
<div class="uno">Jméno</div>
<div class="dos">Jak se dozvěděli</div>
<div class="tres">Datum registrace</div>
</div>
{% for osoba in object_list %}
<div class="osoba">
<div class="uno">{{ osoba.jmeno }} {{ osoba.prijmeni }}</div>
<div class="dos {% if not osoba.jak_se_dozvedeli %}grey{% endif %}">{% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}</div>
<div class="tres">{{ osoba.datum_registrace }}</div>
</div>
{% endfor %}
</div>
{% endblock%}

View file

@ -51,7 +51,6 @@
</h4> </h4>
<table class="form"> <table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.upozornovat_na_opravy_reseni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_papirove %} {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_papirove %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.spam %} {% include "personalni/udaje/prihlaska_field.html" with field=form.spam %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %} {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %}

View file

@ -33,11 +33,4 @@ urlpatterns = [
name='stari_organizatori' name='stari_organizatori'
), ),
# Zpřístupnění dat z "jak jste se o nás dozvěděli" pro orgy propagace
path(
'org/propagace/jak-se-dozvedeli/',
org_required(views.JakSeDozvedeliView.as_view()),
name='jak_se_dozvedeli'
)
] ]

View file

@ -34,7 +34,7 @@ from various.autentizace.utils import posli_reset_hesla
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from .models import Organizator, Osoba from .models import Organizator
def aktivniOrganizatori(datum=timezone.now()): def aktivniOrganizatori(datum=timezone.now()):
@ -62,11 +62,6 @@ class CojemamOrganizatoriStariView(generic.ListView):
id__in=aktivniOrganizatori() id__in=aktivniOrganizatori()
).order_by('-organizuje_do') ).order_by('-organizuje_do')
class JakSeDozvedeliView(generic.ListView):
model = Osoba
template_name = 'personalni/jak_se_dozvedeli.html'
queryset = Osoba.objects.order_by('-datum_registrace')
def obalkyView(request, resitele): def obalkyView(request, resitele):
if len(resitele) == 0: if len(resitele) == 0:
@ -235,7 +230,6 @@ def resitelEditView(request):
resitel_edit.zasilat = fcd['zasilat'] resitel_edit.zasilat = fcd['zasilat']
resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem'] resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
resitel_edit.zasilat_cislo_papirove = fcd['zasilat_cislo_papirove'] resitel_edit.zasilat_cislo_papirove = fcd['zasilat_cislo_papirove']
resitel_edit.upozornovat_na_opravy_reseni = fcd['upozornovat_na_opravy_reseni']
if fcd.get('skola'): if fcd.get('skola'):
resitel_edit.skola = fcd['skola'] resitel_edit.skola = fcd['skola']
else: else:

View file

@ -30,8 +30,6 @@ Jak moc by ses chtěl(a) zúčastnit následujících přednášek?
<INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="1" {% if h == 1 %} CHECKED="checked" {% endif %}> rozhodně chci <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="1" {% if h == 1 %} CHECKED="checked" {% endif %}> rozhodně chci
</td></tr> </td></tr>
<tr><td>&nbsp;</td></tr> <tr><td>&nbsp;</td></tr>
{% empty %}
Nejsou žádné přednášky o kterých by šlo hlasovat.
{% endfor %} {% endfor %}
<tr><td><input name="odeslat" type="submit" value="Odeslat"></td><tr> <tr><td><input name="odeslat" type="submit" value="Odeslat"></td><tr>
</table> </table>

View file

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load humanize %}
{% load static %}
{% block content %}
<h1> Děkujeme. </h1>
{% endblock %}

View file

@ -1,5 +1,3 @@
import http
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.views import generic from django.views import generic
from django.shortcuts import HttpResponseRedirect from django.shortcuts import HttpResponseRedirect
@ -7,22 +5,14 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum from django.db.models import Sum
from django.forms import Form from django.forms import Form
from various.views.pomocne import formularOKView
from various.models import Nastaveni
from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from personalni.models import Osoba from personalni.models import Osoba
def newPrednaska(request): def newPrednaska(request):
# hlasovani se vztahuje k nejnovejsimu soustredeni # hlasovani se vztahuje k nejnovejsimu soustredeni
sous = Nastaveni.get_solo().aktualni_sous sous = Soustredeni.objects.first()
seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first() seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first()
if sous is None or seznam is None:
return render(request, 'universal.html', {
'title': "Nelze hlasovat",
'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.",
}, status=http.HTTPStatus.NOT_FOUND)
osoba = Osoba.objects.filter(user=request.user).first() osoba = Osoba.objects.filter(user=request.user).first()
ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id)
# obsluha formulare # obsluha formulare
@ -64,7 +54,7 @@ def newPrednaska(request):
def Prednaska_hotovo(request): def Prednaska_hotovo(request):
return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.") return render(request, 'prednasky/hotovo.html')
class MetaSeznamListView(generic.ListView): class MetaSeznamListView(generic.ListView):
model = Seznam model = Seznam

View file

@ -17,7 +17,7 @@ django-solo # Singleton model (speciálně Nastavení)
django-ckeditor-5 # Editor htmlka (hlavně v adminu u flatpages) django-ckeditor-5 # Editor htmlka (hlavně v adminu u flatpages)
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
django-taggit # Taggy v djangu (speciálně zaměření problémů) django-taggit # Taggy v djangu (speciálně zaměření problémů)
django-autocomplete-light>=3.9.0,<3.12.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích django-autocomplete-light>=3.9.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích
django-imagekit # Všechny možné obrázky v Djangu django-imagekit # Všechny možné obrázky v Djangu
django-polymorphic # Polymorfismus na django modelech (hlavně Problém nebo treenode) django-polymorphic # Polymorfismus na django modelech (hlavně Problém nebo treenode)
django-sitetree # Struktura stránek, hlavně pro meníčko django-sitetree # Struktura stránek, hlavně pro meníčko

View file

@ -1,27 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-21 20:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'),
('tvorba', '0007_alter_deadline_typ'),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[],
state_operations=[
migrations.AlterField(
model_name='problem',
name='opravovatele',
field=models.ManyToManyField(blank=True, db_table='seminar_problemy_opravovatele', related_name='opravovatele_%(class)s', to='personalni.organizator', verbose_name='opravovatelé'),
),
migrations.DeleteModel(
name='Problemy_Opravovatele',
),
]
),
]

View file

@ -393,6 +393,20 @@ class ZmrazenaVysledkovka(SeminarModelBase):
html = models.TextField(null=False, blank=False) html = models.TextField(null=False, blank=False)
class Problemy_Opravovatele(SeminarModelBase):
"""Jen vazebná tabulka pro opravovatele.
Ona stejně existovala, při přesunu mezi aplikacemi jen potřebujeme zajistit nepřejmenování DB tabulky.
Proto taky nepotřebuje žádná specifika, ze :py:class:SeminarModelBase: dědí ze zvyku než že by to k něčemu kdy měo být.
"""
class Meta:
db_table = 'seminar_problemy_opravovatele'
id = models.AutoField(primary_key = True)
problem = models.ForeignKey('Problem', on_delete=models.CASCADE)
organizator = models.ForeignKey(Organizator, on_delete=models.CASCADE)
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
# Pozor na následující řádek. *Nekrmit, asi kouše!* # Pozor na následující řádek. *Nekrmit, asi kouše!*
class Problem(SeminarModelBase,PolymorphicModel): class Problem(SeminarModelBase,PolymorphicModel):
@ -448,7 +462,7 @@ class Problem(SeminarModelBase,PolymorphicModel):
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé',
blank=True, related_name='opravovatele_%(class)s', db_table='seminar_problemy_opravovatele') blank=True, related_name='opravovatele_%(class)s', through=Problemy_Opravovatele)
kod = models.CharField('lokální kód', max_length=32, blank=True, default='', kod = models.CharField('lokální kód', max_length=32, blank=True, default='',
help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku')

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-21 20:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('soustredeni', '0013_alter_soustredeni_kontaktnicek_pdf_and_more'),
('various', '0006_tvorba_post'),
]
operations = [
migrations.AddField(
model_name='nastaveni',
name='aktualni_sous',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='soustredeni.soustredeni', verbose_name='Aktuálně připravovaný sous'),
),
]

View file

@ -26,11 +26,6 @@ class Nastaveni(SingletonModel):
verbose_name="Účastnický poplatek za soustředění", verbose_name="Účastnický poplatek za soustředění",
default=1000) default=1000)
aktualni_sous = models.ForeignKey(
"soustredeni.Soustredeni", verbose_name='Aktuálně připravovaný sous',
null=True, blank=True, on_delete=models.PROTECT,
)
@property @property
def aktualni_rocnik(self): def aktualni_rocnik(self):
return self.aktualni_cislo.rocnik return self.aktualni_cislo.rocnik