Compare commits

..

2 commits

Author SHA1 Message Date
4679dca9bb Merge branch 'master' into doku_predstava 2024-10-24 11:23:19 +02:00
Pavel "LEdoian" Turinsky
acd34f4346 Návrh rozložení dokumentace 2022-11-20 22:16:22 +01:00
242 changed files with 2645 additions and 6727 deletions
aesop
api
data
deploy_v2
docs
galerie
korektury
make
mamweb
novinky
admin.py
templates/novinky
odevzdavatko
personalni

View file

@ -5,16 +5,16 @@ urlpatterns = [
path(
'aesop-export/mam-rocnik-<int:prvni_rok>.csv',
views.ExportRocnikView.as_view(),
name='aesop_export_rocnik'
name='seminar_export_rocnik'
),
path(
'aesop-export/mam-sous-<str:datum_zacatku>.csv',
views.ExportSousView.as_view(),
name='aesop_export_sous'
name='seminar_export_sous'
),
path(
'aesop-export/index.csv',
views.ExportIndexView.as_view(),
name='aesop_export_index'
name='seminar_export_index'
),
]

View file

@ -6,8 +6,7 @@ from django.views import generic
from django.utils.encoding import force_str
from .utils import default_ovvpfile
from soustredeni.models import Soustredeni
from tvorba.models import Rocnik
from seminar.models import Rocnik, Soustredeni
from vysledkovky import utils
from tvorba.utils import aktivniResitele
@ -15,10 +14,10 @@ class ExportIndexView(generic.View):
def get(self, request):
ls = []
for r in Rocnik.objects.filter(exportovat = True):
url = reverse('aesop_export_rocnik', kwargs={'prvni_rok': r.prvni_rok})
url = reverse('seminar_export_rocnik', kwargs={'prvni_rok': r.prvni_rok})
ls.append(url.split('/')[-1])
for s in Soustredeni.objects.filter(exportovat = True):
url = reverse('aesop_export_sous', kwargs={'datum_zacatku': s.datum_zacatku.isoformat()})
url = reverse('seminar_export_sous', kwargs={'datum_zacatku': s.datum_zacatku.isoformat()})
ls.append(url.split('/')[-1])
return HttpResponse('\n'.join(ls) + '\n', content_type='text/plain; charset=utf-8')

View file

@ -1,6 +1,6 @@
from django.test import TestCase, tag
from django.urls import reverse
from personalni.models import Skola
import seminar.models as m
from personalni.utils import sync_skoly
@tag('stejny-model-na-produkci')
@ -48,7 +48,7 @@ class OrgSkolyAutocompleteTestCase(TestCase):
"""Testuje, že pro každého orga je jeho škola ve výsledném QuerySetu"""
for pfx, id in self.spravna_data:
with self.subTest(prefix=pfx, spravne_id=id):
spravna_skola = Skola.objects.get(id=id)
spravna_skola = m.Skola.objects.get(id=id)
# Zeptáme se view, co si myslí
resp = self.client.get(reverse('autocomplete_skola')+'?q='+pfx).json()
ids = [int(x['id']) for x in resp['results']]

View file

@ -17,5 +17,5 @@ urlpatterns = [
# Ceka na autocomplete v3
# path('autocomplete/organizatori/',
# org_member_required(views.OrganizatorAutocomplete.as_view()),
# name='autocomplete_organizator')
# name='seminar_autocomplete_organizator')
]

View file

@ -5,9 +5,7 @@ from dal import autocomplete
from django.shortcuts import get_object_or_404
from django.db.models import Q
from personalni.models import Skola, Resitel
from tvorba.models import Problem
from various.models import Nastaveni
import seminar.models as m
from .helpers import LoginRequiredAjaxMixin
# TODO filosofie - zkratky, jak v databázi, tak ve vyhledávání (SPŠE, GASOŠ, Kpt., soukr)
@ -15,7 +13,7 @@ class SkolaAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání škol hlavně při registraci. """
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
qs = Skola.objects.all()
qs = m.Skola.objects.all()
if self.q:
words = self.q.split(' ') #TODO re split podle bileho znaku
partq = Q()
@ -33,7 +31,7 @@ class SkolaAutocomplete(autocomplete.Select2QuerySetView):
class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání řešitelů především v odevzdávátku. """
def get_queryset(self):
qs = Resitel.objects.all()
qs = m.Resitel.objects.all()
if self.q:
parts = self.q.split()
query = Q()
@ -53,8 +51,8 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
především v odevzdávátku.
"""
def get_queryset(self):
letos = Nastaveni.get_solo().aktualni_rocnik
qs = Resitel.objects.filter(
letos = m.Nastaveni.get_solo().aktualni_rocnik
qs = m.Resitel.objects.filter(
rok_maturity__gte=letos.druhy_rok()
).filter(
prezdivka_resitele__isnull=False
@ -72,7 +70,7 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):
qs = Problem.objects.filter(stav=Problem.STAV_ZADANY)
qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY)
if self.q:
qs = qs.filter(
Q(nazev__icontains=self.q))
@ -89,12 +87,12 @@ class ProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):
# FIXME i starší úlohy
nastaveni = get_object_or_404(Nastaveni)
nastaveni = get_object_or_404(m.Nastaveni)
rocnik = nastaveni.aktualni_rocnik
temaQ = Q(Tema___rocnik = rocnik)
ulohaQ = Q(Uloha___cislo_zadani__rocnik=rocnik)
clanekQ = Q(Clanek___cislo__rocnik=rocnik)
qs = Problem.objects.filter(temaQ | ulohaQ | clanekQ).order_by("-stav", "nazev")
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ).order_by("-stav", "nazev")
if self.q:
qs = qs.filter(
Q(nazev__icontains=self.q))

View file

@ -1,4 +1,4 @@
import personalni.models as m
import seminar.models as m
from django.core import serializers as ser
from django.http import HttpResponse
def exportSkolView(request):

View file

@ -1,655 +0,0 @@
[
{
"fields": {
"name": "org",
"permissions": [
[
"org",
"auth",
"user"
],
[
"add_flatpage",
"flatpages",
"flatpage"
],
[
"change_flatpage",
"flatpages",
"flatpage"
],
[
"delete_flatpage",
"flatpages",
"flatpage"
],
[
"view_flatpage",
"flatpages",
"flatpage"
],
[
"add_galerie",
"galerie",
"galerie"
],
[
"change_galerie",
"galerie",
"galerie"
],
[
"delete_galerie",
"galerie",
"galerie"
],
[
"view_galerie",
"galerie",
"galerie"
],
[
"add_obrazek",
"galerie",
"obrazek"
],
[
"change_obrazek",
"galerie",
"obrazek"
],
[
"delete_obrazek",
"galerie",
"obrazek"
],
[
"view_obrazek",
"galerie",
"obrazek"
],
[
"add_fotkaheader",
"header_fotky",
"fotkaheader"
],
[
"change_fotkaheader",
"header_fotky",
"fotkaheader"
],
[
"view_fotkaheader",
"header_fotky",
"fotkaheader"
],
[
"add_fotkaurlvazba",
"header_fotky",
"fotkaurlvazba"
],
[
"change_fotkaurlvazba",
"header_fotky",
"fotkaurlvazba"
],
[
"view_fotkaurlvazba",
"header_fotky",
"fotkaurlvazba"
],
[
"add_komentar",
"korektury",
"komentar"
],
[
"change_komentar",
"korektury",
"komentar"
],
[
"delete_komentar",
"korektury",
"komentar"
],
[
"view_komentar",
"korektury",
"komentar"
],
[
"add_korekturovanepdf",
"korektury",
"korekturovanepdf"
],
[
"change_korekturovanepdf",
"korektury",
"korekturovanepdf"
],
[
"delete_korekturovanepdf",
"korektury",
"korekturovanepdf"
],
[
"view_korekturovanepdf",
"korektury",
"korekturovanepdf"
],
[
"add_oprava",
"korektury",
"oprava"
],
[
"change_oprava",
"korektury",
"oprava"
],
[
"delete_oprava",
"korektury",
"oprava"
],
[
"view_oprava",
"korektury",
"oprava"
],
[
"add_novinky",
"novinky",
"novinky"
],
[
"change_novinky",
"novinky",
"novinky"
],
[
"delete_novinky",
"novinky",
"novinky"
],
[
"view_novinky",
"novinky",
"novinky"
],
[
"change_organizator",
"personalni",
"organizator"
],
[
"view_organizator",
"personalni",
"organizator"
],
[
"change_osoba",
"personalni",
"osoba"
],
[
"view_osoba",
"personalni",
"osoba"
],
[
"add_prijemce",
"personalni",
"prijemce"
],
[
"change_prijemce",
"personalni",
"prijemce"
],
[
"delete_prijemce",
"personalni",
"prijemce"
],
[
"view_prijemce",
"personalni",
"prijemce"
],
[
"change_resitel",
"personalni",
"resitel"
],
[
"view_resitel",
"personalni",
"resitel"
],
[
"add_skola",
"personalni",
"skola"
],
[
"change_skola",
"personalni",
"skola"
],
[
"delete_skola",
"personalni",
"skola"
],
[
"view_skola",
"personalni",
"skola"
],
[
"view_hlasovani",
"prednasky",
"hlasovani"
],
[
"view_hlasovanioznalostech",
"prednasky",
"hlasovanioznalostech"
],
[
"add_prednaska",
"prednasky",
"prednaska"
],
[
"change_prednaska",
"prednasky",
"prednaska"
],
[
"delete_prednaska",
"prednasky",
"prednaska"
],
[
"view_prednaska",
"prednasky",
"prednaska"
],
[
"add_seznam",
"prednasky",
"seznam"
],
[
"change_seznam",
"prednasky",
"seznam"
],
[
"delete_seznam",
"prednasky",
"seznam"
],
[
"view_seznam",
"prednasky",
"seznam"
],
[
"add_znalost",
"prednasky",
"znalost"
],
[
"change_znalost",
"prednasky",
"znalost"
],
[
"delete_znalost",
"prednasky",
"znalost"
],
[
"view_znalost",
"prednasky",
"znalost"
],
[
"add_konfera",
"soustredeni",
"konfera"
],
[
"change_konfera",
"soustredeni",
"konfera"
],
[
"delete_konfera",
"soustredeni",
"konfera"
],
[
"view_konfera",
"soustredeni",
"konfera"
],
[
"add_konfery_ucastnici",
"soustredeni",
"konfery_ucastnici"
],
[
"change_konfery_ucastnici",
"soustredeni",
"konfery_ucastnici"
],
[
"delete_konfery_ucastnici",
"soustredeni",
"konfery_ucastnici"
],
[
"view_konfery_ucastnici",
"soustredeni",
"konfery_ucastnici"
],
[
"add_soustredeni",
"soustredeni",
"soustredeni"
],
[
"change_soustredeni",
"soustredeni",
"soustredeni"
],
[
"delete_soustredeni",
"soustredeni",
"soustredeni"
],
[
"view_soustredeni",
"soustredeni",
"soustredeni"
],
[
"add_soustredeni_organizatori",
"soustredeni",
"soustredeni_organizatori"
],
[
"change_soustredeni_organizatori",
"soustredeni",
"soustredeni_organizatori"
],
[
"delete_soustredeni_organizatori",
"soustredeni",
"soustredeni_organizatori"
],
[
"view_soustredeni_organizatori",
"soustredeni",
"soustredeni_organizatori"
],
[
"add_soustredeni_ucastnici",
"soustredeni",
"soustredeni_ucastnici"
],
[
"change_soustredeni_ucastnici",
"soustredeni",
"soustredeni_ucastnici"
],
[
"delete_soustredeni_ucastnici",
"soustredeni",
"soustredeni_ucastnici"
],
[
"view_soustredeni_ucastnici",
"soustredeni",
"soustredeni_ucastnici"
],
[
"add_tag",
"taggit",
"tag"
],
[
"change_tag",
"taggit",
"tag"
],
[
"delete_tag",
"taggit",
"tag"
],
[
"view_tag",
"taggit",
"tag"
],
[
"add_taggeditem",
"taggit",
"taggeditem"
],
[
"change_taggeditem",
"taggit",
"taggeditem"
],
[
"delete_taggeditem",
"taggit",
"taggeditem"
],
[
"view_taggeditem",
"taggit",
"taggeditem"
],
[
"add_cislo",
"tvorba",
"cislo"
],
[
"change_cislo",
"tvorba",
"cislo"
],
[
"delete_cislo",
"tvorba",
"cislo"
],
[
"view_cislo",
"tvorba",
"cislo"
],
[
"add_clanek",
"tvorba",
"clanek"
],
[
"change_clanek",
"tvorba",
"clanek"
],
[
"delete_clanek",
"tvorba",
"clanek"
],
[
"view_clanek",
"tvorba",
"clanek"
],
[
"add_deadline",
"tvorba",
"deadline"
],
[
"change_deadline",
"tvorba",
"deadline"
],
[
"view_deadline",
"tvorba",
"deadline"
],
[
"add_pohadka",
"tvorba",
"pohadka"
],
[
"change_pohadka",
"tvorba",
"pohadka"
],
[
"delete_pohadka",
"tvorba",
"pohadka"
],
[
"view_pohadka",
"tvorba",
"pohadka"
],
[
"add_problem",
"tvorba",
"problem"
],
[
"change_problem",
"tvorba",
"problem"
],
[
"delete_problem",
"tvorba",
"problem"
],
[
"view_problem",
"tvorba",
"problem"
],
[
"add_rocnik",
"tvorba",
"rocnik"
],
[
"change_rocnik",
"tvorba",
"rocnik"
],
[
"delete_rocnik",
"tvorba",
"rocnik"
],
[
"view_rocnik",
"tvorba",
"rocnik"
],
[
"add_tema",
"tvorba",
"tema"
],
[
"change_tema",
"tvorba",
"tema"
],
[
"delete_tema",
"tvorba",
"tema"
],
[
"view_tema",
"tvorba",
"tema"
],
[
"add_uloha",
"tvorba",
"uloha"
],
[
"change_uloha",
"tvorba",
"uloha"
],
[
"delete_uloha",
"tvorba",
"uloha"
],
[
"view_uloha",
"tvorba",
"uloha"
],
[
"add_nastaveni",
"various",
"nastaveni"
],
[
"change_nastaveni",
"various",
"nastaveni"
],
[
"delete_nastaveni",
"various",
"nastaveni"
],
[
"view_nastaveni",
"various",
"nastaveni"
]
]
},
"model": "auth.group",
"pk": 1
},
{
"fields": {
"name": "resitel",
"permissions": [
[
"resitel",
"auth",
"user"
]
]
},
"model": "auth.group",
"pk": 2
}
]

View file

@ -73,7 +73,7 @@
"sort_order": 3,
"title": "Aktuální<br/> ročník",
"tree": 1,
"url": "tvorba_aktualni_zadani",
"url": "seminar_aktualni_zadani",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -121,7 +121,7 @@
"sort_order": 5,
"title": "Archiv",
"tree": 1,
"url": "tvorba_archiv_rocniky",
"url": "seminar_archiv_rocniky",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -289,7 +289,7 @@
"sort_order": 43,
"title": "Výsledková listina",
"tree": 1,
"url": "tvorba_aktualni_vysledky",
"url": "seminar_aktualni_vysledky",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -361,7 +361,7 @@
"sort_order": 20,
"title": "Proběhlo",
"tree": 1,
"url": "soustredeni_seznam",
"url": "seminar_seznam_soustredeni",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -409,7 +409,7 @@
"sort_order": 23,
"title": "Osobní údaje",
"tree": 1,
"url": "personalni_resitel_edit",
"url": "seminar_resitel_edit",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -439,7 +439,7 @@
"sort_order": 36,
"title": "Nahrát řešení",
"tree": 1,
"url": "odevzdavatko_nahraj_reseni",
"url": "seminar_nahraj_reseni",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -463,7 +463,7 @@
"sort_order": 35,
"title": "Témata",
"tree": 1,
"url": "tvorba_archiv_temata",
"url": "seminar_archiv_temata",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -589,7 +589,7 @@
"sort_order": 15,
"title": "Aktuální číslo",
"tree": 1,
"url": "tvorba_aktualni_zadani",
"url": "seminar_aktualni_zadani",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -613,7 +613,7 @@
"sort_order": 24,
"title": "Čísla",
"tree": 1,
"url": "tvorba_archiv_rocniky",
"url": "seminar_archiv_rocniky",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -721,7 +721,7 @@
"sort_order": 36,
"title": "Vložit řešení",
"tree": 1,
"url": "odevzdavatko_vloz_reseni",
"url": "seminar_vloz_reseni",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -804,7 +804,7 @@
"sort_order": 37,
"title": "Moje řešení",
"tree": 1,
"url": "odevzdavatko_resitel_odevzdana_reseni",
"url": "seminar_resitel_odevzdana_reseni",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -828,7 +828,7 @@
"sort_order": 33,
"title": "Aktuální ročník",
"tree": 1,
"url": "tvorba_aktualni_rocnik",
"url": "seminar_aktualni_rocnik",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -900,7 +900,7 @@
"sort_order": 46,
"title": "Ročník {{rocnik.rocnik}}",
"tree": 1,
"url": "tvorba_rocnik rocnik.rocnik",
"url": "seminar_rocnik rocnik.rocnik",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -924,7 +924,7 @@
"sort_order": 47,
"title": "Číslo {{ cislo.rocnik.rocnik }}.{{ cislo.poradi }}",
"tree": 1,
"url": "tvorba_cislo cislo.rocnik.rocnik cislo.poradi",
"url": "seminar_cislo cislo.rocnik.rocnik cislo.poradi",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -1007,18 +1007,7 @@
"access_guest": false,
"access_loggedin": false,
"access_perm_type": 1,
"access_permissions": [
[
"org",
"auth",
"user"
],
[
"resitel",
"auth",
"user"
]
],
"access_permissions": [],
"access_restricted": true,
"alias": null,
"description": "",
@ -1061,7 +1050,7 @@
"sort_order": 52,
"title": "Nahrát řešení k nadproblému {{nadproblem_id}}",
"tree": 1,
"url": "odevzdavatko_nahraj_reseni nadproblem_id",
"url": "seminar_nahraj_reseni nadproblem_id",
"urlaspattern": true
},
"model": "sitetree.treeitem",
@ -1097,7 +1086,7 @@
"access_loggedin": false,
"access_perm_type": 1,
"access_permissions": [],
"access_restricted": false,
"access_restricted": true,
"alias": null,
"description": "",
"hidden": false,
@ -1109,7 +1098,7 @@
"sort_order": 54,
"title": "Export do abstraktů sousu {{ soustredeni.id }}",
"tree": 1,
"url": "soustredeni_abstrakty soustredeni.id",
"url": "seminar_soustredeni_abstrakty soustredeni.id",
"urlaspattern": true
},
"model": "sitetree.treeitem",

View file

@ -0,0 +1,622 @@
[
{
"codename": "org",
"ct_app_label": "auth",
"ct_model": "user"
},
{
"codename": "add_flatpage",
"ct_app_label": "flatpages",
"ct_model": "flatpage"
},
{
"codename": "change_flatpage",
"ct_app_label": "flatpages",
"ct_model": "flatpage"
},
{
"codename": "delete_flatpage",
"ct_app_label": "flatpages",
"ct_model": "flatpage"
},
{
"codename": "view_flatpage",
"ct_app_label": "flatpages",
"ct_model": "flatpage"
},
{
"codename": "add_galerie",
"ct_app_label": "galerie",
"ct_model": "galerie"
},
{
"codename": "change_galerie",
"ct_app_label": "galerie",
"ct_model": "galerie"
},
{
"codename": "delete_galerie",
"ct_app_label": "galerie",
"ct_model": "galerie"
},
{
"codename": "view_galerie",
"ct_app_label": "galerie",
"ct_model": "galerie"
},
{
"codename": "add_obrazek",
"ct_app_label": "galerie",
"ct_model": "obrazek"
},
{
"codename": "change_obrazek",
"ct_app_label": "galerie",
"ct_model": "obrazek"
},
{
"codename": "delete_obrazek",
"ct_app_label": "galerie",
"ct_model": "obrazek"
},
{
"codename": "view_obrazek",
"ct_app_label": "galerie",
"ct_model": "obrazek"
},
{
"codename": "add_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "change_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "view_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "add_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "change_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "view_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "add_komentar",
"ct_app_label": "korektury",
"ct_model": "komentar"
},
{
"codename": "change_komentar",
"ct_app_label": "korektury",
"ct_model": "komentar"
},
{
"codename": "delete_komentar",
"ct_app_label": "korektury",
"ct_model": "komentar"
},
{
"codename": "view_komentar",
"ct_app_label": "korektury",
"ct_model": "komentar"
},
{
"codename": "add_korekturovanepdf",
"ct_app_label": "korektury",
"ct_model": "korekturovanepdf"
},
{
"codename": "change_korekturovanepdf",
"ct_app_label": "korektury",
"ct_model": "korekturovanepdf"
},
{
"codename": "delete_korekturovanepdf",
"ct_app_label": "korektury",
"ct_model": "korekturovanepdf"
},
{
"codename": "view_korekturovanepdf",
"ct_app_label": "korektury",
"ct_model": "korekturovanepdf"
},
{
"codename": "add_oprava",
"ct_app_label": "korektury",
"ct_model": "oprava"
},
{
"codename": "change_oprava",
"ct_app_label": "korektury",
"ct_model": "oprava"
},
{
"codename": "delete_oprava",
"ct_app_label": "korektury",
"ct_model": "oprava"
},
{
"codename": "view_oprava",
"ct_app_label": "korektury",
"ct_model": "oprava"
},
{
"codename": "add_hlasovani",
"ct_app_label": "prednasky",
"ct_model": "hlasovani"
},
{
"codename": "change_hlasovani",
"ct_app_label": "prednasky",
"ct_model": "hlasovani"
},
{
"codename": "delete_hlasovani",
"ct_app_label": "prednasky",
"ct_model": "hlasovani"
},
{
"codename": "view_hlasovani",
"ct_app_label": "prednasky",
"ct_model": "hlasovani"
},
{
"codename": "add_prednaska",
"ct_app_label": "prednasky",
"ct_model": "prednaska"
},
{
"codename": "change_prednaska",
"ct_app_label": "prednasky",
"ct_model": "prednaska"
},
{
"codename": "delete_prednaska",
"ct_app_label": "prednasky",
"ct_model": "prednaska"
},
{
"codename": "view_prednaska",
"ct_app_label": "prednasky",
"ct_model": "prednaska"
},
{
"codename": "add_seznam",
"ct_app_label": "prednasky",
"ct_model": "seznam"
},
{
"codename": "change_seznam",
"ct_app_label": "prednasky",
"ct_model": "seznam"
},
{
"codename": "delete_seznam",
"ct_app_label": "prednasky",
"ct_model": "seznam"
},
{
"codename": "view_seznam",
"ct_app_label": "prednasky",
"ct_model": "seznam"
},
{
"codename": "add_cislo",
"ct_app_label": "seminar",
"ct_model": "cislo"
},
{
"codename": "change_cislo",
"ct_app_label": "seminar",
"ct_model": "cislo"
},
{
"codename": "delete_cislo",
"ct_app_label": "seminar",
"ct_model": "cislo"
},
{
"codename": "view_cislo",
"ct_app_label": "seminar",
"ct_model": "cislo"
},
{
"codename": "add_clanek",
"ct_app_label": "seminar",
"ct_model": "clanek"
},
{
"codename": "change_clanek",
"ct_app_label": "seminar",
"ct_model": "clanek"
},
{
"codename": "delete_clanek",
"ct_app_label": "seminar",
"ct_model": "clanek"
},
{
"codename": "view_clanek",
"ct_app_label": "seminar",
"ct_model": "clanek"
},
{
"codename": "add_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "change_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "view_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "add_konfera",
"ct_app_label": "soustredeni",
"ct_model": "konfera"
},
{
"codename": "change_konfera",
"ct_app_label": "soustredeni",
"ct_model": "konfera"
},
{
"codename": "delete_konfera",
"ct_app_label": "soustredeni",
"ct_model": "konfera"
},
{
"codename": "view_konfera",
"ct_app_label": "soustredeni",
"ct_model": "konfera"
},
{
"codename": "add_konfery_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "konfery_ucastnici"
},
{
"codename": "change_konfery_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "konfery_ucastnici"
},
{
"codename": "delete_konfery_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "konfery_ucastnici"
},
{
"codename": "view_konfery_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "konfery_ucastnici"
},
{
"codename": "add_nastaveni",
"ct_app_label": "various",
"ct_model": "nastaveni"
},
{
"codename": "change_nastaveni",
"ct_app_label": "various",
"ct_model": "nastaveni"
},
{
"codename": "delete_nastaveni",
"ct_app_label": "various",
"ct_model": "nastaveni"
},
{
"codename": "view_nastaveni",
"ct_app_label": "various",
"ct_model": "nastaveni"
},
{
"codename": "add_novinky",
"ct_app_label": "novinky",
"ct_model": "novinky"
},
{
"codename": "change_novinky",
"ct_app_label": "novinky",
"ct_model": "novinky"
},
{
"codename": "delete_novinky",
"ct_app_label": "novinky",
"ct_model": "novinky"
},
{
"codename": "view_novinky",
"ct_app_label": "novinky",
"ct_model": "novinky"
},
{
"codename": "change_organizator",
"ct_app_label": "personalni",
"ct_model": "organizator"
},
{
"codename": "view_organizator",
"ct_app_label": "personalni",
"ct_model": "organizator"
},
{
"codename": "change_osoba",
"ct_app_label": "personalni",
"ct_model": "osoba"
},
{
"codename": "view_osoba",
"ct_app_label": "personalni",
"ct_model": "osoba"
},
{
"codename": "add_pohadka",
"ct_app_label": "seminar",
"ct_model": "pohadka"
},
{
"codename": "change_pohadka",
"ct_app_label": "seminar",
"ct_model": "pohadka"
},
{
"codename": "delete_pohadka",
"ct_app_label": "seminar",
"ct_model": "pohadka"
},
{
"codename": "view_pohadka",
"ct_app_label": "seminar",
"ct_model": "pohadka"
},
{
"codename": "add_prijemce",
"ct_app_label": "personalni",
"ct_model": "prijemce"
},
{
"codename": "change_prijemce",
"ct_app_label": "personalni",
"ct_model": "prijemce"
},
{
"codename": "delete_prijemce",
"ct_app_label": "personalni",
"ct_model": "prijemce"
},
{
"codename": "view_prijemce",
"ct_app_label": "personalni",
"ct_model": "prijemce"
},
{
"codename": "add_problem",
"ct_app_label": "seminar",
"ct_model": "problem"
},
{
"codename": "change_problem",
"ct_app_label": "seminar",
"ct_model": "problem"
},
{
"codename": "delete_problem",
"ct_app_label": "seminar",
"ct_model": "problem"
},
{
"codename": "view_problem",
"ct_app_label": "seminar",
"ct_model": "problem"
},
{
"codename": "change_resitel",
"ct_app_label": "personalni",
"ct_model": "resitel"
},
{
"codename": "view_resitel",
"ct_app_label": "personalni",
"ct_model": "resitel"
},
{
"codename": "add_rocnik",
"ct_app_label": "seminar",
"ct_model": "rocnik"
},
{
"codename": "change_rocnik",
"ct_app_label": "seminar",
"ct_model": "rocnik"
},
{
"codename": "delete_rocnik",
"ct_app_label": "seminar",
"ct_model": "rocnik"
},
{
"codename": "view_rocnik",
"ct_app_label": "seminar",
"ct_model": "rocnik"
},
{
"codename": "add_skola",
"ct_app_label": "personalni",
"ct_model": "skola"
},
{
"codename": "change_skola",
"ct_app_label": "personalni",
"ct_model": "skola"
},
{
"codename": "delete_skola",
"ct_app_label": "personalni",
"ct_model": "skola"
},
{
"codename": "view_skola",
"ct_app_label": "personalni",
"ct_model": "skola"
},
{
"codename": "add_soustredeni",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni"
},
{
"codename": "change_soustredeni",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni"
},
{
"codename": "delete_soustredeni",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni"
},
{
"codename": "view_soustredeni",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni"
},
{
"codename": "add_soustredeni_organizatori",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_organizatori"
},
{
"codename": "change_soustredeni_organizatori",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_organizatori"
},
{
"codename": "delete_soustredeni_organizatori",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_organizatori"
},
{
"codename": "view_soustredeni_organizatori",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_organizatori"
},
{
"codename": "add_soustredeni_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_ucastnici"
},
{
"codename": "change_soustredeni_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_ucastnici"
},
{
"codename": "delete_soustredeni_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_ucastnici"
},
{
"codename": "view_soustredeni_ucastnici",
"ct_app_label": "soustredeni",
"ct_model": "soustredeni_ucastnici"
},
{
"codename": "add_tema",
"ct_app_label": "seminar",
"ct_model": "tema"
},
{
"codename": "change_tema",
"ct_app_label": "seminar",
"ct_model": "tema"
},
{
"codename": "delete_tema",
"ct_app_label": "seminar",
"ct_model": "tema"
},
{
"codename": "view_tema",
"ct_app_label": "seminar",
"ct_model": "tema"
},
{
"codename": "add_uloha",
"ct_app_label": "seminar",
"ct_model": "uloha"
},
{
"codename": "change_uloha",
"ct_app_label": "seminar",
"ct_model": "uloha"
},
{
"codename": "delete_uloha",
"ct_app_label": "seminar",
"ct_model": "uloha"
},
{
"codename": "view_uloha",
"ct_app_label": "seminar",
"ct_model": "uloha"
},
{
"codename": "add_tag",
"ct_app_label": "taggit",
"ct_model": "tag"
},
{
"codename": "change_tag",
"ct_app_label": "taggit",
"ct_model": "tag"
},
{
"codename": "delete_tag",
"ct_app_label": "taggit",
"ct_model": "tag"
},
{
"codename": "view_tag",
"ct_app_label": "taggit",
"ct_model": "tag"
},
{
"codename": "add_taggeditem",
"ct_app_label": "taggit",
"ct_model": "taggeditem"
},
{
"codename": "change_taggeditem",
"ct_app_label": "taggit",
"ct_model": "taggeditem"
},
{
"codename": "delete_taggeditem",
"ct_app_label": "taggit",
"ct_model": "taggeditem"
},
{
"codename": "view_taggeditem",
"ct_app_label": "taggit",
"ct_model": "taggeditem"
}
]

View file

@ -13,6 +13,7 @@ make install_venv
make install
deploy_v2/pre_migration.py
make deploy_test
./manage.py load_org_permissions admin_org_prava.json
./manage.py loaddata data/*
systemctl --user start mamweb-test.service
./manage.py generate_thumbnails

View file

@ -36,7 +36,6 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.autosectionlabel',
'myst_parser',
'sphinxcontrib_django',
]
# Add any paths that contain templates here, relative to this directory.

View file

@ -30,7 +30,6 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve
zavislosti
sphinx
skripty
zkratky
modules/modules
dalsi_soubory
zapisy/zapisy

View file

@ -1,15 +1,49 @@
Sphinx na našem webu
====================
Dokumentace se zkompiluje příkazem ``make html`` ve složce ``docs``. (Musíte mít zapnutý virtualenv)
(Návrh) rozložení/členění dokumentace
-------
.. note:: Předpokládám, že tohle je dočasné a že v případě schválení návrhu se
obsah odsud objeví nejspíš na titulní stránce dokumentace. Zatím je tady pro
diskusi, ale rozhodně může být jinde a jinak napsaný…
Představuji si aktuálně tříúrovňovou dokumentaci:
#. Několik málo (určitě ne víc než deset) stránek pro seznámení s webem,
repozitářem apod. Ty by byly spíš povrchové a prostě by cílily na novowebaře
a snažily se ho seznámit s našimi postupy
- Intro rozcestník
- Jak rozběhnout lokální web
- Jak u nás funguje vývoj webu (úzy, coding style, pull-request workflow, tři instance webu a jak je použít, …)
- Co kde je (aplikace, jak dohledat view k URL)
- Možná historie webu (zejména pro zajímavost, ale i pro nějaký náhled, co si táhneme s sebou za historii…)
- Popis částí Djanga (můj fancy obrázek, jen nakreslený použitelněji :-))
#. Freetextová dokumentace k různým částem webu, sepsaná v ``docs/``.
- Různé tipy (aktuálně sepsané na wiki)
- Textovější popis různých věcí
- Dokumentace ne-pythonu (make skripty)
- …
#. Autogenerovaná reference z docstringů
Nepředstavuji si, že by všechno bylo na všech třech úrovních, spíš naopak dává
smysl se mezi úrovněmi odkazovat a neduplikovat.
Bylo by fajn, kdyby ta intro /
tutoriálová část byla spíš stručnější, ale pokud ji budu psát já, tak to zase
nedopadne :-) (ale asi je lepší, když bude nějak, než když nebude vůbec)
Použití
------
Dokumentace se zkompiluje příkazem ``make html`` ve složce ``doc``.
Složka ``modules`` je automaticiky generována a přegenerovávána. (**Nic v ní neupravovat!**)
Jinak všechny rst, co jsou ve složce ``docs`` a jejích podsložkách nezačínajících podtržítkem, budou v dokumentaci a to je přesně to, co editovat pro změnu dokumentace (kromě dokumentace přímo v Pythonu).
Jinak všechny rst, co jsou ve složce ``doc`` a jejích podsložkách nezačínajících podtržítkem, budou v dokumentaci a to je přesně to, co editovat pro změnu dokumentace (kromě dokumentace přímo v Pythonu).
Sphinx se píše v rst: `Návod na syntaxi rst`_ `Cheat sheet`_
Pokud něco chcete protlačit do bočního meníčka, je potřeba to připsat do souboru ``docs/index.rst`` (Zatím není úplně konsensus nad tím, co tam má a nemá být, takže pokud si nejste jistí, cpěte tam *všechno* ☺)
To je snad vše, co je potřeba vědět k dokumentaci mamwebu. Následující sekce jsou o tom, co jsem provedl Sphinxu, aby to fungovalo:
.. _Návod na syntaxi rst: https://sphinx-tutorial.readthedocs.io/step-1/#sections

View file

@ -10,9 +10,11 @@ věci jako chybové hlášky a vzhled M&M stránek (menu, patička, atd.). Aktu
i veškeré csv.
Další jsou pak jednotlivé aplikace (pokud něco hledáte, tak zřejmě chcete najít
tu aplikaci, která tomu odpovídá, respektive se k ní dostat přes url).
tu aplikaci, která tomu odpovídá, respektive se k ní dostat přes url), za
zmínku stojí seminar, kde jsou takové ty věci, co zbyly. Plus jsou tam aktuálně
téměř všechny modely, protože je těžké je přesunout jinam.
**TLDR: Nevšímejte si složek data/ seminar/ a souborů přímo v kořenové složce.**
**TLDR: Nevšímejte si složky data/ a souborů přímo v kořenové složce.**
Kromě věcí potřebných ke gitu, :doc:`ke spuštění <vyvoj>` a fukci djanga,
dalších drobností, lokální databáze a již zmíněných aplikací jsou tu ``data``,
kde je takový ten obsah webu, co by se měl dát snadno měnit (tudíž musí být v
@ -20,9 +22,6 @@ databázi), tj. statické stránky, menu a obrázky v pozadí menu. Ten je třeb
měnit hlavně na produkci a sekundárně tady (může to dělat i newebař a nechcete
přepsat jeho práci). Vše, co nejsou aplikace je popsáno :doc:`tady <dalsi_soubory>`.
Ještě je tu aplikace ``seminar/``, kde bylo původně skoro všechno, a tak nám
tam zbývá spoustu historických migrací (čehož se jen tak nezbavíme).
Základy djanga
--------------

View file

@ -116,7 +116,7 @@ Aktuálně: Jakýsi coding style zhruba existuje, není popsaný, šíří se li
- Nesmí být striktně vynucovaný
- Musel by být hodně nastavitelný
- Nechceme mít kód plný `#NOQA: WTF42`
- Nejspíš vždycky bude mít false positives (`tvorba.utils.roman_numerals`) i false negatives (`tvorba.models.Cislo.posli_cislo_mailem`)
- Nejspíš vždycky bude mít false positives (`tvorba.utils.roman_numerals`) i false negatives (`seminar.models.tvorba.Cislo.posli_cislo_mailem`)
- Možná dobrý sluha, ale určitě špatný pán (also: špatná zkušenost ☺)
- __Důsledek:__ Hrozí, že těch falešných varování bude moc, čímž to ztratí smysl úplně
- Potenciálně by šlo aplikovat jen lokálně na změny?

View file

@ -1,86 +0,0 @@
Zkratky aplikací ve zdrojácích
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Ve zdrojácích (zejména různé ``models.py``, ``views.py`` ap.) používáme spoustu
modelů. Někdy je praktičtější / někdo preferuje importovat celou aplikaci jako
jedno jméno a používat modely bez explicitních importů, tj::
# „hromadné“ importy:
import personalni.models as p
...
p.Organizator.objects.all()
# „explicitní“ importy:
from personalni.models import Organizator
...
Organizator.objects.all()
Na webschůzce 2024-11-05 jsme na toto téma otevřeli diskusi, tady je její závěr.
.. admonition:: Historické okénko
:class: note
Kdysi jsme měli (prakticky) všechny modely v jedné aplikaci, ``seminar``. Na
různých místech se pak ``seminar.models`` importovalo typicky jako ``s``
nebo jako ``m``.
Přirozeně, toto už nejde tak snadno, protože už neexistuje jedno místo, ze
kterého chceme tahat modely v kódu.
Konvence
========
Shodli jsme se, že nám rozhodně nevadí explicitní importy a z pohledu
čitelnosti je preferujeme. Nicméně při psaní kódu to některým webařům přijde
nepohodlné, takže očekáváme, že bude existovat spousta kódu, která bude chtít
importovat hromadně. Usnesli jsme se proto na následujících kanonických
zkratkách, aby se aplikace alespoň zkracovaly konzistentně.
V závorkách je uvedené případné jméno, ale nepředpokládáme, že někdo bude danou
aplikaci chtít importovat hromadně. Některé aplikace zkratku nemají, ty se
importují vždy pod plným jménem nebo explicitně.
.. list-table::
:header-rows: 1
* - Model
- Zkratka
* - ``aesop``
- ---
* - ``api``
- --- (``api``)
* - ``galerie``
- ``g``
* - ``header_fotky``
- --- (``hdr``)
* - ``korektury``
- ``kor``
* - ``novinky``
- ``nov``
* - ``odevzdavatko``
- ``odev``
* - ``personální``
- ``pers``/``p``
* - ``sifrovacka``
- (``sifr``)
* - ``soustredeni``
- ``sou``
* - ``treenode``
- ``tn``
* - ``tvorba``
- ``tv``
* - ``various``
- ``v``/``var``
* - ``vyroci``
- ---
* - ``vysledkovky``
- ``vysl``
.. admonition:: O všech modelech pod jedním jménem
:class: warning
Historické okénko výš zatajuje jeden detail: Při práci v shellu se hodí mít
modely k dispozici a nemuset přemýšlet nad dělením do aplikací, takže ve
skutečnosti existuje ``mamweb.vsechno``, jenž všechny modely obsahuje.
Z čitelnostních důvodů je ale *zakázáno* tento modul používat v kódu.

View file

@ -6,7 +6,7 @@ from django.template import RequestContext
from datetime import datetime
from galerie.models import Obrazek, Galerie
from soustredeni.models import Soustredeni
from seminar.models import Soustredeni
from galerie.forms import KomentarForm, NewGalerieForm
def zobrazit(galerie, request):

View file

@ -1,6 +1,6 @@
from django.contrib import admin
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.urls import reverse
@ -62,11 +62,3 @@ Korekturovátko
).send()
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,9 +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'),
]

View file

@ -1,109 +0,0 @@
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.html import linebreaks
from django.views.decorators.csrf import csrf_exempt
from rest_framework import serializers
from korektury.utils import send_email_notification_komentar
from korektury.models import Oprava, KorekturovanePDF, Komentar, KorekturaTag
from personalni.models import Organizator, Osoba
def korektury_stav_view(request, pdf_id: int, **kwargs):
q = request.POST
pdf = get_object_or_404(KorekturovanePDF, id=pdf_id)
status = q.get('state')
if status is not None:
assert status in KorekturovanePDF.STATUS.values
pdf.status = status
pdf.save()
return JsonResponse({'status': pdf.status})
def oprava_stav_view(request, **kwargs):
q = request.POST
op_id_str = q.get('id')
assert op_id_str is not None
op_id = int(op_id_str)
op = get_object_or_404(Oprava, id=op_id)
status = q.get('action')
if status is not None:
assert status in Oprava.STATUS.values
op.status = status
op.save()
return JsonResponse({'status': op.status})
class KomentarSerializer(serializers.ModelSerializer):
class Meta:
model = Komentar
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["autor"] = str(instance.autor)
ret["text"] = linebreaks(ret["text"], autoescape=True) # Autora není třeba escapovat, ten se vkládá jako text.
return ret
class 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)
komentar.text = text
komentar.autor = autor
komentar.save()
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,
)
tagy_raw = q.get('tagy')
if tagy_raw != "":
tagy = list(map(int, tagy_raw.split(",")))
oprava.tagy.add(*KorekturaTag.objects.filter(id__in=tagy))
Komentar.objects.create(oprava=oprava, autor=autor, text=text)
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,9 +1,5 @@
import os
from colorfield.fields import ColorField
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
@ -55,13 +51,16 @@ class KorekturovanePDF(models.Model):
stran = models.IntegerField(u'počet stran', help_text='Počet stran PDF',
default=0)
class STATUS(models.TextChoices):
PRIDAVANI = 'pridavani', 'Přidávání korektur'
ZANASENI = 'zanaseni', 'Korektury jsou zanášeny'
ZASTARALE = 'zastarale', 'Stará verze, nekorigovat'
status = models.CharField(u'stav PDF',max_length=16, choices=STATUS.choices, blank=False, default = STATUS.PRIDAVANI)
STATUS_PRIDAVANI = 'pridavani'
STATUS_ZANASENI = 'zanaseni'
STATUS_ZASTARALE = 'zastarale'
STATUS_CHOICES = (
(STATUS_PRIDAVANI, u'Přidávání korektur'),
(STATUS_ZANASENI, u'Korektury jsou zanášeny'),
(STATUS_ZASTARALE, u'Stará verze, nekorigovat'),
)
status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False,
default = STATUS_PRIDAVANI)
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.',
@ -130,17 +129,6 @@ class KorekturovanePDF(models.Model):
except IndexError:
return self.nazev
def get_absolute_url(self):
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)
class Oprava(models.Model):
@ -160,22 +148,32 @@ class Oprava(models.Model):
x = models.IntegerField(u'x-ová souřadnice bugu')
y = models.IntegerField(u'y-ová souřadnice bugu')
class STATUS(models.TextChoices):
K_OPRAVE = 'k_oprave', 'K opravě'
OPRAVENO = 'opraveno', 'Opraveno'
NENI_CHYBA = 'neni_chyba', 'Není chyba'
K_ZANESENI = 'k_zaneseni', 'K zanesení do TeXu'
STATUS_K_OPRAVE = 'k_oprave'
STATUS_OPRAVENO = 'opraveno'
STATUS_NENI_CHYBA = 'neni_chyba'
STATUS_K_ZANESENI = 'k_zaneseni'
STATUS_CHOICES = (
(STATUS_K_OPRAVE, u'K opravě'),
(STATUS_OPRAVENO, u'Opraveno'),
(STATUS_NENI_CHYBA, u'Není chyba'),
(STATUS_K_ZANESENI, u'K zanesení do TeXu'),
)
status = models.CharField(u'stav opravy',max_length=16, choices=STATUS_CHOICES, blank=False,
default = STATUS_K_OPRAVE)
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)
informovani_orgove = models.ManyToManyField(
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',
)
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)
@ -201,7 +199,5 @@ class Komentar(models.Model):
def __str__(self):
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

(image error) Size: 1.1 KiB

After

(image error) Size: 697 B

Binary file not shown.

Before

(image error) Size: 1.4 KiB

After

(image error) Size: 717 B

Binary file not shown.

Before

(image error) Size: 307 B

After

(image error) Size: 270 B

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 702 B

Binary file not shown.

Before

(image error) Size: 1.1 KiB

After

(image error) Size: 617 B

View file

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

View file

@ -1,46 +1,283 @@
const W_SKIP = 10;
const H_SKIP = 5;
const POINTER_MIN_H = 30;
function place_comments_one_div(img_id, comments)
{
const img = document.getElementById("img-"+img_id);
if( img == null ) return;
const comments_sorted = comments.sort((a, b) => a.y - b.y);
const par = img.parentNode;
const w = img.clientWidth;
let bott_max = 0;
for (const oprava of comments_sorted) {
const x = oprava.x;
const y = oprava.y;
const htmlElement = oprava.htmlElement;
const pointer = oprava.pointer;
par.appendChild(pointer);
par.appendChild(htmlElement);
const delta_y = (y > bott_max) ? 0: bott_max - y + H_SKIP;
pointer.style.left = x;
pointer.style.top = y;
pointer.style.width = w - x + W_SKIP;
pointer.style.height = POINTER_MIN_H + delta_y;
htmlElement.style.left = w + W_SKIP;
htmlElement.style.top = y + delta_y;
bott_max = Math.max(bott_max, htmlElement.offsetTop + htmlElement.offsetHeight + H_SKIP); // FIXME nemám páru, proč +H_SKIP funguje, ale opravuje to bug, že nově vytvořené korektury za sebou neměly mezeru
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;
if (par.offsetHeight < bott_max) par.style.height = bott_max;
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 (let [img_id, opravy] of Object.entries(comments)) {
place_comments_one_div(img_id, opravy)
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,69 +0,0 @@
{% load static %}
<div id="korektury-sipky">
<button type='button' id="predchozi-korektura" title='Předchozí korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-oprave" title='Předchozí korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<button type='button' id="predchozi-korektura-k-zaneseni" title='Předchozí korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬆'/>
</button>
<br>
<button type='button' id="dalsi-korektura" title='Další korektura'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-oprave" title='Další korektura k opravě'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
<button type='button' id="dalsi-korektura-k-zaneseni" title='Další korektura k zaneseni'>
<img class='toggle-button' src='{% static "korektury/imgs/hide.png" %}' alt='⬇' style="transform: rotate(180deg);"/>
</button>
</div>
<script>
const predchozi_k = document.getElementById('predchozi-korektura');
const dalsi_k = document.getElementById('dalsi-korektura');
const predchozi_k_o = document.getElementById('predchozi-korektura-k-oprave');
const dalsi_k_o = document.getElementById('dalsi-korektura-k-oprave');
const predchozi_k_z = document.getElementById('predchozi-korektura-k-zaneseni');
const dalsi_k_z = document.getElementById('dalsi-korektura-k-zaneseni');
function dalsi_nebo_predchozi_korektura(dalsi=true, stav=null) {
let predchozi = null;
for (let [_, opravy] of Object.entries(comments)) {
for (const oprava of opravy) {
if (stav == null || oprava.status === stav) {
console.log(oprava, oprava.htmlElement.getBoundingClientRect().y);
const y = oprava.htmlElement.getBoundingClientRect().y;
if (y >= -1) {
if (dalsi) {
if (y > 1) {
oprava.htmlElement.scrollIntoView();
return;
}
} else {
console.log(predchozi);
if (predchozi !== null) predchozi.htmlElement.scrollIntoView(); else alert("Výše už není žádná taková korektura.");
return;
}
}
predchozi = oprava;
}
}
}
if (!dalsi && predchozi !== null) {
predchozi.htmlElement.scrollIntoView();
return;
}
alert("Žádná další korektura.");
}
predchozi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false) });
dalsi_k.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true) });
predchozi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_oprave") });
dalsi_k_o.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_oprave") });
predchozi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(false, "k_zaneseni") });
dalsi_k_z.addEventListener('click', _ => { dalsi_nebo_predchozi_korektura(true, "k_zaneseni") });
</script>

View file

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

View file

@ -1,86 +0,0 @@
{% load static %}
<div class='comment' id='prekomentar' {# id='k{{k.id}}' #}>
<div class='corr-header'>
<div class='author'>{# {{k.autor}} #}</div>
<div class='float-right'>
<button type='button' style='display: none' class='del-comment' title='Smaž komentář'>
<img src='{% static "korektury/imgs/delete.png" %}' alt='del'/>
</button>
<button type='button' class='update-comment' title='Uprav komentář'>
<img src='{% static "korektury/imgs/edit.png"%}' alt='edit'/>
</button>
</div>
</div>
<div class='komtext'>{# {{k.text|linebreaks}} #}</div>
</div>
<script>
const prekomentar = document.getElementById('prekomentar');
const komentare = {};
class Komentar {
static update_or_create(komentar_data, oprava) {
const id = komentar_data['id'];
if (id in komentare) komentare[id].update(komentar_data);
else new Komentar(komentar_data, oprava);
}
#autor; #text;
htmlElement;
id; oprava; {# komentar_data; #}
/**
*
* @param komentar_data
* @param {Oprava} oprava
*/
constructor(komentar_data, oprava) {
this.htmlElement = prekomentar.cloneNode(true);
this.#autor = this.htmlElement.getElementsByClassName('author')[0];
this.#text = this.htmlElement.getElementsByClassName('komtext')[0];
this.id = komentar_data['id'];
this.htmlElement.id = 'k' + this.id;
this.oprava = oprava;
this.oprava.add_komentar_htmlElement(this.htmlElement);
this.update(komentar_data);
this.htmlElement.getElementsByClassName('update-comment')[0].addEventListener('click', _ => this.#update_comment());
this.htmlElement.getElementsByClassName('del-comment')[0].addEventListener('click', _ => this.#delete_comment());
komentare[this.id] = this;
}
update(komentar_data) {
{# this.komentar_data = komentar_data; #}
this.set_autor(komentar_data['autor']);
this.set_text(komentar_data['text']);
};
set_autor(autor) {this.#autor.textContent=autor;};
set_text(text) {
this.#text.innerHTML=text;
};
// show comment form when 'update-comment' button pressed
#update_comment() {
return commform.show(this.oprava.img_id, this.oprava.x, this.oprava.y, this.#text.textContent, -1, this.id);
}
#delete_comment() {
if (confirm('Opravdu smazat komentář?')) {
throw {name : 'NotImplementedError', message: '(Webaři jsou) too lazy to implement'};
}
}
}
</script>

View file

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

View file

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

View file

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

View file

@ -1,51 +0,0 @@
Zobrazit:
<input type="checkbox"
id="k_oprave_checkbox"
name="k_oprave_checkbox"
onchange="toggle_corrections('k_oprave')" checked>
<label for="k_oprave_checkbox">K opravě ({{k_oprave_cnt}})</label>
<input type="checkbox"
id="opraveno_checkbox"
name="opraveno_checkbox"
onchange="toggle_corrections('opraveno')" checked>
<label for="opraveno_checkbox">Opraveno ({{opraveno_cnt}})</label>
<input type="checkbox"
id="neni_chyba_checkbox"
name="neni_chyba_checkbox"
onchange="toggle_corrections('neni_chyba')" checked>
<label for="neni_chyba_checkbox">Není chyba ({{neni_chyba_cnt}})</label>
<input type="checkbox"
id="k_zaneseni_checkbox"
name="k_zaneseni_checkbox"
onchange="toggle_corrections('k_zaneseni')" checked>
<label for="k_zaneseni_checkbox">K zanesení ({{k_zaneseni_cnt}})</label>
<hr/>
<script>
function toggle_corrections(aclass)
{
const stylesheets = document.styleSheets;
let ssheet = null;
for (let i=0; i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (let i=0; i<ssheet.cssRules.length; i++){
const rule = ssheet.cssRules[i];
if (rule.selectorText === '[data-opravastatus="'+aclass+'"]'){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
place_comments();
}
</script>

View file

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

View file

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

View file

@ -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

@ -35,27 +35,24 @@ def create_test_pdf(rnd, organizatori):
# TODO silent ghostscript (vypisuje odstavec za každou stránku…)
korekturovane_pdf = KorekturovanePDF.objects.create(
nazev='B', komentar='Neuronové sítě', pdf=gen_filename(filename='B.pdf')
KorekturovanePDF.objects.create(
nazev='B', komentar='Neuronové sítě', org=rnd.choice(organizatori), pdf=gen_filename(filename='B.pdf')
)
korekturovane_pdf.orgove.set((rnd.choice(organizatori),))
korekturovane_pdf = KorekturovanePDF.objects.create(
nazev='A', komentar='M&M: Jak řešit?', pdf=gen_filename(filename='A.pdf')
)
korekturovane_pdf.orgove.set(rnd.sample(organizatori, 2))
KorekturovanePDF.objects.create(
nazev='A', komentar='M&M: Jak řešit?', pdf=gen_filename(filename='A.pdf'),
nazev='A', komentar='M&M: Jak řešit?', org=rnd.choice(organizatori), pdf=gen_filename(filename='A.pdf')
)
korekturovane_pdf = KorekturovanePDF.objects.create(
nazev='A', komentar='M&M: Jak řešit?', org=rnd.choice(organizatori), pdf=gen_filename(filename='A.pdf'),
status='zanaseni'
)
korekturovane_pdf = KorekturovanePDF.objects.create(
nazev='A', komentar='M&M: Jak řešit?', pdf=gen_filename(filename='A.pdf'),
KorekturovanePDF.objects.create(
nazev='A', komentar='M&M: Jak řešit?', org=rnd.choice(organizatori), pdf=gen_filename(filename='A.pdf'),
status='zastarale'
)
korekturovane_pdf.orgove.set((rnd.choice(organizatori),))
except OSError as e:
except (FileNotFoundError, Exception) as e:
# TODO najít správné chyby, které vyhazují různé systémy při neexistenci ImageMagick, nebo knihoven
logger.error(str(e))
logger.error(

View file

@ -1,6 +1,4 @@
from django.urls import path
from django.urls import include
from personalni.utils import org_required
from . import views
@ -9,6 +7,4 @@ urlpatterns = [
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/<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}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
texty = []
for kom in Komentar.objects.filter(oprava=oprava):
texty.append((kom.autor.osoba.plne_jmeno(),kom.text))
optext = "\n\n\n".join([": ".join(t) for t in texty])
text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\
\nodkaz do korekturovátka: {}\n\
\nVaše korekturovátko\n".format(optext, odkaz)
# Prijemci e-mailu
emails = set()
# nalezeni e-mailu na autory komentaru
for 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

@ -2,18 +2,24 @@ from django.shortcuts import get_object_or_404, render
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 .utils import send_email_notification_komentar
from .models import Oprava, Komentar, KorekturovanePDF, Organizator, KorekturaTag
from .models import Oprava,Komentar,KorekturovanePDF, Organizator
from .forms import OpravaForm
import subprocess
import shutil
import os
class KorekturyListView(generic.ListView):
model = KorekturovanePDF
# Nefunguje, filtry se vubec nepouziji
queryset = KorekturovanePDF.objects.annotate(
k_oprave_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.K_OPRAVE)),
opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.OPRAVENO)),
neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.NENI_CHYBA)),
k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status=Oprava.STATUS.K_ZANESENI)),
k_oprave_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_oprave')),
opraveno_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='opraveno')),
neni_chyba_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='neni_chyba')),
k_zaneseni_cnt=Count('oprava',distinct=True,filter=Q(oprava__status='k_zaneseni')),
)
template_name = 'korektury/seznam.html'
@ -52,15 +58,13 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
### Korektury
class KorekturyView(generic.TemplateView):
model = Oprava
template_name = 'korektury/korekturovatko/htmlstrana.html'
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
self.pdf_id = self.kwargs["pdf"]
self.pdf = get_object_or_404(KorekturovanePDF, id=self.pdf_id)
template_name = 'korektury/opraf.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
@ -69,27 +73,50 @@ class KorekturyView(generic.TemplateView):
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:])
op = Oprava(x=x,y=y, strana=strana, pdf=self.pdf)
pdf = KorekturovanePDF.objects.get(id=q.get('pdf'))
op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf)
op.save()
kom = Komentar(oprava=op,autor=autor,text=text)
kom.save()
send_email_notification_komentar(op, autor, request)
self.send_email_notification_komentar(op,autor)
elif (action == 'del'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
for k in Komentar.objects.filter(oprava=op):
k.delete()
op.delete()
elif action in Oprava.STATUS.values:
elif (action == 'update'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = action
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'))
@ -97,7 +124,7 @@ class KorekturyView(generic.TemplateView):
text = q.get('txt')
kom = Komentar(oprava=op,autor=autor,text=text)
kom.save()
send_email_notification_komentar(op, autor, request)
self.send_email_notification_komentar(op,autor)
elif (action == 'update-comment'):
id = int(q.get('id'))
kom = Komentar.objects.get(id=id)
@ -110,23 +137,85 @@ class KorekturyView(generic.TemplateView):
kom = Komentar.objects.get(id=id)
kom.delete()
elif (action == 'set-state'):
status = q.get('state')
assert status in KorekturovanePDF.STATUS.values
self.pdf.status = status
self.pdf.save()
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/korekturovatko/htmlstrana.html', context)
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):
context = super().get_context_data(**kwargs)
context['pdf'] = self.pdf
context['img_prefix'] = self.pdf.get_prefix()
pdf = get_object_or_404(KorekturovanePDF, id=self.kwargs['pdf'])
context['pdf'] = pdf
context['img_prefix'] = pdf.get_prefix()
context['img_path'] = settings.KOREKTURY_IMG_DIR
context['img_indexes'] = range(self.pdf.stran)
opravy = Oprava.objects.filter(pdf=self.pdf_id)
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:
@ -150,7 +239,8 @@ class KorekturyView(generic.TemplateView):
context['opravy'] = opravy
context['zasluhy'] = zasluhy
context['tagy'] = KorekturaTag.objects.all()
return context
def form_valid(self,form):
return super().form_valid(form)

View file

@ -8,3 +8,4 @@ ensure_venv
./manage.py testdata
./manage.py loaddata data/*
#make/sync_prod_flatpages
./manage.py load_org_permissions deploy_v2/admin_org_prava.json

View file

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

View file

@ -4,7 +4,7 @@ set -exuo pipefail
. make/lib.sh
scp vue_frontend/webpack-stats.json "$GIMLI_LOGIN:$TESTWEB/vue_frontend/"
rsync -ave ssh treenode/static/treenode/vue "$GIMLI_LOGIN:$TESTWEB/treenode/static/treenode/"
rsync -ave ssh seminar/static/seminar/vue "$GIMLI_LOGIN:$TESTWEB/seminar/static/seminar/"
ssh "$GIMLI_LOGIN" "
set -euxo pipefail
cd $TESTWEB

View file

@ -5,4 +5,5 @@ set -exuo pipefail
ensure_web_installed
./manage.py graph_models seminar | dot -Tpdf > schema_seminar.pdf
./manage.py graph_models -a -g | dot -Tpdf > schema_all.pdf

View file

@ -7,18 +7,17 @@ import locale
from django.contrib import admin
from django.contrib.admin import AdminSite
from django.contrib.flatpages.models import FlatPage
import logging
# Note: we are renaming the original Admin and Form as we import them!
from django.contrib.flatpages.admin import FlatPageAdmin as FlatPageAdminOld
from django.contrib.flatpages.admin import FlatpageForm as FlatpageFormOld
from django import forms
from django_ckeditor_5.widgets import CKEditor5Widget
from ckeditor_uploader.widgets import CKEditorUploadingWidget
class FlatpageForm(FlatpageFormOld):
content = forms.CharField(widget=CKEditor5Widget())
content = forms.CharField(widget=CKEditorUploadingWidget())
class Meta:
model = FlatPage # this is not automatically inherited from FlatpageFormOld
exclude = []
@ -44,7 +43,7 @@ def get_app_list(self, request, app_label=None):
app_dict = self._build_app_dict(request, label=app_label)
aplikace_nahore = [
'tvorba',
'seminar',
'personalni',
'novinky',
'korektury',
@ -57,13 +56,8 @@ def get_app_list(self, request, app_label=None):
# Sort the models alphabetically within each app.
try: # na macu nefunguje locale.strxfrm :-/ proto je tu try except block
for app in app_list:
app['models'].sort(key=lambda x: locale.strxfrm(x['name'].lower()))
except OSError as e:
# locale.strxfrm nefunguje na macu... :-/ -> neprovede se řazení
logger = logging.getLogger(__name__)
logger.error(e)
for app in app_list:
app['models'].sort(key=lambda x: locale.strxfrm('žž' + x['name'].lower()) if (x['name'].endswith("(Node)")) else locale.strxfrm(x['name'].lower()))
return app_list

View file

@ -57,7 +57,6 @@ DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error'
# Modules configuration
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
@ -111,7 +110,8 @@ INSTALLED_APPS = (
'reversion',
'django_countries',
'solo',
'django_ckeditor_5',
'ckeditor',
'ckeditor_uploader',
'taggit',
'dal',
'dal_select2',
@ -129,15 +129,12 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'colorfield',
# MaMweb
'mamweb',
'seminar',
'tvorba',
'galerie',
'korektury',
'korektury.api',
'prednasky',
'header_fotky',
'various',
@ -189,98 +186,26 @@ SUMMERNOTE_CONFIG = {
]
}
CKEDITOR_5_CUSTOM_CSS = "css/ckeditor5_fix.css"
# customColorPalette = [
# {
# 'color': 'hsl(4, 90%, 58%)',
# 'label': 'Red',
# },
# {
# 'color': 'hsl(340, 82%, 52%)',
# 'label': 'Pink',
# },
# {
# 'color': 'hsl(291, 64%, 42%)',
# 'label': 'Purple',
# },
# {
# 'color': 'hsl(262, 52%, 47%)',
# 'label': 'Deep Purple',
# },
# {
# 'color': 'hsl(231, 48%, 48%)',
# 'label': 'Indigo',
# },
# {
# 'color': 'hsl(207, 90%, 54%)',
# 'label': 'Blue',
# },
# ]
CKEDITOR_5_FILE_STORAGE = "various.storage.UploadStorage"
CKEDITOR_5_CONFIGS = {
CKEDITOR_UPLOAD_PATH = "uploads/"
CKEDITOR_IMAGE_BACKEND = 'pillow'
#CKEDITOR_JQUERY_URL = '//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js'
CKEDITOR_CONFIGS = {
'default': {
'language': 'cs',
'blockToolbar': [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote',
],
'toolbar': ['sourceEditing', '|', 'heading', '|',
# 'outdent', 'indent', '|',
'bold', 'italic', 'link', 'underline', 'strikethrough',
'code',
# 'subscript', 'superscript',
# 'highlight',
'|', 'codeBlock', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|',
# 'blockQuote', '|',
# 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor',
# 'mediaEmbed',
'removeFormat',
# 'insertTable',
],
'image': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
'styles': [
'full',
'side',
'alignLeft',
'alignRight',
'alignCenter',
]
'entities': False,
'toolbar': [
['Source', 'ShowBlocks', '-', 'Maximize'],
['Bold', 'Italic', 'Subscript', 'Superscript', '-', 'RemoveFormat'],
['NumberedList','BulletedList','-','Blockquote','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
['Link', 'Unlink', 'Anchor', '-', 'Image', 'Table', 'HorizontalRule'],
['Format'],
},
# 'table': {
# 'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
# 'tableProperties', 'tableCellProperties' ],
# 'tableProperties': {
# 'borderColors': customColorPalette,
# 'backgroundColors': customColorPalette,
# },
# 'tableCellProperties': {
# 'borderColors': customColorPalette,
# 'backgroundColors': customColorPalette,
# }
# },
'heading' : {
'options': [
{ 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
{ 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
{ 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
{ 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' },
]
},
],
# 'toolbar': 'full',
'height': '40em',
'width': '100%',
'toolbarStartupExpanded': False,
'allowedContent' : True,
},
'list': {
'properties': {
'styles': 'true',
'startIndex': 'true',
'reversed': 'true',
},
}
}
# Webpack loader
@ -354,11 +279,11 @@ LOGGING = {
'filters': ['Http404AsInfo'],
},
'personalni.prihlaska.form':{
'seminar.prihlaska.form':{
'handlers': ['console','registration_logfile'],
'level': 'INFO'
},
'personalni.prihlaska.problem':{
'seminar.prihlaska.problem':{
'handlers': ['console','mail_registration','registration_error_log'],
'level': 'INFO'
},
@ -417,7 +342,6 @@ SEMINAR_KONFERY_DIR = os.path.join('konfery')
KOREKTURY_PDF_DIR = os.path.join('korektury', 'pdf')
KOREKTURY_IMG_DIR = os.path.join('korektury', 'img')
CISLO_IMG_DIR = os.path.join('cislo', 'img')
SOUSTREDENI_KONTAKTNICKY_DIR = os.path.join('soustredeni', 'kontaktnicky')

View file

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

View file

@ -1,3 +0,0 @@
.ck.ck-editor {
color: black !important; /* V tmavém módu zapomene CKEditor přepnout barvu textu. (Bílý text na bílém pozadí je best.) */
}

View file

@ -10,8 +10,6 @@
--orgovska-fialova: #6a0043;
--orgovska-svetla-fialova: #eee4ec;
--resitelska-fialova: #f296b3;
--resitelska-svetla-fialova: #f2E5EF;
--barva-pozadi: #fffbf6;
}

View file

@ -19,10 +19,10 @@ div.kontejner {/* Ne container, aby se to netlouklo s bootstrapem. */
margin-top: var(--login-bar-height);
}
& div.kontent-wrapper {
div.kontent-wrapper {
padding-bottom: var(--footer-height);
& div.kontent {
div.kontent {
padding: 15px 30px;
}
}
@ -67,16 +67,16 @@ div.kontejner {/* Ne container, aby se to netlouklo s bootstrapem. */
background-size: 100%;
top: 58px;
& img.logo {
img.logo {
width: 100%;
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
}
& img.logo-mobile {
img.logo-mobile {
display: none;
}
& .no-mobile {
.no-mobile {
background-size: contain;
}
}
@ -94,12 +94,12 @@ div.kontejner {/* Ne container, aby se to netlouklo s bootstrapem. */
filter: drop-shadow(5px 0px 5px rgba(0, 0, 0, 0.4));
padding-top: 3.5%;
& p.license {
p.license {
text-align: center;
font-weight: 400;
bottom: 0;
& a {
a {
color: #333;
}
}
@ -139,21 +139,21 @@ div.login-bar {
padding-left: 5px;
padding-right: 5px;
& div {
div {
display: inline;
}
& a.LOGIN-ref-admin {
a.LOGIN-ref-admin {
display: inline;
color: var(--barva-pozadi);
}
& .LOGIN_napis-webarum {
.LOGIN_napis-webarum {
display: inline;
color: var(--barva-pozadi);
float: right;
& a {
a {
color: var(--svetla-oranzova);
text-decoration: underline;
}
@ -176,7 +176,7 @@ div.login-bar {
#header {
background-size: 100%;
& img.logo {
img.logo {
width: 100%;
}
}
@ -202,11 +202,11 @@ div.login-bar {
top: 0;
background-image: none;
& img.logo {
img.logo {
display: none;
}
& img.logo-mobile {
img.logo-mobile {
display: block;
top: 0;
left: 0;
@ -215,7 +215,7 @@ div.login-bar {
margin-bottom: 3px;
}
& .no-mobile{
.no-mobile{
display: none;
}
}
@ -241,13 +241,13 @@ ul.menu {
font-variant: small-caps;
& a {
a {
text-decoration: none;
font-weight: bold;
font-size: 105%;
}
& li {
li {
margin: 0;
display: inline-block;
width: 16.666667%;
@ -256,7 +256,7 @@ ul.menu {
font-size: 140%;
font-weight: 400;
&>a:hover, &>a:active {
>a:hover, >a:active {
color: black;
}
@ -265,7 +265,7 @@ ul.menu {
}
}
& ul.submenu {
ul.submenu {
background-color: var(--hlavni-oranzova);
margin-top: 10px; /* mezera mezi hlavním menu a submenu */
@ -277,24 +277,24 @@ ul.menu {
z-index: 50;
font-weight: 400;
& li {
li {
width: auto;
padding: 0 20px 0 20px;
display: inline-block;
&>a {
>a {
color: var(--svetla-oranzova);
text-decoration: none;
text-shadow: none;
&:hover {
:hover {
color: black;
}
}
}
}
& ul.submenu li.active>a, & .parentactive ul li:first-child>a {
ul.submenu li.active>a, .parentactive ul li:first-child>a {
color: black;
}
}
@ -304,7 +304,7 @@ ul.menu {
font-size: 90%;
margin-top: -7px;
& li {
li {
margin-top: 10px; /* posunutí textu hlavního menu níže */
}
}
@ -312,7 +312,7 @@ ul.menu {
ul.submenu {
margin-top: 8px; /* mezera mezi hlavním menu a submenu */
& li {
li {
margin-top: 0; /* aby se spolu s textem hlavního menu neposunoval níže i text submenu */
}
}
@ -323,7 +323,7 @@ ul.menu {
font-size: 80%;
margin-top: -2px;
& li {
li {
margin-top: 10px; /* posunutí textu hlavního menu níže */
}
}
@ -331,7 +331,7 @@ ul.menu {
ul.submenu {
margin-top: 8px; /* mezera mezi hlavním menu a submenu */
& li {
li {
margin-top: 0; /* aby se spolu s textem hlavního menu neposunoval níže i text submenu */
}
}
@ -354,20 +354,20 @@ ul.menu {
padding-bottom: 3px;
padding-left: 12px;
& a {
a {
&:active, &:hover, &:focus {
text-decoration: none;
color: black;
}
}
& ul {
ul {
list-style-type: none;
font-size: 90%;
color: black; /*černé šipky submenu*/
& li {
&>a {
li {
> a {
color: black;
}
@ -377,7 +377,7 @@ ul.menu {
}
}
& br {
br {
display: none;
}
}
@ -435,7 +435,6 @@ body.localweb, body.testweb, body.suprodweb {
height: 100%;
top: 0;
z-index: -1000;
opacity: 0.7;
}
&:before { left: 0; }
@ -460,13 +459,13 @@ body.suprodweb { &:before, &:after { background: red; } }
display: block;
}
& h1 { text-align: center; }
h1 { text-align: center; }
& .TITULNI_STRANA_zjistit_vic{
.TITULNI_STRANA_zjistit_vic{
text-align: center;
margin-bottom: 30px;
& hr {
hr {
display: none;
@media(max-width: 800px){
@ -475,15 +474,15 @@ body.suprodweb { &:before, &:after { background: red; } }
}
}
& .TITULNI_STRANA_graf {
.TITULNI_STRANA_graf {
@media(max-width: 800px) {
padding-top: 40px;
}
& .TITULNI_STRANA_graf-svg {
.TITULNI_STRANA_graf-svg {
display: flex;
& #svg-graf {
#svg-graf {
width: 100%;
height: auto;
margin: 30px;
@ -497,7 +496,7 @@ body.suprodweb { &:before, &:after { background: red; } }
}
}
& .TITULNI_STRANA_obsah {
.TITULNI_STRANA_obsah {
width: 66%;
@media(max-width: 800px){
@ -505,7 +504,7 @@ body.suprodweb { &:before, &:after { background: red; } }
}
}
& .TITULNI_STRANA_vitej_titulka, & .TITULNI_STRANA_temata_titulka {
.TITULNI_STRANA_vitej_titulka, .TITULNI_STRANA_temata_titulka {
width: 49%;
padding: 10px;
display: table-cell;
@ -516,7 +515,7 @@ body.suprodweb { &:before, &:after { background: red; } }
}
}
& .TITULNI_STRANA_novinky {
.TITULNI_STRANA_novinky {
width: 33%;
padding: 10px;
@ -541,11 +540,11 @@ div.odpocet {
.stranka_aktualni_zadani {
text-align: center;
& #AKTUALNI_ZADADNI_obrazek {
#AKTUALNI_ZADADNI_obrazek {
margin-top: 15px;
}
& div.AKTUALNI_ZADANI_termin {
div.AKTUALNI_ZADANI_termin {
text-align: center;
font-size: large;
font-weight: bold;
@ -554,7 +553,7 @@ div.odpocet {
font-size: small;
}
& .AKTUALNI_ZADANI_datum {
.AKTUALNI_ZADANI_datum {
color: var(--hlavni-oranzova);
margin: 0;
}

View file

@ -1,731 +0,0 @@
@charset "utf-8"; /* vynuť utf-8 */
@supports (-webkit-touch-callout: none) and (not (offset-position: normal)) {
.button {
margin: 10px 0 10px 0;
padding: 4px 0; /*vertikální centování textu*/
text-align: center;
background-color: var(--hlavni-oranzova);
color: var(--barva-pozadi);
font-size: 150%;
font-weight: bold;
font-variant: small-caps;
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
}
.button:hover {
position: relative;
top: 2px;
left: 2px;
background-color: #df490e;
}
/******************/
/* Rozložení webu a jeho prvky (hlavička, menu, footer) */
/**** KONTEJNER ****/
div.kontejner { /* Ne container, aby se to netlouklo s bootstrapem. */
width: 970px;
margin: auto;
min-height: 100vh;
position: relative;
padding: 0;
}
.org-logged-in div.kontejner {
margin-top: var(--login-bar-height);
}
div.kontejner div.kontent-wrapper {
padding-bottom: var(--footer-height);
}
div.kontejner div.kontent-wrapper div.kontent {
padding: 15px 30px;
}
/* Roztáhne obsah z containeru na celou šířku obrazovky: */
.full_width {
width: 100vw;
margin-left: calc(-50vw + 485px);
}
/* Na úzkém displeji nechceme nic dělat. */
@media(max-width: 860px) {
.full_width {
margin-left: 0;
width: unset;
}
}
/*******************/
/**** HLAVIČKA ****/
#header {
position: relative;
background: url("../images/header/vikendovka.jpg") no-repeat center top; /* poměr 350:970, TODO: aby to nemuselo být přesně na pixely */
background-size: 100%;
top: 58px;
}
#header img.logo {
width: 100%;
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
}
#header img.logo-mobile {
display: none;
}
#header .no-mobile {
background-size: contain;
}
/******************/
/**** Footer ****/
#footer {
position: absolute;
bottom: 0;
width: 100%;
background: url("../images/mozaika-footer.svg") no-repeat top center;
height: var(--footer-height);
background-size: 100%;
filter: drop-shadow(5px 0px 5px rgba(0, 0, 0, 0.4));
padding-top: 3.5%;
}
#footer p.license {
text-align: center;
font-weight: 400;
bottom: 0;
}
#footer p.license a {
color: #333;
}
@media (max-width: 650px) {
#footer {
display: none;
}
}
p.license-mobile {
display: none;
margin-bottom: 0;
}
@media (max-width: 650px) {
p.license-mobile {
position: relative;
display: block;
font-size: 90%;
background-color: var(--hlavni-oranzova);
padding: 5%;
text-align: justify;
}
}
/****************/
/**** LOGIN BAR ****/
div.login-bar {
background: var(--orgovska-fialova);
color: var(--svetla-oranzova);
width: 100%;
position: fixed;
margin-top: calc(-1 * var(--login-bar-height));
min-height: var(--login-bar-height);
z-index: 4086;
padding-left: 5px;
padding-right: 5px;
}
div.login-bar div {
display: inline;
}
div.login-bar a.LOGIN-ref-admin {
display: inline;
color: var(--barva-pozadi);
}
div.login-bar .LOGIN_napis-webarum {
display: inline;
color: var(--barva-pozadi);
float: right;
}
div.login-bar .LOGIN_napis-webarum a {
color: var(--svetla-oranzova);
text-decoration: underline;
}
/*******************/
/* stránka přes celý displej */
@media (max-width: 970px) {
#header {
background-size: 100%;
}
#header img.logo {
width: 100%;
}
}
/* malý tablet, mobil */
@media (max-width: 650px) {
#header {
width: 100%;
top: 0;
background-image: none;
}
#header img.logo {
display: none;
}
#header img.logo-mobile {
display: block;
top: 0;
left: 0;
width: 100%;
filter: drop-shadow(0px 0 5px rgba(0, 0, 0, 0.4));
margin-bottom: 3px;
}
#header .no-mobile{
display: none;
}
}
/**** MENU ****/
ul.menu {
width: 100%;
padding: 0;
margin-top: -5px; /* posune celé menu nahoru (pak potřeba zvětšit mezeru mezi menu a submenu) */
font-variant: small-caps;
}
ul.menu a {
text-decoration: none;
font-weight: bold;
font-size: 105%;
}
ul.menu li {
margin: 0;
display: inline-block;
width: 16.666667%;
text-align: center;
font-size: 140%;
font-weight: 400;
}
ul.menu li >a:hover, >a:active {
color: black;
}
ul.menu li.active>a {
color: var(--svetla-oranzova);
}
ul.menu ul.submenu {
background-color: var(--hlavni-oranzova);
margin-top: 10px; /* mezera mezi hlavním menu a submenu */
margin-bottom: 10px;
padding-top: 10px;
padding-bottom: 5px;
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
z-index: 50;
font-weight: 400;
}
ul.menu ul.submenu li {
width: auto;
padding: 0 20px 0 20px;
display: inline-block;
}
ul.menu ul.submenu li >a {
color: var(--svetla-oranzova);
text-decoration: none;
text-shadow: none;
}
ul.menu ul.submenu li >a :hover {
color: black;
}
ul.menu ul.submenu li.active>a, .parentactive ul li:first-child>a {
color: black;
}
@media (max-width: 970px) {
ul.menu {
font-size: 90%;
margin-top: -7px;
}
ul.menu li {
margin-top: 10px; /* posunutí textu hlavního menu níže */
}
ul.submenu {
margin-top: 8px; /* mezera mezi hlavním menu a submenu */
}
ul.submenu li {
margin-top: 0; /* aby se spolu s textem hlavního menu neposunoval níže i text submenu */
}
}
@media(max-width: 800px) {
ul.menu {
font-size: 80%;
margin-top: -2px;
}
ul.menu li {
margin-top: 10px; /* posunutí textu hlavního menu níže */
}
ul.submenu {
margin-top: 8px; /* mezera mezi hlavním menu a submenu */
}
ul.submenu li {
margin-top: 0; /* aby se spolu s textem hlavního menu neposunoval níže i text submenu */
}
}
@media (max-width: 650px) {
ul.menu {
display: none;
}
ul.menu_mobile {
display: block;
z-index: 10;
position: sticky;
font-variant: small-caps;
font-size: 150%;
font-weight: bold;
list-style-type: none;
padding-top: 3px;
padding-bottom: 3px;
padding-left: 12px;
}
ul.menu_mobile a:active, ul.menu_mobile a:hover, ul.menu_mobile a:focus {
text-decoration: none;
color: black;
}
ul.menu_mobile ul {
list-style-type: none;
font-size: 90%;
color: black; /*černé šipky submenu*/
}
ul.menu_mobile ul li > a {
color: black;
}
ul.menu_mobile ul li::before {
content: ' \276D '; /*https://www.w3schools.com/cssref/css_entities.asp*/
}
ul.menu_mobile br {
display: none;
}
} /* konec @media */
/**************/
/**** ZBYTEK ****/
/* (konkrétní stránky) */
/* Titulní stránka */
.titulnistrana {
display: flex;
text-align: justify;
@media(max-width: 800px){
.titulnistrana {
display: block;
}
}
.titulnistrana h1 { text-align: center; }
.titulnistrana .TITULNI_STRANA_zjistit_vic {
text-align: center;
margin-bottom: 30px;
}
.titulnistrana .TITULNI_STRANA_zjistit_vic hr {
display: none;
}
@media(max-width: 800px){
.titulnistrana .TITULNI_STRANA_zjistit_vic hr {
display: flex;
}
}
@media(max-width: 800px) {
.titulnistrana .TITULNI_STRANA_graf {
padding-top: 40px;
}
}
.titulnistrana .TITULNI_STRANA_graf .TITULNI_STRANA_graf-svg {
display: flex;
}
.titulnistrana .TITULNI_STRANA_graf .TITULNI_STRANA_graf-svg #svg-graf {
width: 100%;
height: auto;
margin: 30px;
}
@media(max-width: 800px){
.titulnistrana .TITULNI_STRANA_graf .TITULNI_STRANA_graf-svg #svg-graf {
max-width: 500px;
padding: 10px;
margin: auto;
}
}
.titulnistrana .TITULNI_STRANA_obsah {
width: 66%;
}
@media(max-width: 800px){
.titulnistrana .TITULNI_STRANA_obsah {
width: 100%;
}
}
.titulnistrana .TITULNI_STRANA_vitej_titulka, .TITULNI_STRANA_temata_titulka {
width: 49%;
padding: 10px;
display: table-cell;
}
@media (max-width: 650px) {
.titulnistrana .TITULNI_STRANA_vitej_titulka, .TITULNI_STRANA_temata_titulka {
width: 100%;
display: block;
}
}
.titulnistrana .TITULNI_STRANA_novinky {
width: 33%;
padding: 10px;
}
@media(max-width: 800px){
.titulnistrana .TITULNI_STRANA_novinky {
width: 100%;
max-width: 500px;
margin: auto;
}
}
/* Stránky Aktuální ročník */
.stranka_aktualni_zadani {
text-align: center;
}
.stranka_aktualni_zadani #AKTUALNI_ZADADNI_obrazek {
margin-top: 15px;
}
.stranka_aktualni_zadani div.AKTUALNI_ZADANI_termin {
text-align: center;
font-size: large;
font-weight: bold;
}
@media (max-width: 420px) {
.stranka_aktualni_zadani div.AKTUALNI_ZADANI_termin {
font-size: small;
}
}
.stranka_aktualni_zadani .AKTUALNI_ZADANI_datum {
color: var(--hlavni-oranzova);
margin: 0;
}
}
/* Stránka Jak řešit */
.jakresit svg {
width: 33%;
padding: 10px;
filter: none;
}
@media(max-width: 860px) {
.jakresit svg {
margin: auto;
display: grid;
width: 100%;
max-width: 360px;
}
}
/**** OZNAČENÍ NE-PUBLIC ČÁSTÍ ****/
.mam-org-only {
background: var(--orgovska-svetla-fialova);
padding: 10px;
margin: 10px -10px;
border: var(--orgovska-fialova) 2px dashed;
}
.mam-org-only .mam-org-only {
border: 0;
}
.mam-org-only li {
padding: 3px 0;
margin: -2px 0;
}
/**********************************/
/**** OTÁČECÍ KARTY ****/
/* (orgové, archiv) */
.flip-card {
perspective: 1000px; /* Remove this if you don't want the 3D effect */
margin-left: auto;
margin-right: auto;
}
/* This container is needed to position the front and back side */
.flip-card .flip-card-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.8s;
transform-style: preserve-3d;
}
/* Do an horizontal flip when you move the mouse over the flip box container */
.flip-card:hover .flip-card-inner {
transform: rotateY(180deg);
}
/* Position the front and back side */
.flip-card .flip-card-front, .flip-card-back {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden; /* Safari */
backface-visibility: hidden;
}
.flip-card div.flip-card-foto, div.flip-card-foto img {
width: 100%;
height: 100%;
/* Pokud je na přední straně něco proklikávacího (třeba celá fotka), tak na dotykových zařízeních nemůže proklikávat, aby se dalo otáčet */
@media(hover: none) {
.flip-card div.flip-card-foto, div.flip-card-foto img a { pointer-events: none; }
}
/* Style the back side */
.flip-card-back {
transform: rotateY(180deg);
padding: 10px;
padding-top: 20px;
}
}
/***********************/
/**** TABULKY ****/
/** Tabulka s čárami mezi sloupci **/
/* Např. výsledkovky */
.tabulka_oramovane_sloupce {
border: solid 2px;
}
.tabulka_oramovane_sloupce td:first-child, .tabulka_oramovane_sloupce th:first-child {
border-left: none;
border-right: solid 1px;
}
.tabulka_oramovane_sloupce td:nth-child(2), .tabulka_oramovane_sloupce th:nth-child(2) {
border-left: none;
}
.tabulka_oramovane_sloupce td, .tabulka_oramovane_sloupce th {
padding: 0.1em 0.3em;
border-left: solid 1px;
}
.tabulka_oramovane_sloupce thead th, .tabulka_oramovane_sloupce thead td {
border-bottom: solid 1px;
}
/***********************************/
/** Tabulka se střídajícími se barvami řádků **/
/* Skoro jakákoliv tabulka kromě výsledkovek */
.barevna_tabulka td th {
padding: 1px 10px 1px 10px;
}
.barevna_tabulka tbody tr:nth-child(even), thead tr {
background: var(--svetlounka-oranzova);
}
.barevna_tabulka tbody tr:nth-child(odd) {
background: var(--barva-pozadi);
}
/**********************************************/
/** Tabulka, kde první řádek a sloupec je pořád vidět **/
/* Např. tabulka odevzdaných řešení, nebo výsledkovky */
.tabulka_s_uchycenym_radkem_a_sloupcem {
/* Omezí výšku a šířku, aby bylo příjemné na scrollování a zapne scrollování */
display: block;
width: fit-content; /* display: block; roztahuje na celou šířku */
max-height: 80vh;
overflow: auto;
max-width: 90%; /* (FIXME asi není potřeba u tabulek, co nejsou na celou obrazovku) */
margin-left: 5%; /* Vystředování (FIXME není potřeba u tabulek, co nejsou na celou obrazovku) */
border-collapse: separate; /* Pokud má tabulka orámování, je potřeba ho separovat, aby dodrželo position: sticky; */
border-spacing: 0;
}
/* Uchytí první řádek */
.tabulka_s_uchycenym_radkem_a_sloupcem thead tr {
position: sticky;
top: 0;
z-index: 2;
}
/* Uchytí první sloupec */
.tabulka_s_uchycenym_radkem_a_sloupcem td:first-child, .tabulka_s_uchycenym_radkem_a_sloupcem th:first-child {
position: sticky;
left: 0;
background: inherit; /* (Snad) zneprůhlední první sloupec */
z-index: 1;
}
/*******************************************************/
/** Tabulka mající všechna ohraničení **/
.plne_ohranicena_tabulka {
border-collapse: collapse;
}
.plne_ohranicena_tabulka tr th, .plne_ohranicena_tabulka tr td {
border: 1px solid black;
padding: 1px 10px 1px 10px;
}
/***************************************/
/** Výsledkovky **/
.vysledkovka, .tabulka_oramovane_sloupce td:first-child, .vysledkovka, .tabulka_oramovane_sloupce th:first-child { position: unset; }
.vysledkovka, .tabulka_oramovane_sloupce td:nth-child(2), .vysledkovka, .tabulka_oramovane_sloupce th:nth-child(2) {
border-right: solid 1px;
position: sticky;
left: 0;
background: inherit; /* (Snad) zneprůhlední druhý sloupec */
z-index: 1;
}
.vysledkovka, .tabulka_oramovane_sloupce td:nth-child(3), .vysledkovka, .tabulka_oramovane_sloupce th:nth-child(3) {
border-left: none;
}
/*****************/
/** Tabulka mých (řešitelových) řešení **/
.moje_reseni tr th, .moje_reseni tr td {
text-align: center;
}
.moje_reseni tr td.problem { text-align: left; }
/****************************************/
/** Detail řešení **/
.bodovani>input {
width: 4em;
}
.bodovani>input::placeholder {
color: lightgray;
opacity: 1;
}
.bodovani>input::-webkit-input-placeholder { /* Edge */
color: lightgray;
}
/*******************/
/*****************/
.novinka .novinka_obrazek {
margin: 10px 0 10px 0;
width: 100%;
}
.novinka .novinka_datum {
font-weight: bold;
}
.novinka .novinka_autor {
text-align: right;
font-style: italic;
}
/**** FORMULÁŘE ****/
div.gdpr {
font-size: 6pt;
}
div.gdpr p {
font-size: 6pt;
margin-bottom: .66em;
}
/*******************/
}

View file

@ -17,30 +17,10 @@
border: var(--orgovska-fialova) 2px dashed;
& .mam-org-only {
/* Vnitřní rámečky mají být taky vidět */
border-width: 1px;
background-color: rgba(0, 0, 0, 0.06);
border: 0;
}
& li {
padding: 3px 0;
margin: -2px 0;
}
}
.mam-resitel-only {
background: var(--resitelska-svetla-fialova);
padding: 10px;
margin: 10px -10px;
border: var(--resitelska-fialova) 2px dashed;
& .mam-resitel-only {
/* Vnitřní rámečky mají být taky vidět */
border-width: 1px;
background-color: rgba(0, 0, 0, 0.06);
}
& li {
&li {
padding: 3px 0;
margin: -2px 0;
}
@ -77,7 +57,7 @@
margin-right: auto;
/* This container is needed to position the front and back side */
& .flip-card-inner {
.flip-card-inner {
position: relative;
width: 100%;
height: 100%;
@ -91,7 +71,7 @@
}
/* Position the front and back side */
& .flip-card-front, & .flip-card-back {
.flip-card-front, .flip-card-back {
position: absolute;
width: 100%;
height: 100%;
@ -99,18 +79,18 @@
backface-visibility: hidden;
}
& div.flip-card-foto, & div.flip-card-foto img {
div.flip-card-foto, div.flip-card-foto img {
width: 100%;
height: 100%;
/* Pokud je na přední straně něco proklikávacího (třeba celá fotka), tak na dotykových zařízeních nemůže proklikávat, aby se dalo otáčet */
@media(hover: none) {
& a { pointer-events: none; }
a { pointer-events: none; }
}
}
/* Style the back side */
& .flip-card-back {
.flip-card-back {
transform: rotateY(180deg);
padding: 10px;
padding-top: 20px;
@ -223,7 +203,7 @@ div.org_email {
.tabulka_oramovane_sloupce {
border: solid 2px;
& td, & th {
td, th {
&:first-child, &:first-child {
border-left: none;
border-right: solid 1px;
@ -237,7 +217,7 @@ div.org_email {
border-left: solid 1px;
}
& thead { & th, & td {
thead { th, td {
border-bottom: solid 1px;
} }
}
@ -248,15 +228,15 @@ div.org_email {
/* Skoro jakákoliv tabulka kromě výsledkovek */
.barevna_tabulka {
& td th {
td th {
padding: 1px 10px 1px 10px;
}
& tbody tr:nth-child(even), & thead tr {
tbody tr:nth-child(even), thead tr {
background: var(--svetlounka-oranzova);
}
& tbody tr:nth-child(odd) {
tbody tr:nth-child(odd) {
background: var(--barva-pozadi);
}
}
@ -278,14 +258,14 @@ div.org_email {
border-spacing: 0;
/* Uchytí první řádek */
& thead tr {
thead tr {
position: sticky;
top: 0;
z-index: 2;
}
/* Uchytí první sloupec */
& td, & th { &:first-child {
td, th { &:first-child {
position: sticky;
left: 0;
background: inherit; /* (Snad) zneprůhlední první sloupec */
@ -310,7 +290,7 @@ div.org_email {
.plne_ohranicena_tabulka {
border-collapse: collapse;
& tr { & th, & td {
tr { th, td {
border: 1px solid black;
padding: 1px 10px 1px 10px;
} }
@ -325,7 +305,7 @@ div.org_email {
/** Výsledkovky **/
.vysledkovka, .tabulka_oramovane_sloupce {
& td, & th {
td, th {
&:first-child { position: unset; }
&:nth-child(2) {
@ -347,11 +327,11 @@ div.org_email {
/** Tabulka mých (řešitelových) řešení **/
.moje_reseni tr {
& th, & td {
th, td {
text-align: center;
}
& td.problem { text-align: left; }
td.problem { text-align: left; }
}
/* Různá šířka problému */
@ -389,7 +369,7 @@ div.org_email {
}
/* td obsahující křížek v detailu řešení se nesmí smrštit na 0 */
td:has(.smazat_hodnoceni) {
.td:has(.smazat_hodnoceni) {
min-width: 20px;
padding: 3px;
}
@ -398,16 +378,16 @@ td:has(.smazat_hodnoceni) {
.novinka {
& .novinka_obrazek {
.novinka_obrazek {
margin: 10px 0 10px 0;
width: 100%;
}
& .novinka_datum {
.novinka_datum {
font-weight: bold;
}
& .novinka_autor {
.novinka_autor {
text-align: right;
font-style: italic;
}
@ -433,6 +413,7 @@ table#reseni.form td, table#reseni.form tr {
}
@media(max-width: 800px) {
table#reseni.form td, table#reseni.form tr {
display: inline-grid;
max-width: 300px;
@ -492,7 +473,7 @@ ul.form li{
div.gdpr {
font-size: 6pt;
& p {
p {
font-size: 6pt;
margin-bottom: .66em;
}
@ -503,10 +484,5 @@ label[for=id_skola] {
font-weight: bold;
}
/* Přednášky */
.textznalosti, .textprednasky {
font-style: italic;
}
/*******************/

View file

@ -1,20 +0,0 @@
/**** ROZLIŠENÍ MEZI LOKÁLNÍM, TESTOVACÍM A PRODUKČNÍM WEBEM ****/
body.localweb, body.testweb, body.suprodweb {
&:before, &:after {
content: "";
position: fixed;
width: 20px;
height: 100%;
top: 0;
z-index: -1000;
opacity: 0.7;
}
&: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

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

View file

@ -8,12 +8,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon">
{% block custom_css %}{% endblock %}
<link href="{% static 'bootstrap/css/bootstrap.css' %}?version=3" rel="stylesheet">
<link href="{% static 'css/constants.css' %}?version=3" rel="stylesheet">
<link href="{% static 'css/mamweb_legacy.css' %}?version=3" rel="stylesheet">
<link href="{% static 'css/base.css' %}?version=3" rel="stylesheet">
<link href="{% static 'css/layout.css' %}?version=3" rel="stylesheet">
<link href="{% static 'css/modules.css' %}?version=3" rel="stylesheet">
<link href="{% static 'bootstrap/css/bootstrap.css' %}?version=2" rel="stylesheet">
<link href="{% static 'css/constants.css' %}?version=2" rel="stylesheet">
<link href="{% static 'css/base.css' %}?version=2" rel="stylesheet">
<link href="{% static 'css/layout.css' %}?version=2" rel="stylesheet">
<link href="{% static 'css/modules.css' %}?version=2" rel="stylesheet">
<script src="{% static 'js/jquery-1.11.1.js' %}"></script>
<script src="{% static 'js/jquery-3.4.1.js' %}"></script>

View file

@ -15,7 +15,7 @@ urlpatterns = [
# Admin a nastroje
path('admin/', admin.site.urls), # NOQA
path("ckeditor5/", include('django_ckeditor_5.urls')),
path('ckeditor/', include('ckeditor_uploader.urls')),
# Tvorba = ročníky, čísla, problémy atd. (ma vlastni podadresare)
path('', include('tvorba.urls')),

View file

@ -1,15 +0,0 @@
# Tento soubor slouží pouze pro shell a podobné. Nikde neimportovat v kódu!
print("Naimportoval jsi `mamweb.vsechno`. Pevně věřím, že to nebylo nikde v kódu. Díky.")
from galerie.models import *
from header_fotky.models import *
from korektury.models import *
from novinky.models import *
from odevzdavatko.models import *
from personalni.models import *
from prednasky.models import *
from soustredeni.models import *
from treenode.models import *
from tvorba.models import *
from various.models import *

View file

@ -1,20 +1,5 @@
import django.forms
from django.contrib import admin
from django_ckeditor_5.widgets import CKEditor5Widget
from .models import Novinky
class NovinkyAdminForm(django.forms.ModelForm):
class Meta:
model = Novinky
widgets = {
'text': CKEditor5Widget,
}
fields = '__all__'
@admin.register(Novinky)
class NovinkyAdmin(admin.ModelAdmin):
form = NovinkyAdminForm
admin.site.register(Novinky)

View file

@ -4,7 +4,7 @@
{% if not novinka.zverejneno and user.je_org %}
<div class="mam-org-only">
<ul>
<li><a href="{% url 'admin:novinky_novinky_change' novinka.pk %}">Upravit novinku</a>
<li><a href="/admin/seminar/novinky/{{novinka.pk}}">Upravit novinku</a>
</ul>
{% endif %}
{% if novinka.zverejneno or user.je_org %}

View file

@ -1,6 +1,6 @@
from django.contrib import admin
from django_reverse_admin import ReverseModelAdmin
import odevzdavatko.models as m
import seminar.models as m
class PrilohaReseniInline(admin.TabularInline):

View file

@ -4,11 +4,8 @@ from django.forms import formset_factory
from django.forms.models import inlineformset_factory
from django.utils import timezone
from personalni.models import Resitel
from tvorba.models import Problem, Deadline
from various.models import Nastaveni
from odevzdavatko.models import Reseni, PrilohaReseni, Hodnoceni
from seminar.models import Resitel
import seminar.models as m
import logging
@ -25,7 +22,7 @@ class DateInput(forms.DateInput):
class PosliReseniForm(forms.Form):
problem = forms.ModelMultipleChoiceField(
queryset=Problem.objects.all(),
queryset=m.Problem.objects.all(),
label="Problémy",
widget=autocomplete.ModelSelect2Multiple(
url='autocomplete_problem',
@ -61,7 +58,7 @@ class PosliReseniForm(forms.Form):
#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
forma = forms.ChoiceField(label="Forma řešení",choices = Reseni.FORMA_CHOICES)
forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES)
#forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
# default=FORMA_EMAIL)
@ -72,7 +69,7 @@ class PosliReseniForm(forms.Form):
class NahrajReseniForm(forms.ModelForm):
class Meta:
model = Reseni
model = m.Reseni
fields = ('problem', 'resitele')
help_texts = {'problem':''} # Nezobrazovat help text ve formuláři
@ -112,11 +109,11 @@ class NahrajReseniForm(forms.ModelForm):
def clean_problem(self):
problem = self.cleaned_data.get('problem')
for p in problem:
if p.stav != Problem.STAV_ZADANY:
if p.stav != m.Problem.STAV_ZADANY:
raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!")
return problem
ReseniSPrilohamiFormSet = inlineformset_factory(Reseni, PrilohaReseni,
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm,
fields = ('soubor','res_poznamka'),
widgets = {'res_poznamka':forms.TextInput()},
@ -128,7 +125,7 @@ ReseniSPrilohamiFormSet = inlineformset_factory(Reseni, PrilohaReseni,
class JednoHodnoceniForm(forms.ModelForm):
class Meta:
model = Hodnoceni
model = m.Hodnoceni
fields = ('problem', 'body', 'deadline_body', 'feedback',)
widgets = {
'problem': autocomplete.ModelSelect2(
@ -161,7 +158,7 @@ OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
class PoznamkaReseniForm(forms.ModelForm):
class Meta:
model = Reseni
model = m.Reseni
fields = ('poznamka',)
# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat
@ -201,7 +198,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
from django.db.utils import OperationalError
try:
aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
except OperationalError:
# django.db.utils.OperationalError: no such table: seminar_nastaveni
# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál
@ -217,7 +214,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
result.append(("0001-01-01", f"Odjakživa"))
for deadline in Deadline.objects.filter(
for deadline in m.Deadline.objects.filter(
deadline__lte=timezone.now(),
cislo__rocnik=aktualni_rocnik
).order_by("deadline"):

View file

@ -1,99 +0,0 @@
# Generated by Django 4.2.13 on 2024-10-22 22:51
from django.db import migrations, models
import django.utils.timezone
import odevzdavatko.models
def nastav_nove_contenttypes(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
for m in ('reseni', 'hodnoceni', 'reseni_resitele', 'prilohareseni'):
ContentType.objects.filter(app_label='seminar', model=m).update(app_label='odevzdavatko')
def nastav_stare_contenttypes(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
for m in ('reseni', 'hodnoceni', 'reseni_resitele', 'prilohareseni'):
ContentType.objects.filter(app_label='odevzdavatko', model=m).update(app_label='seminar')
class Migration(migrations.Migration):
initial = True
dependencies = [
('seminar', '0132_unmanage_odevzdavatko'),
]
operations = [
migrations.CreateModel(
name='Hodnoceni',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('body', models.DecimalField(blank=True, decimal_places=1, max_digits=8, null=True, verbose_name='body')),
('feedback', models.TextField(blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)', verbose_name='zpětná vazba')),
('cislo_body', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.cislo', verbose_name='číslo pro body')),
('deadline_body', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.deadline', verbose_name='deadline pro body')),
('problem', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='seminar.problem', verbose_name='problém')),
('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='odevzdavatko.reseni', verbose_name='řešení')),
],
options={
'verbose_name': 'Hodnocení',
'verbose_name_plural': 'Hodnocení',
'db_table': 'seminar_hodnoceni',
'managed': False,
},
),
migrations.CreateModel(
name='PrilohaReseni',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('vytvoreno', models.DateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='vytvořeno')),
('soubor', models.FileField(upload_to=odevzdavatko.models.generate_filename, verbose_name='soubor')),
('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu', verbose_name='neveřejná poznámka')),
('res_poznamka', models.TextField(blank=True, help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje', verbose_name='poznámka řešitele')),
('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prilohy', to='odevzdavatko.reseni', verbose_name='řešení')),
],
options={
'verbose_name': 'Příloha řešení',
'verbose_name_plural': 'Přílohy řešení',
'db_table': 'seminar_priloha_reseni',
'ordering': ['reseni', 'vytvoreno'],
'managed': False,
},
),
migrations.CreateModel(
name='Reseni',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('cas_doruceni', models.DateTimeField(blank=True, default=django.utils.timezone.now, verbose_name='čas_doručení')),
('forma', models.CharField(choices=[('papir', 'Papírové řešení'), ('email', 'Emailem'), ('upload', 'Upload přes web')], default='email', max_length=16, verbose_name='forma řešení')),
('poznamka', models.TextField(blank=True, help_text='Neveřejná poznámka k řešení (plain text)', verbose_name='neveřejná poznámka')),
('zverejneno', models.BooleanField(default=False, help_text='Udává, zda je řešení zveřejněno', verbose_name='řešení zveřejněno')),
('problem', models.ManyToManyField(help_text='Problém', through='odevzdavatko.Hodnoceni', to='seminar.problem', verbose_name='problém')),
('resitele', models.ManyToManyField(help_text='Seznam autorů řešení', through='odevzdavatko.Reseni_Resitele', to='personalni.resitel', verbose_name='autoři řešení')),
('text_cely', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reseni_cely_set', to='seminar.reseninode', verbose_name='Plná verze textu řešení')),
],
options={
'verbose_name': 'Řešení',
'verbose_name_plural': 'Řešení',
'db_table': 'seminar_reseni',
'ordering': ['-cas_doruceni'],
'managed': False,
},
),
migrations.CreateModel(
name='Reseni_Resitele',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('reseni', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='odevzdavatko.reseni', verbose_name='řešení')),
('resitele', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='personalni.resitel', verbose_name='řešitel')),
],
options={
'verbose_name': 'Řešení řešitelů',
'verbose_name_plural': 'Řešení řešitelů',
'db_table': 'seminar_reseni_resitele',
'ordering': ['reseni', 'resitele'],
'managed': False,
},
),
migrations.RunPython(nastav_nove_contenttypes, nastav_stare_contenttypes),
]

View file

@ -1,30 +0,0 @@
# Generated by Django 4.2.13 on 2024-10-23 21:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0001_create'),
('seminar', '0134_delete_odevzdavatko'),
]
operations = [
migrations.AlterModelOptions(
name='hodnoceni',
options={'verbose_name': 'Hodnocení', 'verbose_name_plural': 'Hodnocení'},
),
migrations.AlterModelOptions(
name='prilohareseni',
options={'ordering': ['reseni', 'vytvoreno'], 'verbose_name': 'Příloha řešení', 'verbose_name_plural': 'Přílohy řešení'},
),
migrations.AlterModelOptions(
name='reseni',
options={'ordering': ['-cas_doruceni'], 'verbose_name': 'Řešení', 'verbose_name_plural': 'Řešení'},
),
migrations.AlterModelOptions(
name='reseni_resitele',
options={'ordering': ['reseni', 'resitele'], 'verbose_name': 'Řešení řešitelů', 'verbose_name_plural': 'Řešení řešitelů'},
),
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.13 on 2024-10-23 21:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0002_manage'),
]
operations = [
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-30 01:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0003_odstrel_odevzdavatka_post'),
]
operations = [
]

View file

@ -1,35 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-30 13:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tvorba', '0001_tvorba_create'),
('odevzdavatko', '0004_tvorba_pre'),
]
operations = [
migrations.AlterField(
model_name='hodnoceni',
name='cislo_body',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.cislo', verbose_name='číslo pro body'),
),
migrations.AlterField(
model_name='hodnoceni',
name='deadline_body',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.deadline', verbose_name='deadline pro body'),
),
migrations.AlterField(
model_name='hodnoceni',
name='problem',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='hodnoceni', to='tvorba.problem', verbose_name='problém'),
),
migrations.AlterField(
model_name='reseni',
name='problem',
field=models.ManyToManyField(help_text='Problém', through='odevzdavatko.Hodnoceni', to='tvorba.problem', verbose_name='problém'),
),
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-30 21:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0005_tvorba_relink'),
('tvorba', '0003_tvorba_post'),
]
operations = [
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-02 19:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0006_tvorba_post'),
]
operations = [
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-02 20:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('treenode', '0001_odstrel_treenode_create'),
('odevzdavatko', '0007_odstrel_treenode_pre'),
]
operations = [
migrations.AlterField(
model_name='reseni',
name='text_cely',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reseni_cely_set', to='treenode.reseninode', verbose_name='Plná verze textu řešení'),
),
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-02 20:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('odevzdavatko', '0008_odstrel_treenode_relink'),
('treenode', '0003_odstrel_treenode_post'),
]
operations = [
]

View file

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

View file

@ -11,7 +11,7 @@
{% endblock %}
</h1>
<form enctype="multipart/form-data" action="{% url 'odevzdavatko_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %}
<table class='form'>
<tr>

View file

@ -12,7 +12,7 @@
<ul>
{% for problem in object_list %}
<li><a href="{% url 'odevzdavatko_nahraj_reseni' problem.id %}">{{ problem }}</a></li>
<li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li>
{% empty %}
<li>Nelze nic odevzdávat.</li>
{% endfor %}

View file

@ -12,7 +12,7 @@
<br>
{% for rocnik, hodnoceni, suma_bodu in podle_rocniku %}
{% for rocnik, hodnoceni in podle_rocniku %}
<h1>Ročník {{ rocnik }}</h1>
<table class="moje_reseni plne_ohranicena_tabulka">
<tr>
@ -33,7 +33,7 @@
</tr>
{% endfor %}
</table>
<p>Celkový počet bodů {{suma_bodu}}</p>
<br>
{% endfor %}

View file

@ -12,7 +12,7 @@
Vložit řešení
{% endblock %}
</h1>
<form enctype="multipart/form-data" action="{% url 'odevzdavatko_vloz_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
<form enctype="multipart/form-data" action="{% url 'seminar_vloz_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %}
{{form.as_p}}

View file

@ -2,11 +2,11 @@ from django import template
register = template.Library()
from functools import cache
from odevzdavatko.models import Reseni
import seminar.models as m
@register.filter
@cache
def barva_reseni(r: Reseni):
def barva_reseni(r: m.Reseni):
"""Vrátí nějakou barvu pro daný problém, ve tvaru '#RRGGBB'
Efektivně hešujeme do barev."""

View file

@ -2,8 +2,8 @@ from django import template
register = template.Library()
from personalni.utils import normalizuj_jmeno
from personalni.models import Osoba # jen kvůli typové anotaci…
import seminar.models as m # jen kvůli typové anotaci…
@register.filter
def jmeno_jako_prefix(o: Osoba):
def jmeno_jako_prefix(o: m.Osoba):
return normalizuj_jmeno(o).replace(' ', '_')

View file

@ -1,7 +1,7 @@
import datetime
import random
from odevzdavatko.models import Reseni, Hodnoceni
from seminar.models.odevzdavatko import Reseni, Hodnoceni
def gen_reseni_ulohy(rnd, cisla, uloha, pocet_resitelu, poradi_cisla, resitele_cisla, resitele):

View file

@ -5,10 +5,10 @@ from various.views.generic import viewMethodSwitch
from . import views
urlpatterns = [
path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='odevzdavatko_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='odevzdavatko_nahraj_reseni'),
path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='odevzdavatko_nahraj_reseni'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='odevzdavatko_resitel_odevzdana_reseni'),
path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),

View file

@ -17,14 +17,10 @@ from decimal import Decimal
from itertools import groupby
import logging
import seminar.models as m
from . import forms as f
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from .models import Hodnoceni, Reseni
from personalni.models import Resitel, Osoba, Organizator
from tvorba.models import Problem, Deadline, Rocnik
from tvorba.utils import resi_v_rocniku
from various.models import Nastaveni
from various.views.pomocne import formularOKView
logger = logging.getLogger(__name__)
@ -44,20 +40,20 @@ logger = logging.getLogger(__name__)
class TabulkaOdevzdanychReseniView(ListView):
template_name = 'odevzdavatko/tabulka.html'
model = Hodnoceni
model = m.Hodnoceni
def inicializuj_osy_tabulky(self):
"""Vyrobí prvotní querysety pro sloupce a řádky, tj. seznam všech řešitelů a problémů"""
# FIXME: jméno metody není vypovídající...
# NOTE: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistují ty objekty (?). TODO: Otestovat
# TODO: Prefetches, Select related, ...
self.resitele = Resitel.objects.all()
self.problemy = Problem.objects.all()
self.reseni = Reseni.objects.all()
self.resitele = m.Resitel.objects.all()
self.problemy = m.Problem.objects.all()
self.reseni = m.Reseni.objects.all()
self.aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
self.aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
if 'rocnik' in self.kwargs:
self.aktualni_rocnik = get_object_or_404(Rocnik, rocnik=self.kwargs['rocnik'])
self.aktualni_rocnik = get_object_or_404(m.Rocnik, rocnik=self.kwargs['rocnik'])
form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik)
if form.is_valid():
@ -90,14 +86,14 @@ class TabulkaOdevzdanychReseniView(ListView):
self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)
if problemy == FiltrForm.PROBLEMY_MOJE:
org = Organizator.objects.get(osoba__user=self.request.user)
org = m.Organizator.objects.get(osoba__user=self.request.user)
self.problemy = self.problemy.filter(
Q(autor=org)|Q(garant=org)|Q(opravovatele=org),
Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY),
)
elif problemy == FiltrForm.PROBLEMY_LETOSNI:
self.problemy = self.problemy.filter(
Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY),
)
#self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník....
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
@ -125,8 +121,8 @@ class TabulkaOdevzdanychReseniView(ListView):
ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.problemy
ctx['resitele'] = self.resitele
tabulka: dict[Problem, dict[Resitel, list[tuple[Reseni, Hodnoceni]]]] = dict()
soucty: dict[Problem, dict[Resitel, Decimal]] = dict()
tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict()
soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict()
def pridej_reseni(resitel, hodnoceni):
problem = hodnoceni.problem
@ -147,11 +143,11 @@ class TabulkaOdevzdanychReseniView(ListView):
for resitel in hodnoceni.reseni.resitele.all():
pridej_reseni(resitel, hodnoceni)
hodnoty: list[list[tuple[Decimal,list[tuple[Reseni, Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
resitele_do_tabulky: list[Resitel] = []
hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
resitele_do_tabulky: list[m.Resitel] = []
for resitel in self.resitele:
dostal_body = False
resiteluv_radek: list[tuple[Decimal,list[tuple[Reseni, Hodnoceni]]]] = [] # podle pořadí v self.problemy
resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy
for problem in self.problemy:
if problem in tabulka and resitel in tabulka[problem]:
resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel]))
@ -166,7 +162,7 @@ class TabulkaOdevzdanychReseniView(ListView):
# Pro použití hacku na automatické {{form.media}} v template:
ctx['form'] = ctx['filtr']
# Pro maximum v přesměrovátku ročníků
ctx['aktualni_rocnik'] = Nastaveni.get_solo().aktualni_rocnik
ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik
ctx['barvicky'] = self.barvicky
if 'rocnik' in self.kwargs:
ctx['rocnik'] = self.kwargs['rocnik']
@ -182,7 +178,7 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
Asi bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-)
V případě, že takové řešení existuje jen jedno, tak na něj přesměruje."""
model = Reseni
model = m.Reseni
template_name = 'odevzdavatko/seznam.html'
def get_queryset(self):
@ -194,8 +190,8 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
if problem_id is None:
raise ValueError("Nemám problém! (To je problém!)")
resitel = Resitel.objects.get(id=resitel_id)
problem = Problem.objects.get(id=problem_id)
resitel = m.Resitel.objects.get(id=resitel_id)
problem = m.Problem.objects.get(id=problem_id)
qs = qs.filter(
problem__in=[problem],
resitele__in=[resitel],
@ -222,28 +218,28 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
ctx["problem_id"] = self.kwargs['problem']
return ctx
HODNOCENI_INITIAL_DATA = [
"problem",
"body",
"body_celkem",
"body_neprepocitane",
"body_neprepocitane_celkem",
"body_max",
"body_neprepocitane_max",
"deadline_body",
"feedback",
]
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView(DetailView):
""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
model = Reseni
model = m.Reseni
template_name = 'odevzdavatko/detail.html'
def aktualni_hodnoceni(self):
self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk'])
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in Hodnoceni.objects.filter(reseni=self.reseni):
result.append({attr: getattr(hodn, attr) for attr in HODNOCENI_INITIAL_DATA})
for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni):
seznam_atributu = [
"problem",
"body",
"body_celkem",
"body_neprepocitane",
"body_neprepocitane_celkem",
"body_max",
"body_neprepocitane_max",
"deadline_body",
"feedback",
]
result.append({attr: getattr(hodn, attr) for attr in seznam_atributu})
return result
def get_context_data(self, **kw):
@ -288,12 +284,12 @@ class EditReseniView(DetailReseniView):
def hodnoceniReseniView(request, pk, *args, **kwargs):
reseni = get_object_or_404(Reseni, pk=pk)
reseni = get_object_or_404(m.Reseni, pk=pk)
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
formset = f.OhodnoceniReseniFormSet(request.POST, initial=[
{k: getattr(h, k) for k in HODNOCENI_INITIAL_DATA} for h in Hodnoceni.objects.filter(reseni=reseni)
])
# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
# Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm
formset = f.OhodnoceniReseniFormSet(request.POST)
poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni)
# TODO: Napsat validaci formuláře a formsetu
if not (formset.is_valid() and poznamka_form.is_valid()):
@ -304,76 +300,46 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
poznamka_form.save()
# Smažeme všechna dosavadní hodnocení tohoto řešení
qs = Hodnoceni.objects.filter(reseni=reseni)
qs = m.Hodnoceni.objects.filter(reseni=reseni)
logger.info(f"Will delete {qs.count()} objects: {qs}")
qs.delete()
# Vyrobíme nová podle formsetu
notifikace = False
for form in formset:
notifikace |= 'feedback' in form.changed_data
data_for_hodnoceni = form.cleaned_data
data_for_body = data_for_hodnoceni.copy()
del(data_for_hodnoceni["body_celkem"])
del(data_for_hodnoceni["body_neprepocitane"])
del(data_for_hodnoceni["body_neprepocitane_celkem"])
hodnoceni = Hodnoceni(
hodnoceni = m.Hodnoceni(
reseni=reseni,
**form.cleaned_data,
)
logger.info(f"Creating Hodnoceni: {hodnoceni}")
# FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak
zmeny_bodu = [it for it in form.changed_data if it.startswith("body")]
if len(zmeny_bodu) != 0:
body_nastaveny: None | tuple[str, object] = None
def nastav_body(jake, na_kolik):
nonlocal body_nastaveny
if body_nastaveny is not None:
logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.")
hodnoceni.body = -0.1
else:
body_nastaveny = (jake, na_kolik)
hodnoceni.__setattr__(jake, na_kolik)
for key, value in data_for_body.items():
if key.startswith("body") and value is not None:
nastav_body(key, value)
# Něco se změnilo, ale nic není nastavené = něco bylo smazáno
if body_nastaveny is None:
hodnoceni.body = None
if len(zmeny_bodu) == 1:
hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]])
# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno
if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2:
# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.")
hodnoceni.body = -0.1
hodnoceni.save()
adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True)
if notifikace and adresati:
email = EmailMessage(
subject='Změna hodnocení odevzdaného řešení',
body=f"""Milá řešitelko, milý řešiteli,
došlo ke změně zpětné vazby k Tebou odevzdanému řešení. Zobrazit si ji můžeš na {reseni.resitel_url()}.
Tvoji organizátoři M&M
---
Nechceš-li tato upozornění dostávat, můžeš si to nastavit ve svém profilu.""",
from_email='odevzdavatko@mam.mff.cuni.cz',
bcc=adresati,
)
email.send()
return redirect(success_url)
class PrehledOdevzdanychReseni(ListView):
model = Hodnoceni
model = m.Hodnoceni
template_name = 'odevzdavatko/prehled_reseni.html'
def get_queryset(self):
if not self.request.user.is_authenticated:
raise RuntimeError("Uživatel měl být přihlášený!")
# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
resitel = Resitel.objects.filter(osoba__user=self.request.user).first()
resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first()
qs = super().get_queryset()
qs = qs.filter(reseni__resitele__in=[resitel])
# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení
@ -386,12 +352,7 @@ class PrehledOdevzdanychReseni(ListView):
# Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/
podle_rocniku = []
for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: ho.deadline_body.cislo.rocnik if ho.deadline_body is not None else None):
suma_bodu = 0
hodnoceni = list(hodnoceni)
for i in hodnoceni :
if i.body != None : suma_bodu += i.body
podle_rocniku.append((rocnik, hodnoceni, suma_bodu))
podle_rocniku.append((rocnik, list(hodnoceni)))
ctx['podle_rocniku'] = reversed(podle_rocniku) # Od nejnovějšího ročníku
# TODO: Umožnit stažení / zobrazení řešení
return ctx
@ -399,13 +360,13 @@ class PrehledOdevzdanychReseni(ListView):
# Přehled všech řešení kvůli debugování
class SeznamReseniView(ListView):
model = Reseni
model = m.Reseni
template_name = 'odevzdavatko/seznam.html'
class SeznamAktualnichReseniView(SeznamReseniView):
def get_queryset(self):
qs = super().get_queryset()
akt_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
resitele = resi_v_rocniku(akt_rocnik)
qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
return qs
@ -417,7 +378,7 @@ class VlozReseniView(LoginRequiredMixin, FormView):
def form_valid(self, form):
data = form.cleaned_data
nove_reseni = Reseni.objects.create(
nove_reseni = m.Reseni.objects.create(
cas_doruceni=data['cas_doruceni'],
forma=data['forma'],
poznamka=data['poznamka'],
@ -444,35 +405,35 @@ class VlozReseniView(LoginRequiredMixin, FormView):
class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
model = Problem
model = m.Problem
template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html'
def get_queryset(self):
return super().get_queryset().filter(stav=Problem.STAV_ZADANY, nadproblem__isnull=True)
return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True)
class NahrajReseniView(LoginRequiredMixin, CreateView):
model = Reseni
model = m.Reseni
template_name = 'odevzdavatko/nahraj_reseni.html'
form_class = f.NahrajReseniForm
nadproblem: Problem
nadproblem: m.Problem
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
nadproblem_id = self.kwargs["nadproblem_id"]
self.nadproblem = get_object_or_404(Problem, id=nadproblem_id)
self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id)
def get(self, request, *args, **kwargs):
# Zaříznutí nezadaných problémů
if self.nadproblem.stav != Problem.STAV_ZADANY:
if self.nadproblem.stav != m.Problem.STAV_ZADANY:
raise PermissionDenied()
# Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
osoba = Osoba.objects.get(user=self.request.user)
osoba = m.Osoba.objects.get(user=self.request.user)
resitel = osoba.resitel
if resitel.rok_maturity <= Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
return render(request, 'universal.html', {
'title': 'Nelze odevzdat',
'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
@ -484,7 +445,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
nadproblem_id = self.nadproblem.id
return {
"nadproblem_id": nadproblem_id,
"problem": [] if self.nadproblem.podproblem.filter(stav=Problem.STAV_ZADANY).exists() else nadproblem_id
"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id
}
@ -496,7 +457,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
data['prilohy'] = f.ReseniSPrilohamiFormSet()
data["nadproblem_id"] = self.nadproblem.id
data["nadproblem"] = get_object_or_404(Problem, id=self.nadproblem.id)
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
@ -508,17 +469,17 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
return super().form_invalid(form)
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user))
self.object.resitele.add(*form.cleaned_data["resitele"])
self.object.cas_doruceni = timezone.now()
self.object.forma = Reseni.FORMA_UPLOAD
self.object.forma = m.Reseni.FORMA_UPLOAD
self.object.save()
prilohy.instance = self.object
prilohy.save()
for hodnoceni in self.object.hodnoceni_set.all():
hodnoceni.deadline_body = Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first()
hodnoceni.deadline_body = m.Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first()
hodnoceni.save()
# Pošleme mail opravovatelům a garantovi
@ -536,7 +497,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
prijemci = map(lambda it: it.osoba.email, prijemci)
resitel = Osoba.objects.get(user = self.request.user)
resitel = m.Osoba.objects.get(user = self.request.user)
seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy))
seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })")
@ -551,5 +512,5 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
return formularOKView(
self.request,
text='Řešení úspěšně odevzdáno',
dalsi_odkazy=[("Odevzdat další řešení", reverse("odevzdavatko_nahraj_reseni"))],
dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))],
)

View file

@ -2,7 +2,7 @@ from django.contrib import admin
from django.contrib.auth.models import Group
from django_reverse_admin import ReverseModelAdmin
from django.contrib.messages import WARNING, ERROR, SUCCESS
import personalni.models as m
import seminar.models as m
from datetime import datetime
@admin.action(description="Sjednoť telefony")

View file

@ -4,7 +4,7 @@ from django.contrib.auth.forms import PasswordResetForm
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
from personalni.models import Skola, Resitel, Osoba
from seminar.models import Skola, Resitel, Osoba
from datetime import date
import logging
@ -27,7 +27,7 @@ class TelInput(forms.TextInput):
class UdajeForm(forms.Form):
username = None
err_logger = logging.getLogger('personalni.prihlaska.problem')
err_logger = logging.getLogger('seminar.prihlaska.problem')
jmeno = forms.CharField(label='Jméno', max_length=256, required=True)
prezdivka_resitele = forms.CharField(label='Přezdívka (veřejná)', max_length=256, required=False)
@ -62,7 +62,7 @@ class UdajeForm(forms.Form):
rok_maturity = forms.IntegerField(
label='Rok maturity',
min_value=date.today().year,
max_value=date.today().year+13,
max_value=date.today().year+8,
required=True,
)
@ -71,8 +71,6 @@ class UdajeForm(forms.Form):
zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False, initial=True)
spam = forms.BooleanField(label='Souhlasím se zasíláním propagačních materiálů od MFF UK', required=False)
upozornovat_na_opravy_reseni = forms.BooleanField(label='Chci dostávat emailová upozornění na změnu zpětné vazby k mým řešením', required=False, initial=True)
def clean_prezdivka_resitele(self):
prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele')
if prezdivka_resitele == '':
@ -149,7 +147,7 @@ class PrihlaskaForm(PasswordResetForm, UdajeForm):
class ProfileEditForm(UdajeForm):
err_logger = logging.getLogger('personalni.prihlaska.problem.edit')
err_logger = logging.getLogger('seminar.edit.problem')
username = forms.CharField(
label='Přihlašovací jméno',
max_length=256,

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.13 on 2024-10-22 22:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0011_osloveni_vsechny_choices'),
]
operations = [
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.13 on 2024-10-23 21:10
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0012_odstrel_odevzdavatka_pre'),
('odevzdavatko', '0003_odstrel_odevzdavatka_post'),
]
operations = [
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-30 01:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0013_odstrel_odevzdavatka_post'),
]
operations = [
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.16 on 2024-10-30 21:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0014_tvorba_pre'),
('tvorba', '0003_tvorba_post'),
]
operations = [
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-02 19:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0015_tvorba_post'),
]
operations = [
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.16 on 2024-11-02 20:52
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('personalni', '0016_odstrel_treenode_pre'),
('treenode', '0003_odstrel_treenode_post'),
]
operations = [
]

View file

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

View file

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

View file

@ -11,7 +11,7 @@ from django_countries.fields import CountryField
from reversion import revisions as reversion
from various.models import SeminarModelBase
from seminar.models.base import SeminarModelBase
logger = logging.getLogger(__name__)
@ -250,8 +250,6 @@ class Resitel(SeminarModelBase):
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešiteli (plain text)')
upozornovat_na_opravy_reseni = models.BooleanField('zasílat upozornění na změnu zpětné vazby k řešení emailem', default=True)
def export_row(self):
"Slovnik pro pouziti v AESOP exportu"
@ -298,7 +296,7 @@ class Resitel(SeminarModelBase):
def vsechny_body(self):
"Spočítá body odjakživa."
vsechna_reseni = self.reseni_set.all()
from odevzdavatko.models import Hodnoceni
from seminar.models.odevzdavatko import Hodnoceni
vsechna_hodnoceni = Hodnoceni.objects.filter(
reseni__in=vsechna_reseni)
return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None)
@ -345,7 +343,7 @@ class Resitel(SeminarModelBase):
# - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími
# - proto se započítávají dvojnásobně a byly posunuté hranice titulů
# - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád.
from odevzdavatko.models import Hodnoceni
from seminar.models.odevzdavatko import Hodnoceni
hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all())
novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku)
@ -383,7 +381,7 @@ class Resitel(SeminarModelBase):
else:
return Titul.akad
from odevzdavatko.models import Hodnoceni
from seminar.models.odevzdavatko import Hodnoceni
hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all())
novejsi_body = body_z_hodnoceni(
Hodnoceni.objects.filter(reseni__in=self.reseni_set.all())

Some files were not shown because too many files have changed in this diff Show more