Compare commits

..

61 commits

Author SHA1 Message Date
cb924f846f Vytáhnuty konstanty do settings_common.py
(Takže teď lze jednoduše měnit e-maily a také testovací data (settings_local.py) nemusí mít 30 ročníků pro otestování zlomů)
2023-08-11 11:25:53 +02:00
206d37e21c Fix c00f60b38f 2023-08-11 11:25:52 +02:00
f707cc02f7 Uhlazena konfigurace (settings_*.py) 2023-08-11 11:25:47 +02:00
e85c333bf8 Smazáno staré testování e-mailů 2023-08-10 23:41:15 +02:00
5cccd40800 Na některých serverech už produkce nepoběží… A testweb rozhodně nesmí běhat na místě produkce. 2023-08-10 23:30:48 +02:00
ff04978a95 More hierarchy to URL 2023-08-10 22:58:42 +02:00
c2613af9a6 Doteď nevadilo, že to tu není, tak mažu 2023-08-10 22:43:42 +02:00
bbaf109bbe Vhled je dělaný přes context processor, mažu middleware 2023-08-10 22:40:32 +02:00
56ac6f74b6 Smazány 2 malinkaté automaticky vygenerované věci 2023-08-10 22:27:50 +02:00
2319087bcb TODO přesunuto do Kanboardu 2023-08-10 22:25:44 +02:00
06d0a8cce3 Odstraněny další generické dokumentace 2023-08-10 22:25:21 +02:00
afc238ba74 Tohle vypadá staře a navíc ve staré verzi, mažu 2023-08-10 22:08:22 +02:00
c6805ad7b5 Další zastaralé coding: utf-8 2023-08-10 22:05:26 +02:00
6ff0438874 TODO přesunuto do kanboardu 2023-08-10 21:59:27 +02:00
1f96234a0f Update dokumentace (deploy v2 a práva skupin) 2023-08-10 21:20:44 +02:00
cdd1759976 Práva skupin (hlavně orgů) 2023-08-10 21:01:50 +02:00
1790c0faf7 Usměrnění importů import * 2023-08-10 19:32:48 +02:00
0c45d8051f Rozházeny seminar.testutils (stále je potřeba je upravit do příčetného stavu a opravit import *) 2023-08-10 17:27:18 +02:00
8939a26ec8 Rozházeny seminar.templatetags 2023-08-10 15:53:19 +02:00
d40265ba74 Commandy ze semináře do tvorby 2023-08-10 15:44:28 +02:00
8301cbdb4d Už nebude potřeba (pak to udělám pomocí loaddata) 2023-08-10 15:41:29 +02:00
c00f60b38f FormularOKView a pracuje_se přesunuto ze semináře do various 2023-08-10 15:34:32 +02:00
5878d1bf7e Zapomenutý template (stav_databaze) v seminari 2023-08-10 15:30:57 +02:00
c96ada8b37 Vypreparování views, templates a urls tvorby ze semináře 2023-08-10 15:21:22 +02:00
f3c38f1f02 Nepoužitý logger v seminar.views 2023-08-10 14:31:19 +02:00
1f9966c51f Nepoužitý ResitelInline v seminar.admin 2023-08-10 14:15:50 +02:00
a346c4d49a Přesun adminu tvorby ze semináře 2023-08-10 14:14:41 +02:00
9412a52567 Přesun stavu databáze do various (dokončeno rozebírání seminar.utils)
Důvodem je, že mi přijde, že stav databáze je dosti složitá a neviditelná věc, na to, aby byla přímo v semináři
2023-08-10 14:04:31 +02:00
7a02a826cd Nepoužitý logger? (v seminar.utils) 2023-08-10 13:47:56 +02:00
9c68eac050 Přesunutí "switche view podle GET/POST" ze seminar.utils do various.utils 2023-08-10 13:46:15 +02:00
15b17023de Přesunutí římských čísel ze seminar.utils do various.utils 2023-08-10 13:43:49 +02:00
c89a982440 Odstraněn nepoužívaný FirstTagParser (z seminar.utils) 2023-08-10 13:40:38 +02:00
39afe79da7 Vypreparování odevzdávátka ze seminar.utils 2023-08-10 13:40:01 +02:00
5fcf9bac15 Vypreparování personálního ze seminar.utils 2023-08-10 13:20:18 +02:00
7dc0e1d71b Přejmenování tabulek modelů zbylých v aplikaci seminar 2023-08-10 12:45:28 +02:00
9ac0d06e1e Další drobné úpravy: admin_url potřebuje app_label 2023-08-10 12:39:11 +02:00
6bb39fc0a0 Ještě jedna drobná chybička (testdata ale stále nefungují, protože import * přepisují importy) 2023-08-10 12:27:50 +02:00
5c9ed5e5ed Ruční přepsání migrací, aby se nedalo namigrovat do rozbitého stavu. POZOR: odmigrovat vše od tagu v3_uklizeno
Teď už je nejhorší stav, že dvě aplikace ukazují na tu samou tabulku.
2023-08-10 12:14:18 +02:00
9a20fc7c79 Oprava drobných chybiček při migracích 2023-08-10 09:46:46 +02:00
e569346274 Migrace a přejmenování tabulek modelů treenode 2023-08-10 09:42:26 +02:00
78923f5237 Přesun kódu modelů treenode 2023-08-10 09:12:23 +02:00
0fd3526a87 Migrace a přejmenování tabulek modelů soustředění 2023-08-09 21:49:54 +02:00
d240774022 Přesun kódu modelů soustředění 2023-08-09 21:29:14 +02:00
acea74bc6e Migrace a přejmenování tabulek modelů odevzdávátka 2023-08-09 21:06:14 +02:00
9b3cbb512c Přesun kódu modelů odevzdávátka 2023-08-09 19:32:55 +02:00
1af4a13a62 Migrace a přejmenování tabulek modelů tvorby 2023-08-09 18:58:28 +02:00
115589e770 Přesun kódu modelů tvorby 2023-08-09 17:11:20 +02:00
2b52ec028e Přesun Natavení z tvorby 2023-08-09 16:40:23 +02:00
5346da5107 Init tvorby 2023-08-09 16:12:57 +02:00
75a7e607d5 Migrace a přejmenování tabulek modelů personálního 2023-07-31 20:57:28 +02:00
ebf8165c53 Přesun kódu modelů personálního 2023-07-31 19:53:12 +02:00
8c881621b0 Přesun ReseniNode do treenode 2023-07-31 19:31:23 +02:00
666b455bbd Vyřešení cyklických importů po předchozím commitu 2023-07-31 19:21:36 +02:00
e45c819424 import seminar.models as m na from seminar.models.neco import neco 2023-07-31 19:13:56 +02:00
8319b28272 Návod na ilustrace odměn nepatří do kořenové složky 2023-07-31 17:45:49 +02:00
6d3a70165f Deploy v2 už nebude aktuální 2023-07-31 17:41:18 +02:00
59d9589162 Generické dokumentace djangovských souborů to chce řešit jinak… 2023-07-31 17:33:39 +02:00
44b10449af `# -*- coding: utf-8 -*- už přestalo být relevantní 2023-07-31 17:26:39 +02:00
3631ec3c5b AESOP přestal býti aktuální 2023-07-31 17:23:06 +02:00
1868f96594 Měnit mezery na tabulátory už bychom také neměli potřebovat 2023-07-31 16:21:58 +02:00
f8379b8b67 Kontroly Pythonu už nevedeme 2023-07-31 16:20:26 +02:00
537 changed files with 20781 additions and 32299 deletions

15
.gitignore vendored
View file

@ -25,21 +25,12 @@ TODO
# .htpasswd kvůli přihlášení
.htpasswd
# .htpasswd pro AESOPa
/.htpasswd-aesop
# reversion kvůli historii objektů v reversion
**/reversion
# dokumentace
docs/_build
docs/modules
# logy týracího skriptu (./checklinks.sh)
/wget.log.*
# pro lidi, co programují v nástrojích od JetBrains
.idea
# Mac users
.DS_Store
# dokumentace
docs/_build
docs/modules

View file

@ -1,3 +0,0 @@
"""
Obsahuje vše, co se týká aesopu (exportu, který po nás vyžaduje OPMK).
"""

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class AesopConfig(AppConfig):
name = 'aesop'
verbose_name = 'Export do AESOPa'

View file

@ -1,30 +0,0 @@
from django.http import HttpResponse
from django.utils.encoding import force_str
class OvvpFile:
def __init__(self):
# { header: value, ... }
self.headers = {}
# [ 'column-name', ... ]
self.columns = []
# [ { column: value, ...}, ...]
self.rows = []
def to_lines(self):
# header
for hk in sorted(self.headers.keys()):
yield f'{hk}\t{self.headers[hk]}\n'
yield '\n'
# columns
yield '\t'.join(self.columns) + '\n'
# rows
for r in self.rows:
yield '\t'.join([force_str(r[c]) for c in self.columns]) + '\n'
def to_string(self):
return ''.join(self.to_lines())
# Pozn: tohle je ta jediná funkce, která se reálně používá…
def to_HttpResponse(self):
return HttpResponse(self.to_string(), content_type='text/plain; charset=utf-8')

View file

@ -1,20 +0,0 @@
from django.urls import path
from aesop import views
urlpatterns = [
path(
'aesop-export/mam-rocnik-<int:prvni_rok>.csv',
views.ExportRocnikView.as_view(),
name='aesop_export_rocnik'
),
path(
'aesop-export/mam-sous-<str:datum_zacatku>.csv',
views.ExportSousView.as_view(),
name='aesop_export_sous'
),
path(
'aesop-export/index.csv',
views.ExportIndexView.as_view(),
name='aesop_export_index'
),
]

View file

@ -1,16 +0,0 @@
import datetime
from django.utils.encoding import force_str
from aesop.ovvpfile import OvvpFile
def default_ovvpfile(event, rocnik):
of = OvvpFile()
of.headers['version'] = '1'
of.headers['event'] = event
of.headers['year'] = force_str(rocnik.prvni_rok)
of.headers['date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
of.headers['id-scope'] = 'mam'
of.headers['id-generation'] = '1'
return of

View file

@ -1,97 +0,0 @@
import django
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.urls import reverse
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 vysledkovky import utils
from tvorba.utils import aktivniResitele
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})
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()})
ls.append(url.split('/')[-1])
return HttpResponse('\n'.join(ls) + '\n', content_type='text/plain; charset=utf-8')
class ExportSousView(generic.View):
def get(self, request, datum_zacatku=None):
try:
dz = django.utils.dateparse.parse_date(datum_zacatku)
except:
dz = None
if dz is None:
raise django.http.Http404()
s = get_object_or_404(Soustredeni, datum_zacatku=dz, exportovat=True)
akce = {Soustredeni.TYP_JARNI: 'MaM.sous.jaro',
Soustredeni.TYP_PODZIMNI: 'MaM.sous.podzim',
Soustredeni.TYP_VIKEND: 'MaM.vikend',
}[s.typ]
of = default_ovvpfile(akce, s.rocnik)
of.headers['x-event-begin'] = s.datum_zacatku.isoformat()
of.headers['x-event-end'] = s.datum_konce.isoformat()
of.headers['x-event-location'] = s.misto
of.headers['comment'] = u'MaM-Web export ucastniku soustredeni v {x-event-location} od {x-event-begin} do {x-event-end}'.format(**of.headers)
of.columns = ['id', 'name', 'surname', 'gender', 'email', 'end-year', 'school', 'school-name']
for u in s.ucastnici.all():
of.rows.append(u.export_row())
return of.to_HttpResponse()
# POZOR! Předělání na nový model neotestováno v reálu (ale zase jen drobné změny)
class ExportRocnikView(generic.View):
def get(self, request, prvni_rok=None):
try:
pr = int(prvni_rok)
except:
pr = None
if pr is None:
raise django.http.Http404()
rocnik = get_object_or_404(Rocnik, prvni_rok=pr, exportovat=True)
cislo = rocnik.posledni_zverejnena_vysledkovka_cislo()
resitele = aktivniResitele(cislo, True)
slovnik_body = utils.secti_body_za_rocnik(cislo, resitele, False)
setrizeni_resitele, body = utils.setrid_resitele_a_body(slovnik_body)
of = default_ovvpfile('MaM.rocnik', rocnik)
of.headers['comment'] = u'MaM-Web export aktivnich resitelu rocniku {rocnik} do cisla {cislo}'.format(rocnik=rocnik, cislo=cislo)
of.columns = ['id', 'name', 'surname', 'gender', 'born', 'email', 'end-year',
'street', 'town', 'postcode', 'country', 'spam-flag', 'spam-date',
'school', 'school-name', 'points', 'rank',]
resitele_slovnik = {}
for r in resitele:
resitele_slovnik[r.id] = r
# počítání pořadí řešitelů
posledni_body = 100000
posledni_poradi = 0
for i in range(len(setrizeni_resitele)):
rd = resitele_slovnik[setrizeni_resitele[i]].export_row()
if posledni_body > body[i]:
posledni_body = body[i]
posledni_poradi = i + 1
rd['rank'] = posledni_poradi
rd['points'] = body[i]
of.rows.append(rd)
return of.to_HttpResponse()

View file

@ -3,4 +3,3 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'
verbose_name = 'Různá webová API'

View file

@ -1,9 +1,9 @@
from django.test import TestCase, tag
from django.test import TestCase
from django.urls import reverse
from personalni.models import Skola
import seminar.views as v
from personalni.utils import sync_skoly
@tag('stejny-model-na-produkci')
class OrgSkolyAutocompleteTestCase(TestCase):
@classmethod
def setUpClass(cls):

View file

@ -4,18 +4,18 @@ from personalni.utils import org_required
urlpatterns = [
# Export škol
path('api/export/skoly/', views.exportSkolView, name='export_skoly'),
path('export/skoly/', views.exportSkolView, name='export_skoly'),
# Autocomplete
path('api/autocomplete/skola/', views.SkolaAutocomplete.as_view(), name='autocomplete_skola'),
path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'),
path('api/autocomplete/resitel_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'),
path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
path('api/autocomplete/problem/vsechny', views.ProblemAutocomplete.as_view(), name='autocomplete_problem'),
path('autocomplete/skola/', views.SkolaAutocomplete.as_view(), name='autocomplete_skola'),
path('autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'),
path('autocomplete/resitel_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'),
path('autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
path('autocomplete/problem/vsechny', views.ProblemAutocomplete.as_view(), name='autocomplete_problem'),
# Ceka na autocomplete v3
# path('autocomplete/organizatori/',
# org_member_required(views.OrganizatorAutocomplete.as_view()),
# name='autocomplete_organizator')
# name='seminar_autocomplete_organizator')
]

View file

@ -7,7 +7,7 @@ from django.db.models import Q
from personalni.models import Skola, Resitel
from tvorba.models import Problem
from various.models import Nastaveni
from seminar.models.nastaveni import Nastaveni
from .helpers import LoginRequiredAjaxMixin
# TODO filosofie - zkratky, jak v databázi, tak ve vyhledávání (SPŠE, GASOŠ, Kpt., soukr)

View file

@ -1,4 +1,4 @@
import personalni.models as m
from personalni.models import Skola
from django.core import serializers as ser
from django.http import HttpResponse
def exportSkolView(request):
@ -8,7 +8,7 @@ def exportSkolView(request):
# Některé fieldy nechceme: Kontaktní osoby, AESOP ID, org poznámky.
fields = ('id', 'izo', 'nazev', 'kratky_nazev', 'ulice', 'mesto', 'psc', 'stat', 'je_zs', 'je_ss')
# TODO: Použít JSONL, aby protistrana mohla číst po řádkách a nesežralo to tunu paměti úplně hned
skoly_json = ser.serialize("json", m.Skola.objects.all(), fields=fields)
skoly_json = ser.serialize("json", Skola.objects.all(), fields=fields)
response = HttpResponse(
content = skoly_json,
content_type = 'text/json',

View file

@ -1,17 +0,0 @@
#!/bin/sh
if test "$#" -lt 1
then
echo "Usage: $0 file ..."
exit 2
fi
for file in "$@"
do
# Do the sed magic: keep replacing 4 spaces at the begining of line
sed -i -re '
: loop
s/^( *) /\1 /
t loop
' "$file"
done

View file

@ -78,6 +78,11 @@
"header_fotky",
"fotkaheader"
],
[
"delete_fotkaheader",
"header_fotky",
"fotkaheader"
],
[
"view_fotkaheader",
"header_fotky",
@ -93,6 +98,11 @@
"header_fotky",
"fotkaurlvazba"
],
[
"delete_fotkaurlvazba",
"header_fotky",
"fotkaurlvazba"
],
[
"view_fotkaurlvazba",
"header_fotky",
@ -158,26 +168,6 @@
"korektury",
"oprava"
],
[
"add_novinky",
"novinky",
"novinky"
],
[
"change_novinky",
"novinky",
"novinky"
],
[
"delete_novinky",
"novinky",
"novinky"
],
[
"view_novinky",
"novinky",
"novinky"
],
[
"change_organizator",
"personalni",
@ -228,35 +218,35 @@
"personalni",
"resitel"
],
[
"add_skola",
"personalni",
"skola"
],
[
"change_skola",
"personalni",
"skola"
],
[
"delete_skola",
"personalni",
"skola"
],
[
"view_skola",
"personalni",
"skola"
],
[
"view_hlasovani",
"add_hlasovani",
"prednasky",
"hlasovani"
],
[
"view_hlasovanioznalostech",
"change_hlasovani",
"prednasky",
"hlasovanioznalostech"
"hlasovani"
],
[
"delete_hlasovani",
"prednasky",
"hlasovani"
],
[
"view_hlasovani",
"prednasky",
"hlasovani"
],
[
"add_prednaska",
@ -299,24 +289,34 @@
"seznam"
],
[
"add_znalost",
"prednasky",
"znalost"
"change_nastaveni",
"seminar",
"nastaveni"
],
[
"change_znalost",
"prednasky",
"znalost"
"view_nastaveni",
"seminar",
"nastaveni"
],
[
"delete_znalost",
"prednasky",
"znalost"
"add_novinky",
"seminar",
"novinky"
],
[
"view_znalost",
"prednasky",
"znalost"
"change_novinky",
"seminar",
"novinky"
],
[
"delete_novinky",
"seminar",
"novinky"
],
[
"view_novinky",
"seminar",
"novinky"
],
[
"add_konfera",
@ -418,46 +418,6 @@
"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",
@ -508,6 +468,11 @@
"tvorba",
"deadline"
],
[
"delete_deadline",
"tvorba",
"deadline"
],
[
"view_deadline",
"tvorba",
@ -612,26 +577,6 @@
"view_uloha",
"tvorba",
"uloha"
],
[
"add_nastaveni",
"various",
"nastaveni"
],
[
"change_nastaveni",
"various",
"nastaveni"
],
[
"delete_nastaveni",
"various",
"nastaveni"
],
[
"view_nastaveni",
"various",
"nastaveni"
]
]
},

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",
@ -1090,29 +1079,5 @@
},
"model": "sitetree.treeitem",
"pk": 53
},
{
"fields": {
"access_guest": false,
"access_loggedin": false,
"access_perm_type": 1,
"access_permissions": [],
"access_restricted": false,
"alias": null,
"description": "",
"hidden": false,
"hint": "",
"inbreadcrumbs": true,
"inmenu": true,
"insitetree": true,
"parent": 20,
"sort_order": 54,
"title": "Export do abstraktů sousu {{ soustredeni.id }}",
"tree": 1,
"url": "soustredeni_abstrakty soustredeni.id",
"urlaspattern": true
},
"model": "sitetree.treeitem",
"pk": 54
}
]

View file

@ -1,3 +0,0 @@
Tahle slozka obsahuje vsechny detaily a popisy, jak nasadit "druhou verzi" M&M webu.
TODO: chybi tu popis na zprovozneni flatpages, na loaddata &c.

Binary file not shown.

View file

@ -1,513 +0,0 @@
#!/usr/bin/env python3
import psycopg2
import psycopg2.extras
OLD_DB = "mam_old"
NEW_DB = "mamweb"
oldconn = psycopg2.connect(f"dbname={OLD_DB}")
newconn = psycopg2.connect(f"dbname={NEW_DB}")
oldcur = oldconn.cursor(cursor_factory=psycopg2.extras.DictCursor)
newcur = newconn.cursor(cursor_factory=psycopg2.extras.DictCursor)
# Uses global variables oldcur, newcur!
def execute_simple(old_query, new_query=None):
if new_query is None:
new_query = old_query
oldcur.execute(old_query)
newcur.execute(new_query)
if oldcur.rowcount != newcur.rowcount:
raise ValueError(f"Queries '{old_query}' and '{new_query}' returned different number of rows ({oldcur.rowcount} and {newcur.rowcount})")
return(oldcur.fetchall(), newcur.fetchall())
def check_same(old_row, new_row, old_fields, new_fields=None):
if type(old_fields) != list:
old_fields = [old_fields]
if new_fields is None:
new_fields = old_fields
fields = zip(old_fields, new_fields)
for old_field, new_field in fields:
if old_row[old_field] == new_row[new_field]:
continue
raise ValueError(f"Fields '{old_field}'({old_row[old_field]}) and '{new_field}'({new_row[new_field]}) differs for rows \n'{old_row}' and \n'{new_row}'")
return True
def get_user_id_for_org_id(org_id):
query = """SELECT auth_user.id FROM auth_user
INNER JOIN seminar_osoby ON seminar_osoby.user_id = auth_user.id
INNER JOIN seminar_organizator ON seminar_organizator.osoba_id = seminar_osoby.id
WHERE seminar_organizator.id = %s """
newcur.execute(query,(org_id,))
return newcur.fetchone()['id']
def check_skola():
old_query = "SELECT * FROM seminar_skoly ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','aesop_id','izo','nazev','kratky_nazev','ulice','mesto','psc','stat','je_zs','je_ss','poznamka'])
def check_resitel():
old_query = 'SELECT * FROM seminar_resitele ORDER BY id'
new_query = '''SELECT seminar_resitele.id, skola_id, rok_maturity, zasilat, seminar_resitele.poznamka,
o.jmeno AS jmeno, o.prijmeni AS prijmeni, o.user_id AS user_id, o.pohlavi_muz AS pohlavi_muz, o.email AS email, o.telefon AS telefon, o.datum_narozeni AS datum_narozeni,
o.datum_souhlasu_udaje AS datum_souhlasu_udaje, o.datum_souhlasu_zasilani AS datum_souhlasu_zasilani, o.datum_registrace AS datum_prihlaseni, o.ulice AS ulice, o.mesto AS mesto, o.psc AS psc, o.stat AS stat
FROM seminar_resitele JOIN seminar_osoby AS o ON seminar_resitele.osoba_id = o.id ORDER BY seminar_resitele.id'''
old_res, new_res = execute_simple(old_query,new_query)
res = zip(old_res,new_res)
fields_osoba = [
'jmeno',
'prijmeni',
'user_id',
'pohlavi_muz',
#'email', #vyreseno separatne
'telefon',
'datum_narozeni',
'datum_souhlasu_udaje',
'datum_souhlasu_zasilani',
'datum_prihlaseni',
'ulice',
'mesto',
'psc',
'stat',
]
fields_keep = [
'id',
'skola_id',
'rok_maturity',
'zasilat',
'poznamka',
]
fields = fields_keep+fields_osoba
for o,n in res:
check_same(o,n,fields)
if o['email'] != n['email'] and o['email'] != '':
print(f"WARNING: Emails differ: old: {o['email']}, new: {n['email']}")
def check_reseni():
# Migrace 0058 zamerne meni (zmensuje) pocet reseni, aby kazdy clanek mel
# jen jedno reseni (s vice resiteli, coz postaru neslo)
# Kvuli tomu je potreba kontrolovat dve veci:
# 1) Ze kazdy resitel dostal za kazdy problem spravne bodu
# 2) Ze detaily reseni zustaly zachovany
# Cast 1)
old_query = 'SELECT * FROM seminar_reseni ORDER BY problem_id, resitel_id, body, timestamp'
new_query = '''SELECT seminar_reseni.id, forma, seminar_reseni.poznamka, cas_doruceni, hodnoceni.problem_id AS problem_id, hodnoceni.body AS body, hodnoceni.cislo_body_id AS cislo_body_id, res.id AS resitel_id
FROM seminar_reseni
JOIN seminar_hodnoceni AS hodnoceni ON seminar_reseni.id = hodnoceni.reseni_id
JOIN seminar_reseni_resitele AS rr ON seminar_reseni.id = rr.reseni_id
JOIN seminar_resitele AS res ON res.id = rr.resitele_id
ORDER BY problem_id, resitel_id, body, cas_doruceni'''
# Po spojeni nekterych problemu se lisi casy doruceni a poznamky, proto je nebudeme kontrolovat (jde v podstate o triviality, tak je to snad jedno)
same_fields = ['forma', 'problem_id', 'body', 'cislo_body_id', 'resitel_id']
renamed_fields = [
#('timestamp', 'cas_doruceni'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query, new_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,old_fields, new_fields)
# Cast 2)
# Query se lisi tim, ze uz nejoinujeme resitele.
old_query = 'SELECT * FROM seminar_reseni ORDER BY id'
new_query = '''SELECT seminar_reseni.id, forma, poznamka, cas_doruceni AS timestamp, h.problem_id AS problem_id, h.body AS body, h.cislo_body_id AS cislo_body_id
FROM seminar_reseni
JOIN seminar_hodnoceni AS h ON h.reseni_id = seminar_reseni.id
ORDER BY id'''
# execute_simple kontroluje stejnost poctu radku, to nechceme.
oldcur.execute(old_query)
newcur.execute(new_query)
old_res, new_res = oldcur.fetchall(), newcur.fetchall()
# Zkontrolujeme, ze pro kazde nove reseni ma stare reseni spravna data.
new_ids = [n['id'] for n in new_res]
spravna_old = list(filter(lambda o: o['id'] in new_ids, old_res))
res = zip(spravna_old,new_res)
for o,n in res:
# Tady by se poznamky i timestampy mely zachovat
# Z nejakeho duvodu se ale poznamky lisi ve whitespace, tak je zkontrolujeme separatne
check_same(o,n,['id', 'forma', 'timestamp', 'problem_id', 'body', 'cislo_body_id'])
old_pozn = o['poznamka'].strip()
new_pozn = n['poznamka'].strip()
if old_pozn != new_pozn:
raise ValueError('Poznamky se lisi pro radky {dict(o)} a {dict(n)}')
def check_organizator():
old_query = 'SELECT * FROM seminar_organizator ORDER BY id'
new_query = '''SELECT seminar_organizator.id AS id, studuje, strucny_popis_organizatora, users.id AS uid, osoba.prezdivka AS o_prezdivka, osoba.foto AS o_foto, organizuje_od, organizuje_do
FROM seminar_organizator
JOIN seminar_osoby AS osoba ON osoba_id = osoba.id
JOIN auth_user AS users ON osoba.user_id = users.id
ORDER BY seminar_organizator.id'''
same_fields = ['studuje', 'strucny_popis_organizatora']
renamed_fields = [
('user_id', 'uid'),
#('prezdivka', 'o_prezdivka'),
('foto', 'o_foto'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query,new_query)
res = zip(old_res, new_res)
for o,n in res:
check_same(o,n,old_fields, new_fields)
# organizuje od, do:
# Migrace prirazuje aktualni casovou zonu, takze chceme tady rucne vynutit CET.
from datetime import timedelta, timezone
cet = timezone(timedelta(hours=1))
if o['organizuje_od_roku'] is None and n['organizuje_od'] is None:
pass
elif o['organizuje_od_roku'] != n['organizuje_od'].astimezone(cet).year:
raise ValueError(f'Not matching organizuje_od for org id={o["id"]}: old {o["organizuje_od_roku"]}, new {n["organizuje_od"]}')
if o['organizuje_do_roku'] is None and n['organizuje_do'] is None:
pass
elif o['organizuje_do_roku'] != n['organizuje_do'].astimezone(cet).year:
raise ValueError(f'Not matching organizuje_do for org id={o["id"]}: old {o["organizuje_do_roku"]}, new {n["organizuje_do"]}')
if o['prezdivka'] == n['o_prezdivka']:
continue
if o['prezdivka'] is None and n['o_prezdivka'] == '':
continue
raise ValueError(f'Not matching prezdivka for org id={o["id"]}: old {o["prezdivka"]}, new {n["o_prezdivka"]}')
def check_rocnik():
old_query = "SELECT * FROM seminar_rocniky ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','prvni_rok', 'rocnik', 'exportovat'])
def check_cislo():
old_query = "SELECT * FROM seminar_cisla ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n, ['id','rocnik_id','cislo', 'datum_vydani','datum_deadline','verejne','poznamka','pdf'],
['id','rocnik_id','poradi','datum_vydani','datum_deadline','verejne','poznamka','pdf'])
def check_priloha_reseni():
old_query = "SELECT * FROM seminar_priloha_reseni"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n, ['id','reseni_id', 'timestamp', 'soubor', 'poznamka'],
['id','reseni_id', 'vytvoreno', 'soubor', 'poznamka'])
def check_soustredeni():
old_query = "SELECT * FROM seminar_soustredeni ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','rocnik_id','datum_zacatku','datum_konce','verejne','misto','text','typ','exportovat'])
#Kontrola ucasnici, organizatori v samostatnych funkcich
def check_soustredeni_ucastnici():
old_query = "SELECT * FROM seminar_soustredeni_ucastnici ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','resitel_id','soustredeni_id','poznamka'])
def check_soustredeni_organizatori():
old_query = "SELECT * FROM seminar_soustredeni_organizatori ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','organizator_id','soustredeni_id','poznamka'])
def check_nastaveni():
old_query = "SELECT * FROM seminar_nastaveni ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','aktualni_cislo_id'])
def check_novinky():
old_query = "SELECT * FROM seminar_novinky ORDER BY id"
old_res, new_res = execute_simple(old_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','datum','text','obrazek','zverejneno'])
if get_user_id_for_org_id(n['autor_id']) != o['autor_id']:
raise ValueError("Nesedi autori u novinek")
def check_pohadka():
old_query = "SELECT * FROM seminar_pohadky ORDER BY id"
new_query = """SELECT sp.id AS id, sp.autor_id AS autor_id, sp.vytvoreno AS vytvoreno, snp.treenode_ptr_id AS treenode_ptr_id, st.na_web AS text,
zn_pred.uloha_id AS uloha_pred, zn_po.uloha_id AS uloha_po
FROM seminar_pohadky AS sp
-- Text pohádky
INNER JOIN seminar_nodes_pohadka AS snp ON sp.id = snp.pohadka_id
INNER JOIN seminar_nodes_treenode AS snt ON snt.id = snp.treenode_ptr_id
INNER JOIN seminar_nodes_obsah AS sno ON sno.treenode_ptr_id = snt.first_child_id
INNER JOIN seminar_texty AS st ON sno.text_id = st.id
-- Predchozí úloha
LEFT OUTER JOIN seminar_nodes_treenode AS ztn_pred ON ztn_pred.succ_id = snt.id
LEFT OUTER JOIN seminar_nodes_uloha_zadani AS zn_pred ON zn_pred.treenode_ptr_id = ztn_pred.id
-- Následující úloha
LEFT OUTER JOIN seminar_nodes_uloha_zadani AS zn_po ON zn_po.treenode_ptr_id = snt.succ_id
ORDER BY sp.id"""
old_res, new_res = execute_simple(old_query,new_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n,['id','timestamp','text'],['id','vytvoreno','text'])
if o['autor_id'] is not None:
if get_user_id_for_org_id(n['autor_id']) != o['autor_id']:
raise ValueError("Nesedi autori u pohadky")
# Správné úlohy
# NOTE: o['pred'] rika, zda je pohadka pred ulohou, nikoliv zda je relevantni uloha pred pohadkou!
spravny_klic = 'uloha_po' if o['pred'] else 'uloha_pred'
if o['uloha_id'] != n[spravny_klic]:
raise ValueError(f"Pohádka přidružená ke špatné úloze! old: {o['uloha_id']}, new: {n[spravny_klic]}, pozice: {spravny_klic}")
# Problémy jsou rozdělené podle typů:
def check_problem_common():
old_query = "SELECT id, nazev, stav, kod, autor_id, text_org, timestamp, typ FROM seminar_problemy ORDER BY id"
new_query = """SELECT sp.id AS id, sp.nazev AS nazev, sp.stav AS stav, sp.kod AS kod, au.id AS autor_id, sp.poznamka AS poznamka, sp.vytvoreno AS vytvoreno
FROM seminar_problemy AS sp
LEFT OUTER JOIN seminar_organizator AS so ON sp.autor_id = so.id
LEFT OUTER JOIN seminar_osoby AS sos ON so.osoba_id = sos.id
LEFT OUTER JOIN auth_user AS au ON sos.user_id = au.id
ORDER BY sp.id"""
same_fields = ['id', 'nazev', 'stav', 'autor_id', 'kod']
renamed_fields = [
('text_org', 'poznamka'),
('timestamp', 'vytvoreno'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query,new_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n, old_fields, new_fields)
# Opravovatelé
# Po staru byli opravovatele organizatori, takze je potreba je dohledat.
old_query = """SELECT seminar_problemy.id, org.id AS opravovatel_id FROM seminar_problemy
JOIN seminar_organizator AS org ON seminar_problemy.opravovatel_id = org.user_id;"""
new_query = "SELECT problem_id, organizator_id FROM seminar_problemy_opravovatele"
# Simple cursors
#oldcur = oldconn.cursor()
oldcur.execute(old_query)
old_results = oldcur.fetchall()
#newcur = newconn.cursor()
newcur.execute(new_query)
new_results = newcur.fetchall()
for oldr in old_results:
if oldr not in new_results:
raise ValueError(f'Opravovatel pair {oldr} not found in new db.')
# Zaměření se vyřeší okometricky (#1186)
def check_uloha():
old_query = "SELECT * FROM seminar_problemy WHERE typ = 'uloha' ORDER BY id"
new_query = """SELECT cislo_zadani_id, cislo_reseni_id, problem_ptr_id, max_body, COALESCE(uzt.na_web, '') AS text_zadani, COALESCE(uvt.na_web, '') AS text_reseni, cislo_deadline_id
FROM seminar_ulohy
-- Problém:
JOIN seminar_problemy AS problem ON problem_ptr_id = problem.id
-- Text zadání:
-- ZadaniNode a VzorakNode maji existovat vzdy, ale obsah nemusi (pokud ho nemaji)
INNER JOIN seminar_nodes_uloha_zadani AS uzn ON problem.id = uzn.uloha_id
INNER JOIN seminar_nodes_treenode AS uztn ON uztn.id = uzn.treenode_ptr_id
LEFT OUTER JOIN seminar_nodes_obsah AS uzo ON uzo.treenode_ptr_id = uztn.first_child_id
LEFT OUTER JOIN seminar_texty AS uzt ON uzo.text_id = uzt.id
-- Text vzoráku:
INNER JOIN seminar_nodes_uloha_vzorak AS uvn ON problem.id = uvn.uloha_id
INNER JOIN seminar_nodes_treenode AS uvtn ON uvtn.id = uvn.treenode_ptr_id
LEFT OUTER JOIN seminar_nodes_obsah AS uvo ON uvo.treenode_ptr_id = uvtn.first_child_id
LEFT OUTER JOIN seminar_texty AS uvt ON uvo.text_id = uvt.id
ORDER BY problem_ptr_id"""
same_fields = ['cislo_zadani_id', 'cislo_reseni_id', 'text_zadani', 'text_reseni']
renamed_fields = [
('id', 'problem_ptr_id'),
('body', 'max_body'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query, new_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n, old_fields, new_fields)
# Datum deadline vypadá prázdně, tak to budeme předpokládat.
if n['cislo_deadline_id'] is not None:
raise ValueError("Úloha má deadline.")
def check_tema():
old_query = """SELECT text_zadani, text_reseni, typ, c.rocnik_id AS rocnik_id
FROM seminar_problemy
LEFT OUTER JOIN seminar_cisla AS c ON c.id = cislo_zadani_id
WHERE typ IN ('tema', 'serial')
ORDER BY seminar_problemy.id"""
new_query = """SELECT tema_typ, COALESCE(zad_text.na_web, '') AS text_zadani, COALESCE(res_text.na_web, '') AS text_reseni, rn.rocnik_id AS rocnik_id
FROM seminar_temata
-- Problém:
JOIN seminar_problemy AS problem ON problem_ptr_id = problem.id
-- Text:
-- TvCNode dva potomky, oba TextNode. První drží původní text zadání, druhý řešení.
INNER JOIN seminar_nodes_temavcisle AS tvcn ON tvcn.tema_id = id
INNER JOIN seminar_nodes_treenode AS ttn ON tvcn.treenode_ptr_id = ttn.id
LEFT OUTER JOIN seminar_nodes_treenode AS zad_tn ON ttn.first_child_id = zad_tn.id -- jen 33 z nich ma zadani
LEFT OUTER JOIN seminar_nodes_treenode AS res_tn ON zad_tn.succ_id = res_tn.id -- jen 4 z nich ma reseni
LEFT OUTER JOIN seminar_nodes_obsah AS zad_on ON zad_on.treenode_ptr_id = zad_tn.id
LEFT OUTER JOIN seminar_nodes_obsah AS res_on ON res_on.treenode_ptr_id = res_tn.id
LEFT OUTER JOIN seminar_texty AS zad_text ON zad_on.text_id = zad_text.id
LEFT OUTER JOIN seminar_texty AS res_text ON res_on.text_id = res_text.id -- vsechny 4
-- Ročník tématu:
-- Podle rootu TvCN
LEFT OUTER JOIN seminar_nodes_rocnik AS rn ON ttn.root_id = rn.treenode_ptr_id
ORDER BY problem_ptr_id"""
same_fields = ['text_zadani', 'text_reseni', 'rocnik_id']
renamed_fields = [
('typ', 'tema_typ'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query, new_query)
res = zip(old_res,new_res)
for o,n in res:
check_same(o,n, old_fields, new_fields)
def check_konfera():
old_query = "SELECT * FROM seminar_problemy WHERE typ = 'konfera'"
new_query = "SELECT * FROM seminar_konfera"
oldcur.execute(old_query)
newcur.execute(new_query)
if oldcur.rowcount != 0 or newcur.rowcount != 0:
raise ValueError('There exists a Konfera!')
def check_org_clanek():
old_query = "SELECT * FROM seminar_problemy WHERE typ = 'org-clanek'"
oldcur.execute(old_query)
if oldcur.rowcount != 0:
raise ValueError('There exists a Org-clanek!')
def check_res_clanek():
# Dva(!) články mají text (zadání), který se má zachovat.
old_query = "SELECT * FROM seminar_problemy WHERE typ = 'res-clanek' ORDER BY id"
new_query = """SELECT cislo_id, text.na_web AS text_zadani
FROM seminar_clanky
JOIN seminar_problemy AS problem ON problem_ptr_id = problem.id
INNER JOIN seminar_hodnoceni AS hodn ON problem.id = hodn.problem_id
INNER JOIN seminar_reseni AS rese ON rese.id = hodn.reseni_id
INNER JOIN seminar_nodes_otistene_reseni AS rn ON rese.text_cely_id = rn.treenode_ptr_id -- Tenhle radek neni potreba, ale ujistuje se mj. o spravnem typu TreeNode.
INNER JOIN seminar_nodes_treenode AS tn ON rn.treenode_ptr_id = tn.id
-- Nektere clanky vubec nemely text, tak jim migr 0058 nevyrobila dalsi treenody
LEFT OUTER JOIN seminar_nodes_obsah AS son ON son.treenode_ptr_id = tn.first_child_id
LEFT OUTER JOIN seminar_texty AS text ON text.id = son.text_id
ORDER BY problem_ptr_id"""
same_fields = ['text_zadani']
renamed_fields = [
('cislo_zadani_id', 'cislo_id'),
]
old_fields = same_fields + [f[0] for f in renamed_fields]
new_fields = same_fields + [f[1] for f in renamed_fields]
old_res, new_res = execute_simple(old_query, new_query)
res = zip(old_res,new_res)
for o,n in res:
# text_zadani po novu mohl byt None
if n['text_zadani'] is None:
n['text_zadani'] = ''
check_same(o,n, old_fields, new_fields)
assert(o['text_reseni'] == '')
def check_untyped_problem():
old_query = "SELECT * FROM seminar_problemy WHERE typ NOT IN ('uloha', 'tema', 'serial', 'konfera', 'org-clanek', 'res-clanek')"
oldcur.execute(old_query)
if oldcur.rowcount != 0:
raise ValueError('There exists a Problem without type!')
check_skola()
check_resitel()
check_reseni()
check_organizator()
check_rocnik()
check_cislo()
check_priloha_reseni()
check_soustredeni()
check_soustredeni_ucastnici()
check_soustredeni_organizatori()
check_nastaveni()
check_novinky()
check_pohadka()
check_problem_common()
check_uloha()
check_tema()
check_konfera()
check_org_clanek()
check_res_clanek()
check_untyped_problem()

View file

@ -1,21 +0,0 @@
#!/bin/bash
set -u
deactivate || true
cd /akce/mam/www/mamweb-test/
make sync_test
systemctl --user stop mamweb-test.service
rm -rvf env
make install_venv
. env/bin/activate
make install
deploy_v2/pre_migration.py
make deploy_test
./manage.py loaddata data/*
systemctl --user start mamweb-test.service
./manage.py generate_thumbnails
echo 'Et voilá!'
echo 'Nezapomeň opravit práva pro sitetree!'

View file

@ -1,14 +0,0 @@
Milí řešitelé M&M,
web M&Mka dostal nový kabátek a zároveň se v něm objevilo odevzdávátko. Navíc,
pokud se zaregistrujete pod e-mailem, na který jsme vám poslali tuhle zprávu,
uvidíte rovnou i body za svá dosud odevzdaná řešení.
Web budeme i nadále vylepšovat, a zajímá nás i jak to vidíte vy. Pokud byste
na webu našli nějaký nedostatek, nebo nám prostě jen chtěli napsat, nebojte se
k tomu použít adresu mam@matfyz.cz.
Těšíme se na vaše řešení! (Připomínáme termín pro účast na soustředění 21. září.)
Za časopis M&M,
Pavel Turinský

View file

@ -1,3 +0,0 @@
Jsou špatně práva k sitetree (protože se používají primární klíče, které jsou jiné :-()
Špatné položky se dají najít pomocí následujícího příkazu:
grep -E 'access_permissions": \[$' data/sitetree.json -A17 | grep -E 'acc|tit' -A2

View file

@ -1,42 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import django
#### Inicializace Djanga
sys.path.append(os.path.dirname(os.path.realpath(__file__))+'/..')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mamweb.settings')
django.setup()
## Pozor, nejde pouzit ORM, protoze kod je na jine verzi nez databaze a nejde namigrovat.
from django.db import connection
def smaz_zle_clanky():
# Tyhle clanky vubec nejsou clanky, bude potreba je udelat cele jinak a spravne.
#m.Problem.objects.filter(id__in=[1981, 1970, 2222]).delete()
## with connection.cursor() as cursor:
## # Nejdriv musime smazat reseni:
## cursor.execute('DELETE FROM seminar_reseni WHERE problem_id IN (1981, 1970, 2222);')
## # Nakonec i ty clanky samotne
## cursor.execute('DELETE FROM seminar_problemy WHERE id IN (1981, 1970, 2222);')
# Update: stejně je v DB bordel, tak z nich prostě jen udělám témata a všechno zhruba přežije…
with connection.cursor() as cursor:
cursor.execute("UPDATE seminar_problemy SET typ = 'tema' WHERE id IN (1981, 1970, 2222);")
def smaz_divne_uzivatele():
# U techto uzivatelu neexistuje Organizator s nimi spojeny
# Takze pak delaji akorat neporadek
with connection.cursor() as cursor:
# Jeste je potreba zrusit vazby
cursor.execute('UPDATE django_comments SET user_id = NULL WHERE user_id = 34;')
cursor.execute('UPDATE seminar_problemy SET autor_id = NULL WHERE autor_id = 34;')
cursor.execute('DELETE FROM django_admin_log WHERE user_id = 34;')
cursor.execute('DELETE FROM auth_user_groups WHERE user_id = 34;')
cursor.execute('DELETE FROM auth_user WHERE id IN (34, 40, 30, 50, 54, 58, 43);')
smaz_zle_clanky()
smaz_divne_uzivatele()

View file

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

View file

@ -1,27 +0,0 @@
CSS (a další styly na webu)
===========================
Inspirován `css-trick článkem <https://css-tricks.com/methods-organize-css/>`_ jsem se rozhodl rozdělit
CSSka do
- Konstant (``constants.css``), které jsou využívány na mnoha místech CSSek
- Nastylování html tagů (``base.css``)
- Layoutu (``layout.css``), což je to, co určuje celkové rozložení stránky
- Jednotlivých prvků (``modules.css``)
Dále jsem separoval CSSka pro **galerii** (potřebuje hodně specifických stylů). Stejně tak **korekturovátko** má styly separátně.
Dále web (asi) používá externí frameworky (v separátních složkách mají k sobě i JS a podobné věci):
- bootstrap: dělá nějaké basic stylování, *web je na něm hodně závislý* (například jsem zjistil, že bootstrap kdysi přidával ``font-size:14px``, bez čehož se web úplně rozpadnul) (také na něm běží mobilní meníčko, které navíc vyžaduje Popper, tedy bootstrap.bundle.js místo bootstrap.js)
Pak jsou tu ``mamweb-dev.css`` a ``printtable.css``, co jsem si ještě nerozmyslel, co s tím.
Pár myšlenek
------------
- Až na pár výjimek (galerii a korekturovátko) bych styly držel v jedné složce a málo souborech,
protože CSS šíleně dědí všechno možné
- Chce to dobře pojmenovávat třídy (speciálně aby bylo vidět, co ta třída dělá nebo kde se používá)
- Chce to hodně komentovat kód (speciálně tam, kde není splněn předchozí bod)

View file

@ -9,6 +9,12 @@ static
------
Složka, kam django nakopíruje všechno ze složek static a pak na to z templatů / kódu jde ukazovat pomocí ``static``.
_git_hooks
----------
Hooky do gitu pro kontrolu Pythoního stylu. Především ``flake8``.
Zbylo tu z minulosti mamwebu.
data
----
Obsahuje data, která patří do databáze, ale jsou přímo součástí webu jako
@ -25,9 +31,10 @@ nebo (v případě meníčka)::
./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json
./fix_json.py data/sitetree_new.json data/sitetree.json
deploy_v2
---------
Věci, které byly potřeba při nasazování nového (2021) webu.
nebo (v případě práv)::
./manage.py dumpdata auth.group --natural-foreign > data/prava_skupin_new.json
./fix_json.py data/prava_skupin_new.json data/prava_skupin.json
docs
----

View file

@ -27,10 +27,8 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve
:titlesonly:
vyvoj
zavislosti
sphinx
skripty
zkratky
modules/modules
dalsi_soubory
zapisy/zapisy

View file

@ -1,4 +1,4 @@
FIXME přepsat do rst, přidat i další věci a případně přesunout na wiki
TODO přepsat do rst případně přesunout na wiki
Přidání obrázků do odměn:
admin -> flatpage odměn -> ikona přidat obrázek
záložka odeslat, vybrat obrázek, odeslat

View file

@ -1,15 +1,13 @@
Sphinx na našem webu
====================
Dokumentace se zkompiluje příkazem ``make html`` ve složce ``docs``. (Musíte mít zapnutý virtualenv)
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

@ -0,0 +1,25 @@
.. Není odkázaná z menu, je to záměr
Tabulka prerekvizit v různých distribucích
=========
.. admonition:: Metodika
Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme
``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme
přiinstalovali. Protože větev ``makefiles`` aktuálně není mergenutá do
masteru, nefunguje synchronizace flatpages (a stejně nemáme SSH klíč), takže
tam ``make/init_local`` sestřelíme a vyzkoušíme, že ``make/test`` spustí
testy.
.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV…
.. csv-table:: Prerekvizity v jednotlivých distribucích
:header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky
Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?"
Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, ""
Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)"
openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config."
Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu."

View file

@ -37,7 +37,7 @@ Kromě toho je potřeba mít účet na `Gitee <https://gitea.ks.matfyz.cz>`_, kd
bydlí gitový repozitář s kódem.
.. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce
prerekvizit <Alternativní jména balíčků>`.
prerekvizit <Tabulka prerekvizit v různých distribucích>`.
Doporučené
^^^^^^^^^^

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 (`seminar.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,97 +0,0 @@
Závislosti webu
@@@@@@@@@@@@@@@
Web ke svému běhu potřebuje různé další programy. Tahle stránka se snaží je pokrýt.
Stránka je koncipována jako odrážkový seznam balíčků pro Ubuntu s případnými
komentáři, na konci stránky jsou uvedena :ref:`jména balíčků <Alternativní jména
balíčků>` v různých dalších distribucích. (Seznam mj. cílí na lokální
rozchození, proto popisuji Ubuntu a ne Debian. I tak se ale snažíme popsat web
v úplnosti.)
.. I use Arch, btw.
Základ webu
===========
- ``python3`` Ideálně Python 3.9, jenž je na Gimlim
- ``python3-pip`` pro instalaci dalších Pythoních balíčků podle ``requirements.txt``
- ``python3-venv``
- ``gcc`` kompilace Pythoních knihoven ze zdrojových distribucí (sdist), možná (neotestováno) jde jako alternativu použít ``python3-wheel`` a stahovat bdists
- ``python3-dev`` taktéž
- ``libpq-dev`` do třetice…
- ``ghostscript`` TODO konverze PDF v korekturovátku
- ``pdflatex`` FIXME! generování obálek a stvrzenek
- ``git`` používán :ref:`Make skripty`
- ``locales`` pro české formáty
Nasazení na produkci / testweb
==============================
(nejsou nutně potřeba k provozu lokální instance)
- ``rsync``
- ``pg_utils`` FIXME
- ``htpasswd`` FIXME aby testweb nepoužívali náhodní kolemjdoucí
- ``postgresql-server`` TODO
- ``acl`` pro nastavování práv přes ``setfacl``
Pro testweb je potřeba i všechno pro :ref:`dokumentaci <Dokumentace>`, vizte níž.
Předpokládá se nasazení v uWSGI pod Nginxem a služba běžící pod systemd, nicméně to už je spíš záležitost infrastruktury a ne specifikum mamwebu.
Dokumentace
===========
- ``make`` pro zbuildění
- Pythoní balíčky podle příslušné části ``requirements.txt``
Vývojové nástroje
=================
(Nejsou nezbytně nutné, ale předpokládáme jejich užitečnost. Mohou se hodit i na produkci.)
- ``psql`` TODO pro manuální dotazy do PostgreSQL
- ``sqlite3`` TODO totéž pro SQLite3
- ``ssh``
- ``graphviz`` pro vygenerování schématu
- ``rsync``
- ``ipython3`` hezčí interaktivní shell (stačí z ``requirements.txt``)
Potenciální usnadnění života
============================
(Úplně zbytečné, ale sdílíme pozitivní zkušenosti :-))
- ``tea`` CLI klient pro Giteu, aby člověk nepotřeboval otevírat web pro založení PR
Alternativní jména balíčků
==========================
Různé distribuce balí SW různě, takže to, co je v jedné distribuci jeden
balíček může být v jiné rozděleno do víc. Pro usnadnění nasazení je tady
přehled známých alternativních jmen.
TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte.
.. admonition:: Jak se pozná, že web funguje, pro účely tabulky?
Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme
``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme
přiinstalovali. Protože nefunguje synchronizace flatpages (nemáme SSH klíč),
``make/init_local`` sestřelíme při pokusu o synchronizaci a vyzkoušíme, že
``make/test`` spustí testy.
.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV…
.. csv-table:: Prerekvizity v jednotlivých distribucích
:header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky
Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?"
Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, ""
Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)"
openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config."
Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu."

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

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

View file

@ -1,4 +1,6 @@
from django import forms
from soustredeni.models import Soustredeni
class KomentarForm(forms.Form):
komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False)

View file

@ -0,0 +1,29 @@
# Generated by Django 2.2.28 on 2023-08-09 19:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('galerie', '0010_auto_20200819_0947'),
('soustredeni', '0001_initial'),
]
run_before = [
('seminar', '0121_smazani_soustredeni'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='galerie',
name='soustredeni',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='soustredeni.Soustredeni'),
),
],
database_operations=[],
),
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.11 on 2024-04-30 21:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('galerie', '0010_auto_20200819_0947'),
]
operations = [
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.2.11 on 2024-05-01 13:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('soustredeni', '0001_split_from_seminar'),
('galerie', '0011_pre_split_soustredeni'),
]
operations = [
migrations.AlterField(
model_name='galerie',
name='soustredeni',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='soustredeni.soustredeni'),
),
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.11 on 2024-05-01 13:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('galerie', '0012_soustredeni_relink'),
('soustredeni', '0003_post_split_soustredeni'),
]
operations = [
]

View file

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

View file

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

View file

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

View file

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

View file

@ -1,191 +0,0 @@
@charset "utf-8"; /* vynuť utf-8 */
/* Galerie */
/* velká fotka */
/* zmenšování spolu s oknem prohlížeče */
.galerie .obrazek, .titulni_obrazek {
max-width: 100%;
height: auto;
width: auto\9; /* ie8 */
}
.predchozi_obrazek{
position: absolute;
z-index: 1;
width: 33%;
height: 100%;
left: 0;
top: 0;
}
.predchozi_obrazek:hover{
background-image: url("/static/galerie/prvky/predchozi.svg");
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
background-position: left center;
background-repeat: no-repeat;
}
.dalsi_obrazek{
position: absolute;
z-index: 1;
width: 33%;
height: 100%;
left: 67%;
top: 0;
}
.dalsi_obrazek:hover{
background-image: url("/static/galerie/prvky/dalsi.svg");
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
background-position: right center;
background-repeat: no-repeat;
}
.galerie {
position: relative;
text-align: center;
margin: 20px auto 0 auto;
}
.galerie h1 {
text-align: center;
}
.galerie_hlavicka {
margin: 30px auto 30px auto;
}
.popis {
margin: 10px 10px 30px 0px;
text-align: center;
}
#nahoru {
text-align: center;
}
/* titulní obrázek hlavní galerie soustředění */
.galerie_nahledy{
/*margin: 1em 0;*/
margin: auto;
padding: 10px;
text-align: center;
overflow: auto;
}
.galerie_nahledy img {
margin: 10px;
}
.galerie_nahledy div.navigace {
display: inline-block;
}
.galerie_nahled, .podgalerie_nahled { /* frame */
display: block;
position: relative;
float: left;
width: 200px;
height: 200px;
text-align: center;
border: solid;
border-width: 1px;
border-radius: 4px;
border-color: var(--svetla-oranzova);
background-color: var(--barva-pozadi);
white-space: nowrap;
margin: 10px;
font-weight: bold;
}
.galerie_nahled:hover, .podgalerie_nahled:hover {
background-color: var(--svetla-oranzova);
filter: drop-shadow(0px 5px 5px rgba(0, 0, 0, 0.4));
color: var(--tmava-oranzova);
}
.vystredeno{ /* helper */
display: inline-block;
height: 100%;
vertical-align: middle;
}
.galerie_nahled img {
vertical-align: middle;
max-height: 180px;
max-width: 180px;
}
.galerie_nahled div {
position: absolute;
bottom: 0px;
width: 100%;
text-align: center;
}
.podgalerie_nahled img {
margin-top: 20px;
margin-bottom: 15px;
max-height: 125px;
max-width: 167px;
}
.podgalerie_nahled .nazev_galerie {
position: absolute;
width: 100%;
top: 160px;
}
.podgalerie_nahled.mam-org-only, .podgalerie_nahled.mam-resitel-only {
margin: 10px;
padding: 0;
}
/* Odkazy na předchozí a následující podgalerii */
.galerie_predchozi_nasledujici {
overflow: auto;
margin: 10px auto 10px auto;
}
.galerie_predchozi_nasledujici .predchozi {
float: left;
}
.galerie_predchozi_nasledujici .nasledujici {
float: right;
}
/* posune kotvu obrázku v galerii o oranžový pruh dolu, aby se pod ním obrázek neschovával */
/* https://stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjust-for-fixed-header */
.kotva_obrazku {
position: absolute;
width: 0;
height: 55px; /* viz #title */
margin-top: -55px; /* viz #title */
}
@media(max-width: 860px) {
.kotva_obrazku {
height: 3em; /* #FIXME nemám páru, jak zjistit výšku toho elementu */
margin-top: -3em; /* #FIXME */
}
}
/* plus a minus tlacitka */
.mam-org-only-galerie {
background: var(--orgovska-svetla-fialova);
padding: 10px;
margin: 10px 10px 10px -20px;
border: #333 2px dashed;
float: left;
}
.mam-org-only-galerie a{
padding: 3px 5px;
margin: 5px;
border-radius: 20px;
background-color: var(--tmava-oranzova);;
color: var(--barva-pozadi);
float: left;
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
{% extends "galerie/base.html" %}
{% extends "base.html" %}
{% block content %}

View file

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

View file

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

View file

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

View file

@ -3,4 +3,3 @@ from django.apps import AppConfig
class HeaderFotkyConfig(AppConfig):
name = 'header_fotky'
verbose_name = 'Fotky v záhlaví'

View file

@ -1,8 +1,3 @@
"""
Context processory lze přidat do djanga v :mod:`~mamweb.settings` a dělají to,
že do contextu (tj. to, z čeho se např. berou proměnné v templatech) libovolné
stránky přidají další věci.
"""
from datetime import datetime, date
import random

View file

@ -1,10 +1,12 @@
from django.conf import settings
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
class KorekturovanePDFAdmin(VersionAdmin):
"""
nastaví čas vložení (:attr:`~koretkury.models.KorekturovanePDF.cas`) a počet
@ -25,13 +27,12 @@ class KorekturovanePDFAdmin(VersionAdmin):
fieldsets = [
(None,
{'fields':
['pdf', 'cas', 'stran', 'nazev', 'orgove', 'komentar', 'poslat_mail']}),
['pdf', 'cas', 'org', 'stran', 'nazev', 'komentar', 'poslat_mail']}),
# (u'PDF', {'fields': ['pdf']}),
]
list_display = ['nazev', 'cas', 'stran']
list_display = ['nazev', 'cas', 'stran', 'org']
list_filter = []
search_fields = []
autocomplete_fields = ['orgove']
def save_model(self, request, obj, form, change):
"""
@ -40,8 +41,8 @@ class KorekturovanePDFAdmin(VersionAdmin):
super().save_model(request, obj, form, change)
if not change and obj.poslat_mail: # Je nový a má se poslat mail
odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': obj.id}))
odesilatel = 'korekturovatko-nove-pdf@mam.mff.cuni.cz'
prijemce = 'org@mam.mff.cuni.cz'
odesilatel = settings.KOREKTURY_NOVE_PDF_EMAIL
prijemce = settings.KONFERA_ORGOVE_EMAIL
predmet = f'Nové korektury: {obj.nazev}'
text = f'''\
V korekturovátku se objevil nový soubor: {obj.nazev}
@ -62,11 +63,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,11 +0,0 @@
from django.urls import path
from personalni.utils import org_required
from . import views
urlpatterns = [
path('<int:pdf_id>/stav', org_required(views.korektury_stav_view), name='korektury_api_pdf_stav'),
path('oprava/stav', org_required(views.oprava_stav_view), name='korektury_api_oprava_stav'),
path('<int:pdf_id>/opravy_a_komentare', org_required(views.opravy_a_komentare_view), name='korektury_api_opravy_a_komentare'),
path('oprava/smaz', org_required(views.oprava_smaz_view), name='korektury_api_oprava_smaz'),
path('komentar/smaz', org_required(views.komentar_smaz_view), name='korektury_api_komentar_smaz'),
]

View file

@ -1,134 +0,0 @@
from http import HTTPStatus
from django.http import JsonResponse, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.html import linebreaks
from rest_framework import serializers
from korektury.utils import send_email_notification_komentar
from korektury.models import Oprava, KorekturovanePDF, Komentar, KorekturaTag
from personalni.models import Organizator
def korektury_stav_view(request, pdf_id: int, **kwargs):
q = request.POST
pdf = get_object_or_404(KorekturovanePDF, id=pdf_id)
status = q.get('state')
if status is not None:
assert status in KorekturovanePDF.STATUS.values
pdf.status = status
pdf.save()
return JsonResponse({'status': pdf.status})
def oprava_stav_view(request, **kwargs):
q = request.POST
op_id_str = q.get('id')
assert op_id_str is not None
op_id = int(op_id_str)
op = get_object_or_404(Oprava, id=op_id)
status = q.get('action')
if status is not None:
assert status in Oprava.STATUS.values
op.status = status
op.save()
return JsonResponse({'status': op.status})
def oprava_smaz_view(request, **kwargs):
q = request.POST
op_id_str = q.get('oprava_id')
assert op_id_str is not None
op_id = int(op_id_str)
oprava = get_object_or_404(Oprava, id=op_id)
oprava.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
def komentar_smaz_view(request, **kwargs):
q = request.POST
kom_id_str = q.get('komentar_id')
assert kom_id_str is not None
kom_id = int(kom_id_str)
komentar = get_object_or_404(Komentar, id=kom_id)
komentar.delete()
return HttpResponse(status=HTTPStatus.NO_CONTENT)
class KomentarSerializer(serializers.ModelSerializer):
class Meta:
model = Komentar
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["autor"] = str(instance.autor)
ret["text"] = linebreaks(ret["text"], autoescape=True) # Autora není třeba escapovat, ten se vkládá jako text.
return ret
class KorekturaTagSerializer(serializers.ModelSerializer):
class Meta:
model = KorekturaTag
fields = '__all__'
class OpravaSerializer(serializers.ModelSerializer):
class Meta:
model = Oprava
fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["komentare"] = [KomentarSerializer(komentar).data for komentar in instance.komentar_set.all()]
ret["tagy"] = [KorekturaTagSerializer(tag).data for tag in instance.tagy.all()]
return ret
# komentar_set = serializers.ListField(child=KomentarSerializer())
def opravy_a_komentare_view(request, pdf_id: int, **kwargs):
if request.method == 'POST':
q = request.POST
x = int(q.get('x'))
y = int(q.get('y'))
img_id = int(q.get('img_id'))
oprava_id = int(q.get('oprava_id'))
komentar_id = int(q.get('komentar_id'))
text = q.get('text')
# prirazeni autora podle prihlaseni
autor_user = request.user
# pokud existuje ucet (user), ale neni to organizator = 403
autor = Organizator.objects.filter(osoba__user=autor_user).first()
if komentar_id != -1:
komentar = get_object_or_404(Komentar, id=komentar_id)
if komentar.text != text:
komentar.text = text
komentar.autor = autor
komentar.save()
oprava = komentar.oprava
else:
if oprava_id != -1:
oprava = get_object_or_404(Oprava, id=oprava_id)
else:
pdf = get_object_or_404(KorekturovanePDF, id=pdf_id)
oprava = Oprava.objects.create(
pdf=pdf,
strana=img_id,
x=x,
y=y,
)
Komentar.objects.create(oprava=oprava, autor=autor, text=text)
send_email_notification_komentar(oprava, autor, request)
tagy_raw = q.get('tagy')
if tagy_raw is not None:
oprava.tagy.clear()
if tagy_raw != "":
tagy = list(map(int, tagy_raw.split(",")))
oprava.tagy.add(*KorekturaTag.objects.filter(id__in=tagy))
opravy = Oprava.objects.filter(pdf=pdf_id).all()
# Serializovat list je prý security vulnerability, tedy je přidán slovník pro bezpečnost
return JsonResponse({"context": [OpravaSerializer(oprava).data for oprava in opravy]})

14
korektury/forms.py Normal file
View file

@ -0,0 +1,14 @@
from django import forms
class OpravaForm(forms.Form):
""" formulář k přidání opravy (:class:`korektury.models.Oprava`) """
text = forms.CharField(max_length=256)
autor = forms.CharField(max_length=20)
x = forms.IntegerField()
y = forms.IntegerField()
scroll = forms.CharField(max_length=256)
pdf = forms.CharField(max_length=256)
img_id = forms.CharField(max_length=256)
id = forms.CharField(max_length=256)
action = forms.CharField(max_length=256)

View file

@ -0,0 +1,39 @@
# Generated by Django 2.2.28 on 2023-07-31 17:54
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('korektury', '0020_lepsi_popis_nazvu_PDF_v_adminu'),
('personalni', '0002_initial'),
]
run_before = [
('seminar', '0116_smazani_personalniho'),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.AlterField(
model_name='komentar',
name='autor',
field=models.ForeignKey(blank=True, help_text='Autor komentáře', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.Organizator'),
),
migrations.AlterField(
model_name='korekturovanepdf',
name='org',
field=models.ForeignKey(blank=True, default=None, help_text='Zodpovědný organizátor za obsah', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.Organizator'),
),
migrations.AlterField(
model_name='oprava',
name='autor',
field=models.ForeignKey(blank=True, help_text='Autor opravy', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.Organizator'),
),
],
database_operations=[],
),
]

View file

@ -1,13 +0,0 @@
# Generated by Django 4.2.8 on 2024-03-12 20:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('korektury', '0020_lepsi_popis_nazvu_PDF_v_adminu'),
]
operations = [
]

View file

@ -1,30 +0,0 @@
# Generated by Django 4.2.11 on 2024-03-19 21:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('personalni', '0003_initial'),
('korektury', '0021_auto_20240312_2124'),
]
operations = [
migrations.AlterField(
model_name='komentar',
name='autor',
field=models.ForeignKey(blank=True, help_text='Autor komentáře', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.organizator'),
),
migrations.AlterField(
model_name='korekturovanepdf',
name='org',
field=models.ForeignKey(blank=True, default=None, help_text='Zodpovědný organizátor za obsah', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.organizator'),
),
migrations.AlterField(
model_name='oprava',
name='autor',
field=models.ForeignKey(blank=True, help_text='Autor opravy', null=True, on_delete=django.db.models.deletion.SET_NULL, to='personalni.organizator'),
),
]

View file

@ -1,14 +0,0 @@
# Generated by Django 4.2.11 on 2024-03-26 21:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('korektury', '0022_alter_komentar_autor_alter_korekturovanepdf_org_and_more'),
('personalni', '0005_personalni_post_migrate'),
]
operations = [
]

View file

@ -1,41 +0,0 @@
# Generated by Django 4.2.13 on 2024-06-11 23:53
from django.db import migrations, models
def pridej_orgy(apps, schema_editor):
PDF = apps.get_model('korektury', 'KorekturovanePDF')
for pdf in PDF.objects.all(): # Tohle by asi mělo jít udělat pomocí update, ale moc práce a rychlé hledání taky nepomohlo.
if pdf.org is not None: pdf.orgove.add(pdf.org)
pdf.save() # ig?
def vyber_orga(apps, schema_editor):
PDF = apps.get_model('korektury', 'KorekturovanePDF')
for pdf in PDF.objects.all():
orgove = pdf.orgove.all()
if len(orgove) > 1:
raise migrations.exceptions.IrreversibleError(f'PDF {pdf.id} má víc než jednoho zodpovědného orga, nejde odmigrovat na verzi, která umí jen jednoho.')
if len(orgove) == 0:
pdf.org = None
else:
pdf.org = orgove[0]
pdf.save()
class Migration(migrations.Migration):
dependencies = [
('personalni', '0011_osloveni_vsechny_choices'),
('korektury', '0023_personalni_post_migrate'),
]
operations = [
migrations.AddField(
model_name='korekturovanepdf',
name='orgove',
field=models.ManyToManyField(blank=True, default=None, help_text='Zodpovědní organizátoři za obsah (chodí jim maily o nových korekturách)', to='personalni.organizator'),
),
migrations.RunPython(pridej_orgy, vyber_orga),
migrations.RemoveField(
model_name='korekturovanepdf',
name='org',
),
]

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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
@ -30,38 +26,42 @@ def generate_filename(self, filename):
clean)
return os.path.join(settings.KOREKTURY_PDF_DIR, fname)
#@reversion.register(ignore_duplicates=True)
class KorekturovanePDF(models.Model):
class Meta:
ordering = ['-cas']
db_table = 'korekturovane_cislo'
verbose_name = 'PDF ke korekturování'
verbose_name_plural = 'PDFka ke korekturování'
verbose_name = u'PDF k opravám'
verbose_name_plural = u'PDF k opravám'
#Interní ID
id = models.AutoField(primary_key = True)
cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF')
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. „42.6 | analýza v4“ nebo „propagace | letáček v0“) korekturovaného PDF, v seznamu se seskupují podle textu do první mezery, tedy zde „42.6“ respektive „propagace“.')
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF')
komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)')
pdf = models.FileField(u'PDF', upload_to = generate_filename)
orgove = models.ManyToManyField(Organizator, blank=True,
help_text='Zodpovědní organizátoři za obsah (chodí jim maily o nových korekturách)',
default=None)
org = models.ForeignKey(Organizator, blank=True,
help_text='Zodpovědný organizátor za obsah',
null=True, default=None, on_delete=models.SET_NULL)
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.',
@ -129,17 +129,6 @@ class KorekturovanePDF(models.Model):
return nazev_split[0] # + " " + nazev_split[2]
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)
@ -160,22 +149,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)
text = models.TextField(u'text opravy',blank = True, help_text='Text opravy')
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',
)
# 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 +200,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

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 270 B

View file

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

Before

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 617 B

View file

@ -1,220 +1,136 @@
.textzanaseni { display:none; }
.textzastarale { display:none; }
#prekomentar, #prekorektura, #prepointer { display: none; }
body {
&[data-stav_pdf="pridavani"] {
background: #f3f3f3;
}
&[data-stav_pdf="zanaseni"] {
background: yellow;
.textzanaseni { display: unset; }
}
&[data-stav_pdf="zastarale"] {
background: red;
.textzastarale { display: unset; }
}
body,
.adding{
background: #f3f3f3;
color: black;
}
#sbal-korektury, #rozbal-korektury {
float: right;
margin-left: 4pt;
.comitting
{
background: yellow;
}
.deprecated {
background: red;
}
img{background:white;}
/* Barvy korektur */
[data-stav_korektury="k_oprave"] {
.k_oprave {
--rgb: 255, 0, 0;
[value="k_oprave"] { display: none }
.komentovat_disabled { display: none }
}
[data-stav_korektury="opraveno"] {
.opraveno {
--rgb: 0, 0, 255;
[value="opraveno"] { display: none }
.komentovat { display: none }
}
[data-stav_korektury="neni_chyba"] {
.neni_chyba {
--rgb: 128, 128, 128;
[value="neni_chyba"] { display: none }
.komentovat { display: none }
}
[data-stav_korektury="k_zaneseni"] {
.k_zaneseni {
--rgb: 0, 255, 0;
[value="k_zaneseni"] { display: none }
.komentovat { display: none }
}
/* Skrývání korektur */
[data-korektura_sbalena="true"] {
.korektura-telo { display: none; }
.korektura-tlacitka { display: none; }
.sbal-rozbal-img { transform: rotate(180deg); }
}
/* Skrývání komentářů */
[data-komentar_sbalen="true"] {
.sbal-rozbal-img { transform: rotate(180deg); }
.uprav-komentar { display: none; }
.komtext { display: none; }
}
/* Čára od textu k místu korektury */
.pointer-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-hover="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á */
.korektura {
.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; }
.hlavicka-komentare {
overflow: auto;
}
.autor {
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 */
#korekturovaci-formular-div {
.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-vybran="false"] { background: unset !important; }
/*&[data-vybran="true"] { border-color: unset !important; }*/
}
/* Šipky na posouvání korektur */
#korektury-sipky {
position: fixed;
bottom: 5px;
left: 5px;
opacity: 50%;
button, img {
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;
}
/* Tlačítko na aktualizaci */
#korektury-aktualizace {
background-color: #e84e10;
}
.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

@ -0,0 +1,283 @@
function place_comments_one_div(img_id, comments)
{
var img = document.getElementById(img_id);
if( img == null ) {
return;
}
var par = img.parentNode;
var w = img.clientWidth;
var h = img.clientHeight;
var w_skip = 10;
var h_skip = 5;
var pointer_min_h = 30;
var bott_max = 0;
var comments_sorted = comments.sort(function (a,b) {
return a[2] - b[2];
//pokus o hezci kladeni poiteru, ale nic moc
if( a[3] < b[3] ) {
return (a[2] + pointer_min_h)- b[2];
} else {
return (a[2] - pointer_min_h)- b[2];
}
});
//console.log("w:" + w);
for (c in comments_sorted) {
var id = comments_sorted[c][0];
var x = comments_sorted[c][1];
var y = comments_sorted[c][2];
var el = document.getElementById(id);
var elp = document.getElementById(id + "-pointer");
if( el == null || elp == null ) {
continue;
}
par.appendChild(elp);
par.appendChild(el);
var delta_y = (y > bott_max) ? 0: bott_max - y + h_skip;
elp.style.left = x;
elp.style.top = y ;
elp.style.width = w - x + w_skip;
elp.style.height = pointer_min_h + delta_y;
elp.img_id = img_id;
el.img_id = img_id;
el.style.position = 'absolute';
el.style.left = w + w_skip;
el.style.top = y + delta_y;
var bott = el.offsetTop + el.offsetHeight;
bott_max = ( bott_max > bott ) ? bott_max : bott;
//console.log( "par.w:" + par.style.width);
}
if( par.offsetHeight < bott_max ) {
//par.style.height = bott_max;
//alert("preteklo to:"+ par.offsetHeight +",mx:" + bott_max );
par.style.height = bott_max;
}
}
function place_comments() {
for (var i=0; i < comments.length-1; i++) {
place_comments_one_div(comments[i][0], comments[i][1])
}
}
// ctrl-enter submits form
function textarea_onkey(ev)
{
//console.log("ev:" + ev.keyCode + "," + ev.ctrlKey);
if( (ev.keyCode == 13 || ev.keyCode == 10 ) && ev.ctrlKey ) {
var form = document.getElementById('commform');
if( form ) {
save_scroll(form);
//form.action ='';
form.submit();
}
return true;
}
return false;
}
//hide comment form
function close_commform() {
var formdiv = document.getElementById('commform-div');
if( formdiv == null ) {
alert("form null");
return true;
}
formdiv.style.display = 'none';
return false;
}
// show comment form, when clicked to image
function img_click(element, ev) {
var body_class = document.body.className;
switch(body_class){
case "comitting":
if (!confirm("Právě jsou zanášeny korektury, opravdu chcete přidat novou?"))
return;
break;
case "deprecated":
if (!confirm("Toto PDF je již zastaralé, opravdu chcete vytvořit korekturu?"))
return;
break;
}
var dx, dy;
var par = element.parentNode;
if( ev.pageX != null ) {
dx = ev.pageX - par.offsetLeft;
dy = ev.pageY - par.offsetTop;
} else { //IE
dx = ev.offsetX;
dy = ev.offsetY;
}
var img_id = element.id;
if( element.img_id != null ) {
// click was to '-pointer'
img_id = element.img_id;
}
return show_form(img_id, dx, dy, '', '', '', '');
}
// hide or show text of correction
function toggle_visibility(oid){
var buttondiv = document.getElementById(oid+'-buttons')
var text = document.getElementById(oid+'-body');
if (text.style.display == 'none'){
text.style.display = 'block';
buttondiv.style.display = 'inline-block';
}else {
text.style.display = 'none';
buttondiv.style.display = 'none';
}
for (var i=0;i<comments.length-1;i++){
place_comments_one_div(comments[i][0], comments[i][1])
}
}
// show comment form, when 'edit' or 'comment' button pressed
function box_edit(oid, action)
{
var divpointer = document.getElementById(oid + '-pointer');
var text;
if (action == 'update') {
var text_el = document.getElementById(oid + '-text');
text = text_el.textContent; // FIXME původně tu bylo innerHTML.unescapeHTML()
} else {
text = '';
}
var dx = parseInt(divpointer.style.left);
var dy = parseInt(divpointer.style.top);
var divbox = document.getElementById(oid);
//alert('not yet 2:' + text + text_el); // + divpointer.style.top "x" + divpo );
id = oid.substring(2);
return show_form(divbox.img_id, dx, dy, id, text, action);
}
// show comment form when 'update-comment' button pressed
function update_comment(oid,ktid)
{
var divpointer = document.getElementById(oid + '-pointer');
var dx = parseInt(divpointer.style.left);
var dy = parseInt(divpointer.style.top);
var divbox = document.getElementById(oid);
var text = document.getElementById(ktid).textContent; // FIXME původně tu bylo innerHTML.unescapeHTML()
return show_form(divbox.img_id, dx, dy, ktid.substring(2), text, 'update-comment');
}
//fill up comment form and show him
function show_form(img_id, dx, dy, id, text, action) {
var form = document.getElementById('commform');
var formdiv = document.getElementById('commform-div');
var textarea = document.getElementById('commform-text');
var inputX = document.getElementById('commform-x');
var inputY = document.getElementById('commform-y');
var inputImgId = document.getElementById('commform-img-id');
var inputId = document.getElementById('commform-id');
var inputAction = document.getElementById('commform-action');
var img = document.getElementById(img_id);
if( formdiv == null || textarea == null ) {
alert("form null");
return 1;
}
//form.action = "#" + img_id;
// set hidden values
inputX.value = dx;
inputY.value = dy;
inputImgId.value = img_id;
inputId.value = id;
inputAction.value = action;
textarea.value = text;
//textarea.value = "dxy:"+ dx + "x" + dy + "\n" + 'id:' + img_id;
// show form
formdiv.style.display = 'block';
formdiv.style.left = dx;
formdiv.style.top = dy;
img.parentNode.appendChild(formdiv);
textarea.focus();
return true;
}
function box_onmouseover(box)
{
var id = box.id;
var pointer = document.getElementById(box.id + '-pointer');
pointer.classList.remove('pointer');
pointer.classList.add('pointer-hi');
}
function box_onmouseout(box)
{
var id = box.id;
var pointer = document.getElementById(box.id + '-pointer');
pointer.classList.remove('pointer-hi');
pointer.classList.add('pointer');
}
function save_scroll(form)
{
//alert('save_scroll:' + document.body.scrollTop);
form.scroll.value = document.body.scrollTop;
//alert('save_scroll:' + form.scroll.value);
return true;
}
function toggle_corrections(aclass)
{
var stylesheets = document.styleSheets;
var ssheet = null;
for (var i=0;i<stylesheets.length; i++){
if (stylesheets[i].title === "opraf-css"){
ssheet = stylesheets[i];
break;
}
}
if (! ssheet){
return;
}
for (var i=0;i<ssheet.cssRules.length;i++){
var rule = ssheet.cssRules[i];
if (rule.selectorText === '.'+aclass){
if (rule.style.display === ""){
rule.style.display = "none";
} else {
rule.style.display = "";
}
}
}
place_comments();
}
String.prototype.unescapeHTML = function () {
return(
this.replace(/&amp;/g,'&').
replace(/&gt;/g,'>').
replace(/&lt;/g,'<').
replace(/&quot;/g,'"')
);
};

View file

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

View file

@ -1,64 +0,0 @@
{# Okolí samotného hlavni_cast_korekturovatka.html, tedy „povinné HTML věci“, informace o korekturovaném PDF a starání se o stav PDF #}
{% load static %}
<html lang='cs'>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}?version=3" />
<link href="{% static 'css/rozliseni.css' %}?version=3" rel="stylesheet">
<title>Korektury {{korekturovanepdf.nazev}}</title>
</head>
<body class="{{ LOCAL_TEST_PROD }}web" data-stav_pdf="{{ korekturovanepdf.status }}">
<h1>Korektury {{korekturovanepdf.nazev}}</h1>
<h2 class="textzanaseni"> Probíhá zanášení korektur, zvažte, zda chcete přidávat nové </h2>
<h2 class="textzastarale"> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2>
<i>{{korekturovanepdf.komentar}}</i>
<br>
<i>Klikni na chybu, napiš komentář</i> |
<a href="{{korekturovanepdf.pdf.url}}">stáhnout PDF (bez korektur)</a> |
<a href="../">seznam souborů</a> |
<a href="/admin/korektury/korekturovanepdf/">Spravovat PDF</a> |
<a href="../help">nápověda</a> |
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|
<a href="/">hlavní stránka</a> |
<a href="https://mam.mff.cuni.cz/wiki">wiki</a> |
<hr />
{% include "korektury/korekturovatko/hlavni_cast_korekturovatka.html" %}
{% include "korektury/korekturovatko/zmena_stavu_pdf.html" %}
<hr/>
<p>
Děkujeme opravovatelům: <span id="pocty_autoru"></span></p>
<hr>
<script>
/**
* HTML prvek, kam se zapíší (pomocí .innerHTML) počty korektur jednotlivých autorů
* @type {HTMLElement}
*/
const span_s_pocty_autoru = document.getElementById("pocty_autoru")
/** Aktualizuje, kolik který autor má komentářů u daného korekturovaného PDF. */
function aktualizuj_pocty_zasluh() {
const pocty_autoru = {};
for (let komentar of Object.values(komentare)) {
if (!(komentar.autor in pocty_autoru)) pocty_autoru[komentar.autor] = 0;
pocty_autoru[komentar.autor] += 1;
}
const setrizene = [];
for (const keyval of Object.entries(pocty_autoru)) setrizene.push(keyval);
setrizene.sort(function(a, b) {return a[1] - b[1];});
let ans = "";
for (let [autor, pocet] of setrizene) ans += `, ${autor} (${pocet})`;
span_s_pocty_autoru.innerHTML = ans.substring(2);
}
</script>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,3 @@
"""
Soubor sloužící ke generování testdat.
"""
import logging
import os
from shutil import copyfile, rmtree
@ -35,27 +32,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,14 +1,10 @@
from django.urls import path
from django.urls import include
from personalni.utils import org_required
from . import views
urlpatterns = [
path('korektury/', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'),
path('korektury/neseskupene/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury_neseskupene_list'),
path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'),
path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'),
path('korektury/api/', include('korektury.api.urls')),
path('', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'),
path('neseskupene/', org_required(views.KorekturyAktualniListView.as_view()), name='korektury_neseskupene_list'),
path('zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'),
path('<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'),
]

View file

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

View file

@ -1,15 +1,26 @@
from django.shortcuts import get_object_or_404, render
from django.views import generic
from django.utils.translation import ugettext as _
from django.conf import settings
from django.http import HttpResponseForbidden
from django.core.mail import EmailMessage
from django.db.models import Count,Q
from .models import Oprava, KorekturovanePDF, KorekturaTag
from .models import Oprava,Komentar,KorekturovanePDF, Organizator
from .forms import OpravaForm
import subprocess
import shutil
import os
class KorekturyListView(generic.ListView):
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'
@ -46,14 +57,191 @@ class KorekturySeskupeneListView(KorekturyAktualniListView):
return reversed(sorted(qs, key=lambda it: it.cislo_a_tema))
### Korektury
class KorekturyView(generic.DetailView):
model = KorekturovanePDF
pk_url_kwarg = "pdf"
template_name = 'korektury/korekturovatko/html_obal.html'
class KorekturyView(generic.TemplateView):
model = Oprava
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
# pokud existuje ucet (user), ale neni to organizator = 403
autor = Organizator.objects.filter(osoba__user=autor_user).first()
if not autor:
return HttpResponseForbidden()
if not scroll:
scroll = 0
action = q.get('action')
if (action == ''): # Přidej
x = int(q.get('x'))
y = int(q.get('y'))
text = q.get('txt')
strana = int(q.get('img-id')[4:])
pdf = KorekturovanePDF.objects.get(id=q.get('pdf'))
op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf)
op.save()
self.send_email_notification_komentar(op,autor)
elif (action == 'del'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.delete()
elif (action == 'update'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
text = q.get('txt')
op.autor = autor
op.text = text
op.save()
elif (action == 'undone'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_K_OPRAVE
op.save()
elif (action == 'done'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_OPRAVENO
op.save()
elif (action == 'ready'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_K_ZANESENI
op.save()
elif (action == 'wontfix'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
op.status = op.STATUS_NENI_CHYBA
op.save()
elif (action == 'comment'):
id = int(q.get('id'))
op = Oprava.objects.get(id=id)
text = q.get('txt')
kom = Komentar(oprava=op,autor=autor,text=text)
kom.save()
self.send_email_notification_komentar(op,autor)
elif (action == 'update-comment'):
id = int(q.get('id'))
kom = Komentar.objects.get(id=id)
text = q.get('txt')
kom.text = text
kom.autor = autor
kom.save()
elif (action == 'del-comment'):
id = int(q.get('id'))
kom = Komentar.objects.get(id=id)
kom.delete()
elif (action == 'set-state'):
pdf = KorekturovanePDF.objects.get(id=q.get('pdf'))
if (q.get('state') == 'adding'):
pdf.status = pdf.STATUS_PRIDAVANI
elif (q.get('state') == 'comitting'):
pdf.status = pdf.STATUS_ZANASENI
elif (q.get('state') == 'deprecated'):
pdf.status = pdf.STATUS_ZASTARALE
pdf.save()
context = self.get_context_data()
context['scroll'] = scroll
context['autor'] = autor
return render(request, 'korektury/opraf.html',context)
def send_email_notification_komentar(self, oprava, autor):
''' Rozesle e-mail pri pridani komentare / opravy,
ktery obsahuje text vlakna opravy.
'''
# parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
from django.urls import reverse
odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = settings.KOREKTURY_EMAIL
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)
# zodpovedny org
if oprava.pdf.org:
email_zobpovedny = oprava.pdf.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['indexy_stran'] = range(self.object.stran)
context['tagy'] = KorekturaTag.objects.all()
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(pdf.stran)
context['form_oprava'] = OpravaForm()
opravy = Oprava.objects.filter(pdf=self.kwargs['pdf'])
zasluhy = {}
for o in opravy:
if o.autor in zasluhy:
zasluhy[o.autor]+=1
else:
zasluhy[o.autor]=1
o.komentare = o.komentar_set.all()
for k in o.komentare:
if k.autor in zasluhy:
zasluhy[k.autor] += 1
else:
zasluhy[k.autor] = 1
zasluhy = [
{'autor': jmeno, 'pocet': pocet}
for (jmeno, pocet) in zasluhy.items()
]
zasluhy.sort(key=lambda z: z['pocet'], reverse=True)
strany = set(o.strana for o in opravy)
opravy_na_stranu = [{'strana': s, 'op_id': opravy.filter(strana=s)} for s in strany]
context['opravy_strany'] = opravy_na_stranu
context['k_oprave_cnt'] = opravy.filter(status='k_oprave').count()
context['opraveno_cnt'] = opravy.filter(status='opraveno').count()
context['neni_chyba_cnt'] = opravy.filter(status='neni_chyba').count()
context['k_zaneseni_cnt'] = opravy.filter(status='k_zaneseni').count()
context['opravy'] = opravy
context['zasluhy'] = zasluhy
return context
def form_valid(self,form):
return super().form_valid(form)

View file

@ -7,4 +7,5 @@ make/install_web
ensure_venv
./manage.py testdata
./manage.py loaddata data/*
#make/sync_prod_flatpages
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 = []
@ -36,34 +35,19 @@ locale.setlocale(locale.LC_COLLATE, 'cs_CZ.UTF-8')
# https://books.agiliq.com/projects/django-admin-cookbook/en/latest/set_ordering.html
# FIXME zpraseno pomocí toho, že Python umí bez problému přepisovat funkce
def get_app_list(self, request, app_label=None):
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request, label=app_label)
aplikace_nahore = [
'tvorba',
'personalni',
'novinky',
'korektury',
'various',
'prednasky',
'soustredeni',
]
# Odhlášený admin má prázdný app_dict :-/
app_list = [app_dict[label] for label in aplikace_nahore if label in app_dict] + [app_dict[label] for label in app_dict if label not in aplikace_nahore]
app_dict = self._build_app_dict(request)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower()))
# 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

@ -1,71 +1,56 @@
"""
Django settings for mamweb project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import traceback
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Application definition
SITE_ID = 1
ROOT_URLCONF = 'mamweb.urls'
WSGI_APPLICATION = 'mamweb.wsgi.application'
APPEND_SLASH = True
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
# Lokalizace
LANGUAGE_CODE = 'cs'
TIME_ZONE = 'Europe/Prague'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
USE_L10N = True # S přechodem k djangu>=4 lze smazat (localized formatting)
USE_TZ = True # S přechodem k djangu>=5 lze smazat (timezone aware datetimes)
# Statické soubory (CSS, JavaScript, obrázky) a další média
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder',
)
# Where redirect for login required services
# URL pro přihlášení (default je account/login)
LOGIN_URL = 'login'
LOGIN_REDIRECT_URL = 'profil'
# Odhlášení po zavření prohlížeče
# (pozor nefunguje na firefox se znovuotevíráním oken po startu firefoxu)
# default je False a SESSION_COOKIE_AGE = 3600*24*14 = 2 týdny
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
# View pro chybu s CSRF tokenem (např. se sušenkami)
CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error'
# Modules configuration
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
# SECURITY WARNING: keep the secret key used in production secret!
# Create file 'django.secret' in every install (it is not kept in git)
try:
with open(os.path.join(os.path.dirname(__file__), '..', 'django.secret')) as f:
SECRET_KEY = f.readline().strip()
except:
SECRET_KEY = '12345zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzdqwkdlkj'
# Přidávání dalších součástí (do) djangovské mašinérie
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
MIDDLEWARE = (
# 'reversion.middleware.RevisionMiddleware',
# 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -74,7 +59,6 @@ MIDDLEWARE = (
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -83,22 +67,17 @@ TEMPLATES = [
'OPTIONS': {
'context_processors': (
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.request',
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai',
'header_fotky.context_processors.vzhled',
'various.context_processors.rozliseni',
'various.context_processors.april',
'various.context_processors.halloween',
)
},
},
]
INSTALLED_APPS = (
# Basic
'django.contrib.contenttypes',
'django.contrib.sessions',
@ -108,14 +87,19 @@ INSTALLED_APPS = (
'django.contrib.auth',
# Utilities
'sekizai',
'reversion',
'django_countries',
'solo',
'django_ckeditor_5',
'ckeditor',
'ckeditor_uploader',
'taggit',
'dal',
'dal_select2',
'crispy_forms',
'django_comments',
'django.contrib.flatpages',
'django.contrib.humanize',
@ -129,158 +113,82 @@ INSTALLED_APPS = (
'rest_framework',
'rest_framework.authtoken',
'colorfield',
# MaMweb
'mamweb',
'seminar',
'tvorba',
'galerie',
'korektury',
'korektury.api',
'prednasky',
'header_fotky',
'various',
'various.autentizace',
'api',
'aesop',
'odevzdavatko',
'vysledkovky',
'personalni',
'soustredeni',
'tvorba',
'treenode',
'vyroci',
'sifrovacka',
'novinky',
# Admin upravy:
# 'material',
# 'material.admin',
# 'admin_tools',
# 'admin_tools.theming',
# 'admin_tools.menu',
# 'admin_tools.dashboard',
# 'material',
# 'material.admin',
# 'admin_tools',
# 'admin_tools.theming',
# 'admin_tools.menu',
# 'admin_tools.dashboard',
'django.contrib.admin',
# Nechat na konci (INSTALLED_APPS je uspořádané):
'django_cleanup.apps.CleanupConfig', # Uklízí media/
)
DEBUG_TOOLBAR_CONFIG = {
'SHOW_COLLAPSED': True,
}
SUMMERNOTE_CONFIG = {
'iframe': False,
'airMode': False,
'attachment_require_authentication': True,
'width': '80%',
# 'height': '30em',
'toolbar': [
['style', ['style']],
['font', ['bold', 'italic', 'superscript', 'subscript', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'hr']],
['view', ['fullscreen', 'codeview']],
['help', ['help']],
]
}
# MaM-specifické složky
SEMINAR_RESENI_DIR = os.path.join('reseni')
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')
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 = {
# MaM-specifické konstanty
ROCNIK_ZRUSENI_TEMAT = 25
ROCNIK_INFLACE_BODU = 25
ROCNIK_INFLACE_TITULU = 26
#
NOVE_CISLO_EMAIL = 'zadani@mam.mff.cuni.cz'
NOVE_RESENI_EMAIL = 'submitovatko@mam.mff.cuni.cz'
KOREKTURY_NOVE_PDF_EMAIL = 'korekturovatko-nove-pdf@mam.mff.cuni.cz'
KOREKTURY_EMAIL = 'korekturovatko@mam.mff.cuni.cz'
REGISTRACE_EMAIL = 'registrace@mam.mff.cuni.cz'
PASSWD_RESET_EMAIL = 'login@mam.mff.cuni.cz'
#
KONFERA_ORGOVE_EMAIL = 'org@mam.mff.cuni.cz'
# CKEditor = WYSIWYG html editor
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',
'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'],
],
'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',
]
},
# '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
@ -299,31 +207,21 @@ WEBPACK_LOADER = {
# Dajngo REST Framework
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100
}
# SECURITY WARNING: keep the secret key used in production secret!
# Create file 'django.secret' in every install (it is not kept in git)
try:
with open(os.path.join(os.path.dirname(__file__), '..', 'django.secret')) as f:
SECRET_KEY = f.readline().strip()
except:
SECRET_KEY = '12345zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzdqwkdlkj'
# Logging
# Logování
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s (logger %(name)s): %(message)s'
'format':
'%(levelname)s %(asctime)s %(module)s (logger %(name)s): %(message)s'
},
},
@ -354,18 +252,18 @@ LOGGING = {
'filters': ['Http404AsInfo'],
},
'personalni.prihlaska.form':{
'handlers': ['console','registration_logfile'],
'level': 'INFO'
},
'personalni.prihlaska.problem':{
'handlers': ['console','mail_registration','registration_error_log'],
'level': 'INFO'
},
'seminar.prihlaska.form': {
'handlers': ['console', 'registration_logfile'],
'level': 'INFO'
},
'seminar.prihlaska.problem': {
'handlers': ['console', 'mail_registration', 'registration_error_log'],
'level': 'INFO'
},
# Catch-all logger
'': {
'handlers': ['console'], # Add 'mail_admins' in prod and test
'handlers': ['console'],
'level': 'DEBUG',
'filters': ['StripSensitiveFormData'],
},
@ -375,7 +273,7 @@ LOGGING = {
'handlers': {
'console': {
'level': 'WARNING', ## Set to 'DEBUG' in local
'level': 'WARNING',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
@ -391,37 +289,24 @@ LOGGING = {
'class': 'django.utils.log.AdminEmailHandler',
'formatter': 'verbose',
},
'registration_logfile':{
'registration_logfile': {
'level': 'INFO',
'class': 'logging.FileHandler',
# filename declared in specific configuration files
'formatter': 'verbose',
},
'registration_error_log':{
'class': 'logging.FileHandler',
# filename declared in specific configuration files
'formatter': 'verbose',
},
'registration_error_log': {
'level': 'INFO',
'class': 'logging.FileHandler',
# filename declared in specific configuration files
'class': 'logging.FileHandler',
# filename declared in specific configuration files
'formatter': 'verbose',
},
},
},
}
# Permissions for uploads
FILE_UPLOAD_PERMISSIONS = 0o0644
# MaM specific
SEMINAR_RESENI_DIR = os.path.join('reseni')
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')
# Logování chyb
# Logování neexistujících proměnných v templatech
class InvalidTemplateVariable(str):
def __mod__(self, variable):
import logging
@ -433,5 +318,38 @@ class InvalidTemplateVariable(str):
return ''
TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidTemplateVariable('%s')
# Django 3.2 vyžaduje explicitní nastavení autoklíče, zatím nechápu proč
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# TODO odstranit? (Je to default.)
# Whether to append trailing slashes to URLs.
APPEND_SLASH = True
# TODO odstranit? (Je to default.)
# Permissions for uploads
FILE_UPLOAD_PERMISSIONS = 0o0644
# TODO odstranit? (Je to default.)
# Automatická lokalizace
USE_I18N = True
# TODO odstranit? (Nevím o tom, že bychom ho někde používali.)
# Summernote = WYSIWYG editor (pro admin?)
SUMMERNOTE_CONFIG = {
'iframe': False,
'airMode': False,
'attachment_require_authentication': True,
'width': '80%',
# 'height': '30em',
'toolbar': [
['style', ['style']],
['font', ['bold', 'italic', 'superscript', 'subscript', 'clear']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'hr']],
['view', ['fullscreen', 'codeview']],
['help', ['help']],
]
}

View file

@ -0,0 +1,22 @@
from .settings_common import *
DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = True
MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
)
INSTALLED_APPS += (
'debug_toolbar', # Takovéto užitečné (pro debug) na html stránce vpravo
'django_extensions', # Kolekce zajímavých ./manage.py commandů
)
DEBUG_TOOLBAR_CONFIG = {
'SHOW_COLLAPSED': True,
}
# Nechceme nikomu omylem poslat e-mail
# Když někdo spustí omylem tohle nastavení, prostě to při poslání mailu spadne
# V settings_local a settings_test přepíšeme...
EMAIL_BACKEND = None

View file

@ -1,5 +1,3 @@
import os.path
#
# Lokalni / vyvojove nastaveni settings.py
#
@ -7,35 +5,20 @@ import os.path
# DJANGO_SETTINGS_MODULE=mamweb.settings_local ./manage.py ...
#
# Import common settings
from .settings_common import *
MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += (
'debug_toolbar',
'django_extensions',
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
INTERNAL_IPS = ['127.0.0.1']
TEMPLATES[0]['OPTIONS']['debug'] = True
import os.path
from ipaddress import ip_network
from .settings_common_debug import *
LOCAL_TEST_PROD = "local"
INTERNAL_IPS = ['127.0.0.1']
ALLOWED_HOSTS = [str(ip) for ip in ip_network('192.168.0.0/16')]
ALLOWED_HOSTS.append('127.0.0.1')
ALLOWED_HOSTS.append('localhost')
ALLOWED_HOSTS += ['127.0.0.1', 'localhost']
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
# Databáze
# SQLite
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
@ -45,15 +28,21 @@ DATABASES = {
},
},
}
#DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
# 'NAME': 'mam_local',
# 'USER': 'mam',
# },
#}
# # PostgreSQL
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.postgresql_psycopg2',
# 'NAME': 'mam_local',
# 'USER': 'mam',
# },
# }
# LOGGING
# E-maily posílat chceme, ale do terminálu :-)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Logování
LOGGING = {
'version': 1,
'disable_existing_loggers': True,
@ -78,12 +67,6 @@ LOGGING = {
},
},
'loggers': {
# Vypisovani databazovych dotazu do konzole
#'django.db.backends': {
# 'level': 'DEBUG',
# 'handlers': ['console'],
# 'propagate': False,
#},
'werkzeug': {
'handlers': ['console'],
'level': 'DEBUG',
@ -96,11 +79,3 @@ LOGGING = {
},
},
}
# set to 'DEBUG' for EXTRA verbose output
# LOGGING['handlers']['console']['level'] = 'INFO'
# E-maily posílat chceme, ale do terminálu :-)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SEND_EMAIL_NOTIFICATIONS = True
LOCAL_TEST_PROD = "local"

View file

@ -1,5 +1,3 @@
import os.path
#
# Produkcni nastaveni settings.py
#
@ -9,31 +7,27 @@ import os.path
# Import common settings
from .settings_common import *
LOCAL_TEST_PROD = "prod"
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += (
'django_extensions',
)
# SECURITY WARNING: keep the secret key used in production secret!
# `'DOCUTILSCONFIG' in os.environ` kvůli sphinxu
# FIXME zjistit, zda je bezpečné a zda se to nedá udělat lépe
assert 'DOCUTILSCONFIG' in os.environ or not SECRET_KEY.startswith('12345')
assert not SECRET_KEY.startswith('12345')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
TEMPLATE_DEBUG = False
# SECURITY: only send sensitive cookies via HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa
'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny…
]
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'gimli.ms.mff.cuni.cz']
# Přidání aplikací
INSTALLED_APPS += (
'django_extensions', # Kolekce zajímavých ./manage.py commandů
)
# Databáze
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
@ -45,27 +39,14 @@ DATABASES = {
},
}
import os
# Nastavení e-mailů (odkud a kam mají chodit)
SERVER_EMAIL = 'mamweb-prod-errors@mam.mff.cuni.cz'
ADMINS = [('M&M ERRORs', 'mam-errors@mam.mff.cuni.cz')]
# SECURITY: only send sensitive cookies via HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# LOGGING
# Logování
LOGGING['loggers']['']['handlers'] = ['console', 'mail_admins']
LOGGING['loggers']['django']['handlers'] = ['console', 'mail_admins']
LOGGING['loggers']['django.security.csrf']['level'] = 'ERROR'
LOGGING['handlers']['registration_logfile']['filename'] = '/home/mam-web/logs/prod/registration.log'
LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/prod/registration_errors.log'
# E-MAIL NOTIFICATIONS
LOCAL_TEST_PROD = "prod"

View file

@ -1,43 +1,24 @@
import os.path
#
# Testovaci nastaveni settings.py (testovani na atreyi)
# Testovaci nastaveni settings.py (testovani na mam-test.ks.marfyz.cz)
#
# Pro vyber tohoto nastaveni muzete pouzit tez:
# DJANGO_SETTINGS_MODULE=mamweb.settings_test ./manage.py ...
#
# Import common settings
from .settings_common import * # zatim nutne, casem snad vyresime # noqa
from .settings_common_debug import *
LOCAL_TEST_PROD = "test"
MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += (
'debug_toolbar',
'django_extensions',
)
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = ')^u=i65*zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzv9l+zo)n'
# SECURITY: only send sensitive cookies via HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = True
ALLOWED_HOSTS = ['mam-test.kam.mff.cuni.cz', 'gimli.ms.mff.cuni.cz', 'mam-test.ks.matfyz.cz']
ALLOWED_HOSTS = [
'mam-test.ks.matfyz.cz',
'*.mam.mff.cuni.cz', # Asi se nikdy nepoužije…
]
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
# Databáze
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
@ -49,31 +30,27 @@ DATABASES = {
},
}
import os
# Nastavení e-mailů (odkud a kam mají chodit)
SERVER_EMAIL = 'mamweb-test-errors@mam.mff.cuni.cz'
ADMINS = [
('M&M ERRORs', 'mam-errors@mam.mff.cuni.cz'),
]
# SECURITY: only send sensitive cookies via HTTPS
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# LOGGING
LOGGING['loggers']['']['handlers'] = ['console', 'mail_admins']
LOGGING['loggers']['django']['handlers'] = ['console', 'mail_admins']
LOGGING['handlers']['registration_logfile']['filename'] = '/home/mam-web/logs/test/registration.log'
LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/test/registration_errors.log'
FILE_UPLOAD_PERMISSIONS = 0o440
# Testování e-mailů
EMAIL_BACKEND = 'various.mail_prefixer.PrefixingMailBackend'
# TODO Pouze na otestování testu… Zvolit konferu!
# XXX: Je to pole, protože implementační detail backendu.
TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz']
LOCAL_TEST_PROD = "test"
# Netuším proč (Jidáš)...
FILE_UPLOAD_PERMISSIONS = 0o440
# Logování
LOGGING['loggers']['']['handlers'] = ['console', 'mail_admins']
LOGGING['loggers']['django']['handlers'] = ['console', 'mail_admins']
LOGGING['handlers']['registration_logfile']['filename'] = '/home/mam-web/logs/test/registration.log'
LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/test/registration_errors.log'

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