Compare commits

...

114 commits

Author SHA1 Message Date
Pavel "LEdoian" Turinsky
b1204c1a20 Merge remote-tracking branch 'gitea/master' into kontrola-prav-orgu 2023-11-20 21:12:08 +01:00
Pavel "LEdoian" Turinsky
dbf9fc67b9 Merge branch 'master' into kontrola-prav-orgu 2023-11-20 21:10:46 +01:00
2afa250df6 Merge branch 'django4' 2023-11-20 20:30:44 +01:00
78866fbc93 Merge pull request 'Neodkazovat neveřejné výsledkovky' (!41) from neodkazovat_neverejne_vysledkovky into master
Reviewed-on: #41
2023-11-20 20:09:22 +01:00
Pavel 'LEdoian' Turinsky
a9a426ca91 Odkaz na výsledkovku a ne na horní část stránky 2023-11-13 20:59:06 +01:00
Pavel 'LEdoian' Turinsky
b456b8c861 Neodkazovat z archivu na výsledkovku, když tam není 2023-11-13 20:58:39 +01:00
6fe9beb0f0 Deprecated věci 2023-10-31 21:20:48 +01:00
f457520d7d Takhle už by šlo pustit (ugettext->gettext, force_text->force_str) 2023-10-30 23:33:13 +01:00
cda00bbc4c Merge pull request 'Lepsi Delani Orgu' (!35) from lepsi-delani-orgu into master
Reviewed-on: #35
2023-10-30 22:21:13 +01:00
Pavel "LEdoian" Turinsky
668546c720 Fix testu 2023-10-30 22:19:50 +01:00
e889620ddb Merge remote-tracking branch 'origin/master' 2023-10-30 21:09:42 +01:00
c673d04082 Merge branch 'prehlednejsi_hodnotitko' 2023-10-30 21:08:02 +01:00
36124fa69e Merge pull request 'Dokumentace závislostí webu' (!38) from doc_prereq into master
Reviewed-on: #38
2023-10-30 20:56:19 +01:00
14b3caaa04 Merge pull request 'Úpravy výsledkovky' (!36) from pregenerovavani-deadlinu into master
Reviewed-on: #36
2023-10-30 20:19:53 +01:00
e9782c73e4 V předchozím commitu bylo špatně trefené {% endif %} 2023-10-24 13:16:48 +02:00
Pavel "LEdoian" Turinsky
93cfa504a8 Nepadat pro hodně nevalidní CSRF
Když to nemá referer, tak je to hodně divné, ale mail o tom nechci.
2023-10-22 15:21:24 +02:00
29b5361545 (sifrovacka) stringifikace správných odpovědí 2023-10-16 20:01:41 +02:00
e589564840 (sifrovacka) odlišení přijmutých a nepřijmutých odpovědí 2023-10-16 19:55:37 +02:00
10c6f4be16 Merge pull request 'sifrovacka' (!40) from sifrovacka into master
Reviewed-on: #40
2023-10-15 20:02:24 +02:00
04e2007c3a (sifrovacka) Méně řádků v tajence v účastnickém formuláři (bez diakritiky bylo přidáno už v předchozím commitu) 2023-10-15 19:55:23 +02:00
1f7b770a5c Odpovědi od účastníků mají nově i timestamp 2023-10-15 19:52:11 +02:00
b8eee44ed0 Hrajeme si se styly. 2023-10-14 12:37:23 +02:00
b36c030af5 Merge branch 'sifrovacka' of gitea.ks.matfyz.cz:mam/mamweb into sifrovacka 2023-10-14 12:28:48 +02:00
dca7a7bedd Trochu přepsány textíky. 2023-10-14 12:28:22 +02:00
c635b0a280 K formuláři mají mít přístup i orgové… 2023-10-14 12:09:14 +02:00
1aa389414d Návrh na webappku na šifrovačku (na Sklené) 2023-10-14 11:26:38 +02:00
MaM Web user
aff9a39262 uwsgi v requirements.txt nevypadá potřebně
a navíc to pak Jidášovi nefunguje, zatím nevím proč.

P.
2023-10-09 23:00:49 +02:00
Pavel "LEdoian" Turinsky
2d416472e8 Tak superuser už může
lol…
2023-10-09 22:50:28 +02:00
Pavel "LEdoian" Turinsky
7e1644d5c1 Parciální fix testu dělání orgů 2023-10-09 22:43:27 +02:00
b69ebcd6b6 U CSRF chyby má být reason asi předvyplněný 2023-10-09 22:29:29 +02:00
Pavel "LEdoian" Turinsky
002e33002c Přegenerovávat výsledkovky nemůže běžný org 2023-10-09 22:25:25 +02:00
205409abc5 Merge pull request 'oprava_chybovych_hlasek' (!39) from oprava_chybovych_hlasek into master
Reviewed-on: #39 (by @ledoian)
2023-10-09 22:19:33 +02:00
68d51a0bf1 Překlep 2023-10-09 22:14:27 +02:00
0e24c1d9ad Komentář k CSRF chybám 2023-10-09 22:04:01 +02:00
29b3271200 CSRF chyba má vrátit 403 2023-10-09 22:02:05 +02:00
60346d6839 Stránka pro CSRF chybu 2023-10-09 21:50:50 +02:00
bb127832dc Oprava „názvu záložky“ při Ojojojojoj 2023-10-09 21:11:12 +02:00
Pavel "LEdoian" Turinsky
1f0d70b31e Dokumentace závislostí
podle dnešní schůzky, zatím velmi předběžná verze
2023-10-02 23:29:51 +02:00
0738aedc9e Oprava pipu instalujícího prereleasy 2023-10-02 21:32:09 +02:00
d006f6ebfa Oprava vytváření galerie (Pillow odstranil něco, co django-imagekit používá) 2023-10-02 10:08:49 +02:00
bfb7a75b97 Oprava vytváření galerie (Pillow odstranil něco, co django-imagekit používá) 2023-10-02 09:28:23 +02:00
MaM Web user
8a473a5097 Popisky v záhlaví výsledkovky
Sice to na mobilu nic moc neudělá, ale aspoň myší se to má šanci používat trochu snáz.
2023-09-20 11:10:55 +02:00
MaM Web user
f14df7b579 Nové kódy úloh ve výsledkovce
(Zprasený commit, má být na jiné větvi, já vím :-D)
2023-09-20 10:47:11 +02:00
MaM Web user
e182785e48 Přidány akce pro přegenerování deadlinů do Admina
Nikdo o tom neví, a když se o tom dozví, tak se buď nic nezmění, nebo to bylo
tak dávno, že si toho nikdo nevšimne :-D Ale na testování se to hodí…

Also: někde jsem přepsal mezery na taby.
2023-09-20 01:11:03 +02:00
fb31b10d66 Oprava odevzdávátka (čtyřrozměrné body) 2023-09-16 12:31:30 +02:00
30ce5a0888 Vynucení UTF-8 (v html a css) 2023-09-06 12:59:04 +02:00
0c90c2bd06 Lepší řazení problémů ve výsledkovce 2023-08-30 22:06:46 +02:00
Pavel "LEdoian" Turinsky
df657953ab Pročištění ALLOWED_HOSTS 2023-08-13 13:52:41 +02:00
527c06d91e Připomenutí orgům, že mají psát webařům. (Malilinkato rozbíjí styl na užších displejích.) 2023-08-12 00:58:09 +02:00
MaM Web user
449a3db56b Odebrání práv na mazání lidí (diff je trochu pokažený, protože save_org_prava nedrží jednotné pořadí, ale co už) 2023-08-10 23:23:48 +02:00
767358ca73 Trochu světlejší placeholder, ať vypadá méně jako vyplněné body 2023-07-08 12:56:15 +02:00
5b36650e6f Oprava nahrání řešení (Chceme nastavovat v setupu a ne v getu) 2023-06-24 17:55:51 +02:00
24c790185e Revert "Pokus o fix pádu webu"
This reverts commit 10b2d11249.
2023-06-24 17:48:49 +02:00
Pavel "LEdoian" Turinsky
14c6706bf3 Použijme konstantu, ne hardcodovaný řetězec 2023-06-24 15:23:12 +02:00
Pavel "LEdoian" Turinsky
10b2d11249 Pokus o fix pádu webu 2023-06-24 15:20:43 +02:00
Pavel "LEdoian" Turinsky
3eb9c7998d Zprávy do admina 2023-06-21 15:51:08 +02:00
Pavel "LEdoian" Turinsky
36a7a5af5d Test pro dělání orgů. Nefunguje :-/ 2023-06-21 15:41:36 +02:00
Pavel "LEdoian" Turinsky
844c55d5a7 Oprava drobností 2023-06-21 15:41:17 +02:00
Pavel "LEdoian" Turinsky
2cf4d87c6b Marginální zlepšení vyrábítka orgů
Podle diskuse na Matrixu
2023-06-21 14:45:46 +02:00
01ad1daba9 WTF (špatné právo v meníčku u korektur) 2023-06-20 17:50:17 +02:00
Pavel "LEdoian" Turinsky
feffa99d79 Fix typu v hodnotítku 2023-06-20 00:26:40 +02:00
cefbd4df7a Oprava výsledkovky do posledního čísla? 2023-06-19 21:52:01 +02:00
363776ba4f Lepší popis názvu PDF k opravám v adminu 2023-06-19 21:41:59 +02:00
13c48ae1c2 Oprava OJOJOJOJ (error 500) 2023-06-19 21:35:03 +02:00
0528dbbb9c Merge pull request 'Upgrade odevzdavatka' (!30) from upgrade_odevzdavatka into master
Reviewed-on: #30
2023-06-19 20:48:29 +02:00
37d29fcf58 Posli -> Vloz 2023-06-19 20:46:12 +02:00
baaaa0829a Pro bezpečnost Iterable -> Sequence 2023-06-19 20:31:59 +02:00
e641724eab „Překlep“ 2023-06-19 20:28:43 +02:00
205aa0b900 V NahrajReseniView z nadproblému udělat property 2023-06-19 20:24:19 +02:00
c64294099e NahrajReseniNadproblemView -> NahrajReseniRozcestnikTematekView 2023-06-19 20:15:35 +02:00
94ea718c20 Drobná úprava textu pro řešitele 2023-06-19 20:13:56 +02:00
0bf309fd78 Přejmenování .select2multiple .s2m-se-zaskrtavatky 2023-06-19 20:12:09 +02:00
dae3864ba4 Oranžové zvýraznění pro všechny Select2 2023-06-19 20:08:45 +02:00
18f55153be filter místo [… for … if …] 2023-06-19 20:06:18 +02:00
50ca3ac71f Merge branch 'master' into upgrade_odevzdavatka
# Conflicts:
#	data/sitetree.json
2023-06-17 09:25:04 +02:00
bb2332dc27 Lepší popis, kdy mají posílat PDF 2023-06-15 18:57:11 +02:00
b67525a11f Merge branch 'master' into upgrade_odevzdavatka 2023-06-15 17:10:29 +02:00
dce1de7a99 Checkboxy pouze u multiple selectu (přesněji řečeno pouze u těch, kde je select2multiple) 2023-06-13 09:12:00 +02:00
2ef3311bff Multiple select se nemají zavírat
Už jsem se internetu správně zeptal a dostal odpověď: select2 bere parametry jako atributy data-camel-case-vec=cosi (stejné jako select2({camelCaseVec: cosi}))
2023-06-13 08:50:16 +02:00
727d0b2955 Revert "Multiple select se nemají zavírat"
This reverts commit 4a3f8c66
2023-06-13 08:47:17 +02:00
bed107aeac Merge remote-tracking branch 'origin/master' into upgrade_odevzdavatka
# Conflicts:
#	mamweb/static/css/mamweb.css
2023-06-12 22:20:24 +02:00
MaM Web user
a07dbf4ab3 Jidáš: jsem vůl (soubor se nemůže jmenovat select2.js, když select2.js už importujeme) 2023-06-01 17:34:40 +02:00
4a3f8c669d Multiple select se nemají zavírat 2023-05-23 01:39:46 +02:00
317cc3056e Zdroj kódu 2023-05-23 01:15:06 +02:00
c3b42e09f2 Multiple select odteď mají oranžovou barvu a obsahují checkboxy 2023-05-23 01:10:37 +02:00
Pavel "LEdoian" Turinsky
d304e46ceb Barvičky jdou vypnout 2023-05-23 00:28:13 +02:00
Pavel "LEdoian" Turinsky
fe144e6de7 Barvičkyyy! 2023-05-22 23:58:04 +02:00
Pavel "LEdoian" Turinsky
86f6d47f10 Součty
Hnusně, ale přece…
2023-05-22 23:30:57 +02:00
74a26affa7 Odevzdávátko: Odevzdatelné problémy jsou všechny zadané (včetně minulých ročníků) 2023-05-22 23:02:32 +02:00
Pavel "LEdoian" Turinsky
bef932345c Kladivoo! 2023-05-22 22:47:13 +02:00
Pavel "LEdoian" Turinsky
468443e062 V tabulce potřebujeme znát i Hodnocení, kvůli bodům… 2023-05-22 22:46:47 +02:00
e26df01729 Odevzdávátko: Zaříznutí přístupu k nezadaným problémům 2023-05-22 22:28:14 +02:00
512f14ed4d Odevzdávátko: Nezadané problémy nelze odevzdat 2023-05-22 22:24:14 +02:00
Pavel "LEdoian" Turinsky
53032a49d8 Neumím psát filtry.
todo squash
2023-05-22 22:23:15 +02:00
Pavel "LEdoian" Turinsky
8f2aa72f6d Fix zapomenuté fieldy v tabulce
TODO: rebase squash
2023-05-22 22:21:42 +02:00
Pavel "LEdoian" Turinsky
ed5e7a1d9f Template pro zobrazení řešení přímo v tabulce
Snad, ještě jsem to netestoval :-)
2023-05-22 22:16:02 +02:00
Pavel "LEdoian" Turinsky
96bc21a727 Zrušen SourhnReseni
Už není potřeba, v tabulce stejně jsou už přímo řešení.
2023-05-22 22:15:27 +02:00
Pavel "LEdoian" Turinsky
659ad62b52 Ještě docstring k ReseniProblemuView 2023-05-22 22:15:07 +02:00
92cb8ec206 Odevzdávátko: Osamocený nadproblém se vybere automaticky 2023-05-22 21:53:14 +02:00
dee1b2bb2c Staré věci 2023-05-22 21:48:35 +02:00
Pavel "LEdoian" Turinsky
dd49c0e1ba Malá úprava TabulkaOdevzdanychReseniView.get_context_data.pridej_reseni 2023-05-22 21:48:33 +02:00
Pavel "LEdoian" Turinsky
dd5d8886ee Trocha popisu TabulkaOdevzdanychReseniView, ať se v tom dá vyznat 2023-05-22 21:27:04 +02:00
192ae6912f Vložit řešení -> nahrát řešení 2023-05-22 21:22:48 +02:00
bbb85b3f6a Poslat řešení -> nahrát řešení -> vložit řešení 2023-05-22 21:18:47 +02:00
b11429eaea Odevzdávátko: Seřazení problémů k odevzdání 2023-05-16 13:27:10 +02:00
a3526419a9 Odevzdávátko: Zlepšení UI 2023-05-16 00:06:12 +02:00
e04c116b80 Á, já jsem vůl 2023-05-15 23:09:56 +02:00
9ff223428b Lepší formularOKView 2023-05-15 23:06:52 +02:00
b8fc56773c Odevzdávátko: oprava meníčka 2023-05-15 22:30:20 +02:00
ee1db52114 Odevzdávátko: oprava dokumentace 2023-05-15 22:28:57 +02:00
7e99466166 Odevzdávátko: oprava POSTu 2023-05-15 22:22:03 +02:00
fbcfe7e93f Odevzdávátko: Filtrovat pouze odevzdatelná 2023-05-15 22:00:37 +02:00
ce4ee94fed Odevzdávátko: Nejprve vybrat téma, pak podúlohy – part 1 2023-05-15 21:49:35 +02:00
93fa8c6f2e Odevzdávátko: Autoři řešení -> Další autoři 2023-05-15 20:26:48 +02:00
62 changed files with 985 additions and 243 deletions

View file

@ -1,5 +1,5 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.encoding import force_text from django.utils.encoding import force_str
class OvvpFile: class OvvpFile:
@ -20,7 +20,7 @@ class OvvpFile:
yield '\t'.join(self.columns) + '\n' yield '\t'.join(self.columns) + '\n'
# rows # rows
for r in self.rows: for r in self.rows:
yield '\t'.join([force_text(r[c]) for c in self.columns]) + '\n' yield '\t'.join([force_str(r[c]) for c in self.columns]) + '\n'
def to_string(self): def to_string(self):
return ''.join(self.to_lines()) return ''.join(self.to_lines())

View file

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

View file

@ -8,7 +8,7 @@ from django.shortcuts import get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.views import generic from django.views import generic
from django.utils.encoding import force_text from django.utils.encoding import force_str
from .utils import default_ovvpfile from .utils import default_ovvpfile
from seminar.models import Rocnik, Soustredeni from seminar.models import Rocnik, Soustredeni

View file

@ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView): class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """ """ View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self): def get_queryset(self):
nastaveni = get_object_or_404(m.Nastaveni) qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY)
rocnik = nastaveni.aktualni_rocnik
# Od tohoto místa dál jsem zkoušel spoustu variací podle https://django-polymorphic.readthedocs.io/en/stable/advanced.html
temaQ = Q(Tema___rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
ulohaQ = Q(Uloha___cislo_zadani__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
clanekQ = Q(Clanek___cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ)
#print(temata, ulohy, clanky)
#ulohy.union(temata, all=True)
#print(ulohy)
#ulohy.union(clanky, all=True)
#print(ulohy)
#qs = ulohy
print(qs)
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
Q(nazev__icontains=self.q)) Q(nazev__icontains=self.q))
nadproblem_id = int(self.forwarded.get("nadproblem_id", -1))
if nadproblem_id != -1:
# Seřadíme tak, aby ty s nadproblem==None byly dole (větší motivace tam naklikat konkrétní úlohy) a pak nějak rozumně.
# Tohle je řazení pro odevzdávátko, kde je definován nadproblém, proto je to v tomto ifu. (Jinde si to netroufám řadit)
qs = qs.order_by("nadproblem", "kod", "nazev")
qs = list(filter(lambda problem: problem.hlavni_problem.id == nadproblem_id, qs))
return qs return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView): class ProblemAutocomplete(autocomplete.Select2QuerySetView):

View file

@ -437,7 +437,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Poslat řešení", "title": "Nahrát řešení",
"tree": 1, "tree": 1,
"url": "seminar_nahraj_reseni", "url": "seminar_nahraj_reseni",
"urlaspattern": true "urlaspattern": true
@ -476,9 +476,9 @@
"access_perm_type": 1, "access_perm_type": 1,
"access_permissions": [ "access_permissions": [
[ [
"change_hodnoceni", "org",
"seminar", "auth",
"hodnoceni" "user"
] ]
], ],
"access_restricted": true, "access_restricted": true,
@ -719,7 +719,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Nahrát řešení", "title": "Vložit řešení",
"tree": 1, "tree": 1,
"url": "seminar_vloz_reseni", "url": "seminar_vloz_reseni",
"urlaspattern": true "urlaspattern": true
@ -1026,6 +1026,36 @@
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 51 "pk": 51
}, },
{
"fields": {
"access_guest": false,
"access_loggedin": false,
"access_perm_type": 1,
"access_permissions": [
[
"resitel",
"auth",
"user"
]
],
"access_restricted": true,
"alias": null,
"description": "",
"hidden": false,
"hint": "",
"inbreadcrumbs": true,
"inmenu": true,
"insitetree": true,
"parent": 23,
"sort_order": 52,
"title": "Nahrát řešení k nadproblému {{nadproblem_id}}",
"tree": 1,
"url": "seminar_nahraj_reseni nadproblem_id",
"urlaspattern": true
},
"model": "sitetree.treeitem",
"pk": 52
},
{ {
"fields": { "fields": {
"access_guest": false, "access_guest": false,
@ -1041,13 +1071,13 @@
"inmenu": true, "inmenu": true,
"insitetree": true, "insitetree": true,
"parent": 28, "parent": 28,
"sort_order": 52, "sort_order": 53,
"title": "Přidat PDF", "title": "Přidat PDF",
"tree": 1, "tree": 1,
"url": "/admin/korektury/korekturovanepdf/add/", "url": "/admin/korektury/korekturovanepdf/add/",
"urlaspattern": false "urlaspattern": false
}, },
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 52 "pk": 53
} }
] ]

View file

@ -64,6 +64,36 @@
"ct_app_label": "galerie", "ct_app_label": "galerie",
"ct_model": "obrazek" "ct_model": "obrazek"
}, },
{
"codename": "add_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "change_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "view_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "add_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "change_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "view_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{ {
"codename": "add_komentar", "codename": "add_komentar",
"ct_app_label": "korektury", "ct_app_label": "korektury",
@ -224,6 +254,21 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "clanek" "ct_model": "clanek"
}, },
{
"codename": "add_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "change_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "view_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{ {
"codename": "add_konfera", "codename": "add_konfera",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -304,41 +349,21 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "novinky" "ct_model": "novinky"
}, },
{
"codename": "add_organizator",
"ct_app_label": "seminar",
"ct_model": "organizator"
},
{ {
"codename": "change_organizator", "codename": "change_organizator",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "organizator" "ct_model": "organizator"
}, },
{
"codename": "delete_organizator",
"ct_app_label": "seminar",
"ct_model": "organizator"
},
{ {
"codename": "view_organizator", "codename": "view_organizator",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "organizator" "ct_model": "organizator"
}, },
{
"codename": "add_osoba",
"ct_app_label": "seminar",
"ct_model": "osoba"
},
{ {
"codename": "change_osoba", "codename": "change_osoba",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "osoba" "ct_model": "osoba"
}, },
{
"codename": "delete_osoba",
"ct_app_label": "seminar",
"ct_model": "osoba"
},
{ {
"codename": "view_osoba", "codename": "view_osoba",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -404,21 +429,11 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "problem" "ct_model": "problem"
}, },
{
"codename": "add_resitel",
"ct_app_label": "seminar",
"ct_model": "resitel"
},
{ {
"codename": "change_resitel", "codename": "change_resitel",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "resitel" "ct_model": "resitel"
}, },
{
"codename": "delete_resitel",
"ct_app_label": "seminar",
"ct_model": "resitel"
},
{ {
"codename": "view_resitel", "codename": "view_resitel",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -603,50 +618,5 @@
"codename": "view_taggeditem", "codename": "view_taggeditem",
"ct_app_label": "taggit", "ct_app_label": "taggit",
"ct_model": "taggeditem" "ct_model": "taggeditem"
},
{
"codename": "add_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "change_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "view_fotkaheader",
"ct_app_label": "header_fotky",
"ct_model": "fotkaheader"
},
{
"codename": "add_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "change_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "view_fotkaurlvazba",
"ct_app_label": "header_fotky",
"ct_model": "fotkaurlvazba"
},
{
"codename": "add_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "change_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
},
{
"codename": "view_deadline",
"ct_app_label": "seminar",
"ct_model": "deadline"
} }
] ]

View file

@ -28,7 +28,7 @@ Generuje se za pomocí::
nebo (v případě meníčka):: nebo (v případě meníčka)::
./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json ./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json
./fix_json.py data/sitetree_new.json data/sitetree.json ./fix_json.py data/sitetree_new.json data/sitetree.json
deploy_v2 deploy_v2

View file

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

View file

@ -1,25 +0,0 @@
.. 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. bydlí gitový repozitář s kódem.
.. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce .. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce
prerekvizit <Tabulka prerekvizit v různých distribucích>`. prerekvizit <Alternativní jména balíčků>`.
Doporučené Doporučené
^^^^^^^^^^ ^^^^^^^^^^

97
docs/zavislosti.rst Normal file
View file

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

@ -2,7 +2,6 @@
from django.db import models from django.db import models
#from django.db.models import Q #from django.db.models import Q
from django.utils.encoding import force_text
from imagekit.models import ImageSpecField from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit, Transpose from imagekit.processors import ResizeToFit, Transpose

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-06-19 19:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('korektury', '0019_auto_20221205_2014'),
]
operations = [
migrations.AlterField(
model_name='korekturovanepdf',
name='nazev',
field=models.CharField(help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF', max_length=50, verbose_name='název PDF'),
),
]

View file

@ -16,7 +16,6 @@ import os
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_text
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename
@ -55,7 +54,7 @@ class KorekturovanePDF(models.Model):
cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF') cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF')
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. 22.1 verze 4) korekturovaného PDF') nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF')
komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)') komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)')

View file

@ -5,7 +5,6 @@ třídy většinou rozšiřující nějakou třídu z :mod:`django.views.generic
""" """
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.views import generic from django.views import generic
from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.core.mail import EmailMessage from django.core.mail import EmailMessage

View file

@ -28,7 +28,6 @@ APPEND_SLASH = True
LANGUAGE_CODE = 'cs' LANGUAGE_CODE = 'cs'
TIME_ZONE = 'Europe/Prague' TIME_ZONE = 'Europe/Prague'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@ -54,6 +53,9 @@ LOGIN_REDIRECT_URL = 'profil'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True
DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok 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_error'
# Modules configuration # Modules configuration
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
@ -151,6 +153,7 @@ INSTALLED_APPS = (
'soustredeni', 'soustredeni',
'treenode', 'treenode',
'vyroci', 'vyroci',
'sifrovacka',
# Admin upravy: # Admin upravy:

View file

@ -27,8 +27,9 @@ DEBUG = False
TEMPLATE_DEBUG = False TEMPLATE_DEBUG = False
ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa
'mamweb.bezva.org','gimli.ms.mff.cuni.cz'] 'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny…
]
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

View file

@ -32,7 +32,10 @@ DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = True TEMPLATES[0]['OPTIONS']['debug'] = True
ALLOWED_HOSTS = ['*.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', 'mam.mff.cuni.cz', '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 # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

View file

@ -1,3 +1,4 @@
@charset "utf-8"; /* vynuť utf-8 */
@import url("rozliseni.css"); @import url("rozliseni.css");
@font-face { @font-face {
@ -53,6 +54,17 @@ a.login-ref-admin {
color: #fffbf6; color: #fffbf6;
} }
.napis-webarum {
display: inline;
color: #fffbf6;
float: right;
}
.napis-webarum a {
color: #f9d59e;
text-decoration: underline;
}
/* odkazy a nadpisy */ /* odkazy a nadpisy */
a { a {
@ -1233,6 +1245,7 @@ div.gdpr {
.dosla_reseni tr th, .dosla_reseni tr td { .dosla_reseni tr th, .dosla_reseni tr td {
padding: 1px 10px 1px 10px; padding: 1px 10px 1px 10px;
border-collapse: collapse; border-collapse: collapse;
min-width: 8em; /*Nastřeleno, aby se řádky s řešeními nezalamovaly. Teoreticky není potřeba pro th, ale whatever.*/
} }
.dosla_reseni tr:nth-child(even) { .dosla_reseni tr:nth-child(even) {
@ -1260,3 +1273,31 @@ label[for=id_skola] {
.bodovani>input { .bodovani>input {
width: 4em; width: 4em;
} }
.bodovani>input::placeholder {
color: lightgray;
opacity: 1;
}
.bodovani>input::-webkit-input-placeholder { /* Edge */
color: lightgray;
}
/* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */
/* Podle https://stackoverflow.com/a/48290544 */
/* U autocomplete.ModelSelect2Multiple vyžaduje 'data-dropdown-css-class': 's2m-se-zaskrtavatky' */
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=true]:before {
content: '☑ ';
padding: 0 0 0 8px;
}
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=false]:before {
content: '◻ ';
padding: 0 0 0 8px;
}
/* Oranžové zvýraznění v Select2 */
.select2-results__option--highlighted {
background-color: #e84e10 !important;
}

View file

@ -3,7 +3,10 @@
{% load static %} {% load static %}
{% block errorheading %} {% block errorheading %}
<br> {# Meníčko nedostaneme, protože dostáváme prázdný kontext. Tak alespoň ať se O-JO-JO-JO-JOJ neschovává pod ním #}
{% block nadpis1a %}
O-jo-jo-jo-joj O-jo-jo-jo-joj
{% endblock %}
{% endblock %} {% endblock %}
{% block errortext %} {% block errortext %}

View file

@ -3,6 +3,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang='cs'> <html lang='cs'>
<head> <head>
<meta charset="utf-8"> {# vynuť UTF-8. #}
<title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&amp;M{% endblock title %}</title> <title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&amp;M{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon"> <link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon">
@ -49,6 +50,8 @@
<a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a> <a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a>
{% endif %} {% endif %}
<a class="login-ref-admin" href='/admin'>[admin mainpage]</a> <a class="login-ref-admin" href='/admin'>[admin mainpage]</a>
<span class="napis-webarum">Něco ti nejde/nefunguje/mate tě? <a class="login-ref-admin" href='mailto:web@mam.mff.cuni.cz'>Napiš webařům!</a></span>
</div> </div>
{% endif %} {% endif %}

View file

@ -71,6 +71,8 @@ urlpatterns = [
# Výroční sraz # Výroční sraz
path('sraz/30-let/', include('vyroci.urls')), path('sraz/30-let/', include('vyroci.urls')),
# Miniapka na šifrovačku
path('sifrovacka/', include('sifrovacka.urls')),
] ]
# This is only needed when using runserver. # This is only needed when using runserver.

View file

@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš
Slovníček: Slovníček:
Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám.
Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný.
Poslat řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) Nahrát řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. Vlož řešení = Vložit řešení bez vztahu k aktuálnímu uživateli.
TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
""" """

View file

@ -29,6 +29,8 @@ class PosliReseniForm(forms.Form):
attrs={ attrs={
'data-placeholder--id': '-1', 'data-placeholder--id': '-1',
'data-placeholder--text': '---', 'data-placeholder--text': '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true' 'data-allow-clear': 'true'
}, },
), ),
@ -43,6 +45,8 @@ class PosliReseniForm(forms.Form):
url='autocomplete_resitel', url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}) 'data-allow-clear': 'true'})
) )
@ -62,12 +66,6 @@ class PosliReseniForm(forms.Form):
#poznamka = models.TextField('neveřejná poznámka', blank=True, #poznamka = models.TextField('neveřejná poznámka', blank=True,
# help_text='Neveřejná poznámka k řešení (plain text)') # help_text='Neveřejná poznámka k řešení (plain text)')
#TODO body do cisla
#TODO prilohy
##def __init__(self, *args, **kwargs):
## super().__init__(*args, **kwargs)
## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()])
class NahrajReseniForm(forms.ModelForm): class NahrajReseniForm(forms.ModelForm):
class Meta: class Meta:
@ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm):
url='autocomplete_problem_odevzdatelny', url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
forward=["nadproblem_id"],
), ),
'resitele': 'resitele':
autocomplete.ModelSelect2Multiple( autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel_public', url='autocomplete_resitel_public',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
) )
} }
nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele # FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
if 'resitele' in self.fields: if 'resitele' in self.fields:
# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to. # FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to.
self.fields['resitele'].required = False self.fields['resitele'].required = False
self.fields['resitele'].label = "Další autoři"
if 'problem' in self.fields:
self.fields['problem'].label = "Všechny řešené problémy"
def clean_problem(self):
problem = self.cleaned_data.get('problem')
for p in problem:
if p.stav != m.Problem.STAV_ZADANY:
raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!")
return problem
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm, form = NahrajReseniForm,
@ -223,6 +238,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
'reseni_od': terminy[-2] if rocnik is None else terminy[0], 'reseni_od': terminy[-2] if rocnik is None else terminy[0],
'reseni_do': terminy[-1], 'reseni_do': terminy[-1],
'neobodovane': False, 'neobodovane': False,
'barvicky': True,
} }
return initial return initial
@ -247,3 +263,4 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) reseni_od = forms.DateField(input_formats=[DATE_FORMAT])
reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) reseni_do = forms.DateField(input_formats=[DATE_FORMAT])
neobodovane = forms.BooleanField(required=False) neobodovane = forms.BooleanField(required=False)
barvicky = forms.BooleanField(required=False)

View file

@ -7,19 +7,20 @@
{% block content %} {% block content %}
<h1> <h1>
{% block nadpis1a %} {% block nadpis1a %}
Vložit řešení Nahrát řešení
{% endblock %} {% endblock %}
</h1> </h1>
<p style="text-align: justify">Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a&nbsp;třeba ti je i&nbsp;rychleji opravíme.</p> <form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
<p>Pokud řešíte ve více lidech, je <strong>nutné</strong> přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %} {% csrf_token %}
<table class='form' id="reseni"> <table class='form'>
<tr>
<td><label class="field-label field-required" for="tema">Téma:</label></td>
<td><input id="tema" disabled="" type="text" value="{{ nadproblem }}"></td>
</tr>
{% with field=form.problem %}
<tr> <tr>
{% for field in form %}
<td> <td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}: {{ field.label }}:
@ -28,15 +29,54 @@
<td> <td>
{{ field }} {{ field }}
</td> </td>
{% endfor %}
</tr> </tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</table> </table>
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
<hr>
<h4>Spolupráce s&nbsp;dalšími řešiteli</h4>
<p>Pokud řešíte ve více lidech, je <strong>potřeba</strong> přidat tyto lidi jako „Další autory“. V&nbsp;tomto poli se vyhledává podle přezdívek, které si lze nastavit v&nbsp;„Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
<table class='form'>
{% with field=form.resitele %}
<tr>
<td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}:
</label>
</td>
<td>
{{ field }}
</td>
</tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</table>
<hr> <hr>
{% include "odevzdavatko/prilohy.html" %} {% include "odevzdavatko/prilohy.html" %}
{{form.non_field_errors}}
<hr> <hr>
<h4>Odevzdat řešení</h4> <h4>Odevzdat řešení</h4>
<input type="submit" value="Odevzdat"> <input type="submit" value="Odevzdat">

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<h1>
{% block nadpis1a %}
Nahrát řešení
{% endblock %}
</h1>
<h4>Seznam témat k odevzdání</h4>
<ul>
{% for problem in object_list %}
<li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li>
{% empty %}
<li>Nelze nic odevzdávat.</li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -2,8 +2,9 @@
<h4>Soubory s řešením</h4> <h4>Soubory s řešením</h4>
<p style="text-align: justify">Maximální součet velikostí příloh je cca 49&nbsp;MB. Pokud je to možné a&nbsp;dává to smysl, pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.</p> <p style="text-align: justify">Pokud je to možné a&nbsp;dává to smysl (tj.&nbsp;není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu <strong>PDF</strong>, ostatní formáty nemusíme umět otevřít.</p>
<p style="text-align: justify">Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i&nbsp;poznámku řešitele.</p> <p style="text-align: justify">Pokud svůj soubor <strong>rozumně pojmenuješ</strong>, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i&nbsp;poznámku řešitele.</p>
<p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49&nbsp;MB</strong>.</p>
<div id="form_set"> <div id="form_set">
{% for form in prilohy.forms %} {% for form in prilohy.forms %}

View file

@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #}
{% load barvy_reseni %}
{% block content %} {% block content %}
@ -11,6 +12,7 @@
Od data (vyjma): {{ filtr.reseni_od }} Od data (vyjma): {{ filtr.reseni_od }}
Do data (včetně): {{ filtr.reseni_do }} Do data (včetně): {{ filtr.reseni_do }}
<span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }} <span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }}
<span title="Obarvit shodná řešení shodně">🎨?</span> {{ filtr.barvicky }}
<input type=submit value="→"> <input type=submit value="→">
</form> </form>
@ -36,12 +38,15 @@ Do data (včetně): {{ filtr.reseni_do }}
{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #} {# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #}
{{ resitel }} {{ resitel }}
</td> </td>
{% for hodn in hodnoty %} {% for soucet,bunka in hodnoty %}
<td> <td>
{% if hodn %} {% for reseni,hodnoceni in bunka %}
<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}"> <a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}">
{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} {{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b)
</a> </a><br>
{% endfor %}
{% if bunka|length > 1 %}
<b>Σ: {{soucet}} b</b>
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}

View file

@ -0,0 +1,15 @@
from django import template
register = template.Library()
from functools import cache
import seminar.models as m
@register.filter
@cache
def barva_reseni(r: m.Reseni):
"""Vrátí nějakou barvu pro daný problém, ve tvaru '#RRGGBB'
Efektivně hešujeme do barev."""
#TODO: ne všechny barvy jsou dobře rozlišitelné a vidět…
return f'#{hash(str(r.id)) & 0xffffff:06x}'

View file

@ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),

View file

@ -13,6 +13,7 @@ from django.db.models import Q
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
from decimal import Decimal
from itertools import groupby from itertools import groupby
import logging import logging
@ -37,14 +38,6 @@ logger = logging.getLogger(__name__)
# Taky se může hodit: # Taky se může hodit:
# - Tabulka všech řešitelů x všech problémů? # - Tabulka všech řešitelů x všech problémů?
@dataclass
class SouhrnReseni:
"""Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce."""
pocet_reseni : int
posledni_odevzdani : datetime.datetime
body : float
class TabulkaOdevzdanychReseniView(ListView): class TabulkaOdevzdanychReseniView(ListView):
template_name = 'odevzdavatko/tabulka.html' template_name = 'odevzdavatko/tabulka.html'
model = m.Hodnoceni model = m.Hodnoceni
@ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView):
reseni_od = fcd["reseni_od"] reseni_od = fcd["reseni_od"]
reseni_do = fcd["reseni_do"] reseni_do = fcd["reseni_do"]
jen_neobodovane = fcd["neobodovane"] jen_neobodovane = fcd["neobodovane"]
self.barvicky = fcd["barvicky"]
else: else:
initial = FiltrForm.gen_initial(self.aktualni_rocnik) initial = FiltrForm.gen_initial(self.aktualni_rocnik)
resitele = initial['resitele'] resitele = initial['resitele']
@ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView):
reseni_od = initial['reseni_od'][0] reseni_od = initial['reseni_od'][0]
reseni_do = initial['reseni_do'][0] reseni_do = initial['reseni_do'][0]
jen_neobodovane = initial["neobodovane"] jen_neobodovane = initial["neobodovane"]
self.barvicky = initial["barvicky"]
# Chceme jen letošní problémy # Chceme jen letošní problémy
@ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView):
return qs return qs
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
# TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl…
# self.resitele, self.reseni a self.problemy jsou již nastavené # self.resitele, self.reseni a self.problemy jsou již nastavené
ctx = super().get_context_data(*args, **kwargs) ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.problemy ctx['problemy'] = self.problemy
ctx['resitele'] = self.resitele ctx['resitele'] = self.resitele
tabulka = dict() tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict()
soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict()
def pridej_reseni(problem, resitel, body, cas): def pridej_reseni(resitel, hodnoceni):
problem = hodnoceni.problem
body = hodnoceni.body
cas = hodnoceni.reseni.cas_doruceni
reseni = hodnoceni.reseni
if problem not in tabulka: if problem not in tabulka:
tabulka[problem] = dict() tabulka[problem] = dict()
soucty[problem] = dict()
if resitel not in tabulka[problem]: if resitel not in tabulka[problem]:
tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body) tabulka[problem][resitel] = [(reseni, hodnoceni)]
soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme
else: else:
tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) tabulka[problem][resitel].append((reseni, hodnoceni))
# Zvětšení počtu bodů o aktuální počet, pokud se tam někde nevyskytuje None pak je součet taky None ("Pozor, nezadané body") soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme
tabulka[problem][resitel].body = tabulka[problem][resitel].body + body if body is not None and tabulka[problem][resitel].body is not None else None
tabulka[problem][resitel].pocet_reseni += 1
# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele
tabulka[problem][resitel].problem_id = problem.id
tabulka[problem][resitel].resitel_id = resitel.id
for hodnoceni in self.get_queryset(): for hodnoceni in self.get_queryset():
for resitel in hodnoceni.reseni.resitele.all(): for resitel in hodnoceni.reseni.resitele.all():
pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) pridej_reseni(resitel, hodnoceni)
hodnoty = [] hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
resitele_do_tabulky = [] resitele_do_tabulky: list[m.Resitel] = []
for resitel in self.resitele: for resitel in self.resitele:
dostal_body = False dostal_body = False
resiteluv_radek = [] resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy
for problem in self.problemy: for problem in self.problemy:
if problem in tabulka and resitel in tabulka[problem]: if problem in tabulka and resitel in tabulka[problem]:
resiteluv_radek.append(tabulka[problem][resitel]) resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel]))
dostal_body = True dostal_body = True
else: else:
resiteluv_radek.append(None) resiteluv_radek.append((Decimal(0),[]))
if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body: if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body:
hodnoty.append(resiteluv_radek) hodnoty.append(resiteluv_radek)
resitele_do_tabulky.append(resitel) resitele_do_tabulky.append(resitel)
@ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView):
ctx['form'] = ctx['filtr'] ctx['form'] = ctx['filtr']
# Pro maximum v přesměrovátku ročníků # Pro maximum v přesměrovátku ročníků
ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik
ctx['barvicky'] = self.barvicky
if 'rocnik' in self.kwargs: if 'rocnik' in self.kwargs:
ctx['rocnik'] = self.kwargs['rocnik'] ctx['rocnik'] = self.kwargs['rocnik']
else: else:
@ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView):
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
"""Rozskok mezi více řešeními téhož problému od téhož řešitele.
Asi bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-)
V případě, že takové řešení existuje jen jedno, tak na něj přesměruje."""
model = m.Reseni model = m.Reseni
template_name = 'odevzdavatko/seznam.html' template_name = 'odevzdavatko/seznam.html'
@ -316,7 +320,8 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
if len(zmeny_bodu) == 1: if len(zmeny_bodu) == 1:
hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]]) hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]])
# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno # > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno
if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4: if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2:
# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.") logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.")
hodnoceni.body = -0.1 hodnoceni.body = -0.1
hodnoceni.save() hodnoceni.save()
@ -367,8 +372,8 @@ class SeznamAktualnichReseniView(SeznamReseniView):
return qs return qs
class PosliReseniView(LoginRequiredMixin, FormView): class VlozReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/posli_reseni.html' template_name = 'odevzdavatko/vloz_reseni.html'
form_class = f.PosliReseniForm form_class = f.PosliReseniForm
def form_valid(self, form): def form_valid(self, form):
@ -399,12 +404,31 @@ class PosliReseniView(LoginRequiredMixin, FormView):
return data return data
class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
model = m.Problem
template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html'
def get_queryset(self):
return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True)
class NahrajReseniView(LoginRequiredMixin, CreateView): class NahrajReseniView(LoginRequiredMixin, CreateView):
model = m.Reseni model = m.Reseni
template_name = 'odevzdavatko/nahraj_reseni.html' template_name = 'odevzdavatko/nahraj_reseni.html'
form_class = f.NahrajReseniForm form_class = f.NahrajReseniForm
nadproblem: m.Problem
def setup(self, request, *args, **kwargs):
super().setup(request, *args, **kwargs)
nadproblem_id = self.kwargs["nadproblem_id"]
self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
# Zaříznutí nezadaných problémů
if self.nadproblem.stav != m.Problem.STAV_ZADANY:
raise PermissionDenied()
# Zaříznutí starých řešitelů: # Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… # FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
osoba = m.Osoba.objects.get(user=self.request.user) osoba = m.Osoba.objects.get(user=self.request.user)
@ -417,12 +441,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
}) })
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_initial(self):
nadproblem_id = self.nadproblem.id
return {
"nadproblem_id": nadproblem_id,
"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id
}
def get_context_data(self,**kwargs): def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
if self.request.POST: if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else: else:
data['prilohy'] = f.ReseniSPrilohamiFormSet() data['prilohy'] = f.ReseniSPrilohamiFormSet()
data["nadproblem_id"] = self.nadproblem.id
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
return data return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
@ -474,4 +509,8 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
to=list(prijemci), to=list(prijemci),
).send() ).send()
return formularOKView(self.request, text='Řešení úspěšně odevzdáno') return formularOKView(
self.request,
text='Řešení úspěšně odevzdáno',
dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))],
)

View file

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django_reverse_admin import ReverseModelAdmin from django_reverse_admin import ReverseModelAdmin
from django.contrib.messages import WARNING, ERROR, SUCCESS
import seminar.models as m import seminar.models as m
from datetime import datetime
@admin.register(m.Osoba) @admin.register(m.Osoba)
@ -20,16 +22,24 @@ class OsobaAdmin(admin.ModelAdmin):
def udelej_orgem(self,request,queryset): def udelej_orgem(self,request,queryset):
org_group = Group.objects.get(name='org') org_group = Group.objects.get(name='org')
print(queryset) uspesne_vytvoreni_orgove = 0
for o in queryset: for o in queryset:
if m.Organizator.objects.filter(osoba=o).exists():
# Ref: https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.message_user
self.message_user(request, f"Osoba {o} už je org, přeskakuji.", level=WARNING)
continue
user = o.user user = o.user
print(user) if user is None:
self.message_user(request, f"Osoba {o} nemá uživatele! Přeskakuji.", level=ERROR)
continue
user.groups.add(org_group) user.groups.add(org_group)
user.is_staff = True user.is_staff = True
user.save() user.save()
org = m.Organizator.objects.create(osoba=o) org = m.Organizator.objects.create(osoba=o, organizuje_od=datetime.now())
org.save() org.save()
udelej_orgem.short_description = "Udělej vybraných osob organizátory" uspesne_vytvoreni_orgove += 1
self.message_user(request, f'Úspěšně vytvořeno {uspesne_vytvoreni_orgove} orgů.', level=SUCCESS)
udelej_orgem.short_description = "Udělej z vybraných osob organizátory"
class OsobaInline(admin.TabularInline): class OsobaInline(admin.TabularInline):
model = m.Osoba model = m.Osoba

View file

@ -11,7 +11,7 @@
<a href="{% url 'logout' %}">Odhlásit se</a><br> <a href="{% url 'logout' %}">Odhlásit se</a><br>
<a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br> <a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br>
<a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br> <a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br>
<a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br> <a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br>

63
personalni/tests.py Normal file
View file

@ -0,0 +1,63 @@
from django.test import TestCase, RequestFactory
from django.contrib.auth.models import User, Group
from django.contrib.admin.sites import AdminSite
from personalni.admin import OsobaAdmin
# Tohle bude peklo, až jednou ty modely fakt rozstřelíme… Možná vyrobit various.all_models, které půjdou importovat jako m? :-)
import seminar.models as m
import logging
logger = logging.getLogger(__name__)
class DelaniOrguTest(TestCase):
def setUp(self):
# Admin musí mít instanci
# Ref: https://www.argpar.se/posts/programming/testing-django-admin/
adm_site = AdminSite()
self.admin = OsobaAdmin(m.Osoba, adm_site)
from django.contrib.messages.storage.cookie import CookieStorage
self.request = RequestFactory().get('/admin')
self.request._messages = CookieStorage(self.request)
self.org_group = Group.objects.get(name='org')
novy_user = User.objects.create(username='osoba')
self.nova_osoba = m.Osoba.objects.create(
jmeno='Milada',
prijmeni='Von Kolej',
user = novy_user,
# Snad nic dalšího nepotřebujeme, kdyžtak se doplní…
)
stary_user = User.objects.create(username='stary_user')
stara_osoba = m.Osoba.objects.create(user=stary_user)
self.stary_org = m.Organizator.objects.create(osoba=stara_osoba)
def test_pridani_orga(self):
# Nejdřív to není org…
self.assertFalse(m.Organizator.objects.filter(osoba=self.nova_osoba).exists())
self.assertNotIn(self.org_group, self.nova_osoba.user.groups.all())
self.assertFalse(self.nova_osoba.user.has_perm('auth.org'))
self.assertFalse(self.nova_osoba.user.is_staff)
# Pak orga uděláme…
qs = m.Osoba.objects.filter(id=self.nova_osoba.id)
self.admin.udelej_orgem(self.request, qs)
# A pak už to org má být.
self.nova_osoba.refresh_from_db()
self.assertTrue(self.nova_osoba.user.is_staff)
# FIXME: V db nejsou práva. Nový org je sice ve skupině "org", ale ta nemá právo "auth.org"
# Očekávané řešení: dodat fixture, která to přidá.
#self.assertTrue(self.nova_osoba.user.has_perm('auth.org'))
self.assertIn(self.org_group, self.nova_osoba.user.groups.all())
self.assertTrue(m.Organizator.objects.filter(osoba=self.nova_osoba).exists())
novy_org = m.Organizator.objects.get(osoba=self.nova_osoba)
self.assertIsNotNone(novy_org.organizuje_od)
def test_pridani_stareho_orga(self):
self.admin.udelej_orgem(self.request, m.Osoba.objects.filter(id=self.stary_org.osoba.id)) # Ugly
# Když to spadne, tak jsem se to dozvěděl, takže už nepotřebuju nic kontrolovat.
# Jestli to funguje správně má řešit jiný test.

View file

@ -173,7 +173,11 @@ def resitelEditView(request):
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa']) msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_edit.save() resitel_edit.save()
osoba_edit.save() osoba_edit.save()
return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>') return formularOKView(
request,
text='Údaje byly úspěšně uloženy.',
dalsi_odkazy=[("Vrátit se zpět na profil", reverse("profil"))],
)
return render(request, 'personalni/udaje/edit.html', {'form': form}) return render(request, 'personalni/udaje/edit.html', {'form': form})

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.utils.encoding import force_text
from seminar.models import Organizator, Soustredeni from seminar.models import Organizator, Soustredeni

View file

@ -5,6 +5,7 @@ psycopg2
html5lib html5lib
ipython ipython
Pillow Pillow
pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0
pytz pytz
six six
pexpect pexpect
@ -13,7 +14,7 @@ Unidecode
# Django and modules # Django and modules
Django<3.3 Django<5.0
#django-bootstrap-sass #django-bootstrap-sass
django-mptt django-mptt
django-reversion django-reversion
@ -24,7 +25,7 @@ django-ckeditor
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
django-flat-theme django-flat-theme
django-taggit django-taggit
django-autocomplete-light>=3.9.0rc1 django-autocomplete-light>=3.9.0
django-crispy-forms django-crispy-forms
django-imagekit django-imagekit
django-polymorphic django-polymorphic
@ -52,9 +53,6 @@ Werkzeug
requests requests
# requests-oauthlib # requests-oauthlib
# uWSGI
uWSGI
# Potřeba pro test data # Potřeba pro test data
lorem lorem

View file

@ -12,15 +12,25 @@ from django.utils.safestring import mark_safe
import seminar.models as m import seminar.models as m
admin.site.register(m.Rocnik) admin.site.register(m.Rocnik)
admin.site.register(m.Deadline)
admin.site.register(m.ZmrazenaVysledkovka) admin.site.register(m.ZmrazenaVysledkovka)
@admin.register(m.Deadline)
class DeadlineAdmin(admin.ModelAdmin):
actions = ['pregeneruj_vysledkovku']
# Nikomu nezobrazovat, ale superuživatelům se může hodit :-)
@admin.action(permissions=['bazmek'], description= 'Přegeneruj výsledkovky vybraných deadlinů')
def pregeneruj_vysledkovku(self, req, qs):
for deadline in qs:
deadline.vygeneruj_vysledkovku()
def has_bazmek_permission(self, request):
# Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu…
return request.user.is_superuser
class DeadlineAdminInline(admin.TabularInline): class DeadlineAdminInline(admin.TabularInline):
model = m.Deadline model = m.Deadline
extra = 0 extra = 0
class CisloForm(ModelForm): class CisloForm(ModelForm):
class Meta: class Meta:
@ -71,7 +81,7 @@ class CisloForm(ModelForm):
@admin.register(m.Cislo) @admin.register(m.Cislo)
class CisloAdmin(admin.ModelAdmin): class CisloAdmin(admin.ModelAdmin):
form = CisloForm form = CisloForm
actions = ['force_publish'] actions = ['force_publish', 'pregeneruj_vysledkovky']
inlines = (DeadlineAdminInline,) inlines = (DeadlineAdminInline,)
def force_publish(self,request,queryset): def force_publish(self,request,queryset):
@ -111,6 +121,17 @@ class CisloAdmin(admin.ModelAdmin):
force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými'
# Jen pro superuživatele
@admin.action(permissions=['bazmek'], description='Přegenerovat výsledkovky všech deadlinů vybraných čísel')
def pregeneruj_vysledkovky(self, req, qs):
for cislo in qs:
for deadline in cislo.deadline_v_cisle.all():
deadline.vygeneruj_vysledkovku()
def has_bazmek_permission(self, request):
# Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu…
return request.user.is_superuser
@admin.register(m.Problem) @admin.register(m.Problem)
class ProblemAdmin(PolymorphicParentModelAdmin): class ProblemAdmin(PolymorphicParentModelAdmin):

View file

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
import datetime import datetime
from django.utils.timezone import utc
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -16,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='problem', model_name='problem',
name='timestamp', name='timestamp',
field=models.DateTimeField(default=datetime.datetime(2015, 5, 15, 8, 54, 56, 319985, tzinfo=utc), verbose_name='vytvo\u0159eno', auto_now=True), field=models.DateTimeField(default=datetime.datetime(2015, 5, 15, 8, 54, 56, 319985, tzinfo=datetime.timezone.utc), verbose_name='vytvo\u0159eno', auto_now=True),
preserve_default=False, preserve_default=False,
), ),
migrations.AlterField( migrations.AlterField(

View file

@ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel):
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
return str(self.kod) return str(self.kod)
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
return '<Není zadaný>' return f'<Není zadaný: {self.kod}>'
# def verejne(self): # def verejne(self):
# # aktuálně podle stavu problému # # aktuálně podle stavu problému
@ -571,9 +571,9 @@ class Tema(Problem):
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
if self.nadproblem: if self.nadproblem:
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod)
return "t{}".format(self.kod) return 't'+self.kod
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
return '<Není zadaný>' return f'<Není zadaný: {self.kod}>'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -607,9 +607,9 @@ class Clanek(Problem):
# Nemělo by být potřeba # Nemělo by být potřeba
# if self.nadproblem: # if self.nadproblem:
# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) # return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod)
return "c{}".format(self.kod) return "c" + self.kod
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
return '<Není zadaný>' return f'<Není zadaný: {self.kod}>'
def node(self): def node(self):
return None return None
@ -642,12 +642,9 @@ class Uloha(Problem):
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) return f"{self.cislo_zadani.poradi}.{self.kod}"
if self.nadproblem:
return self.nadproblem.kod_v_rocniku+name
return name
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
return '<Není zadaný>' return f'<Není zadaný: {self.kod}>'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

@ -40,7 +40,9 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
<a href='{{ rocnik.verejne_url }}'>Výsledková listina</a> <!-- FIXME: url výsledkovky--> {% if rocnik.verejne_vysledkovky_cisla %} {# Tohle jsem asi neměl tady použít, ale šlo to… #}
<a href='{{ rocnik.verejne_url }}#vysledky'>Výsledková listina</a> <!-- FIXME: url výsledkovky-->
{% endif %}
</div> </div>
</div> </div>

View file

@ -113,7 +113,7 @@
{% if vysledkovka.radky_vysledkovky %} {% if vysledkovka.radky_vysledkovky %}
<h2>Výsledková listina</h2> <h2 id=vysledky>Výsledková listina</h2>
{% include "vysledkovky/vysledkovka_rocnik.html" %} {% include "vysledkovky/vysledkovka_rocnik.html" %}
{% endif %} {% endif %}

View file

@ -3,7 +3,7 @@ from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.views import generic from django.views import generic
from django.utils.translation import ugettext as _ from django.utils.translation import gettext as _
from django.http import Http404 from django.http import Http404
from django.db.models import Q, Sum, Count from django.db.models import Q, Sum, Count
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
@ -35,6 +35,7 @@ from django.conf import settings
import unicodedata import unicodedata
import logging import logging
import time import time
from collections.abc import Sequence
from seminar.utils import aktivniResitele from seminar.utils import aktivniResitele
@ -534,7 +535,9 @@ class PosledniCisloVysledkovkaView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PosledniCisloVysledkovkaView, self).get_context_data() context = super(PosledniCisloVysledkovkaView, self).get_context_data()
rocnik = context['rocnik'] rocnik = context['rocnik']
cislo = rocnik.cisla.order_by("poradi").last() cislo = rocnik.cisla.order_by("poradi").filter(deadline_v_cisle__isnull=False).last()
if cislo is None:
raise Http404(f"Ročník {rocnik.rocnik} nemá číslo s deadlinem.")
cislopred = cislo.predchozi() cislopred = cislo.predchozi()
context['vysledkovka'] = VysledkovkaDoTeXu( context['vysledkovka'] = VysledkovkaDoTeXu(
cislo, cislo,
@ -677,9 +680,9 @@ def StavDatabazeView(request):
# Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí)
def formularOKView(request, text=''): def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()):
template_name = 'seminar/formular_ok.html' template_name = 'seminar/formular_ok.html'
odkazy = [ odkazy = list(dalsi_odkazy) + [
# (Text, odkaz) # (Text, odkaz)
('Vrátit se na titulní stránku', reverse('titulni_strana')), ('Vrátit se na titulní stránku', reverse('titulni_strana')),
('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')), ('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')),

0
sifrovacka/__init__.py Normal file
View file

8
sifrovacka/admin.py Normal file
View file

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import OdpovedUcastnika, SpravnaOdpoved
# Register your models here.
admin.site.register(OdpovedUcastnika)
admin.site.register(SpravnaOdpoved)

5
sifrovacka/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SifrovackaConfig(AppConfig):
name = 'sifrovacka'

18
sifrovacka/forms.py Normal file
View file

@ -0,0 +1,18 @@
from django.core.exceptions import ValidationError
from django.forms import ModelForm, Textarea
from .models import OdpovedUcastnika, SpravnaOdpoved
class SifrovackaForm(ModelForm):
class Meta:
model = OdpovedUcastnika
fields = ["sifra", "odpoved", ]
widgets = {
"odpoved": Textarea(attrs={'rows': 1, 'cols': 30}),
}
def clean_sifra(self):
sifra = self.cleaned_data.get('sifra')
if SpravnaOdpoved.objects.filter(sifra=sifra).count() == 0:
raise ValidationError("Tohle číslo šifry v databázi nemáme. Zkontrolujte si ho prosím.")
return sifra

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.22 on 2023-10-14 09:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('seminar', '0113_resitel_zasilat_cislo_papirove'),
]
operations = [
migrations.CreateModel(
name='SpravnaOdpoved',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('odpoved', models.TextField()),
('sifra', models.IntegerField()),
('skryty_text', models.TextField()),
],
),
migrations.CreateModel(
name='OdpovedUcastnika',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('odpoved', models.TextField(verbose_name='Tajenka')),
('sifra', models.IntegerField(verbose_name='Číslo šifry')),
('resitel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.resitel')),
],
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.22 on 2023-10-15 17:44
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('sifrovacka', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='odpoveducastnika',
options={'ordering': ['-timestamp']},
),
migrations.AddField(
model_name='odpoveducastnika',
name='timestamp',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp'),
),
migrations.AlterField(
model_name='odpoveducastnika',
name='odpoved',
field=models.TextField(verbose_name='Tajenka bez diakritiky'),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.22 on 2023-10-16 17:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sifrovacka', '0002_auto_20231015_1944'),
]
operations = [
migrations.AddField(
model_name='odpoveducastnika',
name='uspech',
field=models.BooleanField(default=False, verbose_name='Úspěch'),
),
]

View file

27
sifrovacka/models.py Normal file
View file

@ -0,0 +1,27 @@
from django.db import models
from django.utils import timezone
from seminar.models.personalni import Resitel
# Create your models here.
class OdpovedUcastnika(models.Model):
class Meta:
ordering = ["-timestamp"]
resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE)
odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,)
sifra = models.IntegerField("Číslo šifry", blank=False, null=False,)
timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now)
uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False)
class SpravnaOdpoved(models.Model):
odpoved = models.TextField(blank=False, null=False,)
sifra = models.IntegerField(blank=False, null=False,)
skryty_text = models.TextField(blank=False, null=False,)
def __str__(self):
return f"{self.sifra}: {self.odpoved}"

View file

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block content %}
<h1>{% block nadpis1a %}Šifrovačka odpovědi{% endblock nadpis1a %}</h1>
<table class="dosla_reseni">
<tr>
<th>Timestamp</th>
<th>Řešitel</th>
<th>Šifra</th>
<th>Odpověď</th>
</tr>
{% for u in object_list %}
<tr>
<td>{{ u.timestamp }}</td>
<td>{{ u.resitel }}</td>
<td>{{ u.sifra }}</td>
<td style="color: {% if u.uspech %}green{% else %}red{% endif %};">{{ u.odpoved }}</td>
</tr>
{% endfor %}
</table>
{% endblock content %}

View file

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block content %}
<br>
<h1>{% block nadpis1a %}M&Mí šifrovačka{% endblock nadpis1a %}</h1>
<br>
<h2>Zadat tajenku šifry:</h2>
<form action="{% url 'sifrovacka' %}" method="post">
<table class="form">
{{form.non_field_errors}}
{% for field in form %}
<tr>
<td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}
</label>
</td>
<td {% if field.help_text %} class="field-with-comment"{% endif %}>
{{ field }}
<span class="field-comment">{{ field.help_text|safe }}</span>
</td>
</tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endfor %}
</table>
{% csrf_token %}
<input type="submit" value="Tak pravím!">
</form>
{% endblock content %}

17
sifrovacka/urls.py Normal file
View file

@ -0,0 +1,17 @@
from django.urls import path
from seminar.utils import org_required, resitel_or_org_required
from .views import SifrovackaView, SifrovackaListView
urlpatterns = [
path(
'',
resitel_or_org_required(SifrovackaView.as_view()),
name='sifrovacka'
),
path(
'odpovedi/',
org_required(SifrovackaListView.as_view()),
name='sifrovacka_odpovedi'
),
]

33
sifrovacka/views.py Normal file
View file

@ -0,0 +1,33 @@
from django.urls import reverse
from django.views.generic import FormView, ListView
from seminar.views import formularOKView
from .forms import SifrovackaForm
from .models import OdpovedUcastnika, SpravnaOdpoved
from seminar.models.personalni import Resitel
# Create your views here.
class SifrovackaView(FormView):
template_name = 'sifrovacka/sifrovacka.html'
form_class = SifrovackaForm
def form_valid(self, form):
instance = form.save(commit=False)
resitel = Resitel.objects.get(osoba__user=self.request.user)
instance.resitel = resitel
instance.save()
sifra = SpravnaOdpoved.objects.filter(sifra=instance.sifra, odpoved__iexact=instance.odpoved.strip()).first()
if sifra is None:
return formularOKView(self.request, f'<h1>Bohužel vám hvězdy nebyly nakloněny. Rozumějte <i>máte to blbě</i>.</h1> <p><a href="{reverse("sifrovacka")}">Zkusit znovu.</a></p><br><br><br>')
instance.uspech = True
instance.save()
return formularOKView(self.request, f'<h1>{sifra.skryty_text}</h1> <p><a href="{reverse("sifrovacka")}">Odevzdat další.</a></p><br><br><br>')
class SifrovackaListView(ListView):
template_name = 'sifrovacka/odpovedi_list.html'
model = OdpovedUcastnika

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -0,0 +1,19 @@
{#{% extends "error_base.html" %} Z toho nedědíme, protože se nemá přecházet na titulní stránku. #}
{% extends "base.html" %}
{% load static %}
{% block content %}
<h2>{% block nadpis1a %}O-jo-jo-jo-joj{% endblock nadpis1a %}</h2>
<p>
Problém se sušenkami či něčím podobným. Zkuste {% if url %}to prosím znovu: <a href="{{ url }}">{{ url }}</a>. Případně můžete {% endif %}přejít na <a href="/">titulní stránku</a>.
</p>
<p>Pokud problém přetrvává obraťte se na nás přes e-mail: <a href="mailto:mam@matfyz.cz">mailto:mam@matfyz.cz</a> a pošlete nám následující popis chyby: <code>{{ reason }}</code></p>
<img src="{% static 'various/img/zere_kostku.svg' %}">
{% endblock %}

View file

@ -1,3 +1,13 @@
from django.http import HttpResponseForbidden
from django.shortcuts import render from django.shortcuts import render
# Create your views here. # Create your views here.
def csrf_error(request, reason=""):
""" Jednoduchý „template view“ (třída to být nemůže) pro CSRF chyby """
return render(
request, 'various/403_csrf.html',
{"url": request.META.get("HTTP_REFERER", None), "reason": reason},
status=HttpResponseForbidden.status_code,
)

View file

@ -4,11 +4,11 @@
<th class='border-r'>#</th> <th class='border-r'>#</th>
<th class='border-r'>Jméno</th> <th class='border-r'>Jméno</th>
{% for p in vysledkovka.temata_a_spol%} {% for p in vysledkovka.temata_a_spol%}
<th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}</th> <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}<span title="{{ p }}">{{ p.kod_v_rocniku }}</span>{# </a> #}</th>
{# TODELETE #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% for podproblemy in vysledkovka.podproblemy_iter.next %}
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}
@ -17,7 +17,7 @@
{# TODELETE #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% for podproblemy in vysledkovka.podproblemy_iter.next %}
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}

View file

@ -257,7 +257,7 @@ class VysledkovkaCisla(Vysledkovka):
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
hlavni_problemy = set() hlavni_problemy = set()
for p in self.problemy: for p in self.problemy:
hlavni_problemy.add(p.hlavni_problem) hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy…
# zunikátnění # zunikátnění
hlavni_problemy = list(hlavni_problemy) hlavni_problemy = list(hlavni_problemy)
@ -313,7 +313,7 @@ class VysledkovkaCisla(Vysledkovka):
# Sečteme hodnocení # Sečteme hodnocení
for hodnoceni in self.hodnoceni_do_cisla: for hodnoceni in self.hodnoceni_do_cisla:
prob = hodnoceni.problem prob = hodnoceni.problem.get_real_instance()
nadproblem = prob.hlavni_problem.id nadproblem = prob.hlavni_problem.id
# Když nadproblém není "téma", pak je "Ostatní" # Když nadproblém není "téma", pak je "Ostatní"
@ -366,18 +366,12 @@ class VysledkovkaCisla(Vysledkovka):
for problem in self.problemy: for problem in self.problemy:
h_problem = problem.hlavni_problem h_problem = problem.hlavni_problem
if h_problem in temata_a_spol: if h_problem in temata_a_spol:
podproblemy[h_problem.id].append(problem) podproblemy[h_problem.id].append(problem.get_real_instance())
else: else:
podproblemy[-1].append(problem) podproblemy[-1].append(problem.get_real_instance())
for podproblem in podproblemy.keys(): for podproblem in podproblemy.keys():
def int_or_zero(p): podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)
try:
return int(p.kod)
except ValueError:
return 0
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero)
return podproblemy return podproblemy
@cached_property @cached_property