Browse Source

Merge branch 'develop' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into develop

export_seznamu_prednasek
Kateřina Č 3 years ago
parent
commit
ea3b1a7791
  1. 20
      Makefile
  2. 2
      checklinks.sh
  3. 19
      data/flat.json
  4. 173
      docs/zapisy/2021-12-06-testovani_dokumentace_codereview.md
  5. 3
      galerie/templates/galerie/Galerie.html
  6. 24
      korektury/admin.py
  7. 18
      korektury/migrations/0018_korekturovanepdf_poslat_mail.py
  8. 4
      korektury/models.py
  9. 1
      korektury/static/korektury/opraf.css
  10. 3
      korektury/templates/korektury/opraf.html
  11. 3
      korektury/templates/korektury/seskupeny_seznam.html
  12. 1
      korektury/urls.py
  13. 23
      korektury/views.py
  14. 1
      mamweb/settings_common.py
  15. 5
      mamweb/settings_local.py
  16. 1
      mamweb/settings_prod.py
  17. 1
      mamweb/settings_test.py
  18. 17
      mamweb/static/css/mamweb.css
  19. 29
      mamweb/static/css/rozliseni.css
  20. 3
      mamweb/templates/admin/base_site.html
  21. 2
      mamweb/templates/base.html
  22. 2
      mamweb/urls.py
  23. 2
      odevzdavatko/templates/odevzdavatko/tabulka.html
  24. 3
      odevzdavatko/views.py
  25. 70
      personalni/views.py
  26. 2
      requirements.txt
  27. 19
      seminar/migrations/0100_auto_20211129_2354.py
  28. 3
      seminar/models/pomocne.py
  29. 22
      seminar/models/tvorba.py
  30. 4
      seminar/templates/seminar/archiv/rocnik.html
  31. 5
      seminar/urls.py
  32. 13
      seminar/utils.py
  33. 9
      seminar/views/views_all.py
  34. 10
      various/context_processors.py
  35. 14
      vysledkovky/templates/vysledkovky/vysledkovka_cisla.html
  36. 2
      vysledkovky/utils.py

20
Makefile

@ -102,10 +102,10 @@ deploy_prod: venv_check
sync_prod_flatpages: venv_check sync_prod_flatpages: venv_check
@echo Downloading current version of flatpages from mamweb-prod. @echo Downloading current version of flatpages from mamweb-prod.
ssh mam-web@gimli.ms.mff.cuni.cz \ ssh mam-web@gimli.ms.mff.cuni.cz \
"cd /akce/mam/www/mamweb-prod; . env/bin/activate; ./manage.py dumpdata flatpages --indent=2 > flat.json" "cd /akce/mam/www/mamweb-prod; . env/bin/activate; ./manage.py dumpdata flatpages --indent=2 > flat.json; ./fix_json.py flat.json flat_fixed.json"
rsync -ave ssh mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/flat.json ./flat.json rsync -ave ssh mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/flat_fixed.json data/flat.json
@echo "Applying downloaded flatpages." @echo "Applying downloaded flatpages."
./manage.py loaddata flat.json ./manage.py loaddata data/flat.json
@echo "Done." @echo "Done."
# Sync test media directory with production # Sync test media directory with production
@ -114,24 +114,16 @@ sync_test_media:
@if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-test" ]; then echo "Only possible in /akce/mam/www/mamweb-test"; exit 1; fi @if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-test" ]; then echo "Only possible in /akce/mam/www/mamweb-test"; exit 1; fi
rsync -av --delete /akce/mam/www/mamweb-prod/media/ ./media rsync -av --delete /akce/mam/www/mamweb-prod/media/ ./media
# Sync test database with production database # Sync (with drop) test database with production database
sync_test_db:
@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi
pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql
pg_dump -Fc mam_prod > dump-prod.sql
pg_restore -c --if-exists -d mam_test dump-prod.sql
rm dump-prod.sql
@echo Done.
# Aggresive variant: destroy original mam_test db with 'DROP OWNED BY "mam-web";'
sync_test_db_aggressive: sync_test_db_aggressive:
@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi @if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi
pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql
pg_dump -Fc mam_prod > dump-prod.sql pg_dump -Fc mam_prod > dump-prod.sql
@# I am not sure which shell is used, so I am calling bash to make sure @# I am not sure which shell is used, so I am calling bash to make sure
bash -c "psql mam_test <<< 'DROP OWNED BY \"mam-web\";'" psql mam_test -c 'DROP OWNED BY "mam-web";'
pg_restore -c --if-exists -d mam_test dump-prod.sql pg_restore -c --if-exists -d mam_test dump-prod.sql
rm dump-prod.sql rm dump-prod.sql
psql mam_test -c "UPDATE django_site SET name='MaMweb (test)', domain='mam-test.ks.matfyz.cz' WHERE id=1"
@echo Done. @echo Done.
# Sync test with production # Sync test with production

2
checklinks.sh

@ -15,4 +15,4 @@ cd "$tmp"
wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" || true # wget nejspíš skončí s chybou, že něco nestáhl… wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" || true # wget nejspíš skončí s chybou, že něco nestáhl…
echo "Result: (a last few lines of the file $logfile)" echo "Result: (a last few lines of the file $logfile)"
sed -ne '/^Found [0-9]* broken links/,$ p' "$logfile" sed -ne '/^Found [0-9]* broken link/,$ p' "$logfile"

19
data/flat.json

@ -91,7 +91,7 @@
}, },
{ {
"fields": { "fields": {
"content": "<h2>Podzimní soustředění</h2>\r\n\r\n<p>se uskuteční 16. - 24. října 2021.</p>", "content": "<h2>Jarní soustředění</h2>\r\n\r\n<p>se uskuteční 2. – 10. dubna 2022.</p>",
"enable_comments": false, "enable_comments": false,
"registration_required": false, "registration_required": false,
"sites": [ "sites": [
@ -136,7 +136,7 @@
}, },
{ {
"fields": { "fields": {
"content": "<h2>Zážitkové akce</h2>\r\n\r\n<h6>Letní a&nbsp;Zimní Škola Matematiky a&nbsp;Fyziky</h6>\r\n\r\n<p style=\"text-align: justify;\">ŠMFko je zážitková akce určená středoškolákům se zájmem o&nbsp;další sebevzdělání. Krom populárně naučných přednášek se na ŠMFku proběhneš venku, vyřádíš se ve sněhu, užiješ si veselý vnitřní program a&nbsp;taky se pobavíš během společného šarádění, lenošení nebo hraní na kytaru.</p>\r\n\r\n<h6>InterSoB</h6>\r\n\r\n<p style=\"text-align: justify;\">InterSoB je zábavná a&nbsp;poučná jednodenní soutěž středoškolských studentů, při které máte možnost podívat se netradičním způsobem do zákulisí Masarykovy univerzity, vyzkoušet si své schopnosti v&nbsp;mnoha různých oblastech, udělat si s&nbsp;kamarády zajímavý výlet po Brně a&nbsp;v&nbsp;neposlední řadě také poměřit svoje síly s&nbsp;dalšími&nbsp;týmy.</p>\r\n\r\n<h2>Další semináře</h2>\r\n\r\n<h6>Korespondenční Seminář z&nbsp;Programování</h6>\r\n\r\n<p style=\"text-align: justify;\">KSP&nbsp;je seminář určený pro studenty středních a&nbsp;základních škol, kteří mají zájem naučit se něco z&nbsp;oblasti algoritmů, logických úloh, programování a&nbsp;informatiky vůbec. Na své si však přijdou i&nbsp;příznivci matematiky (a vlastně libovolného přemýšlení), ježto oba obory mají mnoho společného.</p>\r\n\r\n<h6>FYzikální KOrespondenční Seminář</h6>\r\n\r\n<p style=\"text-align: justify;\">FYKOS pro vás představuje možnost si zajímavým způsobem rozšířit chápání fyziky a&nbsp;proniknout do dalších, dosud nepoznaných, oblastí této vědy.&nbsp;Cílem FYKOSu je rozvíjet fyzikální myšlení, protože člověk, který se umí nad (nejen fyzikálními) problémy zamyslet a&nbsp;cítí touhu dobrat se k&nbsp;nějakému řešení, se uplatní všude, kde si schopností lidského mozku cení.</p>\r\n\r\n<h6>Matematický korespondenční seminář PraSe (PRAžský SEminář)</h6>\r\n\r\n<p style=\"text-align: justify;\">Řešením úloh tohoto semináře získáš mnoho matematických znalostí a&nbsp;naučíš přesněji a&nbsp;srozumitelněji formulovat své myšlenky a&nbsp;závěry. Seminář je dobrou přípravou pro účast v&nbsp;nejrůznějších matematických soutěžích i&nbsp;pro další studium matematiky, ale schopnost logického myšlení, kterou si můžeš procvičit, se ti v&nbsp;životě bude hodit, i&nbsp;když se v&nbsp;něm třeba právě matematice věnovat nehodláš.</p>\r\n\r\n<h2>Pro mladší sourozence</h2>\r\n\r\n<h6>Pikomat</h6>\r\n\r\n<p style=\"text-align: justify;\">Pikomat je matematický korespondenční seminář určený žákům šestých až devátých tříd základních škol a&nbsp;studentům odpovídajících ročníků víceletých gymnázií.&nbsp;Spočívá v&nbsp;řešení několika úloh propojených příběhem. Na jaře se koná soustředění pro nejlepší řešitele, v&nbsp;létě pak tábor pro všechny zájemce.</p>\r\n\r\n<h6>Výfuk (VÝpočty Fyzikálních UKolů)</h6>\r\n\r\n<p style=\"text-align: justify;\">Výfuk je samostatný korespondenční seminář Matfyzu, který spadá pod Katedru didaktiky fyziky. Během školního roku kromě šesti sérií semináře organizátoři připravují i&nbsp;podzimní a&nbsp;jarní setkání, letní tábor a&nbsp;Náboj junior.</p>\r\n\r\n<h1>Databáze mimoškolních aktivit</h1>\r\n\r\n<p style=\"text-align: justify;\">Je-li ti výčet aktivit výše málo nebo tě žádná z&nbsp;nich nezaujala, doporučujeme navštívit web organizace <a href=\"https://prostredoskolaky.cz\">ProStředoškoláky</a>, jež zde připravila rozsáhlou databázi mimoškolních aktivit a&nbsp;akcí. Krom toho organizace pořádá soutěž <strong>Středoškolák roku</strong>, ve které každý rok oceňuje nejaktivnější středoškoláky. Věnuješ-li se tedy mimoškolně něčemu ve větším měřítku, neváhej se do soutěže přihlásit.</p>", "content": "<h2>Zážitkové akce</h2>\r\n\r\n<a href=\"https://smf.mff.cuni.cz/\"><h6>Letní a&nbsp;Zimní Škola Matematiky a&nbsp;Fyziky</h6></a>\r\n\r\n<p style=\"text-align: justify;\">ŠMFko je zážitková akce určená středoškolákům se zájmem o&nbsp;další sebevzdělání. Krom populárně naučných přednášek se na ŠMFku proběhneš venku, vyřádíš se ve sněhu, užiješ si veselý vnitřní program a&nbsp;taky se pobavíš během společného šarádění, lenošení nebo hraní na kytaru.</p>\r\n\r\n<a href=\"https://intersob.math.muni.cz/\"><h6>InterSoB</h6></a>\r\n\r\n<p style=\"text-align: justify;\">InterSoB je zábavná a&nbsp;poučná jednodenní soutěž středoškolských studentů, při které máte možnost podívat se netradičním způsobem do zákulisí Masarykovy univerzity, vyzkoušet si své schopnosti v&nbsp;mnoha různých oblastech, udělat si s&nbsp;kamarády zajímavý výlet po Brně a&nbsp;v&nbsp;neposlední řadě také poměřit svoje síly s&nbsp;dalšími&nbsp;týmy.</p>\r\n\r\n<h2>Další semináře</h2>\r\n\r\n<a href=\"https://ksp.mff.cuni.cz/\"><h6>Korespondenční Seminář z&nbsp;Programování</h6></a>\r\n\r\n<p style=\"text-align: justify;\">KSP&nbsp;je seminář určený pro studenty středních a&nbsp;základních škol, kteří mají zájem naučit se něco z&nbsp;oblasti algoritmů, logických úloh, programování a&nbsp;informatiky vůbec. Na své si však přijdou i&nbsp;příznivci matematiky (a vlastně libovolného přemýšlení), ježto oba obory mají mnoho společného.</p>\r\n\r\n<a href=\"https://fykos.cz/\"><h6>FYzikální KOrespondenční Seminář</h6></a>\r\n\r\n<p style=\"text-align: justify;\">FYKOS pro vás představuje možnost si zajímavým způsobem rozšířit chápání fyziky a&nbsp;proniknout do dalších, dosud nepoznaných, oblastí této vědy.&nbsp;Cílem FYKOSu je rozvíjet fyzikální myšlení, protože člověk, který se umí nad (nejen fyzikálními) problémy zamyslet a&nbsp;cítí touhu dobrat se k&nbsp;nějakému řešení, se uplatní všude, kde si schopností lidského mozku cení.</p>\r\n\r\n\r\n<a href=\"https://prase.cz/\"><h6>Matematický korespondenční seminář PraSe (PRAžský SEminář)</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Řešením úloh tohoto semináře získáš mnoho matematických znalostí a&nbsp;naučíš přesněji a&nbsp;srozumitelněji formulovat své myšlenky a&nbsp;závěry. Seminář je dobrou přípravou pro účast v&nbsp;nejrůznějších matematických soutěžích i&nbsp;pro další studium matematiky, ale schopnost logického myšlení, kterou si můžeš procvičit, se ti v&nbsp;životě bude hodit, i&nbsp;když se v&nbsp;něm třeba právě matematice věnovat nehodláš.</p>\r\n\r\n<h2>Pro mladší sourozence</h2>\r\n\r\n\r\n<a href=\"https://pikomat.mff.cuni.cz/\"><h6>Pikomat</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Pikomat je matematický korespondenční seminář určený žákům šestých až devátých tříd základních škol a&nbsp;studentům odpovídajících ročníků víceletých gymnázií.&nbsp;Spočívá v&nbsp;řešení několika úloh propojených příběhem. Na jaře se koná soustředění pro nejlepší řešitele, v&nbsp;létě pak tábor pro všechny zájemce.</p>\r\n\r\n\r\n<a href=\"https://vyfuk.mff.cuni.cz/\"><h6>Výfuk (VÝpočty Fyzikálních UKolů)</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Výfuk je samostatný korespondenční seminář Matfyzu, který spadá pod Katedru didaktiky fyziky. Během školního roku kromě šesti sérií semináře organizátoři připravují i&nbsp;podzimní a&nbsp;jarní setkání, letní tábor a&nbsp;Náboj junior.</p>\r\n\r\n<h1>Databáze mimoškolních aktivit</h1>\r\n\r\n<p style=\"text-align: justify;\">Je-li ti výčet aktivit výše málo nebo tě žádná z&nbsp;nich nezaujala, doporučujeme navštívit web organizace <a href=\"https://prostredoskolaky.cz\">ProStředoškoláky</a>, jež zde připravila rozsáhlou databázi mimoškolních aktivit a&nbsp;akcí. Krom toho organizace pořádá soutěž <strong>Středoškolák roku</strong>, ve které každý rok oceňuje nejaktivnější středoškoláky. Věnuješ-li se tedy mimoškolně něčemu ve větším měřítku, neváhej se do soutěže přihlásit.</p>",
"enable_comments": false, "enable_comments": false,
"registration_required": false, "registration_required": false,
"sites": [ "sites": [
@ -178,5 +178,20 @@
}, },
"model": "flatpages.flatpage", "model": "flatpages.flatpage",
"pk": 29 "pk": 29
},
{
"fields": {
"content": "<div class=\"content\">\r\n<h1>Nápověda ke korekturovátku</h1>\r\n\r\n<p>Korekturovátko slouží k přidávání korektur do PDF souborů. Umožňuje přidávat a komentovat korektury a označovat je jako k zanesení, zanesené nebo irelevantní. Rovněž umožňuje o PDF říci, že jsou právě zanášeny korektury nebo že je zastaralé.</p>\r\n\r\n<h2>Použití</h2>\r\n\r\n<p>Kliknu do PDF tam, kam chci zadat korekturu, napíši text a kliknu na Oprav! (nebo Ctrl-Enter). Korektura se zobrazí na pravé straně červeně. Pokud chci korekturu okomentovat, kliknu na ikonu <img src=\"http://127.0.0.1:8000/static/korektury/imgs/comment.png\" />, napíši komentář a kliknu na Oprav! (nebo Ctrl-Enter). Komentář se zobrazí pod původní korekturou.</p>\r\n\r\n<h2>Tlačítka u korektury</h2>\r\n\r\n<ul>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/delete.png\" /> – smazat korekturu</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/check.png\" /> – označt koreturu jako zanesenou</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/cross.png\" /> – označit korekturu jako irelevantní (není to chyba, nebude zaneseno)</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/tex.png\" /> – označt koreturu jako připravenou k zanesení</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/edit.png\" /> – upravit text korektury</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/comment.png\" /> – okomentovat korekturu</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/hide.png\" /> – srolovat korekturu</li>\r\n</ul>\r\n\r\n<h2>Stavy</h2>\r\n\r\n<h3>Korektura</h3>\r\n\r\n<ul>\r\n\t<li>K vyřešení (červená) – bug report či návrh úpravy, probíhá diskuze, zatím nerozhodnuto</li>\r\n\t<li>Zanesená (modrá) – zanesená v TeXu</li>\r\n\t<li>Irelevantní (šedá) – není to chyba, nebude zanesena</li>\r\n\t<li>K zanesení (zelená) – rozhodnuto, čeká na zanesení do TeXu</li>\r\n</ul>\r\n\r\n<h3>PDF</h3>\r\n\r\n<ul>\r\n\t<li>Přidávání – probíhá přidávání korektur</li>\r\n\t<li>Zanášení (žluté pozadí) – probíhá zanášení korektur do TeXu</li>\r\n\t<li>Zastaralé (červené pozadí) – PDF je zastaralé, nepřidávat nové korektury</li>\r\n</ul>\r\n</div>",
"enable_comments": false,
"registration_required": false,
"sites": [
1
],
"template_name": "",
"title": "Nápověda ke korekturovátku",
"url": "/korektury/help/"
},
"model": "flatpages.flatpage",
"pk": 30
} }
] ]

173
docs/zapisy/2021-12-06-testovani_dokumentace_codereview.md

@ -0,0 +1,173 @@
# Postup zkrášlení kódu M&Mího webu
## Obecně o webu
- Python, Django, spousta nějakých rozšíření, frontend HTML + CSS + trocha JS
- Velké břímě historie, kterou nejspíš nechceme zahodit
- Změny v M&M někdy dost zamotají potřebný kód (tituly)
- Občas je potřeba dělat opravy rychle
## Aktuální stav
- Zběsile zbastlený kód
- „Co je to ‚single responsibility principle‘?“ ☺
- Dost netriviální množství objektů a jejich vazeb
- Dost možná do velké míry inherentní složitost
- Webaři aktuálně relativně zběhlí v programování (ale je potřeba myslet na to, že to tak být nemusí)
- Webaři stárnou (Jethro, Kristý, Anet) a mizí (Pavel, Káťa), je potřeba web připravit na předání
- Kód je rozdělený mezi orgy a jeden „nerozumí“ tomu, co druhý napsal (musí to vyčíst z kódu, nezná souvislosti, …)
- Noví orgové se aktuálně musí ptát, což je jim nepříjemné a nutí je umět formulovat dotazy
### Invarianty
- Není to práce, ale zábava-ish → libovolný proces nesmí být (moc) na obtíž.
- I malé nepohodlí je potřeba vyvážit relativně velkým přínosem
- nebo dostatečně zřejmou vidinou budoucího pohodlí / minimalizace nepohodlí
- Nejsme programátoři, spíš jsme bastlíři kódu (kteří znají rozumnou podmnožinu syntaxe Pythonu)
- Zvlášť noví orgové
- Nechceme cílit na mega-profi kód, je to nedosažitelný cíl
- Nejspíš to do nějaké míry ubastlené bude pořád, ta míra závisí na zkušenosti aktuálních webařů
- Nástroje nás nesmí moc mást.
- Děláme to zadarmo jeden večer v týdnu
- Vývoj jde pomalu, často pomaleji než vývoj knihoven
- → kód se rozbíjí i sám
- Běží to na Gimlim
- Debian (old)stable → nemůžeme používat moc nové featury Pythonu
- Aktuálně Python 3.7.3
- Webaři jsou náhodně vzniklá skupina lidí.
- Různé nástroje, různé operační systémy
- Nechceme vynucovat konkrétní metody, multiplatformní nástroje asi požadovat můžeme
- Na serveru může běžet cokoliv, co tam jde rozběhnout
- Kód by neměl být moc složitý / matoucí / kompaktní (?)
- případně fakt hodně okomentovaný
## O co se snažíme
- Zpřístupnit vývoj novým webařům
- Umožnit chápání i cizího kódu co nejjednodušeji
- Nevzít si s sebou implementační tajemství ~~do hrobu~~ pryč z M&Mka
```graphviz
digraph "Závislosti věcí" {
ss -> sdil -> doku -> cs;
ss -> cit ->ref -> cs;
ref -> nrt -> tst -> cs;
sdil -> cr -> cs
ss -> nrt;
ss[label="Současný stav",shape=box];
cit[label="čitelný kód"];
cs[label="Cílový stav",shape=box];
ref[label="refactoring"];
nrt[label="nerozbít to"];
tst[label="testy"];
doku[label="dokumentace (vývojářská)"];
sdil[label="chápání kódu ostatních / stávajícího"];
cr[label="code review"];
nrt,sdil,cit[shape=hexagon];
tst,doku,cr[color=blue];
}
```
## Code review
Aktuálně: Pavel občas z rozmaru čte diffy; párkrát jsme zkoušeli [programovat v páru](https://mam.mff.cuni.cz/wiki/Web/Tipy/PairProgramming), je to relativně časově náročné.
- Nevynucovat
- Primární motivace je umožnit nějak vidět změny a případně k nim dávat komentáře, jak stylu „tohle mi není jasné“, tak i „tohle by chtělo přepsat“.
- Chceme hlavně vytvořit příležitost ke čtení cizího kódu a seznamování se s ním (i v zájmu zaučování nových webařů)
### Názory
- spíš post-hoc
- Možná code-review toho, co jde na produkci
- Chceme umět komentovat konkrétní řádky kódu
- Gitea/gitlab?
- Klikátko a barevný řádky a vyhledávání jsou fajn (gitlab, gitea)
- Jethro: je fajn umět skočit na definici
- Kombinace s CI
- Pokud bude gitea v něčem nedostatečná, tak hrozí, že bude potřeba migrovat znovu, což je nepraktické
- Gitlab je asi častější než gitea → je lepší názor si na to zvyknout
__Závěr:__ Zkusíme to hodit do GitLabu (nejspíš veřejného\*), zavedeme pull-requesty do `master` větve, náhodně se budeme přiřazovat a používat ho nějak intuitivně.
\*: Ve fakultním neumíme přidávat další uživatele, v KAMím zas možná není CI.
## Testy
Aktuálně: pár testů na dohromady řádově 4 funkce, drobný pokus o TDD, jenž narazil na úskalí reálného světa. Lokální spuštění testů trvá relativně dlouho, nejspíš kvůli spoustě migrací.
- Potřebujeme ověřit, že to funguje a že to _pořád_ funguje
- CI? Coverage?
### Názory
- PyTest je fajn
- Testovat frontend?
- Jethro: při nasazení by se mohly dělat screenshoty celých vybraných stránek přes Selenium (nebo obdobné) a hlásit rozdíly
__Závěr:__ Backend testujeme PyTestem (`./manage.py test`), u frontendu výhledově zkusíme ty vizuální diffy a pak se uvidí, CI podle webového klikátka na code review. Bylo by dobré mít rozumná testdata, ať se nemusí mockovat moc věcí.
## Formát kódu
Aktuálně: Jakýsi coding style zhruba existuje, není popsaný, šíří se lidovou slovesností.
- 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 (`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?
### Názory
- P: nemyslím si, že zvládneme mít „průhledný kód“ (dostatečně konzistentní kód, aby ho člověk přestal vnímat a spíš viděl myšlenky).
- P: Kecadla do kódu trochu zavání větším peklem než užitkem (takže black a flake8 jsou ze hry); isort možná dává smysl; je otázka, jestli použít mypy, ale typové značky v kódu spíš chceme (zvlášť u věcí, které nejsou prostě django view – typicky utils.)
- J: Divné (=Pavlovo) groupování importů je spíš matoucí, spíš moc nepomáhalo
- P (doplněno zpětně): pydocstyle vynucuje PEP-257, který se možná tluče se sphinxem…
- P (též zpětně): Možná by se mohl dát použít pylint, tomu jde aspoň vysvětlit coding style a nerazí tupě PEP-8, ale nastavovat ho je asi větší porod, než jak moc pomůže…
__Závěr:__ Kecadla na formát spíš nechceme; isort by mohl být fajn, ale bylo by dobré mít rozdělené bloky „náš kód“, „standardní knihovna“ a „Django a další balíčky z PyPA“; u našeho kódu (utils, obecně ne-Django věci) chceme držet záměr (na úrovni dohody) psát signatury funkcí a v případě jejich přítomnosti se nechat varovat při porušení signatury.
- P (večerní prokrastinace): Bandit vypadá, že hlásí jen strašně obvious věci by default, nepřijde mi jako moc úžasné vylepšení. Do CI možná dobrý
## Dokumentace
Aktuálně: něco málo je na wiki (`/Web/`), občas má nějaká funkce docstring, obecně je toho málo
### Požadavky
- Jedno autoritativní místo a dá se najít
- Dostatečně „blízko“ kódu
- nastavit mindset na psaní dokumentace „rovnou“?
- Umožnit i obecný text, ne jen komentáře kódu (modulů, funkcí)
### Bonusy
- Zajišťování konzistence s uživatelskou dokumentací
- aktuálně wiki
- Podpora i dalších jazyků (Vue, Javascript, CSS, možná django-templates)
### Názory
- Jde to dělat sphinxem
- Stínovlas by mohl vědět v CZ.NICu se sphinx jede. Případně zkusit zjistit, co umí jejich Akademie
- Free-textová dokumentace (architektura ap.) má být nezávislá na dokumentaci konkrétního kódu, ale je fajn, když se to pak spojí dohromady, jde-li to.
- Lepší, když se obojí dá dělat stejným nástrojem
- Káťa má někdy pocit, že tráví spoustu času tím, že hledá který soubor vůbec upravit; to má v naší dokumentaci být.
__Závěr:__ Zkusíme použít sphinx (z nedostatku vlastních zkušeností a kvůli jeho popularitě), když se nám to nebude líbit, tak budeme řešit dál. Bylo by fajn najít nějaký workshop, bude to rychlejší nalejvárna než číst dokumentaci. V krátkodobém výhledu stáhneme vývojovou dokumentaci z webu do repozitáře a zprovozníme někde automatické buildy (aby se dala číst jako člověk a ne jako stroj).
## Uživatelská dokumentace
- K: Dělat ji ve spolupráci s těma uživatelema
- P: Dávný Hedgedoc může případně sloužit jako základ osnovy
- K: Dost možná nevíme, co bolí víc a co míň
- Jethro: Musí být na wiki, jinak tím zmateme oržstvo
__Závěr:__ Dokumentace bude na wiki (<https://mam.mff.cuni.cz/wiki/Web_user/Dokumentace>, výhledově možná ve stejné složce v dalších souborech), bude vznikat podle našich pocitů a dotazů od ostatních; výhledově bude schůzka na ukázání featur nového webu a tam se dosbírají náměty na to, co sepsat.

3
galerie/templates/galerie/Galerie.html

@ -62,11 +62,12 @@
</div> </div>
{% endwith %} {% endwith %}
{% endif%} {% endif%}
<span id="nahoru" class="kotva_obrazku"></span>
<img src="{{obrazek.obrazek_stredni.url}}" <img src="{{obrazek.obrazek_stredni.url}}"
height="{{vyska}}" height="{{vyska}}"
width="{{sirka}}" width="{{sirka}}"
alt="{{obrazek.popis}}" alt="{{obrazek.popis}}"
class="obrazek" id="nahoru"> class="obrazek">
{% if obrazky_dalsi %} {% if obrazky_dalsi %}
{% with obrazky_dalsi|first as dalsi_obrazek %} {% with obrazky_dalsi|first as dalsi_obrazek %}

24
korektury/admin.py

@ -2,6 +2,8 @@ from django.contrib import admin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from korektury.models import KorekturovanePDF from korektury.models import KorekturovanePDF
from django.core.mail import send_mail
from django.urls import reverse
# Register your models here. # Register your models here.
class KorekturovanePDFAdmin(VersionAdmin): class KorekturovanePDFAdmin(VersionAdmin):
@ -16,11 +18,31 @@ class KorekturovanePDFAdmin(VersionAdmin):
fieldsets = [ fieldsets = [
(None, (None,
{'fields': {'fields':
['pdf', 'cas', 'org', 'stran', 'nazev', 'komentar']}), ['pdf', 'cas', 'org', 'stran', 'nazev', 'komentar', 'poslat_mail']}),
# (u'PDF', {'fields': ['pdf']}), # (u'PDF', {'fields': ['pdf']}),
] ]
list_display = ['nazev', 'cas', 'stran', 'org'] list_display = ['nazev', 'cas', 'stran', 'org']
list_filter = [] list_filter = []
search_fields = [] search_fields = []
def save_model(self, request, obj, form, change):
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'
predmet = f'Nové korektury: {obj.nazev}'
text = f'''\
V korekturovátku se objevil nový soubor: {obj.nazev}
{odkaz}
Popis souboru:
{obj.komentar}
---
S pozdravem a korekturám zdar!
Korekturovátko
'''
send_mail(predmet,text,odesilatel,[prijemce])
admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin) admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin)

18
korektury/migrations/0018_korekturovanepdf_poslat_mail.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.24 on 2021-12-05 23:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('korektury', '0017_auto_20190610_2358'),
]
operations = [
migrations.AddField(
model_name='korekturovanepdf',
name='poslat_mail',
field=models.BooleanField(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.', verbose_name='Poslat mail o novém PDF'),
),
]

4
korektury/models.py

@ -63,6 +63,10 @@ class KorekturovanePDF(models.Model):
status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False, status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False,
default = STATUS_PRIDAVANI) 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.',
)
#TODO Nepovinný foreign key k číslu #TODO Nepovinný foreign key k číslu

1
korektury/static/korektury/opraf.css

@ -89,6 +89,7 @@ form {
border: 4px solid red; border: 4px solid red;
border-radius: 10px; border-radius: 10px;
background-color: white; background-color: white;
opacity: 80%;
} }
.close-button{ .close-button{
background-color: yellow; background-color: yellow;

3
korektury/templates/korektury/opraf.html

@ -4,10 +4,11 @@
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <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 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> <script src="{% static "korektury/opraf.js"%}"></script>
<title>Korektury {{pdf.nazev}}</title> <title>Korektury {{pdf.nazev}}</title>
</head> </head>
<body {% if pdf.status == 'zanaseni'%} class="comitting" {% elif pdf.status == 'zastarale' %} class="deprecated" {% endif %} onload='place_comments()'> <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> <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 == '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 %} {% if pdf.status == 'zastarale' %} <h2> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2> {% endif %}

3
korektury/templates/korektury/seskupeny_seznam.html

@ -22,9 +22,8 @@
<ul> <ul>
{% for pdf in skupina.list %} {% for pdf in skupina.list %}
<li><span {% if pdf.status == 'zanaseni'%} class="comitting-text" {% elif pdf.status == 'zastarale' %} class="deprecated-text" {% endif %}> <li><span {% if pdf.status == 'zanaseni'%} class="comitting-text" {% elif pdf.status == 'zastarale' %} class="deprecated-text" {% endif %}>
<b>{{ pdf.nazev }}</b> <b><a href="/korektury/{{pdf.id}}">{{ pdf.nazev }}</a></b>
<i>{{pdf.komentar}}</i> <i>{{pdf.komentar}}</i>
<a href="/korektury/{{pdf.id}}">{{pdf.pdf.name}}</a>
(k opravě: {{pdf.k_oprave_cnt}}, (k opravě: {{pdf.k_oprave_cnt}},
opraveno: {{pdf.opraveno_cnt}}, opraveno: {{pdf.opraveno_cnt}},
není chyba: {{pdf.neni_chyba_cnt}}, není chyba: {{pdf.neni_chyba_cnt}},

1
korektury/urls.py

@ -6,5 +6,4 @@ urlpatterns = [
path('korektury/', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'), path('korektury/', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'),
path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'), path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'),
path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'), path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'),
path('korektury/help/', org_required(views.KorekturyHelpView.as_view()), name='korektury-help'),
] ]

23
korektury/views.py

@ -87,7 +87,7 @@ class KorekturyView(generic.TemplateView):
op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf) op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf)
op.save() op.save()
self.send_email_notification_komentar(op, autor, text) self.send_email_notification_komentar(op,autor)
elif (action == 'del'): elif (action == 'del'):
id = int(q.get('id')) id = int(q.get('id'))
op = Oprava.objects.get(id=id) op = Oprava.objects.get(id=id)
@ -125,7 +125,7 @@ class KorekturyView(generic.TemplateView):
text = q.get('txt') text = q.get('txt')
kom = Komentar(oprava=op,autor=autor,text=text) kom = Komentar(oprava=op,autor=autor,text=text)
kom.save() kom.save()
self.send_email_notification_komentar(op, autor, text) self.send_email_notification_komentar(op,autor)
elif (action == 'update-comment'): elif (action == 'update-comment'):
id = int(q.get('id')) id = int(q.get('id'))
kom = Komentar.objects.get(id=id) kom = Komentar.objects.get(id=id)
@ -151,21 +151,25 @@ class KorekturyView(generic.TemplateView):
context['autor'] = autor context['autor'] = autor
return render(request, 'korektury/opraf.html',context) return render(request, 'korektury/opraf.html',context)
def send_email_notification_komentar(self, oprava, autor, text): def send_email_notification_komentar(self, oprava, autor):
''' Rozesle e-mail pri pridani komentare, ''' Rozesle e-mail pri pridani komentare / opravy,
ktery obsahuje text komentare. ktery obsahuje text vlakna opravy.
''' '''
# parametry e-mailu # parametry e-mailu
#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk) #odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk)
from django.urls import reverse from django.urls import reverse
odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk})) odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk}))
odkaz = f"{odkaz}#op{oprava.id}-pointer"
from_email = 'korekturovatko@mam.mff.cuni.cz' from_email = 'korekturovatko@mam.mff.cuni.cz'
subject = 'Nová korektura od {} v {}'.format(autor, subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev)
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\ text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\
\nodkaz do korekturovátka: {}\n\ \nodkaz do korekturovátka: {}\n\
\nVaše korekturovátko\n".format(text, odkaz) \nVaše korekturovátko\n".format(optext, odkaz)
# Prijemci e-mailu # Prijemci e-mailu
emails = set() emails = set()
@ -193,6 +197,9 @@ class KorekturyView(generic.TemplateView):
if not settings.POSLI_MAILOVOU_NOTIFIKACI: if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emails)) print("Poslal bych upozornění na tyto adresy: ", " ".join(emails))
print("---- Upozornění:")
print(text)
print("---- Konec upozornění")
return return
send_mail(subject, text, from_email, list(emails)) send_mail(subject, text, from_email, list(emails))

1
mamweb/settings_common.py

@ -79,6 +79,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'sekizai.context_processors.sekizai', 'sekizai.context_processors.sekizai',
'header_fotky.context_processors.vzhled', 'header_fotky.context_processors.vzhled',
'various.context_processors.rozliseni',
'various.context_processors.april', 'various.context_processors.april',
) )
}, },

5
mamweb/settings_local.py

@ -28,7 +28,9 @@ INTERNAL_IPS = ['127.0.0.1']
TEMPLATES[0]['OPTIONS']['debug'] = True TEMPLATES[0]['OPTIONS']['debug'] = True
ALLOWED_HOSTS = ['127.0.0.1', '192.168.43.34'] from ipaddress import ip_network
ALLOWED_HOSTS = [str(ip) for ip in ip_network('192.168.0.0/16')]
ALLOWED_HOSTS.append('127.0.0.1')
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
@ -97,3 +99,4 @@ LOGGING = {
# E-maily posílat chceme, ale do terminálu :-) # E-maily posílat chceme, ale do terminálu :-)
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SEND_EMAIL_NOTIFICATIONS = True SEND_EMAIL_NOTIFICATIONS = True
LOCAL_TEST_PROD = "local"

1
mamweb/settings_prod.py

@ -67,3 +67,4 @@ LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/
# E-MAIL NOTIFICATIONS # E-MAIL NOTIFICATIONS
POSLI_MAILOVOU_NOTIFIKACI = True POSLI_MAILOVOU_NOTIFIKACI = True
LOCAL_TEST_PROD = "prod"

1
mamweb/settings_test.py

@ -76,3 +76,4 @@ EMAIL_BACKEND = 'various.mail_prefixer.PrefixingMailBackend'
# TODO Pouze na otestování testu… Zvolit konferu! # TODO Pouze na otestování testu… Zvolit konferu!
# XXX: Je to pole, protože implementační detail backendu. # XXX: Je to pole, protože implementační detail backendu.
TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz'] TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz']
LOCAL_TEST_PROD = "test"

17
mamweb/static/css/mamweb.css

@ -1,3 +1,5 @@
@import url("rozliseni.css");
@font-face { @font-face {
font-family: 'OpenSans'; font-family: 'OpenSans';
src: url("../fonts/OpenSans/OpenSans-Regular.ttf"); src: url("../fonts/OpenSans/OpenSans-Regular.ttf");
@ -1111,6 +1113,21 @@ div.zadani_termin .datum {
float: right; 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 {
display: block;
position: relative;
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 */
}
}
/**/ /**/

29
mamweb/static/css/rozliseni.css

@ -0,0 +1,29 @@
/* Rozlišení mezi lokálním, test a produkčním webem */
.localweb {
border-left: 20px solid greenyellow;
border-right: 20px solid greenyellow;
}
.localweb .login-bar {
margin-left: -20px;
}
.testweb {
border-left: 20px solid darkorange;
border-right: 20px solid darkorange;
}
.testweb .login-bar {
margin-left: -20px;
}
/* Produkční web z pohledu superuživatele */
.suprodweb {
border-left: 20px solid red;
border-right: 20px solid red;
}
.suprodweb .login-bar {
margin-left: -20px;
}

3
mamweb/templates/admin/base_site.html

@ -4,8 +4,11 @@
{% block extrahead %} {% block extrahead %}
<link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> <link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon">
<script src="{% static 'js/jquery-1.11.1.js' %}"></script> <script src="{% static 'js/jquery-1.11.1.js' %}"></script>
<link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet">
{% endblock %} {% endblock %}
{% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %}
{% block branding %} {% block branding %}
<h1 id="site-name"><a href="/"> M&M GWP web </a></h1> <h1 id="site-name"><a href="/"> M&M GWP web </a></h1>
{% endblock %} {% endblock %}

2
mamweb/templates/base.html

@ -36,7 +36,7 @@
{% block script %}{% endblock %} {% block script %}{% endblock %}
</head> </head>
<body class='{% if user.is_staff %}org-logged-in{% endif %}'> <body class='{{ LOCAL_TEST_PROD }}web{% if user.is_staff %} org-logged-in{% endif %}'>
{% if user.is_staff %} {% if user.is_staff %}
<div class="login-bar" > <div class="login-bar" >

2
mamweb/urls.py

@ -49,7 +49,7 @@ urlpatterns = [
path('comments_dj/', include('django_comments.urls')), path('comments_dj/', include('django_comments.urls')),
# REST API # REST API
path('api/', include(router.urls)), # path('api/', include(router.urls)),
] ]

2
odevzdavatko/templates/odevzdavatko/tabulka.html

@ -4,7 +4,7 @@
{% block content %} {% block content %}
<form method=get action=../odevzdavatko> <form method=get action=.>
{{ filtr.resitele }} {{ filtr.resitele }}
{{ filtr.problemy }} {{ filtr.problemy }}
Od: {{ filtr.reseni_od }} Od: {{ filtr.reseni_od }}

3
odevzdavatko/views.py

@ -312,12 +312,15 @@ class PrehledOdevzdanychReseni(ListView):
resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first() resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first()
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.filter(reseni__resitele__in=[resitel]) qs = qs.filter(reseni__resitele__in=[resitel])
# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení
qs = qs.order_by('reseni__cas_doruceni')
return qs return qs
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
ctx = super().get_context_data(*args, **kwargs) ctx = super().get_context_data(*args, **kwargs)
# Ročník určujeme podle čísla, do jehož deadlinu došlo řešení. # Ročník určujeme podle čísla, do jehož deadlinu došlo řešení.
# Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/ # Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/
# TODO: Funkce deadline vrací deadliny v jiném ročníku, zvlášť pokud se vyrobí řešení až po deadlinu (třeba při poslání mailem)
podle_rocniku = [] podle_rocniku = []
for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: deadline(ho.reseni.cas_doruceni)[1].rocnik if deadline(ho.reseni.cas_doruceni) is not None else None): for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: deadline(ho.reseni.cas_doruceni)[1].rocnik if deadline(ho.reseni.cas_doruceni) is not None else None):
podle_rocniku.append((rocnik, list(hodnoceni))) podle_rocniku.append((rocnik, list(hodnoceni)))

70
personalni/views.py

@ -7,6 +7,7 @@ from django.views.generic.base import TemplateView
from django.contrib.auth.models import User, Permission, Group from django.contrib.auth.models import User, Permission, Group
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction from django.db import transaction
from django.http import HttpResponse
import seminar.models as s import seminar.models as s
import seminar.models as m import seminar.models as m
@ -14,6 +15,7 @@ from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm
from datetime import date from datetime import date
import logging import logging
import csv
from seminar.views import formularOKView from seminar.views import formularOKView
from various.autentizace.views import LoginView from various.autentizace.views import LoginView
@ -304,3 +306,71 @@ def profilView(request):
return ResitelView.as_view()(request) return ResitelView.as_view()(request)
else: else:
return LoginView.as_view()(request) return LoginView.as_view()(request)
def dataResiteluCsvResponse(queryset, columns=None, with_header=True):
"""Pomocná funkce pro vracení dat řešitelů jako CSV. Musí dostat správný QuerySet, který dává Řešitele"""
# TODO: Možná nějak zobecnit i na Osoby?
# TODO: Nemá to spíš být class-based? Tohle je efektivně metoda "get", které ale chybí "get_queryset"…
default_columns = (
'id',
'osoba__jmeno',
'osoba__prijmeni',
'osoba__prezdivka',
'osoba__email',
'osoba__telefon',
'osoba__user__username',
'osoba__datum_narozeni',
'osoba__pohlavi_muz',
'osoba__ulice',
'osoba__mesto',
'osoba__psc',
'osoba__stat',
'skola', #FIXME: dává jen ID
'poznamka',
'osoba__poznamka',
'rok_maturity',
'zasilat',
'zasilat_cislo_emailem',
'osoba__datum_registrace',
'osoba__datum_souhlasu_udaje',
'osoba__datum_souhlasu_zasilani',
)
if columns is None: columns = default_columns
field_name_overrides = {
# Zrušení prefixu "osoba__"
'osoba__jmeno': 'jmeno',
'osoba__prijmeni': 'prijmeni',
'osoba__prezdivka': 'prezdivka',
'osoba__email': 'email',
'osoba__telefon': 'telefon',
'osoba__user__username': 'user',
'osoba__datum_narozeni': 'datum_narozeni',
'osoba__pohlavi_muz': 'pohlavi_muz',
'osoba__ulice': 'ulice',
'osoba__mesto': 'mesto',
'osoba__psc': 'psc',
'osoba__stat': 'stat',
'osoba__datum_registrace': 'datum_registrace',
'osoba__datum_souhlasu_udaje': 'datum_souhlasu_udaje',
'osoba__datum_souhlasu_zasilani':'datum_souhlasu_zasilani',
}
def get_field_name(column_name):
if column_name in field_name_overrides:
return field_name_overrides[column_name]
return column_name
response = HttpResponse(content_type='text/csv')
writer = csv.writer(response)
# První řádek je záhlaví
if with_header:
writer.writerow(map(get_field_name, columns))
# Data:
queryset_list = queryset.values_list(*columns)
writer.writerows(queryset_list)
return response

2
requirements.txt

@ -23,7 +23,7 @@ django-solo
django-ckeditor django-ckeditor
django-flat-theme django-flat-theme
django-taggit django-taggit
django-autocomplete-light django-autocomplete-light>=3.9.0rc1
django-crispy-forms django-crispy-forms
django-imagekit django-imagekit
django-polymorphic django-polymorphic

19
seminar/migrations/0100_auto_20211129_2354.py

@ -0,0 +1,19 @@
# Generated by Django 2.2.24 on 2021-11-29 22:54
from django.db import migrations, models
import seminar.models.tvorba
class Migration(migrations.Migration):
dependencies = [
('seminar', '0099_auto_20210916_1509'),
]
operations = [
migrations.AlterField(
model_name='cislo',
name='pdf',
field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=seminar.models.tvorba.OverwriteStorage(), upload_to=seminar.models.tvorba.cislo_pdf_filename, verbose_name='pdf'),
),
]

3
seminar/models/pomocne.py

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import os
from django.db import models from django.db import models
from .base import SeminarModelBase from .base import SeminarModelBase
@ -65,3 +66,5 @@ class Obrazek(SeminarModelBase):
upload_to='obrazky/%Y/%m/%d/', blank=True, null=True) upload_to='obrazky/%Y/%m/%d/', blank=True, null=True)
# TODO placement hint - chci ho tady / pred textem / za textem # TODO placement hint - chci ho tady / pred textem / za textem

22
seminar/models/tvorba.py

@ -12,6 +12,7 @@ from django.conf import settings
from django.urls import reverse from django.urls import reverse
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.storage import FileSystemStorage
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -37,6 +38,13 @@ from .base import SeminarModelBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OverwriteStorage(FileSystemStorage):
""" Varianta FileSystemStorage, která v případě, že soubor cílového
jména již existuje, ho smaže a místo něj uloží soubor nový"""
def get_available_name(self,name, max_length=None):
if self.exists(name):
os.remove(os.path.join(self.location,name))
return super().get_available_name(name,max_length)
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Rocnik(SeminarModelBase): class Rocnik(SeminarModelBase):
@ -173,7 +181,7 @@ class Cislo(SeminarModelBase):
help_text='Neveřejná poznámka k číslu (plain text)') help_text='Neveřejná poznámka k číslu (plain text)')
pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True,
help_text='PDF čísla, které si mohou řešitelé stáhnout') help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage())
titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True,
help_text='Obrázek titulní strany, generuje se automaticky') help_text='Obrázek titulní strany, generuje se automaticky')
@ -390,10 +398,11 @@ class Problem(SeminarModelBase,PolymorphicModel):
# Implicitini implementace, jednotlivé dědící třídy si přepíšou # Implicitini implementace, jednotlivé dědící třídy si přepíšou
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == 'zadany': if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
if self.nadproblem: if self.nadproblem:
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ý.")
return '<Není zadaný>' return '<Není zadaný>'
# def verejne(self): # def verejne(self):
@ -467,10 +476,11 @@ class Tema(Problem):
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == 'zadany': 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{}".format(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ý.")
return '<Není zadaný>' return '<Není zadaný>'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -501,11 +511,12 @@ class Clanek(Problem):
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == 'zadany': if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
# 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{}".format(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ý.")
return '<Není zadaný>' return '<Není zadaný>'
def node(self): def node(self):
@ -538,11 +549,12 @@ class Uloha(Problem):
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == 'zadany': if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) name="{}.u{}".format(self.cislo_zadani.poradi,self.kod)
if self.nadproblem: if self.nadproblem:
return self.nadproblem.kod_v_rocniku+name return self.nadproblem.kod_v_rocniku+name
return 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ý.")
return '<Není zadaný>' return '<Není zadaný>'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

4
seminar/templates/seminar/archiv/rocnik.html

@ -119,7 +119,9 @@
{% if user.je_org %} {% if user.je_org %}
<div class='mam-org-only'> <div class='mam-org-only'>
<a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a> <p><a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a></p>
{# FIXME: Sice to sem asi nepatří sémanticky, ale bylo to nejjednodušší… #}
<p><a href='{% url 'seminar_rocnik_resitele_csv' rocnik=rocnik.rocnik %}' download>CSV export řešitelů</a></p>
<h2>Výsledková listina včetně neveřejných bodů</h2> <h2>Výsledková listina včetně neveřejných bodů</h2>
{% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %} {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %}
</div> </div>

5
seminar/urls.py

@ -36,6 +36,11 @@ urlpatterns = [
org_required(views.RocnikVysledkovkaView.as_view()), org_required(views.RocnikVysledkovkaView.as_view()),
name='seminar_rocnik_vysledkovka' name='seminar_rocnik_vysledkovka'
), ),
path(
'rocnik/<int:rocnik>/resitele.csv',
org_required(views.resiteleRocnikuCsvExportView),
name='seminar_rocnik_resitele_csv'
),
path( path(
'cislo/<int:rocnik>.<str:cislo>/vysledkovka.tex', 'cislo/<int:rocnik>.<str:cislo>/vysledkovka.tex',
org_required(views.CisloVysledkovkaView.as_view()), org_required(views.CisloVysledkovkaView.as_view()),

13
seminar/utils.py

@ -217,10 +217,10 @@ def aktivniResitele(cislo, pouze_letosni=False):
zacatek_rocniku = False zacatek_rocniku = False
if not zacatek_rocniku: if not zacatek_rocniku:
return resi_v_rocniku(letos, cislo) return resi_v_rocniku(letos, cislo).filter(rok_maturity__gte=letos.druhy_rok())
else: else:
# spojíme querysety s řešiteli loni a letos do daného čísla # spojíme querysety s řešiteli loni a letos do daného čísla
return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct() return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct().filter(rok_maturity__gte=letos.druhy_rok())
def viewMethodSwitch(get, post): def viewMethodSwitch(get, post):
""" """
@ -309,6 +309,15 @@ def podproblemy_v_cislu(cislo, problemy=None, hlavni_problemy=None):
else: else:
podproblemy[-1].append(problem) podproblemy[-1].append(problem)
for podproblem in podproblemy.keys():
def int_or_zero(p):
try:
return int(p.kod)
except ValueError:
return 0
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero)
return podproblemy return podproblemy
class TypDeadline(Enum): class TypDeadline(Enum):

9
seminar/views/views_all.py

@ -390,6 +390,15 @@ class RocnikView(generic.DetailView):
return context return context
def resiteleRocnikuCsvExportView(request, rocnik):
from personalni.views import dataResiteluCsvResponse
assert request.method in ('GET', 'HEAD')
return dataResiteluCsvResponse(
utils.resi_v_rocniku(
m.Rocnik.objects.get(rocnik=rocnik)
)
)
# FIXME: Pozor, výš je ještě jeden ProblemView! # FIXME: Pozor, výš je ještě jeden ProblemView!
#class ProblemView(generic.DetailView): #class ProblemView(generic.DetailView):

10
various/context_processors.py

@ -1,3 +1,6 @@
from django.conf import settings
def april(req): def april(req):
if 'X-April' in req.headers: if 'X-April' in req.headers:
try: try:
@ -12,3 +15,10 @@ def april(req):
return {'april': today.year} return {'april': today.year}
return {} return {}
def rozliseni(request):
ltp = settings.LOCAL_TEST_PROD
if request.user.is_superuser and ltp == "prod":
ltp = "su" + ltp
return {"LOCAL_TEST_PROD": ltp}

14
vysledkovky/templates/vysledkovky/vysledkovka_cisla.html

@ -49,17 +49,21 @@
{% endfor %} {% endfor %}
</table> </table>
<p>Po kliknutí na políčko v záhlaví tabulky se u daného problému zobrazí (/skryje) detailní rozpis, za které podproblémy řešitelé dostali body.</p>
{# TODELETE #} {# TODELETE #}
<script> <script>
{% for p in problemy %} {% for p in problemy %}
diplayed{{ forloop.counter }} = false;
$(".podproblem{{ forloop.counter }}").css("display", "none") $(".podproblem{{ forloop.counter }}").css("display", "none")
$("#problem{{ forloop.counter }}")[0].addEventListener('mouseover', podproblem{{ forloop.counter }}) $("#problem{{ forloop.counter }}")[0].addEventListener('click', podproblem{{ forloop.counter }});
$("#problem{{ forloop.counter }}")[0].addEventListener('mouseout', podproblem{{ forloop.counter }}end)
function podproblem{{ forloop.counter }}(event) { function podproblem{{ forloop.counter }}(event) {
$(".podproblem{{ forloop.counter }}").css("display", "") diplayed{{ forloop.counter }} = !diplayed{{ forloop.counter }};
if (diplayed{{ forloop.counter }}) {
$(".podproblem{{ forloop.counter }}").css("display", "");
} else {
$(".podproblem{{ forloop.counter }}").css("display", "none");
} }
function podproblem{{ forloop.counter }}end(event) {
$(".podproblem{{ forloop.counter }}").css("display", "none")
} }
{% endfor %} {% endfor %}
</script> </script>

2
vysledkovky/utils.py

@ -196,6 +196,7 @@ def data_vysledkovky_rocniku(rocnik, jen_verejne=True):
end = time.time() end = time.time()
print("Vysledkovka rocniku",end-start) print("Vysledkovka rocniku",end-start)
radky_vysledkovky = [radek for radek in radky_vysledkovky if radek.body_rocnik > 0]
return radky_vysledkovky, cisla return radky_vysledkovky, cisla
class RadekVysledkovkyCisla(object): class RadekVysledkovkyCisla(object):
@ -451,6 +452,7 @@ def data_vysledkovky_cisla(cislo):
# vytahané informace předáváme do kontextu # vytahané informace předáváme do kontextu
pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]]
radky_vysledkovky = [radek for radek in radky_vysledkovky if radek.body_rocnik > 0]
return ( return (
radky_vysledkovky, radky_vysledkovky,
temata_a_spol, temata_a_spol,

Loading…
Cancel
Save