Compare commits

..

No commits in common. "master" and "merge-vetev-pro-prednasky" have entirely different histories.

84 changed files with 1269 additions and 2959 deletions

View file

@ -14,12 +14,12 @@
"flatpage" "flatpage"
], ],
[ [
"change_flatpage", "delete_flatpage",
"flatpages", "flatpages",
"flatpage" "flatpage"
], ],
[ [
"delete_flatpage", "change_flatpage",
"flatpages", "flatpages",
"flatpage" "flatpage"
], ],
@ -34,12 +34,12 @@
"galerie" "galerie"
], ],
[ [
"change_galerie", "delete_galerie",
"galerie", "galerie",
"galerie" "galerie"
], ],
[ [
"delete_galerie", "change_galerie",
"galerie", "galerie",
"galerie" "galerie"
], ],
@ -54,12 +54,12 @@
"obrazek" "obrazek"
], ],
[ [
"change_obrazek", "delete_obrazek",
"galerie", "galerie",
"obrazek" "obrazek"
], ],
[ [
"delete_obrazek", "change_obrazek",
"galerie", "galerie",
"obrazek" "obrazek"
], ],
@ -104,12 +104,12 @@
"komentar" "komentar"
], ],
[ [
"change_komentar", "delete_komentar",
"korektury", "korektury",
"komentar" "komentar"
], ],
[ [
"delete_komentar", "change_komentar",
"korektury", "korektury",
"komentar" "komentar"
], ],
@ -124,12 +124,12 @@
"korekturovanepdf" "korekturovanepdf"
], ],
[ [
"change_korekturovanepdf", "delete_korekturovanepdf",
"korektury", "korektury",
"korekturovanepdf" "korekturovanepdf"
], ],
[ [
"delete_korekturovanepdf", "change_korekturovanepdf",
"korektury", "korektury",
"korekturovanepdf" "korekturovanepdf"
], ],
@ -144,12 +144,12 @@
"oprava" "oprava"
], ],
[ [
"change_oprava", "delete_oprava",
"korektury", "korektury",
"oprava" "oprava"
], ],
[ [
"delete_oprava", "change_oprava",
"korektury", "korektury",
"oprava" "oprava"
], ],
@ -164,12 +164,12 @@
"novinky" "novinky"
], ],
[ [
"change_novinky", "delete_novinky",
"novinky", "novinky",
"novinky" "novinky"
], ],
[ [
"delete_novinky", "change_novinky",
"novinky", "novinky",
"novinky" "novinky"
], ],
@ -204,12 +204,12 @@
"prijemce" "prijemce"
], ],
[ [
"change_prijemce", "delete_prijemce",
"personalni", "personalni",
"prijemce" "prijemce"
], ],
[ [
"delete_prijemce", "change_prijemce",
"personalni", "personalni",
"prijemce" "prijemce"
], ],
@ -234,12 +234,12 @@
"skola" "skola"
], ],
[ [
"change_skola", "delete_skola",
"personalni", "personalni",
"skola" "skola"
], ],
[ [
"delete_skola", "change_skola",
"personalni", "personalni",
"skola" "skola"
], ],
@ -249,14 +249,24 @@
"skola" "skola"
], ],
[ [
"view_hlasovani", "add_hlasovani",
"prednasky", "prednasky",
"hlasovani" "hlasovani"
], ],
[ [
"view_hlasovanioznalostech", "delete_hlasovani",
"prednasky", "prednasky",
"hlasovanioznalostech" "hlasovani"
],
[
"change_hlasovani",
"prednasky",
"hlasovani"
],
[
"view_hlasovani",
"prednasky",
"hlasovani"
], ],
[ [
"add_prednaska", "add_prednaska",
@ -264,12 +274,12 @@
"prednaska" "prednaska"
], ],
[ [
"change_prednaska", "delete_prednaska",
"prednasky", "prednasky",
"prednaska" "prednaska"
], ],
[ [
"delete_prednaska", "change_prednaska",
"prednasky", "prednasky",
"prednaska" "prednaska"
], ],
@ -284,12 +294,12 @@
"seznam" "seznam"
], ],
[ [
"change_seznam", "delete_seznam",
"prednasky", "prednasky",
"seznam" "seznam"
], ],
[ [
"delete_seznam", "change_seznam",
"prednasky", "prednasky",
"seznam" "seznam"
], ],
@ -298,38 +308,18 @@
"prednasky", "prednasky",
"seznam" "seznam"
], ],
[
"add_znalost",
"prednasky",
"znalost"
],
[
"change_znalost",
"prednasky",
"znalost"
],
[
"delete_znalost",
"prednasky",
"znalost"
],
[
"view_znalost",
"prednasky",
"znalost"
],
[ [
"add_konfera", "add_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
[ [
"change_konfera", "delete_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
[ [
"delete_konfera", "change_konfera",
"soustredeni", "soustredeni",
"konfera" "konfera"
], ],
@ -344,12 +334,12 @@
"konfery_ucastnici" "konfery_ucastnici"
], ],
[ [
"change_konfery_ucastnici", "delete_konfery_ucastnici",
"soustredeni", "soustredeni",
"konfery_ucastnici" "konfery_ucastnici"
], ],
[ [
"delete_konfery_ucastnici", "change_konfery_ucastnici",
"soustredeni", "soustredeni",
"konfery_ucastnici" "konfery_ucastnici"
], ],
@ -364,12 +354,12 @@
"soustredeni" "soustredeni"
], ],
[ [
"change_soustredeni", "delete_soustredeni",
"soustredeni", "soustredeni",
"soustredeni" "soustredeni"
], ],
[ [
"delete_soustredeni", "change_soustredeni",
"soustredeni", "soustredeni",
"soustredeni" "soustredeni"
], ],
@ -384,12 +374,12 @@
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
[ [
"change_soustredeni_organizatori", "delete_soustredeni_organizatori",
"soustredeni", "soustredeni",
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
[ [
"delete_soustredeni_organizatori", "change_soustredeni_organizatori",
"soustredeni", "soustredeni",
"soustredeni_organizatori" "soustredeni_organizatori"
], ],
@ -404,12 +394,12 @@
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
[ [
"change_soustredeni_ucastnici", "delete_soustredeni_ucastnici",
"soustredeni", "soustredeni",
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
[ [
"delete_soustredeni_ucastnici", "change_soustredeni_ucastnici",
"soustredeni", "soustredeni",
"soustredeni_ucastnici" "soustredeni_ucastnici"
], ],
@ -424,12 +414,12 @@
"tag" "tag"
], ],
[ [
"change_tag", "delete_tag",
"taggit", "taggit",
"tag" "tag"
], ],
[ [
"delete_tag", "change_tag",
"taggit", "taggit",
"tag" "tag"
], ],
@ -444,12 +434,12 @@
"taggeditem" "taggeditem"
], ],
[ [
"change_taggeditem", "delete_taggeditem",
"taggit", "taggit",
"taggeditem" "taggeditem"
], ],
[ [
"delete_taggeditem", "change_taggeditem",
"taggit", "taggit",
"taggeditem" "taggeditem"
], ],
@ -464,12 +454,12 @@
"cislo" "cislo"
], ],
[ [
"change_cislo", "delete_cislo",
"tvorba", "tvorba",
"cislo" "cislo"
], ],
[ [
"delete_cislo", "change_cislo",
"tvorba", "tvorba",
"cislo" "cislo"
], ],
@ -484,12 +474,12 @@
"clanek" "clanek"
], ],
[ [
"change_clanek", "delete_clanek",
"tvorba", "tvorba",
"clanek" "clanek"
], ],
[ [
"delete_clanek", "change_clanek",
"tvorba", "tvorba",
"clanek" "clanek"
], ],
@ -519,12 +509,12 @@
"pohadka" "pohadka"
], ],
[ [
"change_pohadka", "delete_pohadka",
"tvorba", "tvorba",
"pohadka" "pohadka"
], ],
[ [
"delete_pohadka", "change_pohadka",
"tvorba", "tvorba",
"pohadka" "pohadka"
], ],
@ -539,12 +529,12 @@
"problem" "problem"
], ],
[ [
"change_problem", "delete_problem",
"tvorba", "tvorba",
"problem" "problem"
], ],
[ [
"delete_problem", "change_problem",
"tvorba", "tvorba",
"problem" "problem"
], ],
@ -559,12 +549,12 @@
"rocnik" "rocnik"
], ],
[ [
"change_rocnik", "delete_rocnik",
"tvorba", "tvorba",
"rocnik" "rocnik"
], ],
[ [
"delete_rocnik", "change_rocnik",
"tvorba", "tvorba",
"rocnik" "rocnik"
], ],
@ -579,12 +569,12 @@
"tema" "tema"
], ],
[ [
"change_tema", "delete_tema",
"tvorba", "tvorba",
"tema" "tema"
], ],
[ [
"delete_tema", "change_tema",
"tvorba", "tvorba",
"tema" "tema"
], ],
@ -599,12 +589,12 @@
"uloha" "uloha"
], ],
[ [
"change_uloha", "delete_uloha",
"tvorba", "tvorba",
"uloha" "uloha"
], ],
[ [
"delete_uloha", "change_uloha",
"tvorba", "tvorba",
"uloha" "uloha"
], ],
@ -619,12 +609,12 @@
"nastaveni" "nastaveni"
], ],
[ [
"change_nastaveni", "delete_nastaveni",
"various", "various",
"nastaveni" "nastaveni"
], ],
[ [
"delete_nastaveni", "change_nastaveni",
"various", "various",
"nastaveni" "nastaveni"
], ],

View file

@ -13,7 +13,6 @@
import os import os
import sys import sys
import django import django
from django.utils.version import get_docs_version
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
os.environ['DJANGO_SETTINGS_MODULE'] = 'mamweb.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'mamweb.settings'
django.setup() django.setup()
@ -74,8 +73,8 @@ html_static_path = ['_static']
# Provázání s jinými dokumentacemi # Provázání s jinými dokumentacemi
intersphinx_mapping = {'python': ('https://docs.python.org/3', None), intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
'django': (f'http://docs.djangoproject.com/en/{get_docs_version()}/', 'django': ('http://docs.djangoproject.com/en/3.2/',
f'http://docs.djangoproject.com/en/{get_docs_version()}/_objects/'),} 'http://docs.djangoproject.com/en/3.2/_objects/'),}
# Generování tříd/funkcí/atributů v pořádí jak jsou naprogramované # Generování tříd/funkcí/atributů v pořádí jak jsou naprogramované
autodoc_member_order = "bysource" autodoc_member_order = "bysource"

View file

@ -1,4 +1,4 @@
from galerie.models import Obrazek, Galerie, VZDY, ORG, NIKDY, UCASTNIK from galerie.models import Obrazek, Galerie
from django.contrib import admin from django.contrib import admin
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django import forms from django import forms
@ -8,9 +8,8 @@ from django.db import models
def zverejnit_fotogalerii(modeladmin, request, queryset): def zverejnit_fotogalerii(modeladmin, request, queryset):
'''zverejni vybranou fotogalerii i jeji vsechny podgalerie''' '''zverejni vybranou fotogalerii i jeji vsechny podgalerie'''
queryset = queryset.filter(zobrazit=ORG)
for galerie in queryset: for galerie in queryset:
galerie.zobrazit = VZDY galerie.zobrazit = 0
galerie.save() galerie.save()
zverejnit_fotogalerii(modeladmin, request, zverejnit_fotogalerii(modeladmin, request,
Galerie.objects.filter(galerie_up = galerie)) Galerie.objects.filter(galerie_up = galerie))
@ -19,9 +18,8 @@ def zverejnit_fotogalerii(modeladmin, request, queryset):
def prepnout_fotogalerii_do_org_rezimu(modeladmin, request, queryset): def prepnout_fotogalerii_do_org_rezimu(modeladmin, request, queryset):
'''zneverjni vybranou fotogalerii i jeji vsechny podgalerie''' '''zneverjni vybranou fotogalerii i jeji vsechny podgalerie'''
queryset = queryset.filter(zobrazit=VZDY)
for galerie in queryset: for galerie in queryset:
galerie.zobrazit = ORG galerie.zobrazit = 1
galerie.save() galerie.save()
prepnout_fotogalerii_do_org_rezimu(modeladmin, request, prepnout_fotogalerii_do_org_rezimu(modeladmin, request,
Galerie.objects.filter(galerie_up = galerie)) Galerie.objects.filter(galerie_up = galerie))

View file

@ -1,18 +0,0 @@
# Generated by Django 4.2.20 on 2025-04-23 18:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('galerie', '0013_post_split_soustredeni'),
]
operations = [
migrations.AlterField(
model_name='galerie',
name='zobrazit',
field=models.IntegerField(choices=[(0, 'Vždy'), (1, 'Organizátorům'), (3, 'Účastníkům a orgům'), (2, 'Nikdy')], default=1, verbose_name='Zobrazit?'),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.2.16 on 2025-04-30 18:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('galerie', '0014_alter_galerie_zobrazit'),
]
operations = [
migrations.AlterField(
model_name='galerie',
name='galerie_up',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='galerie.galerie'),
),
]

View file

@ -1,19 +0,0 @@
# Generated by Django 4.2.16 on 2025-04-30 19:02
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('galerie', '0015_alter_galerie_galerie_up'),
]
operations = [
migrations.AlterField(
model_name='obrazek',
name='galerie',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='galerie.galerie'),
),
]

View file

@ -10,11 +10,9 @@ from soustredeni.models import Soustredeni
VZDY=0 VZDY=0
ORG=1 ORG=1
NIKDY=2 NIKDY=2
UCASTNIK=3
VIDITELNOST = ( VIDITELNOST = (
(VZDY, 'Vždy'), (VZDY, 'Vždy'),
(ORG, 'Organizátorům'), (ORG, 'Organizátorům'),
(UCASTNIK, 'Účastníkům a orgům'),
(NIKDY, 'Nikdy'), (NIKDY, 'Nikdy'),
) )
@ -56,7 +54,7 @@ class Obrazek(models.Model):
nazev = models.CharField('Název', max_length=50, blank=True, null=True) nazev = models.CharField('Název', max_length=50, blank=True, null=True)
popis = models.TextField('Popis', blank=True, null=True) popis = models.TextField('Popis', blank=True, null=True)
datum_vlozeni = models.DateTimeField('Datum vložení', auto_now_add=True) datum_vlozeni = models.DateTimeField('Datum vložení', auto_now_add=True)
galerie = models.ForeignKey('Galerie', blank=True, null=True, on_delete=models.CASCADE) galerie = models.ForeignKey('Galerie', blank=True, null=True, on_delete=models.SET_NULL)
poradi = models.IntegerField('Pořadí', blank=True, null=True) poradi = models.IntegerField('Pořadí', blank=True, null=True)
def __str__(self): def __str__(self):
@ -90,7 +88,7 @@ class Galerie(models.Model):
titulni_obrazek = models.ForeignKey(Obrazek, blank = True, null = True, related_name = "+", on_delete = models.SET_NULL) titulni_obrazek = models.ForeignKey(Obrazek, blank = True, null = True, related_name = "+", on_delete = models.SET_NULL)
zobrazit = models.IntegerField('Zobrazit?', default = ORG, choices = VIDITELNOST) zobrazit = models.IntegerField('Zobrazit?', default = ORG, choices = VIDITELNOST)
galerie_up = models.ForeignKey('Galerie', blank = True, null = True, galerie_up = models.ForeignKey('Galerie', blank = True, null = True,
on_delete=models.PROTECT) on_delete=models.SET_NULL)
soustredeni = models.ForeignKey(Soustredeni, blank = True, null = True, soustredeni = models.ForeignKey(Soustredeni, blank = True, null = True,
on_delete=models.PROTECT) on_delete=models.PROTECT)
poradi = models.IntegerField('Pořadí', blank = True, null = False, default = 0) poradi = models.IntegerField('Pořadí', blank = True, null = False, default = 0)
@ -100,3 +98,25 @@ class Galerie(models.Model):
class Meta: class Meta:
verbose_name = 'Galerie' verbose_name = 'Galerie'
verbose_name_plural = 'Galerie' verbose_name_plural = 'Galerie'
#def link_na_preview(self):
#"""Odkaz na galerii, používá se v admin rozhranní. """
#return '<a href="/fotogalerie/galerie/%s/">Preview</a>' % self.id
#link_na_preview.allow_tags = True
#link_na_preview.short_description = 'Zobrazit galerii'
#
#def je_publikovano(self):
#"""Vraci True, pokud je tato galerie publikovana. """
#if self.zobrazit == VZDY:
#return True
#if self.zobrazit == PODLE_CLANKU:
#for clanek in self.clanek_set.all():
#if clanek.je_publikovano():
#return True
#return False
#
#@staticmethod
#def publikovane_galerie():
#"""Vraci galerie, ktere uz maji byt publikovane."""
#clanky = Blog.models.Clanek.publikovane_clanky()
#return Galerie.objects.filter(Q(zobrazit=VZDY) | (Q(clanek__in=clanky) & Q(zobrazit=PODLE_CLANKU))).distinct()

View file

@ -136,11 +136,6 @@
top: 160px; top: 160px;
} }
.podgalerie_nahled.mam-org-only, .podgalerie_nahled.mam-resitel-only {
margin: 10px;
padding: 0;
}
/* Odkazy na předchozí a následující podgalerii */ /* Odkazy na předchozí a následující podgalerii */
.galerie_predchozi_nasledujici { .galerie_predchozi_nasledujici {

View file

@ -46,7 +46,6 @@
{% block content %} {% block content %}
<div class="{% if obrazek.galerie.zobrazit == 1 or obrazek.galerie.zobrazit == 2 %}mam-org-only{% endif %}{% if obrazek.galerie.zobrazit == 3 %}mam-resitel-only{% endif %}">
<h2> <h2>
{% for g in cesta %} {% for g in cesta %}
@ -84,7 +83,7 @@
{# Popisek fotky #} {# Popisek fotky #}
<div class="popis"> <div class="popis">
{% if upravy_popisku %} {% if preview %}
<form action=".#nahoru" method="post" id="komentarform"> <form action=".#nahoru" method="post" id="komentarform">
{% csrf_token %} {% csrf_token %}
<table> <table>
@ -134,6 +133,4 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View file

@ -6,11 +6,8 @@ Galerie {{galerie.nazev}}
{% block content %} {% block content %}
{# FIXME: použít konstanty… #} {% if galerie.zobrazit > 0 %}
{% if galerie.zobrazit == 1 or galerie.zobrazit == 2 %}
<div class="mam-org-only"> <div class="mam-org-only">
{% elif galerie.zobrazit == 3 %}
<div class="mam-resitel-only">
{% endif %} {% endif %}
<h2> <h2>
@ -48,43 +45,37 @@ Galerie {{galerie.nazev}}
{% if podgalerie %} {% if podgalerie %}
{% with 22 as max_delka_nazvu %} {% with 22 as max_delka_nazvu %}
<div class="galerie_nahledy"> <div class="galerie_nahledy">
{% for pgalerie in podgalerie %} {% for galerie in podgalerie %}
<a href="../{{pgalerie.pk}}" <a href="../{{galerie.pk}}"
{% if pgalerie.nazev|length > max_delka_nazvu %} {% if galerie.nazev|length > max_delka_nazvu %}
title="{{ pgalerie.nazev }}" title="{{ galerie.nazev }}"
{% endif %} {% endif %}
class="podgalerie_nahled {% if pgalerie.zobrazit == 1 or pgalerie.zobrazit == 2 %}mam-org-only{% endif %}{% if pgalerie.zobrazit == 3 %}mam-resitel-only{% endif %}"> class="podgalerie_nahled">
{% if pgalerie.titulni_obrazek %} {% if galerie.titulni_obrazek %}
{% with pgalerie.titulni_obrazek.obrazek_maly as obrazek %} {% with galerie.titulni_obrazek.obrazek_maly as obrazek %}
<img src="{{ obrazek.url }}" <img src="{{ obrazek.url }}"
/> />
{% endwith %} {% endwith %}
{% endif %} {% endif %}
<div class="nazev_galerie"> <div class="nazev_galerie">
{{ pgalerie|truncatechars:max_delka_nazvu }} {{ galerie|truncatechars:max_delka_nazvu }}
</div> </div>
</a> </a>
{% if galerie.zobrazit == 1 or galerie.zobrazit == 2 %} {% if user.je_org and galerie.zobrazit > 0 %}
{% if user.je_org %}
<div class="mam-org-only-galerie"> <div class="mam-org-only-galerie">
({{pgalerie.poradi}}) ({{galerie.poradi}})
<span class="plus"><a href="plus/{{pgalerie.pk}}/">+</a></span> <span class="plus"><a href="plus/{{galerie.pk}}/">+</a></span>
<span class="minus"><a href="minus/{{pgalerie.pk}}/">-</a></span> <span class="minus"><a href="minus/{{galerie.pk}}/">-</a></span>
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if user.je_org %} {% if user.je_org and galerie.zobrazit > 0 %}
<div class="mam-org-only"> <div class="mam-org-only">
{% if galerie.zobrazit == 1 or galerie.zobrazit == 2 %} <a href="./new">Vytvořit novou podgalerii </a>
<a href="./new">Vytvořit novou podgalerii</a>, <a href="{% url 'admin:galerie_galerie_change' galerie.pk %}">upravit galerii v adminu</a>
{% else %}
Jestli chceš změnit pořadí podgalerií nebo přidat novou, nastav zobrazení jen pro orgy v <a href="{% url 'admin:galerie_galerie_change' galerie.pk %}">adminu</a>.
{% endif %}
</div> </div>
{% endif %} {% endif %}
@ -129,10 +120,8 @@ Galerie {{galerie.nazev}}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if galerie.zobrazit == 1 or galerie.zobrazit == 2 %} {% if galerie.zobrazit > 0 %}
</div> {# mam-org-only #} </div> {# mam-org-only #}
{% elif galerie.zobrazit == 2 %}
</div> {# mam-resitel-only #}
{% endif %} {% endif %}
{% endblock content %} {% endblock content %}

View file

@ -2,5 +2,5 @@
{% load static %} {% load static %}
{% block custom_css %} {% block custom_css %}
<link href="{% static 'css/galerie.css' %}?version=2" rel="stylesheet"> <link href="{% static 'css/galerie.css' %}?version=1" rel="stylesheet">
{% endblock %} {% endblock %}

View file

@ -1,6 +0,0 @@
from galerie.models import Galerie
def top_galerie(g: Galerie) -> Galerie:
while g.galerie_up is not None:
g = g.galerie_up
return g

View file

@ -1,41 +1,22 @@
import random import random
from django.http import HttpResponse, Http404, HttpRequest from django.http import HttpResponse, Http404
from django.shortcuts import render, HttpResponseRedirect, get_object_or_404 from django.shortcuts import render, HttpResponseRedirect, get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from datetime import datetime from datetime import datetime
from galerie.utils import top_galerie from galerie.models import Obrazek, Galerie
from personalni.utils import resitel_uzivatele
from galerie.models import Obrazek, Galerie, VZDY, ORG, UCASTNIK, NIKDY
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from galerie.forms import KomentarForm, NewGalerieForm from galerie.forms import KomentarForm, NewGalerieForm
import logging def zobrazit(galerie, request):
logger = logging.getLogger(__name__) preview = False
if galerie.zobrazit >= 1:
def galerie_ke_zobrazeni(soustredeni: Soustredeni | None, request: HttpRequest) -> tuple[int]: if request.user.je_org:
if request.user.is_superuser: return (VZDY, ORG, UCASTNIK, NIKDY) preview = True;
if request.user.je_org: return (VZDY, ORG, UCASTNIK)
if request.user.is_anonymous: return (VZDY,)
if soustredeni is None: return (VZDY,)
if (resitel := resitel_uzivatele(request.user)) is not None:
if resitel.soustredeni_set.contains(soustredeni):
return (VZDY, UCASTNIK)
else: else:
return (VZDY,) raise Http404
logger.warning("Nepodařilo se zjistit, jaké galerie se mají zobrazit!") return preview
return (VZDY,)
def zobrazit(galerie: Galerie, request: HttpRequest) -> bool:
soustredeni = top_galerie(galerie).soustredeni
return galerie.zobrazit in galerie_ke_zobrazeni(soustredeni, request)
def dovolit_upravy_popisku(galerie: Galerie, request: HttpRequest) -> bool:
# FIXME: Dočasné: úpravy jen když je to v org-only stavu. (Odpovídá předchozímu chování)
return request.user.je_org and galerie.zobrazit in (ORG, NIKDY)
def cesta_od_korene(g): def cesta_od_korene(g):
@ -50,19 +31,19 @@ def cesta_od_korene(g):
def nahled(request, pk, soustredeni): def nahled(request, pk, soustredeni):
"""Zobrazeni nahledu vsech fotek ve skupine.""" """Zobrazeni nahledu vsech fotek ve skupine."""
galerie = get_object_or_404(Galerie, pk=pk) galerie = get_object_or_404(Galerie, pk=pk)
soustredeni = top_galerie(galerie).soustredeni
# FIXME: přepsat model a použít přímo dolů…
podgalerie = Galerie.objects.filter(galerie_up = galerie).order_by('poradi') podgalerie = Galerie.objects.filter(galerie_up = galerie).order_by('poradi')
podgalerie = podgalerie.filter(zobrazit__in=galerie_ke_zobrazeni(soustredeni, request)) if not request.user.je_org:
podgalerie = podgalerie.filter(zobrazit__lt=1)
obrazky = galerie.obrazek_set.all().order_by('poradi', 'nazev') obrazky = Obrazek.objects.filter(galerie = galerie).order_by('poradi', 'nazev')
ma_se_zobrazit = zobrazit(galerie, request) preview = zobrazit(galerie, request)
if not ma_se_zobrazit: raise Http404("Galerie sice existuje, ale my se tváříme, že ne :-D")
sourozenci = [] sourozenci = []
if galerie.galerie_up: if galerie.galerie_up:
sourozenci = galerie.galerie_up.galerie_set.filter(zobrazit__in=galerie_ke_zobrazeni(soustredeni, request)).order_by('poradi') sourozenci = galerie.galerie_up.galerie_set.all().order_by('poradi')
if not request.user.je_org:
sourozenci = sourozenci.filter(zobrazit__lt=1)
predchozi = None predchozi = None
nasledujici = None nasledujici = None
@ -81,6 +62,7 @@ def nahled(request, pk, soustredeni):
{'galerie' : galerie, {'galerie' : galerie,
'podgalerie' : podgalerie, 'podgalerie' : podgalerie,
'obrazky' : obrazky, 'obrazky' : obrazky,
'preview' : preview,
'cesta': cesta, 'cesta': cesta,
'sourozenci': sourozenci, 'sourozenci': sourozenci,
'predchozi': predchozi, 'predchozi': predchozi,
@ -96,41 +78,9 @@ def detail(request, pk, fotka, soustredeni):
NAHLEDU = 1 NAHLEDU = 1
galerie = get_object_or_404(Galerie, pk=pk) galerie = get_object_or_404(Galerie, pk=pk)
soustredeni = top_galerie(galerie).soustredeni preview = zobrazit(galerie, request)
ma_se_zobrazit = zobrazit(galerie, request)
if not ma_se_zobrazit: raise Http404("Obrázek neukážu!")
obrazek = get_object_or_404(Obrazek, pk=fotka) obrazek = get_object_or_404(Obrazek, pk=fotka)
# Pořadí není povinné. FIXME: `nazev` je zavádějící… Ale tohle je kanonické pořadí obrázků v galerii…
obrazky = galerie.obrazek_set.all().order_by('poradi', 'nazev') obrazky = galerie.obrazek_set.all().order_by('poradi', 'nazev')
obrazky = list(obrazky)
index_obrazku = obrazky.index(obrazek)
# Podle mě se nemůže stát, že by volání výš selhalo, kdyžtak shodí web. (původně to byl explicitně ošetřený stav dávající 404)
predchozi_obrazky = list(reversed(obrazky[:index_obrazku]))
nasledujici_obrazky = obrazky[index_obrazku+1:]
# Může jich být hodně…
predchozi_obrazky = predchozi_obrazky[:NAHLEDU]
nasledujici_obrazky = nasledujici_obrazky[:NAHLEDU]
# Předchozí obrázky chceme v normálním pořadí
predchozi_obrazky = list(reversed(predchozi_obrazky))
if galerie.galerie_up is not None:
sousedni_galerie = Galerie.objects.filter(galerie_up=galerie.galerie_up, zobrazit__in=galerie_ke_zobrazeni(soustredeni, request)).order_by('poradi')
sousedni_galerie = list(sousedni_galerie)
# Teoreticky se můžeme dívat na galerie.poradi, ale jednak už tenhle pattern stejně je výš a druhak je galerií málo, takže pomalost nevadí.
index_galerie = sousedni_galerie.index(galerie)
predchozi_galerie = sousedni_galerie[index_galerie-1] if index_galerie > 0 else None
nasledujici_galerie = sousedni_galerie[index_galerie+1] if index_galerie < len(sousedni_galerie) - 1 else None
else:
predchozi_galerie = None
nasledujici_galerie = None
# Pokud je obrázků dost, tak další galerii nepotřebujeme
# (jo, mohli jsme si ušetřit práci, ale takhle je kód imho přehlednější a za pár ušetřených dotazů do DB to nestojí)
if len(predchozi_obrazky) >= NAHLEDU:
predchozi_galerie = None
if len(nasledujici_obrazky) >= NAHLEDU:
nasledujici_galerie = None
# vytvoreni a obslouzeni formulare # vytvoreni a obslouzeni formulare
if request.method == 'POST': if request.method == 'POST':
@ -141,6 +91,49 @@ def detail(request, pk, fotka, soustredeni):
else: else:
form = KomentarForm({'komentar': obrazek.popis}) form = KomentarForm({'komentar': obrazek.popis})
# Poradi aktualniho obrazku v galerii/stitku.
for i in range(len(obrazky)):
if obrazky[i] == obrazek:
poradi = i
break
else:
# Obrazek neni v galerii/stitku.
raise Http404
# Nacteni okolnich obrazku a galerii
# TODO vyjmout zjisteni predchozich a nasledujicich galerii
# a udelat z toho funkci, ktera se pouzije u nahledu
predchozi_galerie = None
nasledujici_galerie = None
obrazky_dalsi = obrazky[poradi+1:poradi+NAHLEDU+1]
if (poradi+1) > NAHLEDU:
obrazky_predchozi = obrazky[poradi-NAHLEDU:poradi]
else:
obrazky_predchozi = obrazky[0:poradi]
if galerie.poradi > 1:
predchozi_galerie = Galerie.objects.\
filter(galerie_up=galerie.galerie_up).\
filter(poradi=(galerie.poradi-1))
if predchozi_galerie:
predchozi_galerie = predchozi_galerie[0]
else:
predchozi_galerie = None
if (poradi+1) == len(obrazky): # Tohle je poslední obrázek
if (galerie.poradi is not None
and galerie.galerie_up is not None):
nasledujici_galerie = Galerie.objects.\
filter(galerie_up=galerie.galerie_up).\
filter(poradi=(galerie.poradi+1))
else:
nasledujici_galerie = None
if nasledujici_galerie:
nasledujici_galerie = nasledujici_galerie[0]
else:
nasledujici_galerie = None
# Preskalovani obrazku do vybraneho prostoru. # Preskalovani obrazku do vybraneho prostoru.
vyska = obrazek.obrazek_stredni.height vyska = obrazek.obrazek_stredni.height
sirka = obrazek.obrazek_stredni.width sirka = obrazek.obrazek_stredni.width
@ -158,9 +151,9 @@ def detail(request, pk, fotka, soustredeni):
'obrazek' : obrazek, 'obrazek' : obrazek,
'vyska' : vyska, 'vyska' : vyska,
'sirka' : sirka, 'sirka' : sirka,
'obrazky_predchozi' : predchozi_obrazky, 'obrazky_predchozi' : obrazky_predchozi,
'obrazky_dalsi' : nasledujici_obrazky, 'obrazky_dalsi' : obrazky_dalsi,
'upravy_popisku' : dovolit_upravy_popisku(galerie, request), 'preview' : preview,
'form' : form, 'form' : form,
'cesta': cesta_od_korene(galerie), 'cesta': cesta_od_korene(galerie),
}) })
@ -186,7 +179,7 @@ def new_galerie(request, galerie, soustredeni):
gal = Galerie() gal = Galerie()
gal.nazev = form.cleaned_data['nazev'] gal.nazev = form.cleaned_data['nazev']
#gal.popis = form.cleaned_data['popis'] # popis nepouzivame #gal.popis = form.cleaned_data['popis'] # popis nepouzivame
gal.zobrazit = ORG # galerie je v procesu vytvareni gal.zobrazit = 1 # galerie je v procesu vytvareni
''' pokud je to podgalerie pridej nadrazenou galerii ''' pokud je to podgalerie pridej nadrazenou galerii
a nadrazene soustredeni nechej volne, a nadrazene soustredeni nechej volne,
pokud je to hlavni galerie, tak nadrazena galerie neexistuje, pokud je to hlavni galerie, tak nadrazena galerie neexistuje,

View file

@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from korektury.models import KorekturovanePDF, Oprava, KorekturaTag from korektury.models import KorekturovanePDF
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.urls import reverse from django.urls import reverse
@ -62,11 +62,3 @@ Korekturovátko
).send() ).send()
admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin) admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin)
class OpravaAdmin(admin.ModelAdmin):
model = Oprava
filter_horizontal = ("informovani_orgove", "tagy",)
admin.site.register(Oprava, OpravaAdmin)
admin.site.register(KorekturaTag)

View file

@ -1,7 +0,0 @@
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

@ -1,11 +0,0 @@
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'),
path('oprava/smaz', org_required(views.oprava_smaz_view), name='korektury_api_oprava_smaz'),
path('komentar/smaz', org_required(views.komentar_smaz_view), name='korektury_api_komentar_smaz'),
]

View file

@ -1,134 +0,0 @@
from http import HTTPStatus
from django.http import JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.html import linebreaks
from rest_framework import serializers
from korektury.utils import send_email_notification_komentar
from korektury.models import Oprava, KorekturovanePDF, Komentar, KorekturaTag
from personalni.models import Organizator
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})
def oprava_smaz_view(request, **kwargs):
q = request.POST
op_id_str = q.get('oprava_id')
assert op_id_str is not None
op_id = int(op_id_str)
oprava = get_object_or_404(Oprava, id=op_id)
oprava.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
def komentar_smaz_view(request, **kwargs):
q = request.POST
kom_id_str = q.get('komentar_id')
assert kom_id_str is not None
kom_id = int(kom_id_str)
komentar = get_object_or_404(Komentar, id=kom_id)
komentar.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
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 KorekturaTagSerializer(serializers.ModelSerializer):
class Meta:
model = KorekturaTag
fields = '__all__'
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()]
ret["tagy"] = [KorekturaTagSerializer(tag).data for tag in instance.tagy.all()]
return ret
# komentar_set = serializers.ListField(child=KomentarSerializer())
def opravy_a_komentare_view(request, pdf_id: int, **kwargs):
if request.method == 'POST':
q = request.POST
x = int(q.get('x'))
y = int(q.get('y'))
img_id = int(q.get('img_id'))
oprava_id = int(q.get('oprava_id'))
komentar_id = int(q.get('komentar_id'))
text = q.get('text')
# prirazeni autora podle prihlaseni
autor_user = request.user
# pokud existuje ucet (user), ale neni to organizator = 403
autor = Organizator.objects.filter(osoba__user=autor_user).first()
if komentar_id != -1:
komentar = get_object_or_404(Komentar, id=komentar_id)
if komentar.text != text:
komentar.text = text
komentar.autor = autor
komentar.save()
oprava = komentar.oprava
else:
if oprava_id != -1:
oprava = get_object_or_404(Oprava, id=oprava_id)
else:
pdf = get_object_or_404(KorekturovanePDF, id=pdf_id)
oprava = Oprava.objects.create(
pdf=pdf,
strana=img_id,
x=x,
y=y,
)
Komentar.objects.create(oprava=oprava, autor=autor, text=text)
send_email_notification_komentar(oprava, autor, request)
tagy_raw = q.get('tagy')
if tagy_raw is not None:
oprava.tagy.clear()
if tagy_raw != "":
tagy = list(map(int, tagy_raw.split(",")))
oprava.tagy.add(*KorekturaTag.objects.filter(id__in=tagy))
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]})

14
korektury/forms.py Normal file
View file

@ -0,0 +1,14 @@
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

@ -1,45 +0,0 @@
# 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

@ -1,29 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-11 14:28
from django.db import migrations, models
def pridani_orgu(apps, _schema_editor):
Komentar = apps.get_model('korektury','Komentar')
for komentar in Komentar.objects.all():
org = komentar.autor
if org is not None:
# Tohle jde asi udělat lépe než .all(…). Ale nejhorší na tom je, že .add(…) funguje jinak tady v migracích.
if org not in komentar.oprava.informovani_orgove.all():
komentar.oprava.informovani_orgove.add(org)
class Migration(migrations.Migration):
dependencies = [
('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'),
('korektury', '0025_remove_oprava_autor_remove_oprava_text'),
]
operations = [
migrations.AddField(
model_name='oprava',
name='informovani_orgove',
field=models.ManyToManyField(blank=True, default=None, help_text='Orgové informovaní při přidání komentáře ke korektuře', related_name='informovan_o_opravach', to='personalni.organizator', verbose_name='Informovaní organizátoři'),
),
migrations.RunPython(pridani_orgu, migrations.RunPython.noop),
]

View file

@ -1,27 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-11 16:07
import colorfield.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('korektury', '0026_oprava_informovani_orgove'),
]
operations = [
migrations.CreateModel(
name='KorekturaTag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nazev', models.CharField(help_text='Název daného tagu, <20 znaků', max_length=20, verbose_name='název tagu')),
('barva', colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=None, verbose_name='barva daného tagu')),
],
),
migrations.AddField(
model_name='oprava',
name='tagy',
field=models.ManyToManyField(blank=True, default=None, to='korektury.korekturatag'),
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 4.2.16 on 2025-03-19 18:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('korektury', '0027_korekturatag_oprava_tagy'),
]
operations = [
migrations.AlterModelOptions(
name='korekturovanepdf',
options={'ordering': ['-cas'], 'verbose_name': 'PDF ke korekturování', 'verbose_name_plural': 'PDFka ke korekturování'},
),
migrations.AlterField(
model_name='korekturovanepdf',
name='nazev',
field=models.CharField(help_text='Název (např. „42.6 | analýza v4“ nebo „propagace | letáček v0“) korekturovaného PDF, v seznamu se seskupují podle textu do první mezery, tedy zde „42.6“ respektive „propagace“.', max_length=50, verbose_name='název PDF'),
),
]

View file

@ -1,7 +1,4 @@
import os import os
from colorfield.fields import ColorField
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -35,15 +32,15 @@ class KorekturovanePDF(models.Model):
class Meta: class Meta:
ordering = ['-cas'] ordering = ['-cas']
db_table = 'korekturovane_cislo' db_table = 'korekturovane_cislo'
verbose_name = 'PDF ke korekturování' verbose_name = u'PDF k opravám'
verbose_name_plural = 'PDFka ke korekturování' verbose_name_plural = u'PDF k opravám'
#Interní ID #Interní ID
id = models.AutoField(primary_key = True) id = models.AutoField(primary_key = True)
cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF') cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF')
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. „42.6 | analýza v4“ nebo „propagace | letáček v0“) korekturovaného PDF, v seznamu se seskupují podle textu do první mezery, tedy zde „42.6“ respektive „propagace“.') nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF')
komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)') komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)')
@ -55,13 +52,16 @@ 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'
class STATUS(models.TextChoices): STATUS_ZANASENI = 'zanaseni'
PRIDAVANI = 'pridavani', 'Přidávání korektur' STATUS_ZASTARALE = 'zastarale'
ZANASENI = 'zanaseni', 'Korektury jsou zanášeny' STATUS_CHOICES = (
ZASTARALE = 'zastarale', 'Stará verze, nekorigovat' (STATUS_PRIDAVANI, u'Přidávání korektur'),
(STATUS_ZANASENI, u'Korektury jsou zanášeny'),
status = models.CharField(u'stav PDF',max_length=16, choices=STATUS.choices, blank=False, default = STATUS.PRIDAVANI) (STATUS_ZASTARALE, u'Stará verze, nekorigovat'),
)
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.',
@ -134,14 +134,6 @@ class KorekturovanePDF(models.Model):
return reverse('korektury', kwargs={'pdf': self.id}) return reverse('korektury', kwargs={'pdf': self.id})
class KorekturaTag(models.Model):
nazev = models.CharField("název tagu", blank = False, max_length=20, help_text="Název daného tagu, <20 znaků")
barva = ColorField("barva daného tagu", default="#FFFFFF")
def __str__(self):
return self.nazev
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Oprava(models.Model): class Oprava(models.Model):
class Meta: class Meta:
@ -160,22 +152,32 @@ 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')
class STATUS(models.TextChoices): STATUS_K_OPRAVE = 'k_oprave'
K_OPRAVE = 'k_oprave', 'K opravě' STATUS_OPRAVENO = 'opraveno'
OPRAVENO = 'opraveno', 'Opraveno' STATUS_NENI_CHYBA = 'neni_chyba'
NENI_CHYBA = 'neni_chyba', 'Není chyba' STATUS_K_ZANESENI = 'k_zaneseni'
K_ZANESENI = 'k_zaneseni', 'K zanesení do TeXu' STATUS_CHOICES = (
(STATUS_K_OPRAVE, u'K opravě'),
status = models.CharField(u'stav opravy',max_length=16, choices=STATUS.choices, blank=False, default = STATUS.K_OPRAVE) (STATUS_OPRAVENO, u'Opraveno'),
(STATUS_NENI_CHYBA, u'Není chyba'),
informovani_orgove = models.ManyToManyField( (STATUS_K_ZANESENI, u'K zanesení do TeXu'),
Organizator, blank=True, default=None,
verbose_name='Informovaní organizátoři',
help_text="Orgové informovaní při přidání komentáře ke korektuře",
related_name='informovan_o_opravach',
) )
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)
tagy = models.ManyToManyField(KorekturaTag, blank=True, default=None,)
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
@ -201,7 +203,5 @@ class Komentar(models.Model):
def __str__(self): def __str__(self):
return '{} od {}: {}'.format(self.cas,self.autor,self.text) return '{} od {}: {}'.format(self.cas,self.autor,self.text)
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.autor is not None:
self.oprava.informovani_orgove.add(self.autor)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 270 B

View file

@ -1,10 +0,0 @@
<svg id="icon-reload" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 120 120" xml:space="preserve">
<g>
<path d="M60,95.5c-19.575,0-35.5-15.926-35.5-35.5c0-19.575,15.925-35.5,35.5-35.5c13.62,0,25.467,7.714,31.418,19h22.627
C106.984,20.347,85.462,3.5,60,3.5C28.796,3.5,3.5,28.796,3.5,60c0,31.203,25.296,56.5,56.5,56.5
c16.264,0,30.911-6.882,41.221-17.88L85.889,84.255C79.406,91.168,70.201,95.5,60,95.5z"/>
</g>
<line fill="none" x1="120" y1="0" x2="120" y2="45.336"/>
<line fill="none" x1="91.418" y1="43.5" x2="114.045" y2="43.5"/>
<polygon points="120,21.832 119.992,68.842 74.827,55.811 "/>
</svg>

Before

Width:  |  Height:  |  Size: 712 B

View file

@ -1,220 +1,136 @@
.textzanaseni { display:none; } body,
.textzastarale { display:none; } .adding{
#prekomentar, #prekorektura, #prepointer { display: none; }
body {
&[data-stav_pdf="pridavani"] {
background: #f3f3f3; background: #f3f3f3;
} color: black;
&[data-stav_pdf="zanaseni"] {
background: yellow;
.textzanaseni { display: unset; }
}
&[data-stav_pdf="zastarale"] {
background: red;
.textzastarale { display: unset; }
}
} }
.comitting
#sbal-korektury, #rozbal-korektury { {
float: right; background: yellow;
margin-left: 4pt; }
.deprecated {
background: red;
} }
img{background:white;} img{background:white;}
/* Barvy korektur */ /* Barvy korektur */
[data-stav_korektury="k_oprave"] { .k_oprave {
--rgb: 255, 0, 0; --rgb: 255, 0, 0;
[value="k_oprave"] { display: none }
.komentovat_disabled { display: none }
} }
[data-stav_korektury="opraveno"] { .opraveno {
--rgb: 0, 0, 255; --rgb: 0, 0, 255;
[value="opraveno"] { display: none }
.komentovat { display: none }
} }
[data-stav_korektury="neni_chyba"] { .neni_chyba {
--rgb: 128, 128, 128; --rgb: 128, 128, 128;
[value="neni_chyba"] { display: none }
.komentovat { display: none }
} }
[data-stav_korektury="k_zaneseni"] { .k_zaneseni {
--rgb: 0, 255, 0; --rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none }
.komentovat { display: none }
} }
/* Skrývání korektur */ .pointer-hi,
[data-korektura_sbalena="true"] {
.korektura-telo { display: none; }
.korektura-tlacitka { display: none; }
.sbal-rozbal-img { transform: rotate(180deg); }
}
/* Skrývání komentářů */
[data-komentar_sbalen="true"] {
.sbal-rozbal-img { transform: rotate(180deg); }
.uprav-komentar { display: none; }
.komtext { display: none; }
}
/* Čá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: 1px solid rgb(var(--rgb),var(--alpha)); border-left: 2px solid yellow;
border-bottom: 1px solid rgb(var(--rgb),var(--alpha)); border-bottom: 2px solid yellow;
pointer-events: none; border-color: rgb(var(--rgb),var(--alpha));
}
.pointer {
border-width: 1px;
--alpha: 0.35; --alpha: 0.35;
}
/* Zvýraznění čáry při najetí na korekturu */ .pointer-hi {
&[data-hover="true"] {
border-width: 3px; border-width: 3px;
--alpha: 1; --alpha: 1;
}
} }
/* Korektura samotná */
.korektura {
.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 rgb(var(--rgb)); border: 2px solid black;
border-radius: 10px; border-radius: 10px;
position: absolute; border-color: rgb(var(--rgb));
&: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; }
.hlavicka-komentare {
overflow: auto;
}
.autor {
font-weight: bold;
float: left;
margin-top: 3px;
}
.float-right{
float:right;
}
} }
form { form {
display:inline; display:inline;
} }
/* Zobrazované PDF */ .float-right{
.imgdiv { float:right;
position:relative;
left:0;
top:0;
} }
/* Přidávání korektury / úprava komentáře */ .imgdiv {
#korekturovaci-formular-div { position:relative;
left:0px;
top:0px;
}
#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{
.korektury-tag { background-color: yellow;
border-radius: 5px;
margin: 2px;
padding: 2px;
&[data-vybran="false"] { background: unset !important; }
/*&[data-vybran="true"] { border-color: unset !important; }*/
} }
/* Šipky na posouvání korektur */
#korektury-sipky {
position: fixed;
bottom: 5px;
left: 5px;
opacity: 50%;
button, img { .box button,
.box img,
.box-done button,
.box-done img,
.box-ready button,
.box-ready img,
.box-wontfix button,
.box-wontfix img{
border: 1px solid white; border: 1px solid white;
background-color:transparent; background-color:transparent;
margin:0; margin:0;
padding: 1px; padding: 1px;
}
border-radius: 5px; .box button:hover,
.box img:hover,
&: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; border: 1px solid black;
}
}
button img { pointer-events: none; }
#predchozi-korektura, #dalsi-korektura {
background-color: #EEEEEE;
}
#predchozi-korektura-k-oprave, #dalsi-korektura-k-oprave {
background-color: #FF0000;
}
#predchozi-korektura-k-zaneseni, #dalsi-korektura-k-zaneseni {
background-color: #00FF00;
}
/* Tlačítko na aktualizaci */
#korektury-aktualizace {
background-color: #e84e10;
}
} }
.comment hr {
/**** ROZLIŠENÍ MEZI LOKÁLNÍM, TESTOVACÍM A PRODUKČNÍM WEBEM ****/ height: 0px;
body.localweb, body.testweb, body.suprodweb { }
&:before, &:after {
content: ""; .corr-header {
position: fixed; overflow: auto;
width: 20px; }
height: 100%;
top: 0; .author {
z-index: -1000; font-weight: bold;
} float: left;
margin-top: 3px;
&:before { left: 0; }
&:after { right: 0; }
} }
body.localweb { &:before, &:after { background: greenyellow; } }
body.testweb { &:before, &:after { background: darkorange; } }
body.suprodweb { &:before, &:after { background: red; } }
/****************************************************************/

View file

@ -0,0 +1,283 @@
function place_comments_one_div(img_id, comments)
{
var img = document.getElementById(img_id);
if( img == null ) {
return;
}
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];
}
});
//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() {
for (var i=0; i < comments.length-1; i++) {
place_comments_one_div(comments[i][0], comments[i][1])
}
}
// 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

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

View file

@ -1,64 +0,0 @@
{# Okolí samotného hlavni_cast_korekturovatka.html, tedy „povinné HTML věci“, informace o korekturovaném PDF a starání se o stav PDF #}
{% load static %}
<html lang='cs'>
<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"%}?version=3" />
<link href="{% static 'css/rozliseni.css' %}?version=3" rel="stylesheet">
<title>Korektury {{korekturovanepdf.nazev}}</title>
</head>
<body class="{{ LOCAL_TEST_PROD }}web" data-stav_pdf="{{ korekturovanepdf.status }}">
<h1>Korektury {{korekturovanepdf.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>{{korekturovanepdf.komentar}}</i>
<br>
<i>Klikni na chybu, napiš komentář</i> |
<a href="{{korekturovanepdf.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/hlavni_cast_korekturovatka.html" %}
{% include "korektury/korekturovatko/zmena_stavu_pdf.html" %}
<hr/>
<p>
Děkujeme opravovatelům: <span id="pocty_autoru"></span></p>
<hr>
<script>
/**
* HTML prvek, kam se zapíší (pomocí .innerHTML) počty korektur jednotlivých autorů
* @type {HTMLElement}
*/
const span_s_pocty_autoru = document.getElementById("pocty_autoru")
/** Aktualizuje, kolik který autor má komentářů u daného korekturovaného PDF. */
function aktualizuj_pocty_zasluh() {
const pocty_autoru = {};
for (let komentar of Object.values(komentare)) {
if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0;
pocty_autoru[komentar.autor] += 1;
}
const setrizene = [];
for (const keyval of Object.entries(pocty_autoru)) setrizene.push(keyval);
setrizene.sort(function(a, b) {return a[1] - b[1];});
let ans = "";
for (let [autor, pocet] of setrizene) ans += `, ${autor} (${pocet})`;
span_s_pocty_autoru.innerHTML = ans.substring(2);
}
</script>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,49 +0,0 @@
{# Template starající se o formulář na změnu stavu PDF (včetně jeho odeslání) #}
<b>Změnit stav PDF:</b>
<br>
<form method="post" id="PDFSTAV_FORM">
{% csrf_token %}
<input type="radio" name="state" value="{{ korekturovanepdf.STATUS.PRIDAVANI }}" {% if korekturovanepdf.status == korekturovanepdf.STATUS.PRIDAVANI %} checked {% endif %}>Přidávání korektur
<br>
<input type="radio" name="state" value="{{ korekturovanepdf.STATUS.ZANASENI }}" {% if korekturovanepdf.status == korekturovanepdf.STATUS.ZANASENI %} checked {% endif %}>Zanášení korektur
<br>
<input type="radio" name="state" value="{{ korekturovanepdf.STATUS.ZASTARALE }}" {% if korekturovanepdf.status == korekturovanepdf.STATUS.ZASTARALE %} checked {% endif %}>Zastaralé, nekorigovat
<br>
<input type='submit' value='Změnit stav PDF'/>
</form>
<script>
/**
* Formulář měnící stav korekturovaného PDF
* @type {HTMLFormElement}
*/
const pdfstav_form = document.getElementById('PDFSTAV_FORM');
/**
* Fetchne stav korekturovaného PDF a změní ho na dané stránce.
* FIXME: nemění, který radio-button je vybrán.
* @param {RequestInit} data FormData a jiné náležitosti (method: POST) posílané při změně stavu korekturovaného PDF
* @param {Boolean} catchError jestli padat hlasitě (pokud se aktualizuje automaticky a spadne to např. na nepřítomnost sítě, pak není třeba informovat uživatele)
*/
function fetchStav(data, catchError=true) {
fetch("{% url 'korektury_api_pdf_stav' korekturovanepdf.id %}", data
)
.then(response => {
if (!response.ok) { if (catchError) alert("Něco se nepovedlo:" + response.statusText);}
else response.json().then(data => document.body.dataset.stav_pdf = 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), 120000); // Každý dvě minuty fetchni stav
</script>

View file

@ -0,0 +1,244 @@
{% 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,6 +1,4 @@
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
@ -9,6 +7,4 @@ 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')),
] ]

View file

@ -1,53 +0,0 @@
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}#kor{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 org in oprava.informovani_orgove.all():
email_komentujiciho = org.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

@ -1,15 +1,25 @@
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.http import HttpResponseForbidden
from django.core.mail import EmailMessage
from django.db.models import Count,Q from django.db.models import Count,Q
from .models import Oprava, KorekturovanePDF, KorekturaTag 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=Oprava.STATUS.K_OPRAVE)), k_oprave_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_oprave')),
opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.OPRAVENO)), opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='opraveno')),
neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.NENI_CHYBA)), neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='neni_chyba')),
k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.K_ZANESENI)), k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_zaneseni')),
) )
template_name = 'korektury/seznam.html' template_name = 'korektury/seznam.html'
@ -46,14 +56,191 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
return reversed(sorted(qs, key=lambda it: it.cislo_a_tema)) return reversed(sorted(qs, key=lambda it: it.cislo_a_tema))
### Korektury ### Korektury
class KorekturyView(generic.DetailView): class KorekturyView(generic.TemplateView):
model = KorekturovanePDF model = Oprava
pk_url_kwarg = "pdf" template_name = 'korektury/opraf.html'
template_name = 'korektury/korekturovatko/html_obal.html' form_class = OpravaForm
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
q = request.POST
scroll = q.get('scroll')
# prirazeni autora podle prihlaseni
autor_user = request.user
# pokud existuje ucet (user), ale neni to organizator = 403
autor = Organizator.objects.filter(osoba__user=autor_user).first()
if not autor:
return HttpResponseForbidden()
if not scroll:
scroll = 0
action = q.get('action')
if (action == ''): # Přidej
x = int(q.get('x'))
y = int(q.get('y'))
text = q.get('txt')
strana = int(q.get('img-id')[4:])
pdf = KorekturovanePDF.objects.get(id=q.get('pdf'))
op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf)
op.save()
self.send_email_notification_komentar(op,autor)
elif (action == 'del'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.delete()
elif (action == 'update'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
text = q.get('txt')
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()
elif (action == 'comment'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
text = q.get('txt')
kom = Komentar(oprava=op,autor=autor,text=text)
kom.save()
self.send_email_notification_komentar(op,autor)
elif (action == 'update-comment'):
id = int(q.get('id'))
kom = Komentar.objects.get(id=id)
text = q.get('txt')
kom.text = text
kom.autor = autor
kom.save()
elif (action == 'del-comment'):
id = int(q.get('id'))
kom = Komentar.objects.get(id=id)
kom.delete()
elif (action == 'set-state'):
pdf = KorekturovanePDF.objects.get(id=q.get('pdf'))
if (q.get('state') == 'adding'):
pdf.status = pdf.STATUS_PRIDAVANI
elif (q.get('state') == 'comitting'):
pdf.status = pdf.STATUS_ZANASENI
elif (q.get('state') == 'deprecated'):
pdf.status = pdf.STATUS_ZASTARALE
pdf.save()
context = self.get_context_data()
context['scroll'] = scroll
context['autor'] = autor
return render(request, 'korektury/opraf.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)
context['indexy_stran'] = range(self.object.stran) pdf = get_object_or_404(KorekturovanePDF, id=self.kwargs['pdf'])
context['tagy'] = KorekturaTag.objects.all() context['pdf'] = pdf
context['img_prefix'] = pdf.get_prefix()
context['img_path'] = settings.KOREKTURY_IMG_DIR
context['img_indexes'] = range(pdf.stran)
context['form_oprava'] = OpravaForm()
opravy = Oprava.objects.filter(pdf=self.kwargs['pdf'])
zasluhy = {}
for o in opravy:
if o.autor in zasluhy:
zasluhy[o.autor]+=1
else:
zasluhy[o.autor]=1
o.komentare = o.komentar_set.all()
for k in o.komentare:
if k.autor in zasluhy:
zasluhy[k.autor] += 1
else:
zasluhy[k.autor] = 1
zasluhy = [
{'autor': jmeno, 'pocet': pocet}
for (jmeno, pocet) in zasluhy.items()
]
zasluhy.sort(key=lambda z: z['pocet'], reverse=True)
strany = set(o.strana for o in opravy)
opravy_na_stranu = [{'strana': s, 'op_id': opravy.filter(strana=s)} for s in strany]
context['opravy_strany'] = opravy_na_stranu
context['k_oprave_cnt'] = opravy.filter(status='k_oprave').count()
context['opraveno_cnt'] = opravy.filter(status='opraveno').count()
context['neni_chyba_cnt'] = opravy.filter(status='neni_chyba').count()
context['k_zaneseni_cnt'] = opravy.filter(status='k_zaneseni').count()
context['opravy'] = opravy
context['zasluhy'] = zasluhy
return context return context
def form_valid(self,form):
return super().form_valid(form)

View file

@ -57,7 +57,6 @@ DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error' CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error'
# Modules configuration # Modules configuration
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
@ -129,15 +128,12 @@ INSTALLED_APPS = (
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'colorfield',
# MaMweb # MaMweb
'mamweb', 'mamweb',
'seminar', 'seminar',
'tvorba', 'tvorba',
'galerie', 'galerie',
'korektury', 'korektury',
'korektury.api',
'prednasky', 'prednasky',
'header_fotky', 'header_fotky',
'various', 'various',

View file

@ -1,3 +0,0 @@
.add-related, .delete-related, .change-related {
display: none;
}

View file

@ -503,10 +503,5 @@ label[for=id_skola] {
font-weight: bold; font-weight: bold;
} }
/* Přednášky */
.textznalosti, .textprednasky {
font-style: italic;
}
/*******************/ /*******************/

View file

@ -5,7 +5,6 @@
<link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<script src="{% static 'js/jquery-1.11.1.js' %}"></script> <script src="{% static 'js/jquery-1.11.1.js' %}"></script>
<link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet"> <link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet">
<link href="{% static 'css/admin.css' %}?version=1" rel="stylesheet">
{% endblock %} {% endblock %}
{% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %} {% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %}

View file

@ -1,14 +0,0 @@
.odevzdavatko-role {
font-size: 0.8em;
.vyrazne {
color: var(--hlavni-oranzova);
}
.nevyrazne {
color: #aaa;
}
}
.hodnoceni.zvyraznene {
background-color: var(--svetla-oranzova);
}

View file

@ -1,6 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block custom_css %}
<link href="{% static 'css/odevzdavatko.css' %}?version=1" rel="stylesheet">
{% endblock %}

View file

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

View file

@ -1,7 +1,6 @@
{% extends "odevzdavatko/base.html" %} {% extends "base.html" %}
{% load barvy_reseni %} {% load barvy_reseni %}
{% load orgove %}
{% block content %} {% block content %}
@ -28,15 +27,7 @@ Do data (včetně): {{ filtr.reseni_do }}
{% for p in problemy %} {% for p in problemy %}
<th> <th>
{# TODO: Přehled řešení k problému, odkázaný odsud? #} {# TODO: Přehled řešení k problému, odkázaný odsud? #}
<span title="Autor: {{ p.autor }}, Garant: {{ p.garant }}, Opravovatelé: {{ p.opravovatele.all | join:", " }}">{{ p }} {{ p }}
<span class="odevzdavatko-role">
{% spaceless %}
<span class="{{ p|ma_autora:user|yesno:"vyrazne,nevyrazne" }}">A</span>
<span class="{{ p|ma_garanta:user|yesno:"vyrazne,nevyrazne" }}">G</span>
<span class="{{ p|ma_opravovatele:user|yesno:"vyrazne,nevyrazne" }}">O</span>
{% endspaceless %}
</span>
</span>
</th> </th>
{% endfor %} {% endfor %}
</tr> </tr>

View file

@ -1,27 +0,0 @@
from django import template
register = template.Library()
from personalni.utils import organizator_cehokoliv
# Jen typová anotace
from tvorba.models import Problem
from personalni.models import Osoba, Organizator, Resitel, Prijemce
from django.contrib.auth.models import AnonymousUser, User
@register.filter
def ma_autora(p: Problem, o: Osoba | Organizator | User | AnonymousUser | Resitel | Prijemce) -> bool | None:
o = organizator_cehokoliv(o)
if o is None: return None
return p.autor == o
@register.filter
def ma_garanta(p: Problem, o: Osoba | Organizator | User | AnonymousUser | Resitel | Prijemce) -> bool | None:
o = organizator_cehokoliv(o)
if o is None: return None
return p.garant == o
@register.filter
def ma_opravovatele(p: Problem, o: Osoba | Organizator | User | AnonymousUser | Resitel | Prijemce) -> bool | None:
o = organizator_cehokoliv(o)
if o is None: return None
return p.opravovatele.contains(o)

View file

@ -26,10 +26,9 @@
.tres { .tres {
flex: 1; flex: 1;
text-align: end;
} }
.half-opacity { .grey {
opacity: 0.5; opacity: 0.5;
} }
} }

View file

@ -18,7 +18,7 @@
{% for osoba in object_list %} {% for osoba in object_list %}
<div class="osoba"> <div class="osoba">
<div class="uno">{{ osoba.jmeno }} {{ osoba.prijmeni }}</div> <div class="uno">{{ osoba.jmeno }} {{ osoba.prijmeni }}</div>
<div class="dos {% if not osoba.jak_se_dozvedeli %}half-opacity{% endif %}">{% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}</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 class="tres">{{ osoba.datum_registrace }}</div>
</div> </div>
{% endfor %} {% endfor %}

View file

@ -1,113 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h2><strong>Export lidí</strong></h2>
<p>Vyberte pole, které chcete exportovat</p>
<!-- for loop zde neni pouzit proto, aby se mohlo napsat
data-value="email telefon mesto"
a zabalit tak vice parametru do jednoho checkboxu -->
<p>
<label>( Jméno: <input class="field-check" data-value="jmeno" type="checkbox" checked>)</label>
<label>( Příjmení: <input class="field-check" data-value="prijmeni" type="checkbox" checked>)</label>
<label>( E-mail <input class="field-check" data-value="email" type="checkbox" checked>)</label>
<label>( Telefon <input class="field-check" data-value="telefon" type="checkbox" checked>)</label>
<label>( Ulice <input class="field-check" data-value="ulice" type="checkbox">)</label>
<label>( Město <input class="field-check" data-value="mesto" type="checkbox">)</label>
<label>( PSČ <input class="field-check" data-value="psc" type="checkbox">)</label>
</p>
<select name="select-one" id="select-one">
<option value="0">---</option>
<option value="1">Řešitelé čísla</option>
<option value="2">Řešitelé ročníku</option>
<option value="3">Všichni řešitelé, kteří ještě neodmaturovali</option>
<option value="4">Organizátoři soustředění</option>
<option value="5">Účastníci soustředění</option>
</select>
<select name="select-two" id="select-two">
<!-- will be filled with ajax -->
</select>
<button id="download-button">Stáhnout</button>
<script defer>
const select_one = document.getElementById("select-one")
const select_two = document.getElementById("select-two")
const download_button = document.getElementById("download-button")
download_button.style.display = 'none'
select_two.style.display = 'none'
const fetch_dict_string = '{{ typy_exportu|safe }}'
const fetch_dict = JSON.parse(fetch_dict_string)
select_one.addEventListener('change', (e) => {
value = e.target.value
select_two.style.display = 'none'
select_two.innerHTML = ''
// puvodni stav
if (value == 0) {
download_button.style.display = 'none'
select_two.style.display = 'none'
return
}
// v tomto pripade muzeme rovnou stahnout
if (!(value in fetch_dict)) {
download_button.style.display = 'block'
select_two.style.display = 'none'
return
}
download_button.style.display = 'none'
fetch("/profil/exporty_lidi/get/" + value)
.then(response => response.json())
.then(data => {
const option = document.createElement('option')
option.value = 0
option.text = '---'
select_two.appendChild(option)
for (const [key, value] of Object.entries(data)) {
const option = document.createElement('option')
option.value = value["id"]
option.text = value["display"]
select_two.appendChild(option)
}
select_two.style.display = 'block'
})
})
select_two.addEventListener('change', (e) => {
value = e.target.value
if (value == 0) {
download_button.style.display = 'none'
return
}
download_button.style.display = 'block'
})
download_button.addEventListener('click', (e) => {
// uzivatele vybrana pole
fields = Array.from(document.getElementsByClassName('field-check'))
.filter(e => e.checked)
.map(e => e.getAttribute('data-value'));
params = ""
for (let val of fields) {
for(let s of val.split(' ')) {
params += s + ","
}
}
params = params.slice(0, -1)
if (select_two.innerHTML == '') {
window.location.href = "/profil/exporty_lidi/get_csv_only_one_step/" + select_one.value + "?fields=" + params
} else {
window.location.href = "/profil/exporty_lidi/get_csv/" + select_one.value + "/" + select_two.value + "?fields=" + params
}
})
</script>
{% endblock %}

View file

@ -107,13 +107,6 @@
</li> </li>
</ul> </ul>
<hr />
<h2><strong>Exporty dat lidí v semináří</strong></h2>
<ul>
<li><a href="{% url 'exporty_lidi' %}">dostupné exporty</a></li>
</ul>
<hr /> <hr />
<p>Nemůžeš najít, co hledáš? Může to být v <a href="{% url 'admin:index' %}">administračním rozhraní webu</a>.</p> <p>Nemůžeš najít, co hledáš? Může to být v <a href="{% url 'admin:index' %}">administračním rozhraní webu</a>.</p>
{% endblock content %} {% endblock content %}

View file

@ -38,28 +38,6 @@ urlpatterns = [
'org/propagace/jak-se-dozvedeli/', 'org/propagace/jak-se-dozvedeli/',
org_required(views.JakSeDozvedeliView.as_view()), org_required(views.JakSeDozvedeliView.as_view()),
name='jak_se_dozvedeli' name='jak_se_dozvedeli'
),
# export dat o řešitelích
path(
'profil/exporty_lidi',
org_required(views.ExportLidiView.as_view()),
name='exporty_lidi',
),
path(
'profil/exporty_lidi/get/<int:type>',
org_required(views.get_export_options),
name='exporty_lidi_options',
),
path(
'profil/exporty_lidi/get_csv_only_one_step/<int:type>',
org_required(views.download_export_csv_only_first_step),
name='exporty_lidi_data',
),
path(
'profil/exporty_lidi/get_csv/<int:type>/<int:id>',
org_required(views.download_export_csv),
name='exporty_lidi_download',
) )
] ]

View file

@ -3,7 +3,7 @@ import re
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import permission_required, user_passes_test from django.contrib.auth.decorators import permission_required, user_passes_test
from django.contrib.auth.models import AnonymousUser, User from django.contrib.auth.models import AnonymousUser
from django.db import transaction from django.db import transaction
import soustredeni.models import soustredeni.models
@ -182,46 +182,3 @@ def merge_osoby(cilova, zdrojova):
cilova.save() cilova.save()
input("Potvrdit transakci osob (^C pro zrušení) ") input("Potvrdit transakci osob (^C pro zrušení) ")
def osoba_uzivatele(u: User | AnonymousUser) -> Osoba | None:
if u.is_anonymous: return None
try:
return u.osoba
except User.osoba.RelatedObjectDoesNotExist:
return None
def resitel_osoby(o: Osoba) -> Resitel | None:
try:
return o.resitel
except Osoba.resitel.RelatedObjectDoesNotExist:
return None
def resitel_uzivatele(u: User | AnonymousUser) -> Resitel | None:
o = osoba_uzivatele(u)
if o is None: return None
return resitel_osoby(o)
def resitel_cehokoliv(r: User | AnonymousUser | Osoba | Organizator | Resitel | Prijemce) -> Organizator | None:
if isinstance(r, User): r = resitel_uzivatele(r)
if isinstance(r, Osoba): r = resitel_osoby(r)
if isinstance(r, Resitel) or isinstance(r, Prijemce): r = resitel_osoby(r.osoba)
assert isinstance(r, Resitel) or r is None
return r
def organizator_osoby(o: Osoba) -> Organizator | None:
try:
return o.org
except Osoba.org.RelatedObjectDoesNotExist:
return None
def organizator_uzivatele(u: User | AnonymousUser) -> Organizator | None:
o = osoba_uzivatele(u)
if o is None: return None
return organizator_osoby(o)
def organizator_cehokoliv(o: User | AnonymousUser | Osoba | Organizator | Resitel | Prijemce) -> Organizator | None:
if isinstance(o, User): o = organizator_uzivatele(o)
if isinstance(o, Osoba): o = organizator_osoby(o)
if isinstance(o, Resitel) or isinstance(o, Prijemce): o = organizator_osoby(o.osoba)
assert isinstance(o, Organizator) or o is None
return o

View file

@ -20,16 +20,13 @@ from django.utils import timezone
import personalni.models as m import personalni.models as m
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from odevzdavatko.models import Hodnoceni from odevzdavatko.models import Hodnoceni
from tvorba.models import Clanek, Uloha, Tema, Cislo, Rocnik from tvorba.models import Clanek, Uloha, Tema
import tvorba.utils as tvorba_utils
from various.models import Nastaveni from various.models import Nastaveni
from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
from datetime import date from datetime import date
import logging import logging
import csv import csv
from enum import Enum
import json
from various.views.pomocne import formularOKView from various.views.pomocne import formularOKView
from various.autentizace.views import LoginView from various.autentizace.views import LoginView
@ -144,80 +141,6 @@ class OrgoRozcestnikView(TemplateView):
#content_type = 'text/plain; charset=UTF8' #content_type = 'text/plain; charset=UTF8'
#XXX #XXX
class PrvniTypExportu(Enum):
CISLA = 1
ROCNIKU = 2
SOUSTREDENI_ORG = 4
SOUSTREDENI_UCASTNICI = 5
class ExportLidiView(TemplateView):
template_name = 'personalni/profil/export_lidi.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['typy_exportu'] = json.dumps({member.value: member.name.lower().capitalize() for member in PrvniTypExportu})
return context
def get_export_options(request, type):
if type == PrvniTypExportu.CISLA.value:
data = [{"id": c.id, "display": str(c)} for c in Cislo.objects.all()]
if type == PrvniTypExportu.ROCNIKU.value:
data = [{"id": r.id, "display": str(r)} for r in Rocnik.objects.all()]
if type == PrvniTypExportu.SOUSTREDENI_ORG.value:
data = [{"id": s.id, "display": str(s)} for s in Soustredeni.objects.all()]
if type == PrvniTypExportu.SOUSTREDENI_UCASTNICI.value:
data = [{"id": s.id, "display": str(s)} for s in Soustredeni.objects.all()]
return HttpResponse(json.dumps(data), content_type='application/json')
def getFieldsForExport(request):
if 'fields' not in request.GET or request.GET.get('fields') == '':
return ["jmeno", "prijmeni", "email", "telefon"]
fields = request.GET.get('fields').split(',')
return fields
def download_export_csv_only_first_step(request, type):
fields = getFieldsForExport(request)
if type == 3:
resitele = tvorba_utils.resitele_co_neodmaturovali()
resiteleOsoby = Osoba.objects.filter(resitel__in=resitele)
response = dataOsobCsvResponse(resiteleOsoby, columns=fields)
response['Content-Disposition'] = 'attachment; filename="resitele_co_neodmaturovali.csv"'
return response
def download_export_csv(request, type, id):
fields = getFieldsForExport(request)
if type == PrvniTypExportu.CISLA.value:
resitele = tvorba_utils.resi_cislo(Cislo.objects.get(id=id))
resiteleOsoby = Osoba.objects.filter(resitel__in=resitele)
response = dataOsobCsvResponse(resiteleOsoby, columns=fields)
name = str(Cislo.objects.get(id=id)).replace(" ", "_") + "_resitele_cisla.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
if type == PrvniTypExportu.ROCNIKU.value:
resitele = tvorba_utils.resi_v_rocniku(Rocnik.objects.get(id=id))
resiteleOsoby = Osoba.objects.filter(resitel__in=resitele)
response = dataOsobCsvResponse(resiteleOsoby, columns=fields)
name = str(Rocnik.objects.get(id=id)).replace(" ", "_") + "_resitele_rocniku.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
if type == PrvniTypExportu.SOUSTREDENI_ORG.value:
soustredeni = Soustredeni.objects.get(id=id)
organizatori = soustredeni.organizatori.all()
organizatoriOsoby = Osoba.objects.filter(org__in=organizatori)
response = dataOsobCsvResponse(organizatoriOsoby, columns=fields)
name = str(soustredeni).replace(" ", "_") + "_organizatori_soustredeni.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
if type == PrvniTypExportu.SOUSTREDENI_UCASTNICI.value:
soustredeni = Soustredeni.objects.get(id=id)
ucastnici = soustredeni.ucastnici.all()
ucastniciOsoby = Osoba.objects.filter(resitel__in=ucastnici)
response = dataOsobCsvResponse(ucastniciOsoby, columns=fields)
name = str(soustredeni).replace(" ", "_") + "_ucastnici_soustredeni.csv"
response['Content-Disposition'] = 'attachment; filename="' + name + '"'
return response
class ResitelView(LoginRequiredMixin,generic.DetailView): class ResitelView(LoginRequiredMixin,generic.DetailView):
model = m.Resitel model = m.Resitel
@ -547,46 +470,3 @@ def dataResiteluCsvResponse(queryset, columns=None, with_header=True):
writer.writerows(queryset_list) writer.writerows(queryset_list)
return response return response
def dataOsobCsvResponse(queryset, columns=None, with_header=True):
"""Pomocná funkce pro vracení dat osob jako CSV. Musí dostat správný QuerySet, který dává Ososby"""
default_columns = (
'id',
'jmeno',
'prijmeni',
'prezdivka',
'email',
'telefon',
'datum_narozeni',
'osloveni',
'ulice',
'mesto',
'psc',
'stat',
'jak_se_dozvedeli',
'poznamka',
'datum_registrace',
'datum_souhlasu_udaje',
'datum_souhlasu_zasilani',
)
if columns is None: columns = default_columns
def get_field_name(column_name):
return column_name
response = HttpResponse(content_type='text/csv')
writer = csv.writer(response)
# První řádek je záhlaví
if with_header:
writer.writerow(map(get_field_name, columns))
# Data:
queryset_list = queryset.values_list(*columns)
writer.writerows(queryset_list)
return response

View file

@ -1,3 +0,0 @@
"""
Aplikace umožňující orgům vypisovat si přednášky a účastníkům o nich hlasovat.
"""

View file

@ -4,15 +4,11 @@ from reversion.admin import VersionAdmin
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.html import escape from django.utils.html import escape
from .models import Prednaska, Seznam, Znalost from .models import Prednaska, Seznam, STAV_NAVRH
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
class Seznam_PrednaskaInline(admin.TabularInline): class Seznam_PrednaskaInline(admin.TabularInline):
"""
:py:class:`Inline <django.contrib.admin.TabularInline>` pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující :py:class:`Přednášky <prednasky.models.Prednaska>`
v adminu :py:class:`Seznamu <prednasky.models.Seznam>`.
"""
model = Prednaska.seznamy.through model = Prednaska.seznamy.through
extra = 0 extra = 0
@ -58,57 +54,24 @@ class Seznam_PrednaskaInline(admin.TabularInline):
def has_add_permission(self, req, obj): return False def has_add_permission(self, req, obj): return False
class Seznam_ZnalostInline(admin.TabularInline):
"""
:py:class:`Inline <django.contrib.admin.TabularInline>` pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující :py:class:`Znalosti <prednasky.models.Znalost>`
v adminu :py:class:`Seznamu <prednasky.models.Seznam>`.
"""
model = Znalost.seznamy.through
extra = 0
def znalost__nazev(self, obj):
return mark_safe(
f"<a href='/admin/prednasky/znalost/{obj.znalost.id}'>{obj.znalost.nazev}</a>"
)
def znalost__text(self, obj):
return mark_safe(
f"<div style='width: 200px'>{escape(obj.znalost.text)}</div>"
)
znalost__nazev.short_description = u'Přednáška'
znalost__text.short_description = u'Popis pro orgy'
readonly_fields = [
'znalost__nazev',
'znalost__text',
]
exclude = ['znalost']
def has_add_permission(self, req, obj): return False
class SeznamAdmin(VersionAdmin): class SeznamAdmin(VersionAdmin):
""" Admin pro :py:class:`Seznam <prednasky.models.Seznam>` """
list_display = ['soustredeni', 'stav'] list_display = ['soustredeni', 'stav']
inlines = [Seznam_PrednaskaInline, Seznam_ZnalostInline] inlines = [Seznam_PrednaskaInline]
admin.site.register(Seznam, SeznamAdmin) admin.site.register(Seznam, SeznamAdmin)
class PrednaskaAdmin(VersionAdmin): class PrednaskaAdmin(VersionAdmin):
""" Admin pro :py:class:`Přednášku <prednasky.models.Prednaska>` """
list_display = ['nazev', 'org', 'obor'] list_display = ['nazev', 'org', 'obor']
list_filter = ['org', 'obor'] list_filter = ['org', 'obor']
search_fields = ['nazev'] search_fields = []
filter_horizontal = ('seznamy', ) filter_horizontal = ('seznamy', )
actions = ['move_to_soustredeni'] actions = ['move_to_soustredeni']
def move_to_soustredeni(self, request, queryset): def move_to_soustredeni(self, request, queryset):
""" Přidá dané přednášky do seznamu, o kterém se právě hlasuje """
sous = Soustredeni.objects.first() sous = Soustredeni.objects.first()
seznam = Seznam.objects.filter(soustredeni=sous, stav=Seznam.Stav.NAVRH) seznam = Seznam.objects.filter(soustredeni=sous, stav=STAV_NAVRH)
if len(seznam) == 0: if len(seznam) == 0:
self.message_user( self.message_user(
request, request,
@ -134,14 +97,3 @@ class PrednaskaAdmin(VersionAdmin):
admin.site.register(Prednaska, PrednaskaAdmin) admin.site.register(Prednaska, PrednaskaAdmin)
class ZnalostAdmin(PrednaskaAdmin): # Trochu hack, ať nemusím vypisovat všechno znovu
"""
Admin pro :py:class:`Znalost <prednasky.models.Znalost>`
TODO předělat, aby nedědila z :py:class:`prednasky.admin.PrednaskaAdmin`, ale společné věci byly zvlášť
"""
list_display = ("__str__",)
list_filter = ()
admin.site.register(Znalost, ZnalostAdmin)

View file

@ -1,31 +1,7 @@
from django import forms from django import forms
from .models import Hlasovani, HlasovaniOZnalostech class NewPrednaskyForm(forms.Form):
ucastnik = forms.CharField(label = 'Tvoje jméno', max_length = 100)
class HlasovaniPrednaskaForm(forms.Form):
""" :py:class:`Formulář <django.forms.Form>` pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o jedné :py:class:`Přednášce <prednasky.models.Prednaska>`
(neobsahuje téměř nic, většina se musí doplnit jiným způsobem)
"""
#: ID :py:class:`Přednášky <prednasky.models.Prednaska>`, o které se hlasuje
prednaska_id = forms.IntegerField(widget=forms.HiddenInput)
#: :py:class:`Hodnocení (Body) <prednasky.models.Hlasovani.Body>` této přednášky
body = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=Hlasovani.Body.choices, initial=Hlasovani.Body.JEDNO)
#: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniPrednaskaFormů <prednasky.forms.HlasovaniPrednaskaForm>`)
#: pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o množině :py:class:`Přednášek <prednasky.models.Prednaska>`
HlasovaniPrednaskaFormSet = forms.formset_factory(HlasovaniPrednaskaForm, extra=0)
class HlasovaniZnalostiForm(forms.Form):
""" :py:class:`Formulář <django.forms.Form>` pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o jedné :py:class:`Znalosti <prednasky.models.Znalost>`
(neobsahuje téměř nic, většina se musí doplnit jiným způsobem)
"""
#: ID :py:class:`Znalosti <prednasky.models.Znalost>`, o které hlasujeme
znalost_id = forms.IntegerField(widget=forms.HiddenInput)
#: :py:class:`Odpověď <prednasky.models.HlasovaniOZnalostech.Odpoved>` na tuto znalost
odpoved = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=HlasovaniOZnalostech.Odpoved.choices)
#: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniZnalostiFormů <prednasky.forms.HlasovaniZnalostiForm>`)
#: pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o množině :py:class:`Znalostí <prednasky.models.Znalost>`
HlasovaniZnalostiFormSet = forms.formset_factory(HlasovaniZnalostiForm, extra=0)

View file

@ -1,39 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-24 13:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'),
('prednasky', '0018_post_split_soustredeni'),
]
operations = [
migrations.CreateModel(
name='Znalost',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nazev', models.CharField(help_text='Např. Neuronové sítě', max_length=200, verbose_name='Nadpis')),
('text', models.TextField(blank=True, help_text='Např. Perceptron, vrstevnatá síť, forward a backward propagation', null=True, verbose_name='Detailní popis')),
('seznamy', models.ManyToManyField(to='prednasky.seznam')),
],
options={
'verbose_name': 'Znalost k přednáškám',
'verbose_name_plural': 'Znalosti k přednáškám',
'db_table': 'prednasky_znalost',
},
),
migrations.CreateModel(
name='HlasovaniOZnalostech',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('odpoved', models.CharField(choices=[(-1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (1, 'Tohle vůbec neznám')], max_length=16, verbose_name='odpověď')),
('seznam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='prednasky.seznam')),
('ucastnik', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personalni.osoba')),
('znalost', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prednasky.znalost')),
],
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.2.16 on 2025-01-24 20:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('prednasky', '0019_znalost_hlasovanioznalostech'),
]
operations = [
migrations.AlterField(
model_name='hlasovani',
name='body',
field=models.IntegerField(choices=[(-1, 'rozhodně nechci'), (0, 'je mi to jedno'), (1, 'rozhodně chci')], default=0, verbose_name='Body'),
),
]

View file

@ -1,24 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-04 20:09
from django.db import migrations, models
def zmena_bodu(apps, _schema_editor):
HlasovaniOZnalostech = apps.get_model('prednasky','HlasovaniOZnalostech')
for h in HlasovaniOZnalostech.objects.all():
h.odpoved = -int(h.odpoved)
h.save()
class Migration(migrations.Migration):
dependencies = [
('prednasky', '0020_alter_hlasovani_body'),
]
operations = [
migrations.AlterField(
model_name='hlasovanioznalostech',
name='odpoved',
field=models.IntegerField(choices=[(1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (-1, 'Tohle vůbec neznám')], verbose_name='odpověď'),
),
migrations.RunPython(zmena_bodu, reverse_code=zmena_bodu),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-09 21:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('prednasky', '0021_alter_hlasovanioznalostech_odpoved'),
]
operations = [
migrations.AlterField(
model_name='hlasovanioznalostech',
name='odpoved',
field=models.IntegerField(choices=[(1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bych, že to úplně umím'), (-1, 'Tohle vůbec neznám')], verbose_name='odpověď'),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2025-02-19 17:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'),
('prednasky', '0022_preklep_u_odpovedi_hlasovanioznalostech'),
]
operations = [
migrations.AddField(
model_name='hlasovani',
name='ucastnik_osoba',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='personalni.osoba'),
),
]

View file

@ -1,141 +1,81 @@
from django.db import models from django.db import models
from soustredeni.models import Soustredeni from soustredeni.models import Soustredeni
from personalni.models import Organizator, Osoba from personalni.models import Organizator
STAV_NAVRH = 1
STAV_BUDE = 2
STAV_CHOICES = (
(STAV_NAVRH, 'Návrh'),
(STAV_BUDE, 'Bude')
)
class Seznam(models.Model): class Seznam(models.Model):
"""
Spojuje :py:class:`Přednášky <prednasky.models.Prednaska>` a :py:class:`Znalosti <prednasky.models.Znalost>
se :py:class:`Soustředěními <soustredeni.models.Soustredeni>`,
kde by mohly zaznít, nebo zazní/zazněly.
"""
class Meta: class Meta:
db_table = "prednasky_seznam" db_table = 'prednasky_seznam'
verbose_name = "Seznam přednášek" verbose_name = 'Seznam přednášek'
verbose_name_plural = "Seznamy přednášek" verbose_name_plural = 'Seznamy přednášek'
ordering = ["soustredeni", "stav"] ordering = ['soustredeni', 'stav']
class Stav(models.IntegerChoices): id = models.AutoField(primary_key = True)
""" Stav seznamu přednášek (NAVRH se používá k hlasování viz :py:func:`daný view <prednasky.views.newPrednaska>`). """ soustredeni = models.ForeignKey(Soustredeni,null = True, default = None,
NAVRH = 1, "Návrh" #: odpovídá před-soustřeďkové představě o tom, jaké přednášky dělat (dá se o nich třeba hlasovat ap.) on_delete=models.PROTECT)
BUDE = 2, "Bude" #: odpovídá definitivní představě o tom, co bude/bylo a dá se porovnávat s novými návrhy stav = models.IntegerField('Stav',choices=STAV_CHOICES,default = STAV_NAVRH)
id = models.AutoField(primary_key=True)
soustredeni = models.ForeignKey(Soustredeni, null=True, default=None, on_delete=models.PROTECT)
stav = models.IntegerField("Stav", choices=Stav.choices, default=Stav.NAVRH) #: :py:class:`Stav <prednasky.models.Seznam.Stav>` Seznamu
def __str__(self): def __str__(self):
return f"Seznam {'návrhů ' if self.stav == Seznam.Stav.NAVRH else ''}přednášek na {self.soustredeni}" return "Seznam {}přednášek na {}".format("návrhů "
if self.stav == STAV_NAVRH else "", self.soustredeni)
CHOICES_OBTIZNOST = (
(1, 'Lehká'),
(2, 'Střední'),
(3, 'Těžká'),
)
CHOICES_BODY = (
(-1, '-1'),
(0, '0'),
(1, '1'),
)
class Prednaska(models.Model): class Prednaska(models.Model):
"""
Reprezentuje přednášku, kterou si org může vypsat a účastník o hlasovat.
(Viz :py:class:`Hlasování <prednasky.models.Hlasovani>`.)
"""
class Meta: class Meta:
db_table = "prednasky_prednaska" db_table = 'prednasky_prednaska'
verbose_name = "Přednáška" verbose_name = 'Přednáška'
verbose_name_plural = "Přednášky" verbose_name_plural = 'Přednášky'
ordering = ["org", "nazev"] ordering = ['org', 'nazev']
class Obtiznost(models.IntegerChoices): id = models.AutoField(primary_key = True)
LEHKA = 1, "Lehká" nazev = models.CharField('Název', max_length = 300)
STREDNI = 2, "Střední"
TEZKA = 3, "Těžká"
id = models.AutoField(primary_key=True)
nazev = models.CharField("Název", max_length=300)
org = models.ForeignKey(Organizator, on_delete=models.PROTECT) org = models.ForeignKey(Organizator, on_delete=models.PROTECT)
popis = models.TextField("Popis pro orgy", null=True, blank=True, help_text="Neveřejný popis pro ostatní orgy") popis = models.TextField('Popis pro orgy',null = True, blank = True,help_text = 'Neveřejný popis pro ostatní orgy')
anotace = models.TextField("Anotace", null=True, blank=True, help_text="Veřejná anotace v hlasování") anotace = models.TextField('Anotace',null = True, blank = True, help_text = 'Veřejná anotace v hlasování')
obtiznost = models.IntegerField("Obtížnost", choices=Obtiznost.choices) #: :py:class:`Obtížnost <prednasky.models.Prednaska.Obtiznost>` Přednášky obtiznost = models.IntegerField('Obtížnost', choices=CHOICES_OBTIZNOST)
obor = models.CharField("Obor", max_length=5, help_text="Podmnožina MFIOB") obor = models.CharField('Obor', max_length = 5, help_text = 'Podmnožina MFIOB')
klicova = models.CharField("Klíčová slova", max_length=200, null=True, blank=True) klicova = models.CharField('Klíčová slova', max_length = 200, null = True, blank = True)
seznamy = models.ManyToManyField(Seznam) seznamy = models.ManyToManyField(Seznam)
def __str__(self): def __str__(self):
return f"{self.nazev} ({self.org})" return "{} ({})".format(self.nazev, self.org)
class Hlasovani(models.Model): class Hlasovani(models.Model):
"""
Reprezentuje hlasování jednoho účastníka
o jedné :py:class:`Přednášce <prednasky.models.Prednaska>`
v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit)
"""
class Meta: class Meta:
db_table = "prednasky_hlasovani" db_table = 'prednasky_hlasovani'
verbose_name = "Hlasování" verbose_name = 'Hlasování'
verbose_name_plural = "Hlasování" verbose_name_plural = 'Hlasování'
ordering = ["ucastnik", "prednaska"] ordering = ['ucastnik', 'prednaska']
id = models.AutoField(primary_key = True)
class Body(models.IntegerChoices):
""" Ohodnocení přednášky v daném Hlasování (větší číslo = víc chci) """
NECHCI = -1, "rozhodně nechci"
JEDNO = 0, "je mi to jedno"
CHCI = 1, "rozhodně chci"
id = models.AutoField(primary_key=True)
prednaska = models.ForeignKey(Prednaska, on_delete=models.CASCADE) prednaska = models.ForeignKey(Prednaska, on_delete=models.CASCADE)
#: Příslušné hlasování: :py:class:`Body <prednasky.models.Hlasovani.Body>` body = models.IntegerField('Body', default = 0, choices = CHOICES_BODY)
body = models.IntegerField("Body", default=Body.JEDNO, choices=Body.choices) ucastnik = models.CharField('Účastník', max_length = 100)
seznam = models.ForeignKey(Seznam,null=True,on_delete=models.SET_NULL)
#: Účastník, který hlasoval. Pouze string:
#: *(přechod z jména na objekt Osoby nějak kape na tom,
#: že všechna předchozí hlasování zde mají náhodný string…)
#: TODO Změnit to na Osobu*
ucastnik = models.CharField("Účastník", max_length=100)
ucastnik_osoba = models.ForeignKey(Osoba, on_delete=models.CASCADE, blank=False, null=True)
seznam = models.ForeignKey(Seznam, null=True, on_delete=models.SET_NULL)
def __str__(self): def __str__(self):
return f"{self.ucastnik} dal {self.body} bodů {self.prednaska} v seznamu {self.seznam}" return "{} dal {} bodů {} v seznamu {}".format(self.ucastnik,
self.body, self.prednaska, self.seznam)
class Znalost(models.Model):
"""
Reprezentuje znalost, na kterou se můžeme účastníka ptát (nechat je hlasovat).
(Viz :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`.)
(V podstatě :py:class:`Přednáška <prednasky.models.Prednaska>, jen neobsahuje
tolik detailů a v hlasování jiné odpovědi.)
"""
class Meta:
db_table = "prednasky_znalost"
verbose_name = "Znalost k přednáškám"
verbose_name_plural = "Znalosti k přednáškám"
nazev = models.CharField("Nadpis", max_length=200, blank=False, null=False, help_text="Např. Neuronové sítě")
text = models.TextField("Detailní popis", blank=True, null=True, help_text="Např. Perceptron, vrstevnatá síť, forward a backward propagation")
seznamy = models.ManyToManyField(Seznam)
def __str__(self):
return self.nazev
class HlasovaniOZnalostech(models.Model):
"""
Reprezentuje hlasování jednoho účastníka
o jedné :py:class:`Znalosti <prednasky.models.Znalost>`
v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit)
(V podstatě totéž, co :py:class:`Hlasování <prednasky.models.Hlasovani>`, jen jiné komentáře
u odpovědí a místo přednášky odkazuje na znalost.)
"""
class Odpoved(models.IntegerChoices):
""" Na kolik danou znalost účastník ovládá v daném Hlasování (větší číslo = víc zná) """
UMIM = 1, "Tohle celkem umím"
CIRCA = 0, "Už jsem o tom slyšel, ale neřekl bych, že to úplně umím"
NEUMIM = -1, "Tohle vůbec neznám"
odpoved = models.IntegerField(u"odpověď", choices=Odpoved.choices, blank=False, null=False) #: :py:class:`Odpověď <prednasky.models.Prednaska.Odpoved>` na HlasováníOZnalostech
znalost = models.ForeignKey(Znalost, on_delete=models.CASCADE, blank=False, null=False)
ucastnik = models.ForeignKey(Osoba, on_delete=models.CASCADE, blank=False, null=False)
seznam = models.ForeignKey(Seznam, on_delete=models.SET_NULL, blank=True, null=True)
def __str__(self):
return f"{self.ucastnik} dal {self.znalost} bodů {self.znalost} v seznamu {self.seznam}"

View file

@ -5,40 +5,36 @@
{% block content %} {% block content %}
<h1>{% block nadpis1a %}Hlasování o přednáškách{% endblock %}</h1> <h1>
{% block nadpis1a %}Hlasování o přednáškách{% endblock %}
</h1>
<p>
Jak moc by ses chtěl(a) zúčastnit následujících přednášek?
<br>
<span style="font-size: 75%">Obtížnost 1 je nejlehčí, 3 nejtěžší.</span>
</p>
<form enctype="multipart/form-data" action="." method="post"> <form enctype="multipart/form-data" action="." method="post">
{% csrf_token %} {% csrf_token %}
<table>
<h3>Jak moc by ses chtěl(a) zúčastnit následujících přednášek?</h3> {% for p, h in prednasky %}
<p>Obtížnost 1 je nejlehčí, 3 nejtěžší.</p> <tr><td><label>{{p.org}}: <span style="font-size: 175%">{{p.nazev}}</span></label></td></tr>
{{ form_set_prednasky.management_form }} <tr><td><p><i>{{p.anotace}}</i></p></td></tr>
{% for f, p in formy_a_prednasky %} <tr><td><label>Obor: </label> {{p.obor}}</td></tr>
<div class="hlasovani-prednaska"> <tr><td><label>Obtížnost: </label> {{p.obtiznost}}</td> </tr>
<h4>{{p.nazev}} ({{p.org}})</h4> {% if p.klicova %}<tr><td><label>Klíčová slova: </label> {{p.klicova}}</td></tr>{% endif%}
<p class="textprednasky">{{p.anotace | linebreaksbr}}</p> <tr><td>Hodnocení:
<label>Obor: </label> {{p.obor}}<br> <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="-1" {% if h == -1 %} CHECKED="checked" {% endif %} > rozhodně nechci
<label>Obtížnost: </label> {{p.obtiznost}}<br> <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="0" {% if h == 0 %} CHECKED="checked" {% endif %}> je mi to jedno
{% if p.klicova %}<label>Klíčová slova: </label> {{p.klicova}}<br>{% endif%} <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="1" {% if h == 1 %} CHECKED="checked" {% endif %}> rozhodně chci
<br> </td></tr>
{{ f }} <tr><td>&nbsp;</td></tr>
<br>
</div>
{% empty %} {% empty %}
Nejsou žádné přednášky o kterých by šlo hlasovat. Nejsou žádné přednášky o kterých by šlo hlasovat.
{% endfor %} {% endfor %}
<tr><td><input name="odeslat" type="submit" value="Odeslat"></td><tr>
{{ form_set_znalosti.management_form }} </table>
{% for f, z in formy_a_znalosti %}
<div class="hlasovani-znalost">
{% if forloop.first %}<hr/><h3>Jak moc znáš následující?</h3>{% endif %}
<h4>{{z.nazev}}</h4>
<p class="textznalosti">{{z.text | linebreaksbr}}</p>
{{ f }}
<br>
</div>
{% endfor %}
<input type="submit" value="Odeslat"/>
</form> </form>
{% endblock %} {% endblock %}

View file

@ -2,19 +2,19 @@
{% block content %} {% block content %}
<h1>{% block nadpis1a %} <h1>{% block nadpis1a %}
Výsledky hlasování o přednáškách Hlasování o přednáškách
{% endblock %}</h1> {% endblock %}</h1>
{# Projdi vsechny seznamy #} {# Projdi vsechny seznamy #}
<div class="mam-org-only"> <div class="mam-org-only">
<ul> <ul>
{% for seznam in object_list %} {% for seznam in object_list %}
<li> <li>
{% if seznam.stav == seznam.Stav.NAVRH %} {% if seznam.stav == 1 %} {# STAV_NAHRH = 1 #}
Návrh přednášek na soustředění {{seznam.soustredeni.misto}} <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Návrh přednášek na soustředění {{seznam.soustredeni.misto}} </a>
{% else %} {% else %}
Seznam přednášek na soustředění {{seznam.soustredeni.misto}} <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Seznam přednášek na soustředění {{seznam.soustredeni.misto}} </a>
{% endif %} {% endif %}
(<a href='{% url "seznam-export-csv" seznam=seznam.id %}'>CSV</a>) <a href="/prednasky/seznam_prednasek/{{seznam.id}}/export">Export</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -13,8 +13,13 @@ urlpatterns = [
org_required(views.MetaSeznamListView.as_view()), org_required(views.MetaSeznamListView.as_view()),
name='metaseznam-list'), name='metaseznam-list'),
path( path(
'prednasky/seznam_prednasek/<int:seznam>/hlasovani.csv', 'prednasky/seznam_prednasek/<int:seznam>/export',
org_required(views.PrednaskyExportView), org_required(views.SeznamExportView),
name='seznam-export-csv' name='seznam-export'
),
path(
'prednasky/seznam_prednasek/<int:seznam>/',
org_required(views.SeznamListView.as_view()),
name='seznam-list'
), ),
] ]

View file

@ -1,193 +1,127 @@
import csv
import http import http
import logging
from django.http import HttpResponse, HttpRequest
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
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction from django.db.models import Sum
from django.forms import Form
from various.views.pomocne import formularOKView from various.views.pomocne import formularOKView
from .forms import HlasovaniPrednaskaFormSet, HlasovaniZnalostiFormSet
from various.models import Nastaveni from various.models import Nastaveni
from prednasky.models import Prednaska, Hlasovani, Znalost, HlasovaniOZnalostech, Seznam 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
PREDNASKY_PREFIX = "prednasky" def newPrednaska(request):
ZNALOSTI_PREFIX = "znalosti"
logger = logging.getLogger(__name__)
def newPrednaska(request: HttpRequest) -> HttpResponse:
"""
View zobrazující a ukládající účastnické hlasování
(:py:class:`Hlasování <prednasky.models.Hlasovani>`
a :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`)
o :py:class:`Přednáškách <prednasky.models.Prednaska>`
a :py:class:`Znalostech <prednasky.models.Znalost>`
"""
# hlasovani se vztahuje k nejnovejsimu soustredeni # hlasovani se vztahuje k nejnovejsimu soustredeni
sous = Nastaveni.get_solo().aktualni_sous sous = Nastaveni.get_solo().aktualni_sous
seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first() seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first()
if sous is None or seznam is None: if sous is None or seznam is None:
return render(request, 'universal.html', { return render(request, 'universal.html', {
'title': "Nelze hlasovat", 'title': "Nelze hlasovat",
'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.", 'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.",
}, status=http.HTTPStatus.NOT_FOUND) }, 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) # id, kvůli kolizi jmen ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id)
# obsluha formulare
if request.method == 'POST':
form = Form(request.POST, request.FILES)
if form.is_valid():
# id z důvodu duplicitních jmen (přechod z jména na objekt Osoby nějak kape na tom,
# že všechna předchozí hlasování zde mají náhodný string…)
# TODO Změnit to na Osobu
if request.method == 'POST': # Když to byl POST, tak ukládáme. # TODO v následujících řádcích je zbytečně mnoho dotazů na QuerySet (pokud účastník hlasoval, hlasoval u všech)
# Načteme data do formsetů for i in request.POST:
form_set_prednasky = HlasovaniPrednaskaFormSet(request.POST, prefix=PREDNASKY_PREFIX) if i[0] == 'q':
form_set_znalosti = HlasovaniZnalostiFormSet(request.POST, prefix=ZNALOSTI_PREFIX) prednaska = Prednaska.objects.filter(pk=int(i[1:]))[0]
hlasovani = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first()
if form_set_prednasky.is_valid() and form_set_znalosti.is_valid(): if not hlasovani:
with transaction.atomic(): hlasovani = Hlasovani()
# Místo updatování data prostě smažeme a vytvoříme nová hlasovani.prednaska = prednaska
seznam.hlasovani_set.filter(ucastnik=ucastnik).delete() hlasovani.ucastnik = ucastnik
seznam.hlasovanioznalostech_set.filter(ucastnik=osoba).delete() hlasovani.seznam = seznam
hlasovani.body = int(request.POST[i])
for form in form_set_prednasky: hlasovani.save()
prednaska_id = form.cleaned_data['prednaska_id']
prednaska = Prednaska.objects.filter(id=prednaska_id).first()
if prednaska is None:
logger.error(f"Účastník {ucastnik} hodnotil neexistující přednášku {prednaska_id} číslem {form.cleaned_data['body']}")
continue
Hlasovani.objects.create(
prednaska=prednaska,
body=form.cleaned_data['body'],
ucastnik=ucastnik,
ucastnik_osoba=osoba,
seznam=seznam,
)
for form in form_set_znalosti:
znalost_id = form.cleaned_data['znalost_id']
znalost = Znalost.objects.filter(id=znalost_id).first()
if znalost is None:
logger.error(f"Účastník {ucastnik} hodnotil neexistující znalost {znalost_id} číslem {form.cleaned_data['odpoved']}")
continue
HlasovaniOZnalostech.objects.create(
odpoved=form.cleaned_data['odpoved'],
znalost=znalost,
ucastnik=osoba,
seznam=seznam,
)
# presmerovani na prave vzniklou galerii
return HttpResponseRedirect('./hotovo') return HttpResponseRedirect('./hotovo')
else: # Pokud je nějaký formset nevalidní, vracíme je k přepracování def prednaska_hodnoceni(prednaska):
prednasky = seznam.prednaska_set.all() h = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first()
znalosti = seznam.znalost_set.all() if h:
# FIXME Spadnout, pokud nesedí přednáška/znalost s formulářem. (Nějak se mi to nepovedlo.) return prednaska, h.body
# Může se totiž stát, že se mezitím změnily přednášky (nějaká byla přidána/odebrána) else:
return prednaska, 0
else: # Když to nebyl POST, tak inicializujeme (pokud už o přednášce/znalosti účastník hlasoval, předvyplníme mu to).
def odpoved_prednasky(p: Prednaska) -> Hlasovani.Body:
hlasovani = p.hlasovani_set.filter(ucastnik=ucastnik).first()
return hlasovani.body if hlasovani else Hlasovani.Body.JEDNO
def odpoved_znalosti(z: Znalost) -> HlasovaniOZnalostech.Odpoved:
hlasovani = z.hlasovanioznalostech_set.filter(ucastnik=osoba).first()
return hlasovani.odpoved if hlasovani else HlasovaniOZnalostech.Odpoved.CIRCA
prednasky = seznam.prednaska_set.all()
znalosti = seznam.znalost_set.all()
form_set_prednasky = HlasovaniPrednaskaFormSet(initial=[
{"prednaska_id": p.id, "body": odpoved_prednasky(p)} for p in prednasky
], prefix=PREDNASKY_PREFIX)
form_set_znalosti = HlasovaniZnalostiFormSet(initial=[
{"znalost_id": z.id, "odpoved": odpoved_znalosti(z)} for z in znalosti
], prefix=ZNALOSTI_PREFIX)
# V případě nePOSTu nebo chyby při ukládání vracíme hlasování
return render( return render(
request, request,
'prednasky/base.html', 'prednasky/base.html',
{ {'prednasky': map(prednaska_hodnoceni, seznam.prednaska_set.all())}
'form_set_prednasky': form_set_prednasky, 'form_set_znalosti': form_set_znalosti,
'formy_a_prednasky': list(zip(form_set_prednasky, prednasky)),
'formy_a_znalosti': list(zip(form_set_znalosti, znalosti)),
}
) )
def Prednaska_hotovo(request: HttpRequest) -> HttpResponse: def Prednaska_hotovo(request):
""" View po vyplnění :py:func:`hlasování <prednasky.views.newPrednaska>` """
return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.") return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.")
class MetaSeznamListView(generic.ListView): class MetaSeznamListView(generic.ListView):
""" Seznam všech :py:class:`Seznamů <prednasky.models.Seznam>` s odkazy na exporty """
model = Seznam model = Seznam
template_name = 'prednasky/metaseznam_prednasek.html' template_name = 'prednasky/metaseznam_prednasek.html'
def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResponse: class SeznamListView(generic.ListView):
""" template_name = 'prednasky/seznam_prednasek.html'
Vrátí všechna :py:class:`Hlasování <prednasky.models.Hlasovani>`
i :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`
v daném :py:class:`Seznamu <prednasky.models.Seznam>`
jako csv soubor (řádky = účastníci, sloupce = přednášky&znalosti).
:param seznam: ID daného :py:class:`Seznamu <prednasky.models.Seznam>` def get_queryset(self):
""" self.seznam = get_object_or_404(Seznam, id=self.kwargs["seznam"])
hlasovani = Hlasovani.objects.filter(seznam=seznam).select_related("prednaska") prednasky = Prednaska.objects.filter(seznamy=self.seznam).order_by(
hlasovani_o_znalostech = HlasovaniOZnalostech.objects.filter(seznam=seznam).select_related('ucastnik', 'znalost') 'org__osoba__user__first_name', 'org__osoba__user__last_name'
)
return prednasky
# Inicializujeme sloupce # FIXME nahradit anotaci s filtrem po prechodu na Django 2.2
prednasky = list(Prednaska.objects.filter(seznamy=seznam)) def get_context_data(self,**kwargs):
znalosti = list(Znalost.objects.filter(seznamy=seznam)) context = super(SeznamListView, self).get_context_data(**kwargs)
prednasky_map: dict[int, int] = {p.id: i for i, p in enumerate(prednasky, 1)} # hlasovani se vztahuje k nejnovejsimu soustredeni
offset = len(prednasky_map) sous = Soustredeni.objects.first()
znalosti_map: dict[int, int] = {z.id: i for i, z in enumerate(znalosti, offset + 1)} seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first()
width = offset + len(znalosti_map)
# A po inicializaci sloupců vyplníme tabulku for obj in self.object_list:
table: [str, list[str|Prednaska|Znalost,]] = {} hlasovani_set = obj.hlasovani_set.filter(seznam=seznam).only('body')
obj.body = sum(map(lambda x: x.body,hlasovani_set))
errors = [] return context
def SeznamExportView(request, seznam):
"""Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor"""
# TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro
# lidi?
hlasovani = Hlasovani.objects.filter(seznam=seznam)
prednasky = Prednaska.objects.filter(seznamy=seznam)
orgove = set(p.org for p in prednasky)
ucastnici = set(h.ucastnik for h in hlasovani)
for p in prednasky:
p.body = []
for u in ucastnici:
try:
p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body)
except ObjectDoesNotExist:
# účastník nehlasoval
p.body.append("?")
for h in hlasovani: for h in hlasovani:
if h.ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek h.ucastnik = hash(h.ucastnik)
table[h.ucastnik] = [h.ucastnik] + ([""] * width)
if h.prednaska.id in prednasky_map: return render(
table[h.ucastnik][prednasky_map[h.prednaska.id]] = h.body request,
else: 'prednasky/seznam_prednasek_export.txt',
errors.append(f"Přednáška {h.prednaska.id} ({h.prednaska}) dostala od Účastníka {h.ucastnik} následující hodnocení: {h.body}") {"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove},
content_type="text/plain"
for h in hlasovani_o_znalostech: )
ucastnik = str(h.ucastnik) + ' ' + str(h.ucastnik.id) # id, kvůli kolizi jmen
if ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek
table[ucastnik] = [ucastnik] + ([""] * width)
if h.znalost.id in znalosti_map:
table[ucastnik][znalosti_map[h.znalost.id]] = h.odpoved
else:
errors.append(f"Znalost {h.znalost.id} ({h.znalost}) dostala od Účastníka {h.ucastnik.id} následující odpověď: {h.odpoved}")
if len(errors) > 0:
logger.error("Při exportování hlasování o přednáškách a znalostech se neexportovali hodnocení a přednášky (pravděpodobně se od hlasování vyškrtla nějaká znalost/přednáška ze seznamu):\n" + "\n".join(errors))
response = HttpResponse(content_type="text/csv", charset="utf-8")
response["Content-Disposition"] = 'attachment; filename="hlasovani.csv"'
writer = csv.writer(response)
writer.writerow(["jména \\ přednáška|znalost"] + list(map(str, prednasky + znalosti)))
for row in table.values():
writer.writerow(list(map(str, row)))
return response

View file

@ -25,7 +25,6 @@ django_reverse_admin # Lepší handlování OneToOne fieldů v adminu
django-rest-framework django-rest-framework
django-webpack-loader django-webpack-loader
django-rest-polymorphic django-rest-polymorphic
django-colorfield # Field pro ukládání barvy (např. tagy v korekturovátku)
# debug tools/extensions # debug tools/extensions

View file

@ -1,9 +1,8 @@
from django.contrib import admin from django.contrib import admin
from .models import OdpovedUcastnika, SpravnaOdpoved, NapovezenoUcastnikovi, Napoveda, SeznamSifer from .models import OdpovedUcastnika, SpravnaOdpoved, NapovezenoUcastnikovi, Napoveda
admin.site.register(OdpovedUcastnika) admin.site.register(OdpovedUcastnika)
admin.site.register(SpravnaOdpoved) admin.site.register(SpravnaOdpoved)
admin.site.register(Napoveda) admin.site.register(Napoveda)
admin.site.register(NapovezenoUcastnikovi) admin.site.register(NapovezenoUcastnikovi)
admin.site.register(SeznamSifer)

View file

@ -13,8 +13,8 @@ class SifrovackaForm(ModelForm):
def clean_sifra(self): def clean_sifra(self):
sifra = self.cleaned_data.get('sifra') sifra = self.cleaned_data.get('sifra')
if SpravnaOdpoved.objects.filter(sifra__iexact=sifra).count() == 0: if SpravnaOdpoved.objects.filter(sifra=sifra).count() == 0:
raise ValidationError("Tuhle šifru v databázi nemáme. Zkontrolujte si, že jste zadali název správně.") raise ValidationError("Tohle číslo šifry v databázi nemáme. Zkontrolujte si ho prosím.")
return sifra return sifra
@ -25,6 +25,6 @@ class NapovedaForm(ModelForm):
def clean_sifra(self): def clean_sifra(self):
sifra = self.cleaned_data.get('sifra') sifra = self.cleaned_data.get('sifra')
if Napoveda.objects.filter(sifra__iexact=sifra).count() == 0: if Napoveda.objects.filter(sifra=sifra).count() == 0:
raise ValidationError("K této šifře nemáme nápovědu. Zkontrolujte si, že jste zadali název správně.") raise ValidationError("K tomuto číslu šifry nemáme nápovědu. Zkontrolujte si ho prosím.")
return sifra return sifra

View file

@ -1,33 +0,0 @@
# Generated by Django 4.2.18 on 2025-03-19 20:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sifrovacka', '0006_personalni_post_migrate'),
]
operations = [
migrations.AlterField(
model_name='napoveda',
name='sifra',
field=models.CharField(max_length=255),
),
migrations.AlterField(
model_name='napovezenoucastnikovi',
name='sifra',
field=models.CharField(max_length=255, verbose_name='Šifra'),
),
migrations.AlterField(
model_name='odpoveducastnika',
name='sifra',
field=models.CharField(max_length=255, verbose_name='Šifra'),
),
migrations.AlterField(
model_name='spravnaodpoved',
name='sifra',
field=models.CharField(max_length=255),
),
]

View file

@ -1,21 +0,0 @@
# Generated by Django 4.2.20 on 2025-03-19 21:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sifrovacka', '0007_alter_napoveda_sifra_and_more'),
]
operations = [
migrations.CreateModel(
name='SeznamSifer',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('jmeno', models.CharField(help_text='něco co jde zadat do adresy', max_length=255, verbose_name='Jméno seznamu')),
('sifry', models.ManyToManyField(to='sifrovacka.spravnaodpoved')),
],
),
]

View file

@ -10,14 +10,14 @@ class OdpovedUcastnika(models.Model):
resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE) resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE)
odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,) odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,)
sifra = models.CharField("Šifra", max_length=255, blank=False, null=False,) sifra = models.IntegerField("Číslo šifry", blank=False, null=False,)
timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now) timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now)
uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False) uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False)
class SpravnaOdpoved(models.Model): class SpravnaOdpoved(models.Model):
odpoved = models.TextField(blank=False, null=False,) odpoved = models.TextField(blank=False, null=False,)
sifra = models.CharField(max_length=255, blank=False, null=False,) sifra = models.IntegerField(blank=False, null=False,)
skryty_text = models.TextField(blank=False, null=False,) skryty_text = models.TextField(blank=False, null=False,)
def __str__(self): def __str__(self):
@ -29,20 +29,13 @@ class NapovezenoUcastnikovi(models.Model):
ordering = ["-timestamp"] ordering = ["-timestamp"]
resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE) resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE)
sifra = models.CharField("Šifra", max_length=255, blank=False, null=False,) sifra = models.IntegerField("Číslo šifry", blank=False, null=False,)
timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now) timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now)
class Napoveda(models.Model): class Napoveda(models.Model):
text = models.TextField(blank=False, null=False,) text = models.TextField(blank=False, null=False,)
sifra = models.CharField(max_length=255, blank=False, null=False,) sifra = models.IntegerField(blank=False, null=False,)
def __str__(self): def __str__(self):
return f"{self.sifra}: {self.text}" return f"{self.sifra}: {self.text}"
class SeznamSifer(models.Model):
jmeno = models.CharField("Jméno seznamu", max_length=255, blank=False, null=False, help_text="něco co jde zadat do adresy")
sifry = models.ManyToManyField(SpravnaOdpoved)
def __str__(self):
return f"{self.jmeno}"

View file

@ -1,7 +1,7 @@
from django.urls import path from django.urls import path
from personalni.utils import org_required, resitel_or_org_required from personalni.utils import org_required, resitel_or_org_required
from .views import SifrovackaView, SifrovackaListView, SifrovackaNektereListView, NapovedaView, NapovedaListView, PreskoceniView from .views import SifrovackaView, SifrovackaListView, NapovedaView, NapovedaListView, PreskoceniView
urlpatterns = [ urlpatterns = [
path( path(
@ -14,11 +14,6 @@ urlpatterns = [
org_required(SifrovackaListView.as_view()), org_required(SifrovackaListView.as_view()),
name='sifrovacka_odpovedi' name='sifrovacka_odpovedi'
), ),
path(
'odpovedi/<str:seznam>/',
org_required(SifrovackaNektereListView.as_view()),
name='sifrovacka_odpovedi_nektere'
),
path( path(
'napoveda/', 'napoveda/',
resitel_or_org_required(NapovedaView.as_view()), resitel_or_org_required(NapovedaView.as_view()),

View file

@ -1,10 +1,9 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.views.generic import FormView, ListView from django.views.generic import FormView, ListView
from various.views.pomocne import formularOKView from various.views.pomocne import formularOKView
from .forms import SifrovackaForm, NapovedaForm from .forms import SifrovackaForm, NapovedaForm
from .models import OdpovedUcastnika, SpravnaOdpoved, Napoveda, NapovezenoUcastnikovi, SeznamSifer from .models import OdpovedUcastnika, SpravnaOdpoved, Napoveda, NapovezenoUcastnikovi
from personalni.models import Resitel from personalni.models import Resitel
@ -17,7 +16,7 @@ class SifrovackaView(FormView):
resitel = Resitel.objects.get(osoba__user=self.request.user) resitel = Resitel.objects.get(osoba__user=self.request.user)
instance.resitel = resitel instance.resitel = resitel
instance.save() instance.save()
sifra = SpravnaOdpoved.objects.filter(sifra__iexact=instance.sifra, odpoved__iexact=instance.odpoved.strip()).first() sifra = SpravnaOdpoved.objects.filter(sifra=instance.sifra, odpoved__iexact=instance.odpoved.strip()).first()
if sifra is None: if sifra is None:
return formularOKView(self.request, f'<h1>Bohužel vám hvězdy nebyly nakloněny. Rozumějte <i>máte to blbě</i>.</h1> <p><a href="{reverse("sifrovacka")}">Zkusit znovu.</a></p><br><br><br>') return formularOKView(self.request, f'<h1>Bohužel vám hvězdy nebyly nakloněny. Rozumějte <i>máte to blbě</i>.</h1> <p><a href="{reverse("sifrovacka")}">Zkusit znovu.</a></p><br><br><br>')
@ -31,12 +30,6 @@ class SifrovackaListView(ListView):
template_name = 'sifrovacka/odpovedi_list.html' template_name = 'sifrovacka/odpovedi_list.html'
model = OdpovedUcastnika model = OdpovedUcastnika
class SifrovackaNektereListView(SifrovackaListView):
def get_queryset(self):
seznam = get_object_or_404(SeznamSifer, jmeno=self.kwargs['seznam'])
orig = super().get_queryset()
return orig.filter(sifra__in=seznam.sifry.all().values('sifra')) # poslední je kvůli tomu, že máme odkaz na celý objekt a ne jen na jméno šifry.
class NapovedaView(FormView): class NapovedaView(FormView):
template_name = 'sifrovacka/napoveda.html' template_name = 'sifrovacka/napoveda.html'
@ -47,10 +40,10 @@ class NapovedaView(FormView):
resitel = Resitel.objects.get(osoba__user=self.request.user) resitel = Resitel.objects.get(osoba__user=self.request.user)
instance.resitel = resitel instance.resitel = resitel
if NapovezenoUcastnikovi.objects.filter(resitel=resitel, sifra__iexact=instance.sifra).first() is None: if NapovezenoUcastnikovi.objects.filter(resitel=resitel, sifra=instance.sifra).first() is None:
instance.save() instance.save()
napoveda = Napoveda.objects.filter(sifra__iexact=instance.sifra).first() napoveda = Napoveda.objects.filter(sifra=instance.sifra).first()
return formularOKView(self.request, f'<h1>Nápověda k šifře číslo {instance.sifra} je:</h1><p>{napoveda.text}</p> <p><a href="{reverse("sifrovacka")}">Odevzdat řešení.</a></p><br><br><br>') return formularOKView(self.request, f'<h1>Nápověda k šifře číslo {instance.sifra} je:</h1><p>{napoveda.text}</p> <p><a href="{reverse("sifrovacka")}">Odevzdat řešení.</a></p><br><br><br>')
@ -70,6 +63,6 @@ class PreskoceniView(FormView):
resitel = Resitel.objects.get(osoba__user=self.request.user) resitel = Resitel.objects.get(osoba__user=self.request.user)
instance.resitel = resitel instance.resitel = resitel
instance.save() instance.save()
sifra = SpravnaOdpoved.objects.filter(sifra__iexact=instance.sifra).first() # FIXME co když je více "správných" odpovědí? sifra = SpravnaOdpoved.objects.filter(sifra=instance.sifra).first() # FIXME co když je více "správných" odpovědí?
return formularOKView(self.request, f'<h1>{sifra.skryty_text}</h1> <p><a href="{reverse("sifrovacka")}">Zpět na odevzdávátko.</a></p><br><br><br>') return formularOKView(self.request, f'<h1>{sifra.skryty_text}</h1> <p><a href="{reverse("sifrovacka")}">Zpět na odevzdávátko.</a></p><br><br><br>')

View file

@ -57,7 +57,6 @@
<a href="../{{soustredeni.pk}}/seznam_ucastniku">HTML tabulka pro tisk</a>, <a href="../{{soustredeni.pk}}/seznam_ucastniku">HTML tabulka pro tisk</a>,
<a href="../{{soustredeni.pk}}/export_ucastniku">CSV</a>, <a href="../{{soustredeni.pk}}/export_ucastniku">CSV</a>,
<a href="../{{soustredeni.pk}}/maily_ucastniku">E-maily</a><br> <a href="../{{soustredeni.pk}}/maily_ucastniku">E-maily</a><br>
Exporty pro soustředění: <a href="{% url 'exporty_lidi' %}">exporty</a><br>
<a href="../{{soustredeni.pk}}/stvrzenky.pdf">Stvrzenky</a> <a href="../{{soustredeni.pk}}/stvrzenky.pdf">Stvrzenky</a>
</div> </div>
{% endif %} {% endif %}

View file

@ -16,11 +16,4 @@
{% endfor %} {% endfor %}
</ul> </ul>
<h2>Seznam účastníků &ndash; červená znamená že jim nechodí fyzické číslo</h2>
<ul>
{% for resitel in resitele %}
<li {% if resitel.neposilame %}style="color: white; background-color: red;"{% endif %}>{{ resitel.jmeno }}: {% if resitel.bodydiff > 3 %}🧦{% endif %} {% if resitel.ttitul != resitel.ftitul %} {{resitel.ftitul}} &rarr; {{resitel.ttitul}} {% endif %}</li>
{% endfor %}
</ul>
{% endblock content %} {% endblock content %}

View file

@ -28,30 +28,6 @@ def resi_v_rocniku(rocnik, cislo=None):
reseni__hodnoceni__deadline_body__cislo__poradi__lte=cislo.poradi reseni__hodnoceni__deadline_body__cislo__poradi__lte=cislo.poradi
).distinct() ).distinct()
def resi_cislo(cislo):
""" Vrátí seznam řešitelů, co vyřešili nějaký problém v daném čísle.
Parametry:
cislo (typu Cislo) číslo, ve kterém chci řešitele, co něco odevzdali
Výstup:
QuerySet objektů typu Resitel
"""
return personalni.models.Resitel.objects.filter(
reseni__hodnoceni__deadline_body__cislo=cislo
).distinct()
def resitele_co_neodmaturovali():
""" Vrátí seznam řešitelů, co ještě neodmaturovali.
Pokud ještě není srpen, tak zahrnuje i ty, kteří odmaturovali letos.
Výstup:
QuerySet objektů typu Resitel """
from datetime import datetime
current_year = datetime.now().year
if datetime.now().month < 8:
current_year -= 1
return personalni.models.Resitel.objects.filter(rok_maturity__gte=current_year)
def aktivniResitele(cislo, pouze_letosni=False): def aktivniResitele(cislo, pouze_letosni=False):
""" Vrací QuerySet aktivních řešitelů, což jsou ti, co ještě neodmaturovali """ Vrací QuerySet aktivních řešitelů, což jsou ti, co ještě neodmaturovali

View file

@ -375,8 +375,7 @@ class OdmenyView(generic.TemplateView):
tocislo = get_object_or_404(Cislo, rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo')) tocislo = get_object_or_404(Cislo, rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo'))
resitele = utils.aktivniResitele(tocislo) resitele = utils.aktivniResitele(tocislo)
def get_diff(from_deadline: Deadline, to_deadline: Deadline, probody=False): def get_diff(from_deadline: Deadline, to_deadline: Deadline):
"""Co je probody? pokud True, funkce vrací všechny rešitele a k nim potřebné informace, pokud False, vrací jen ty, kteří mají změnu v titulu."""
frombody = body_resitelu(resitele=resitele, jen_verejne=False, do=from_deadline) frombody = body_resitelu(resitele=resitele, jen_verejne=False, do=from_deadline)
tobody = body_resitelu(resitele=resitele, jen_verejne=False, do=to_deadline) tobody = body_resitelu(resitele=resitele, jen_verejne=False, do=to_deadline)
outlist = [] outlist = []
@ -385,9 +384,6 @@ class OdmenyView(generic.TemplateView):
tbody = tobody.get(resitel.id, 0) tbody = tobody.get(resitel.id, 0)
ftitul = resitel.get_titul(fbody) ftitul = resitel.get_titul(fbody)
ttitul = resitel.get_titul(tbody) ttitul = resitel.get_titul(tbody)
if probody:
outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'fbody': fbody, 'tbody': tbody, 'ftitul': ftitul, 'ttitul': ttitul, 'bodydiff': tbody - fbody, "neposilame": not(resitel.zasilat_cislo_papirove)})
else:
if ftitul != ttitul: if ftitul != ttitul:
outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'ftitul': ftitul, 'ttitul': ttitul}) outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'ftitul': ftitul, 'ttitul': ttitul})
return outlist return outlist
@ -405,7 +401,6 @@ class OdmenyView(generic.TemplateView):
context["from_deadline"] = from_deadline context["from_deadline"] = from_deadline
context["to_deadline"] = to_deadline context["to_deadline"] = to_deadline
context["zmeny"] = get_diff(from_deadline, to_deadline) context["zmeny"] = get_diff(from_deadline, to_deadline)
context["resitele"] = get_diff(from_deadline, to_deadline, probody=resitele.order_by("osoba__prijmeni"))
return context return context

View file

@ -27,7 +27,7 @@ class HromadnePridaniForm(Form):
""" Formulář pro hromadné přidání úložek a problémů """ """ Formulář pro hromadné přidání úložek a problémů """
tema = CharField(label="Název tématu:") tema = CharField(label="Název tématu:")
cislo = IntegerField(label="Číslo:", min_value=1) dil = IntegerField(label="Díl:", min_value=1)
body = CharField(label="Počty bodů (0 pro problém) oddělené čárkami:") body = CharField(label="Počty bodů (0 pro problém) oddělené čárkami:")
def clean_tema(self): def clean_tema(self):
@ -41,7 +41,7 @@ class HromadnePridaniForm(Form):
def clean_body(self): def clean_body(self):
""" Kontrola, že `body` je seznam čísel """ """ Kontrola, že `body` je seznam čísel """
try: try:
list(map(float, self.cleaned_data["body"].split(","))) list(map(int, self.cleaned_data["body"].split(",")))
except ValueError: except ValueError:
raise ValidationError("Špatný formát bodů") raise ValidationError("Špatný formát bodů")
return self.cleaned_data['body'] return self.cleaned_data['body']
@ -64,21 +64,21 @@ class HromadnePridaniView(FormView):
""" Upravený Pavlův skript na hromadné přidání úložek a problémů. """ """ Upravený Pavlův skript na hromadné přidání úložek a problémů. """
cd = form.cleaned_data cd = form.cleaned_data
tema = cd["tema"] tema = cd["tema"]
cislo = cd["cislo"] dil = cd["dil"]
body = list(map(float, cd["body"].split(","))) body = list(map(int, cd["body"].split(",")))
t = Problem.objects.get(nazev__exact=tema, nadproblem=None) t = Problem.objects.get(nazev__exact=tema, nadproblem=None)
with transaction.atomic(): with transaction.atomic():
pfx = f"{t.nazev}, " pfx = f"{t.nazev}, díl {dil}, "
for k, b in enumerate(body, 1): for k, b in enumerate(body, 1):
u = Uloha.objects.create( u = Uloha.objects.create(
nadproblem=t, nadproblem=t,
nazev=pfx + f"{'úloha' if b > 0 else 'problém'} {cislo}.{k}", nazev=pfx + f"{'úloha' if b > 0 else 'problém'} {k}",
autor=t.autor, autor=t.autor,
garant=t.garant, garant=t.garant,
max_body=b, max_body=b,
cislo_zadani=Cislo.get(t.rocnik.rocnik, cislo), cislo_zadani=Cislo.get(t.rocnik.rocnik, dil),
kod=k, kod=k,
stav=Problem.STAV_ZADANY, stav=Problem.STAV_ZADANY,
) )