Browse Source

Merge branch 'master' into vylepseni_odevzdavatka

# Conflicts:
#	odevzdavatko/templates/odevzdavatko/detail.html
pull/13/head
Jonas Havelka 2 years ago
parent
commit
711693c57b
  1. 9
      .editorconfig
  2. 159
      Makefile
  3. 135
      README.md
  4. 1
      api/urls.py
  5. 17
      api/views/autocomplete.py
  6. 1
      docs/index.rst
  7. 108
      docs/skripty.rst
  8. 25
      docs/tabulka_prerekvizit.rst
  9. 200
      docs/vyvoj.rst
  10. 84
      header_fotky/context_processors.py
  11. 18
      korektury/migrations/0019_auto_20221205_2014.py
  12. 2
      korektury/models.py
  13. 34
      korektury/views.py
  14. 8
      make/README.md
  15. 31
      make/deploy
  16. 25
      make/deploy_prod
  17. 10
      make/init_local
  18. 1
      make/install
  19. 16
      make/install_web
  20. 104
      make/lib.sh
  21. 14
      make/push_compiled_vue_to_test
  22. 8
      make/run
  23. 9
      make/schema
  24. 20
      make/sync_prod_flatpages
  25. 11
      make/sync_test
  26. 18
      make/sync_test_db_aggressive
  27. 9
      make/sync_test_media
  28. 9
      make/test
  29. 32
      mamweb/admin.py
  30. 152
      mamweb/middleware.py
  31. 385
      mamweb/settings_common.py
  32. 96
      mamweb/settings_local.py
  33. 20
      mamweb/settings_prod.py
  34. 28
      mamweb/settings_test.py
  35. 17
      mamweb/static/css/mamweb.css
  36. 2
      odevzdavatko/forms.py
  37. 6
      odevzdavatko/templates/odevzdavatko/detail.html
  38. 2
      odevzdavatko/templates/odevzdavatko/tabulka.html
  39. 1
      requirements.txt
  40. 18
      seminar/models/base.py
  41. 42
      seminar/models/novinky.py
  42. 234
      seminar/models/odevzdavatko.py
  43. 3
      seminar/templates/seminar/archiv/cislo.html
  44. 33
      seminar/templates/seminar/archiv/cislo_obalkovani.html
  45. 30
      seminar/templates/seminar/org/obalkovani.html
  46. 18
      seminar/testutils.py
  47. 6
      seminar/urls.py
  48. 41
      seminar/views/views_all.py
  49. 50
      soustredeni/admin.py
  50. 90
      treenode/admin.py
  51. 4
      treenode/permissions.py
  52. 24
      various/autentizace/utils.py
  53. 8
      various/log_filters.py
  54. 32
      vysledkovky/templates/vysledkovky/vysledkovka_cisla.html
  55. 24
      vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html

9
.editorconfig

@ -1,17 +1,20 @@
# Univerzální popis našich konvencí pro nastavení editorů.
# Vizte https://editorconfig.org pro detaily
root = true root = true
[*] [*]
charset = utf-8 charset = utf-8
# Unix-y lines # Unixové řádky
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
[*.{py,css}] [*.{py,css}]
indent_style = tab indent_style = tab
# I do not think we prescribe, how big a tab is. # Nenařizujeme konkrétní šířku tabulátoru
indent_size = unset indent_size = unset
# Migrations are auto-generated and thus follow PEP-8, let's not fight with that # Automaticky generované migrace dodržují PEP-8, nemá smysl s tím moc bojovat.
[*/migrations/*.py] [*/migrations/*.py]
indent_style = space indent_style = space
indent_size = 4 indent_size = 4

159
Makefile

@ -1,154 +1,9 @@
PYTHON ?= python3 # Existence tohohle Makefile je tu jen proto, aby fungovala svalová paměť. Pokud můžete, použijte rovnou `make/…`
VENV ?= ${PYTHON} -m venv %:
# Všechny flagy, které se s venvem/virtualenvem/... mají volat patří sem. Volá se "${VENV} cesta" # Používání make jako příkazu je zastaralé, prosím používej radši skripty ze složky make. Spouštím následující příkaz:
VENV_PATH ?= env make/$*
# Musí být definovaná, i kdyby to měla být "."
.PHONY: all venv_check clean install install_web install_venv clean_venv clean_schema run test deploy_test deploy_prod sync_test_media sync_test_db sync_test sync_local_media sync_local_db sync_local default:
@cat make/README.md
# activate by mělo být předpokladem ke všemu, co volá webový python (i.e. python nasazený do ${VENV}u kvůli webu, např. manage.py) .PHONY: default
all:
@# Just echo:
# Install je trochu magický:
# Spusť následující posloupnost příkazů:
# make install_venv
# . ${VENV_PATH}/bin/activate
# make install_web
#
# Pokud install_web říká Error: pg_config executable not found. nainstaluj si libpq-dev
# Pokud chybová hláška obsahuje #include <Python.h>, nainstaluj si python3-dev
#
# Až skončíš s vývojem webu, spusť "deactivate". Tím zmizí '(${VENV_PATH})' ze začátku promptu.
venv_check:
@# Pokud org nemá zapnutý venv, poradíme mu, aby si ho zapnul a spadneme. Jinak nic.
@expr $$PATH : ".*:*$(shell pwd)/${VENV_PATH}/bin" > /dev/null && exit 0 || echo 'Není zapnutý venv, spusť ". ${VENV_PATH}/bin/activate".\nPokud není venv nainstalovaný, spusť "make install_venv"' && false
clean: clean_venv clean_schema
install: install_web
install_web: venv_check
@# venv může být příšerně starý, takže nejdříve upgradujeme venvové věci
pip install --upgrade pip
pip install --upgrade setuptools
# Instalace závislostí webu
pip install -r requirements.txt --upgrade
# Pro vygenerování tesdat spusť ./manage.py testdata
# Po vygenerování testdat spusť ./manage.py loaddata data/*, ať máš menu a další modely
# Pro synchronizaci flatpages spusť make sync_prod_flatpages
install_venv:
${VENV} ${VENV_PATH}
clean_venv:
# Možná není 100% foolproof...
@test ! ${VENV_PATH} = . || ! echo "Smaž si prosím venv sám, nebudu mazat celý web"
rm -rfv ${VENV_PATH}
rm -f pip-selfcheck.json
clean_schema:
rm -f schema_seminar.pdf schema_all.pdf
run: venv_check
./manage.py runserver
test: venv_check
./manage.py test -v2 seminar mamweb
# DB schemata
schema: schema_seminar.pdf schema_all.pdf
schema_seminar.pdf: venv_check
./manage.py graph_models seminar | dot -Tpdf > schema_seminar.pdf
schema_all.pdf: venv_check
./manage.py graph_models -a -g | dot -Tpdf > schema_all.pdf
# Deploy to current *mamweb-test* directory
deploy_test: venv_check
@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi
@if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-test" ]; then echo "Only possible in directory mamweb-test"; exit 1; fi
@echo "Installing version from origin/test ..."
git pull origin test
git clean -f
make install
./manage.py migrate
./manage.py collectstatic --noinput
(chown -R :mam . || true )
(chmod -R g+rX,go-w . || true )
@echo Restarting systemd unit
systemctl --user restart mamweb-test.service
@echo Done.
# Deploy to current *mamweb-prod* directory
deploy_prod: venv_check
@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi
@if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-prod" ]; then echo "Only possible in directory mamweb-prod"; exit 1; fi
@echo "Backing up production DB ..."
( cd -P .. && ./backup_prod_db.sh )
@echo "Installing version from origin/master ..."
git pull origin master
git clean -f
make install
./manage.py migrate
./manage.py collectstatic --noinput
(chown -R :mam . || true )
(chmod -R g+rX,go-w . || true )
@echo Restarting systemd user unit for MaM web
systemctl --user restart mamweb-prod.service
@echo Done.
sync_prod_flatpages: venv_check
@echo Downloading current version of flatpages from mamweb-prod.
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; ./fix_json.py flat.json flat_fixed.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."
./manage.py loaddata data/flat.json
@echo "Done."
# Sync test media directory with production
sync_test_media:
@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; 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
# Sync (with drop) test database with production database
sync_test_db_aggressive:
@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
@# I am not sure which shell is used, so I am calling bash to make sure
psql mam_test -c 'DROP OWNED BY "mam-web";'
pg_restore -c --if-exists -d mam_test 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.
# Sync test with production
# HACK ALERT: using aggressive variant, due to the schemas being too different.
sync_test: sync_test_media sync_test_db_aggressive
# Sync media directory with atrey. Useful for local development with production database
# Does not sync Galerie and CACHE (too huge).
sync_local_media:
rsync -ave ssh --exclude Galerie --exclude CACHE\
mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/media/ ./media/
# Downloads and restores production database to local database. PostgreSQL only.
sync_local_db:
scp mam-web@gimli.ms.mff.cuni.cz:`ssh mam-web@gimli.ms.mff.cuni.cz 'ls -v /akce/mam/www/backups/mam_prod-*\.pgdump.xz | tail -n 1'` \
./last.pgdump.xz
xz -fd last.pgdump.xz
pg_restore -c -d mam-prod last.pgdump
# Sync database and media. See above lines
sync_local: sync_local_media sync_local_db
# Push local compiled Vue to gimli test site
push_compiled_vue_to_test:
scp vue_frontend/webpack-stats.json mam-web@gimli:/akce/mam/www/mamweb-test/vue_frontend/
rsync -ave ssh seminar/static/seminar/vue mam-web@gimli:/akce/mam/www/mamweb-test/seminar/static/seminar/
ssh mam-web@gimli.ms.mff.cuni.cz 'cd /akce/mam/www/mamweb-test/ && . env/bin/activate && ./manage.py collectstatic --noinput'

135
README.md

@ -1,77 +1,58 @@
Basic commands for web development Web M&M
================================== ======
After you clone this repository, run `make`. It will download, locally install Tohle je repozitář s kódem M&Mího webu. Pokud zde hledáte web samotný nebo
and setup virtualenv and pip, and then locally install all required packages informace o semináři, najdete je na <https://mam.matfyz.cz> (a upřímně nechápu,
from `requirements.txt`. jak jste se dostali k tomuhle textu :-D)
When working with the code, always use the binaries in `./bin/`, such as Pokud jste tu zůstali, tak vás beztak zajímá vývoj webu (a jestli ne, tak
`bin/pip`, `bin/python`, ... never the global python, pip, ... budeme rádi, když začne :-)).
Use `make` and `./manage.py` for most things.
Co je M&Mweb uvnitř
Use git :-) ------
Celý náš web je napsaný v [Pythonu](https://www.python.org) ve frameworku
Quickstart [Django](https://www.djangoproject.com/). Web běží na serveru zvaném Gimli,
---------- jako databázi používá PostgreSQL (pro lokální vývoj naopak SQLite) a všechen
Run the following commands: náš kód je uložený v [Gitu](https://git-scm.com/) na [téhle
make install_venv gitee](https://gitea.ks.matfyz.cz/). Pro dokumentaci používáme
. env/bin/activate [Sphinx](https://www.sphinx-doc.org).
make install_web
<!--TODO: Z odstavce výše by ideálně měla být zachována jen první věta a zbytek
Pokud install_web říká Error: pg_config executable not found. nainstaluj si libpq-dev by měl být někde v docs s podrobnějším popisem…-->
Pokud chybová hláška obsahuje #include <Python.h>, nainstaluj si python3-dev
Jak si web pořídit
After finishing development, run "deactivate". ------
Prosím přečti si podrobnější návod v <docs/vyvoj.rst> (tady by bylo zbytečné
Make commands ho duplikovat).
-------------
Jak web vyvíjet
* `make install` (or `make`) - locally install and setup virtualpy, install ----
required packages. Ran again installs missing packages. Run after changing <!--TODO: Napsat obšírněji, asi zase do docs/-->
`requirements.txt`.
Na webu je mnoho věcí k dělání, některé ani nevyžadují kódění (třeba uhánění
* `make clean` - remove local python packages. orgů, aby si psali medailonky, aktualizace fotek, …), některé se naopak týkají
infrastruktury pod kódem (Gitea, Gimli, …). Je proto těžké mít na to úplně
* `make veryclean` - remove local packages and virtualpy enviroment and obecný návod, tak tady je zhruba návod na úpravy kódu a pokud se něco z toho
binaries. nedá aplikovat, tak to prostě zkus nějak udělat jinak, po svém. (Omlouvám se
neinformatikům, ale líp to teď nesepíšu :-))
* `make run` - runs "./manage.py runserver_plus"
1. Nejprve si stáhni repozitář a rozběhni si lokální web u sebe (viz <docs/vyvoj.rst>).
* `make push_test` - pushes the last commited version to test location. 1. Najdi si problém v Kanboardu (klikni na „Issues“ na Gitee) a/nebo se domluv
Only git-commited changes are pushed! Rest is re-generated from scratch. s webaři, na čem bys tak mohl/a pracovat.
At test server, the media data and database are kept the same. 1. Najdi místo, kde se to dá opravit a zkus to tam opravit. Uznávám, že tenhle
Everything else not in .gitignore is deleted/overwritten on the test server. bod je otravně obecný, pokud tápeš, zkus se zeptat zkušenějších webařů nebo
podívat do dokumentace.
* `make schema` - generates graph of seminar and all schemas as PDF. Supercool! 1. Vyzkoušej, že ti to lokálně funguje tak, jak má.
1. Zvládneš-li a máš-li čas, zkus to i zdokumentovat a/nebo napsat testy (TODO: chybí návod)
* `make sync_prod_flatpages` - downloads and applies static/flat pages from mamweb-prod 1. Po dohodě s webaři to vyzkoušej na testwebu
1. Pošli pull-request a případně zkus reagovat na komentáře
./manage.py commands 1. Až se změna začlení do hlavní větve (`master`) a nasadí se web na produkci,
-------------------- můžeš mít radost, že se web bude používat lépe Tobě i ostatním orgům :-)
* `./manage.py migrate` - update the database schema, initialise the database. ### Proč pull-requesty?
You need to run this in the beginning. <!--FIXME: Tohle ale už úplně patří do docs a ne sem, jen je zatím nemám prozkoumané…-->
* `./manage.py runserver_plus` - run a debugging server for the web. Slightly Účelů pull-requestů je několik. Jednak doufáme, že pomůže webařům se orientovat
enhanced compared to `./manage.py runserver`. v kódu, jednak tím umožňujeme dělat experimenty a dávat si zpětnou vazbu. V
Open [127.0.0.1:8000](127.0.0.1:8000). neposlední řadě pomáhají držet aspoň trochu konzistentní kód, což má pomoci
pohodě při programování… (A asi jsem na něco zapomněl :-))
* `./manage.py testdata` - create pseudo-random seminar data and admin/admin
user.
* `./manage.py test` - run the tests.
* `./manage.py shell` - run commands, list elemements of database, check syntax
by importing files, etc.
Configurations
--------------
* `mamweb/settings_common.py` contains most configuration options.
* `mamweb/settings.py` is used only for local development.
* `mamweb/settings_test.py` is used for testing on atrey.
* `mamweb/settings_prod.py` is used in production deployment.
These are automatically switched by `make`.

1
api/urls.py

@ -24,6 +24,7 @@ urlpatterns = [
path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'), path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'),
path('api/autocomplete/resitel_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'), path('api/autocomplete/resitel_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'),
path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'), path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
path('api/autocomplete/problem/vsechny', views.ProblemAutocomplete.as_view(), name='autocomplete_problem'),
# Ceka na autocomplete v3 # Ceka na autocomplete v3
# path('autocomplete/organizatori/', # path('autocomplete/organizatori/',

17
api/views/autocomplete.py

@ -12,7 +12,7 @@ from .helpers import LoginRequiredAjaxMixin
class SkolaAutocomplete(autocomplete.Select2QuerySetView): class SkolaAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání škol hlavně při registraci. """ """ View k :mod:`dal.autocomplete` pro vyhledávání škol hlavně při registraci. """
def get_queryset(self): def get_queryset(self):
# Don't forget to filter out results depending on the visitor ! # Don't forget to filter out results depending on the visitor !
qs = m.Skola.objects.all() qs = m.Skola.objects.all()
if self.q: if self.q:
words = self.q.split(' ') #TODO re split podle bileho znaku words = self.q.split(' ') #TODO re split podle bileho znaku
@ -86,6 +86,21 @@ class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
Q(nazev__icontains=self.q)) Q(nazev__icontains=self.q))
return qs return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):
# FIXME i starší úlohy
nastaveni = get_object_or_404(m.Nastaveni)
rocnik = nastaveni.aktualni_rocnik
temaQ = Q(Tema___rocnik = rocnik)
ulohaQ = Q(Uloha___cislo_zadani__rocnik=rocnik)
clanekQ = Q(Clanek___cislo__rocnik=rocnik)
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ).order_by("-stav", "nazev")
if self.q:
qs = qs.filter(
Q(nazev__icontains=self.q))
return qs
# Ceka na autocomplete v3 # Ceka na autocomplete v3
# class OrganizatorAutocomplete(autocomplete.Select2QuerySetView): # class OrganizatorAutocomplete(autocomplete.Select2QuerySetView):
# def get_queryset(self): # def get_queryset(self):

1
docs/index.rst

@ -13,6 +13,7 @@ Vítejte v dokumentaci M&Mího webu!
vyvoj vyvoj
sphinx sphinx
dalsi_soubory dalsi_soubory
skripty
modules/modules modules/modules
zapisy/zapisy zapisy/zapisy

108
docs/skripty.rst

@ -0,0 +1,108 @@
Skripty pro práci s repozitářem
===================
Máme dvě hlavní sady skriptů/příkazů na ovládání webu a repozitáře. Skripty pro
práci s webem psané v Pythonu jsou uložené ve složkách
``<aplikace>/management/commands/``, případně vestavěné, a volají se pomocí
``./manage.py <příkaz>``. Oproti tomu skripty pro práci s repozitářem a pro
úpravy databáze a souborů „zvenčí“ se nejčastěji nacházejí ve složce ``make/``
a volají se pomocí cesty: ``make/<příkaz>``.
Občas existují i nějaké další skripty na různých jiných místech. Všechny by
měly být ideálně popsány asi tady.
Make skripty
----
Skripty v ``make/`` se označují jako „Make skripty“. Slouží často k velkým
úkonům s repozitářem, jako je nasazení celého webu, zprovoznění lokálního webu
a podobně.
.. note:: Označení pro tyto skripty je dáno tím, že byly původně volány pomocí
make (tj. z Makefile). Ve skutečnosti je lze stále volat i jako ``make
<skript>``, ale pak není možné předávat parametry a obecně je tato cesta
zastaralá a existuje jen pro zpětnou kompatibilitu se svalovou pamětí.
Tyto skripty jsou samonosné, dají se spustit rovnou a v případě problémů si
budou hlasitě stěžovat. Pro účely debugování různých věcí jsou ale (bohužel?)
hlasité i při normálním spuštění, konkrétně vypisují všechny příkazy, které se
spouštějí (\ ``set -x``). Tyto příkazy jsou vidět za jedním či více plusky (\ ``+``).
.. tip:: Pokud některý make skript selže, tak by na konci měl vypsat, že se něco nepovedlo.
Knihovna ``make/lib.sh``
^^^^^^
Pro pohodlí při psaní velká část z nich využívá knihovnu uloženou
v ``make/lib.sh``. Jsou zde definované užitečné proměnné, kontroly a společný
kód. Kromě toho při inicializaci otestuje, že je skript spuštěn z kořene
repozitáře (takže to pak není potřeba zkoumat v ostatních skriptech).
Proměnné
"""""
Popsány jsou jen užitečné proměnné, ve skutečnosti jich je definovaných víc,
ale jsou triviální a samopopisné.
``VENV_PATH``
Cesta virtuálního prostředí. Též lze přepsat.
``REPO``
Cesta ke gitovému repozitáři na serveru, rovnou použitelná v ``git clone``
``GIMLI_LOGIN``
Přihlašovací údaje ke Gimlimu
``PRODWEB`` a ``TESTWEB``
Cesty ke složkám s produkčním a testovacím webem
Funkce a další zkratky
""""""
``ensure_venv``
Zajistí, že se zbytek skriptu spustí ve virtuálním prostředí, a pokud neexistuje, tak jej založí.
``ensure_web_installed``
Vyzkouší, že je web (django) aspoň elementárně zprovozněno a pokud ne, tak vyzve uživatele, aby to spravil.
``gimli_only``
Otestuje, že je příkaz spuštěn na Gimlim, pokud tomu tak není, zeptá se, jestli si uživatel skutečně přeje zbytek skriptu vykonat
``only_in_directory <složka>``
Otestuje, že skript běží z konkrétní složky. Zejména použitelné s ``gimli_only`` a ``$TESTWEB``
``safe_checkout_branch <větev>``
Bezpečně přepne repozitář na jinou větev. Pokud by mělo dojít k přepsání
knihovny nebo volajícího make skriptu, vyzve uživatele, aby přepnul ručně.
``install_everything``
Společná část kódu pro nasazování produkce a testwebu.
Skripty pro lokální vývoj
^^^^^^^
``make/install_web`` (nebo ekvivalentně ``make/install``)
Vytvoří virtualenv a nainstaluje do něj závislosti webu podle ``requirements.txt``. Následně popíše, jak vyrobit zbytek lokálního webu.
``make/run``
Spustí lokální web (ekvivalentní s ``./manage.py runserver``)
``make/schema``
Vykreslí závislosti a atributy modelů
``make/sync_prod_flatpages``
Stáhne z produkce aktuální statické stránky a uloží je do složky ``data/``
``make/test``
Spustí testy (ekvivalentní s ``./manage.py test -v2``)
``make/init_local``
Zkratka za posloupnost ``make/install_web``, ``./manage.py testdata``, ``./manage.py loaddata data/*``, ``make/sync_prod_flatpages``
Práce s testwebem
^^^^^^^
``make/deploy``
Nasadí testweb. Volitelně bere jako parametr jméno větve, kterou má nasadit.
Rovnou nastaví přihlašování a vygeneruje příslušnou verzi dokumentace `sem <https://mam-test.ks.matfyz.cz/docs>`_.
``make/push_compiled_vue_to_test``
**Neotestováno** Nahraje Vue z lokálního počítače na testweb. (Gimli často má moc starou verzi Node.js, takže nejde zkompilovat tam)
``make/sync_test_db_aggressive``
Zkopíruje databázi z produkčního webu.
``make/sync_test_media``
Zkopíruje média (obrázky, nahrané soubory) z produkčního webu.
``make/sync_test``
Zkratka za ``make/sync_test_db_aggressive`` + ``make/sync_test_media``.
Nasazení produkce
^^^^
``make/deploy_prod``. Před samotným nasazením zálohuje databázi a zkontroluje, že se nasazuje větev ``master``.

25
docs/tabulka_prerekvizit.rst

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

200
docs/vyvoj.rst

@ -1,20 +1,184 @@
Lokální vývoj mamwebu Lokální vývoj mamwebu
===================== =====================
Stačí spustit::
Asi hlavní část vývoje většiny webu probíhá lokálně. Každý tak může pracovat na
## Nahradte svym gimli username vlastních úpravách nezávisle na ostatních.
git clone USER@gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb.git mamweb
cd mamweb Potřebné vybavení
## Instalace je trochu magická, spusť následující posloupnost příkazů: -------
make install_venv
. env/bin/activate Tento soupis cílí na Linuxáky. Jistě je
make install_web spousta dalších možností, které zde nejsou postihnuty – poraď se s webaři,
pokud si nejsi jistý. (Speciálně lze nějak vyvíjet na Windows, leč lze často
## Vygeneruje nejaka testovaci data (spis chuda) narazit na odlišné chování od Linuxu.)
./manage.py testdata
## Nahraje statické stránky, menu a obrázky v pozadí menu Motivace cílení na Linux je to, že Gimli je Linuxový stroj, takže je vývojové
./manage.py loaddata data/* prostředí pak podobné produkci a zmenšuje to množství odlišného chování.
## Spusti testovaci server na http://127.0.0.1:8000/
./manage.py runserver .. TODO: Na dokumentaci odlišného chování (Postgres vs. SQLite, Win vs. Linux, …)
by to asi chtělo výhledově separátní stránku, ale teď píšu tuhle :-)
Když si lokálně spustíte web, běží na http://127.0.0.1:8000/, admin najdete na http://127.0.0.1:8000/admin/ (admin/admin) Až skončíš s vývojem webu, spusť “deactivate”. Tím zmizí ‘(env)’ ze začátku promptu.
Nutné
^^^^
- `Git <https://git-scm.com>`_
- `Python <https://python.org>`_
- Ideálně ve verzi 3.9 (to je to, co je aktuálně (2022) na Gimlim)
- Včetně pip-u (na Ubuntu balíček ``python3-pip``), venvu (``python3-venv``) a knihoven pro kompilaci
Cčkových rozšíření (``python3-dev``)
- Knihovna pro práci s PostgreSQL (``libpq-dev``, protože jsou potřeba všechny db backendy)
- Webový prohlížeč
- \*NIXový shell (typicky ``bash``)
.. TODO: Pokud tu něco chybí, tak to dopiš :-)
Kromě toho je potřeba mít účet na `Gitee <https://gitea.ks.matfyz.cz>`_, kde
bydlí gitový repozitář s kódem.
.. 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>`.
Doporučené
^^^^^^^^^^
- Python wheel (možná řeší problémy s potřebou ``-dev`` balíčků…)
- Editor / IDE podporující `Editorconfig <https://editorconfig.org/>`_
- Uživatelská zkušenost s `produkční verzí webu <https://mam.matfyz.cz>`_
- Účet v `Kanci <https://kanboard.ledoian.cz>`_
.. TODO: A nejspíš další věci, na které jsem si teď nevzpomněl.
Zprovoznění
-------
Nejprve je potřeba stáhnout si repozitář. To se provede příkazem ``git clone
https://gitea.ks.matfyz.cz/mam/mamweb.git``, případně ``git clone
git@gitea.ks.matfyz.cz:mam/mamweb.git``, pokud už máš nahraný SSH klíč na
Giteu. (Obě adresy se dají zkopírovat ze `stránky repozitáře
<https://gitea.ks.matfyz.cz/mam/mamweb>`_.) To vyrobí složku ``mamweb``, přepni
se do ní (``cd mamweb``)
O zprovoznění webu se stará skript ``make/install_web``, stačí ho spustit. Ten
vytvoří virtualenv (neexistuje-li) a nainstaluje do něj závislosti webu (podle
souboru ``requirements.txt``).
.. FIXME: Novowebaři zmínka o requirements.txt tady moc nepomůže, to má být na
stránce o celkové stavbě repozitáře a stacku…
Následně je potřeba nahrát další data do databáze, což uděláš pomocí příkazů
``./manage.py testdata`` a ``./manage.py loaddata data/*``. Skript
``make/install_web`` to kdyžtak na konci připomene.
.. caution:: Zatímco skripty v ``make/`` to nepotřebují, pro použití skriptu
``./manage.py`` (a dalších) se potřebuješ přepnout do virtuálního prostředí.
To uděláš velmi pravděpodobně spuštěním ``source env/bin/activate``, před
začátkem *promptu* by se mělo objevit ``(env)``. Pro opuštění spusť
``deactivate``.
Samotný web se spustí třeba pomocí ``make/run``, nebo ekvivalentně
``./manage.py runserver`` a pak je vidět na `<http://127.0.0.1:8000>`_.
Časté problémy
^^^^^^
- ``make/install_web`` vypíše ``Error: pg_config executable not found.``:
Chybí ``libpq-dev``
- Chybová hláška obsahuje ``#include <Python.h>``: chybí ``python3-dev``
- Na webu není vidět meníčko: spusť ``./manage.py loaddata data/*``
- ``locale.Error: unsupported locale setting``: Chybí podpora pro příslušný
jazyk ve tvém systému. Odkomentuj příslušnou lokalizaci v ``/etc/locale.gen``
a spusť ``locale-gen`` jako root, tím se to spraví.
S dalšími problémy se zkus obrátit na další webaře, třeba někdo bude vědět :-)
Příkazy pro ovládání webu
-------
Příkazy se dělí do několika skupin. Některé souvisí přímo s webem, Djangem,
databází a podobně, ty typicky používají ``./manage.py``. Skripty pro
obhospodařování repozitáře a webu „zvenku“ typicky bydlí ve složce ``make/``.
Ostatní skripty jsou na náhodných místech :-)
Tady jsou rozebrány jen příkazy relevantní pro lokální web a univerzálně
užitečné, ostatní najdeš v :ref:`Skripty pro práci s repozitářem`.
Make skripty
^^^^^^^
- ``make/install_web`` nainstaluje závislosti webu
- ``make/run`` spustí web (ekvivalentní s ``./manage.py runserver``)
- ``make/schema`` nakreslí schéma vazeb modelů (může se hodit pro referenci a představu)
- ``make/test`` spustí testy (ale moc jich zatím není; ekvivalentní s ``./manage.py test``)
- ``make/sync_prod_flatpages`` stáhne statické stránky z produkčního webu a
uloží je do souboru v gitu, což umožňuje jejich verzování
Manage.py skripty
^^^^^
.. note:: Je potřeba je spouštět ve virtuálním prostředí, viz výše.
Všechny skripty kdyžtak mají ``--help``, dá se tak zjistit, co všechno umějí.
- ``./manage.py testdata`` vygeneruje spíše chudá testovací data, aby bylo na
čem testovat web.
- ``./manage.py loaddata <soubor/y>`` nahraje data ze souborů do databáze
- ``./manage.py dumpdata <model>`` naopak z databáze vyrobí textovou reprezentaci
- ``./manage.py shell`` spustí interaktivní pythoní shell, ze kterého lze
interagovat s webem / Djangem.
- ``./manage.py dbshell`` spustí databázový shell (typicky používá SQL)
- ``./manage.py makemigrations`` vyrobí popis migrací, ``./manage.py migrate``
je spustí, ``showmigrations`` ukáže, které migrace jsou aplikované a které
ne.
- ``./manage.py runserver_plus`` spouští o něco lepší vývojový server (ale
nikdy jsem asi ty lepší featury nepoužil)
Může se hodit vědět, že spuštění ``./manage.py`` bez parametrů vypíše seznam
všech příkazů, které lze spustit.
Dokumentace djangových příkazů je v `dokumentaci Djanga
<https://docs.djangoproject.com/en/3.2/ref/django-admin/#available-commands>`_
Ostatní užitečné příkazy
^^^^^
- ``git status`` je univerzální nápověda na aktuální stav repozitáře a co s tím
lze dělat.
- ``git clean -fxd`` smaže všechny soubory, které nejsou uložené v gitu (včetně
ignorovaných). **Nebezpečný příkaz**, zamysli se, než ho spustíš
Specifika lokálního webu
-------
Lokální uživatelé
^^^^^^^
Přihlašovací údaje jsou psány jako ``login:heslo``
- Superuživatel: ``admin:admin``
- Orgovské účty: ``o:o``, ``o1:o````o3:o``
- Řešitelské účty: ``r:r``, ``r1:r````r3:r``
Všechny tyto účty jsou (?) svázané s nějakými fiktivními osobami, není ale zřejmé se
kterými, budeš to muset vyzkoušet a pak tady zdokumentovat :-)
E-maily
^^^^^
Posílání e-mailů se lokálně dá zkoušet, e-mail se vypíše do terminálu, kde je
web spuštěn (e.g. pomocí ``make/run``).
Pruhy
^^^^
To, že má lokální web po stranách zelené pruhy je normální a správně, slouží to
k vizuálnímu odlišení lokálního webu.
.. TODO: Mít někde popis všech tří instancí a tady na něj pak odkázat.
.. - Tahák k používání gitových větví: do workflow, ne sem…
.. - Užitečné odkazy – kam se kouknout
(dohledávání views podle URL, settings_*, )
.. - Zpříjemnění práce (ssh-klíče, tea, --help, …)

84
header_fotky/context_processors.py

@ -12,46 +12,46 @@ from header_fotky.models import FotkaUrlVazba
def vzhled(request): def vzhled(request):
""" """
Podle času přidá do contextu, zdali je nebo není noc. Dále podle dení Podle času přidá do contextu, zdali je nebo není noc. Dále podle dení
doby a url přidá do contextu správnou fotku. doby a url přidá do contextu správnou fotku.
url adresu nejprve vyzkouší celou, pak postupně odřezává věci za url adresu nejprve vyzkouší celou, pak postupně odřezává věci za
lomítkem, dokud nenajde url, pro kterou existuje alespoň jedna fotka. lomítkem, dokud nenajde url, pro kterou existuje alespoň jedna fotka.
Z fotek pro toto url zkusí vybrat tu ve správné denní době a poté Z fotek pro toto url zkusí vybrat tu ve správné denní době a poté
libovolnou. (Z více možných fotek pro 1 url a 1 dobu vybírá náhodně.) libovolnou. (Z více možných fotek pro 1 url a 1 dobu vybírá náhodně.)
""" """
hodin = datetime.now().hour hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 20): if (hodin <= 6) or (hodin >= 20):
noc = True noc = True
nedoba = 'den' nedoba = 'den'
doba = 'noc' doba = 'noc'
else: else:
noc = False noc = False
nedoba = 'noc' nedoba = 'noc'
doba = 'den' doba = 'den'
url = request.path url = request.path
fotky = FotkaUrlVazba.objects.exclude(denni_doba=nedoba) fotky = FotkaUrlVazba.objects.exclude(denni_doba=nedoba)
fotka = None fotka = None
# TODO rychlejší patternmatch? # TODO rychlejší patternmatch?
while (fotka is None) and (url != ''): while (fotka is None) and (url != ''):
presne = fotky.filter(url__exact=url) presne = fotky.filter(url__exact=url)
if presne.count() > 0: if presne.count() > 0:
presne_doba = presne.filter(denni_doba=doba) presne_doba = presne.filter(denni_doba=doba)
if presne_doba.count() > 0: if presne_doba.count() > 0:
fotka = random.choice(presne_doba).url_fotky() fotka = random.choice(presne_doba).url_fotky()
else: else:
fotka = random.choice(presne).url_fotky() fotka = random.choice(presne).url_fotky()
url = url[:-1] url = url[:-1]
index = url.rfind('/') index = url.rfind('/')
if index != -1: if index != -1:
url = url[:index+1] url = url[:index+1]
if fotka is None: if fotka is None:
fotka = settings.STATIC_URL + "images/header/vikendovka.jpg" fotka = settings.STATIC_URL + "images/header/vikendovka.jpg"
return {'noc': noc, 'fotka': fotka} return {'noc': noc, 'fotka': fotka}

18
korektury/migrations/0019_auto_20221205_2014.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2022-12-05 19:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('korektury', '0018_korekturovanepdf_poslat_mail'),
]
operations = [
migrations.AlterField(
model_name='korekturovanepdf',
name='nazev',
field=models.CharField(help_text='Název (např. 22.1 verze 4) korekturovaného PDF', max_length=50, verbose_name='název PDF'),
),
]

2
korektury/models.py

@ -55,7 +55,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 = True,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 verze 4) 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)')

34
korektury/views.py

@ -30,28 +30,28 @@ class KorekturyListView(generic.ListView):
template_name = 'korektury/seznam.html' template_name = 'korektury/seznam.html'
class KorekturyAktualniListView(KorekturyListView): class KorekturyAktualniListView(KorekturyListView):
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset=super().get_queryset() queryset=super().get_queryset()
queryset=queryset.exclude(status="zastarale") queryset=queryset.exclude(status="zastarale")
return queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['selected'] = 'aktualni' context['selected'] = 'aktualni'
return context return context
class KorekturyZastaraleListView(KorekturyListView): class KorekturyZastaraleListView(KorekturyListView):
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
queryset=super().get_queryset() queryset=super().get_queryset()
queryset=queryset.filter(status="zastarale").order_by("-cas") queryset=queryset.filter(status="zastarale").order_by("-cas")
return queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['selected'] = 'zastarale' context['selected'] = 'zastarale'
return context return context
class KorekturySeskupeneListView(KorekturyAktualniListView): class KorekturySeskupeneListView(KorekturyAktualniListView):
template_name = 'korektury/seskupeny_seznam.html' template_name = 'korektury/seskupeny_seznam.html'

8
make/README.md

@ -0,0 +1,8 @@
Milý člověče, M&Mí web tě vítá. Prosím, neděs se, zkusím tě provést lokálním zprovozněním webu.
Pro detaily a nápovědu si prosím přečti dokumentaci v docs/vyvoj.rst.
TL;DR: Web vyrobíš pomocí následující posloupnosti příkazů:
make/init_local
make/run
a web potom najdeš na <http://127.0.0.1:8000>

31
make/deploy

@ -0,0 +1,31 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
gimli_only
only_in_directory "$TESTWEB"
CURRENT_BRANCH="$(git branch --show-current)"
BRANCH="${1:-$CURRENT_BRANCH}"
safe_checkout_branch "$BRANCH"
# Teď máme správnou větev, jdeme vše nainstalovat
install_everything
systemctl --user restart mamweb-test.service
# Přihlášení
htpasswd -bc .htpasswd test lisakjelisak
setfacl -m u:www-data:r .htpasswd
# Build dokumentace
ensure_venv
(
cd docs
make html
)
# Oprava práv k dokumentaci
setfacl -m u:www-data:x . docs docs/_build
setfacl -R -m u:www-data:rX docs/_build/html
setfacl -R -m default:u:www-data:rX docs/_build/html

25
make/deploy_prod

@ -0,0 +1,25 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
gimli_only
only_in_directory "$PRODWEB"
CURRENT_BRANCH="$(git branch --show-current)"
BRANCH="${1:-$CURRENT_BRANCH}"
if test "$BRANCH" != master
then
echo "Pozor, nasazuješ na produkci větev, která není master ($BRANCH), chceš pokračovat? Pokud ne, sestřel tento skript."
read
fi
# Záloha DB
( cd -P .. && ./backup_prod_db.sh )
safe_checkout_branch "$BRANCH"
# Teď máme správnou větev, jdeme vše nainstalovat
install_everything
systemctl --user restart mamweb-prod.service

10
make/init_local

@ -0,0 +1,10 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
make/install_web
ensure_venv
./manage.py testdata
./manage.py loaddata data/*
make/sync_prod_flatpages

1
make/install

@ -0,0 +1 @@
install_web

16
make/install_web

@ -0,0 +1,16 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
ensure_venv
# Aktualizace toolchainu
pip install --upgrade pip setuptools
# Instalace závislostí webu
pip install -r requirements.txt --upgrade
# XXX: Připomínka z původního Makefile:
echo 'Pro vygenerování tesdat spusť ./manage.py testdata
Po vygenerování testdat spusť ./manage.py loaddata data/*, ať máš menu a další modely
Pro synchronizaci flatpages spusť make/sync_prod_flatpages'

104
make/lib.sh

@ -0,0 +1,104 @@
#!/bin/false Tohle je knihovna, nemá se spouštět, ale načítat pomocí source(1) nebo '.'
PYTHON="${PYTHON:-python3}"
VENV="${VENV:-${PYTHON} -m venv}"
VENV_PATH="${VENV_PATH:-env}"
BRANCH="${BRANCH:-master}"
REPO="${REPO:-git@gitea.ks.matfyz.cz:mam/mamweb.git}"
GIMLI='gimli.ms.mff.cuni.cz'
GIMLI_LOGIN="mam-web@$GIMLI"
# Skutečné cesty, jak je vrátí `realpath`
PRODWEB="/aux/akce/mam/www/mamweb-prod"
TESTWEB="/aux/akce/mam/www/mamweb-test"
function die {
echo "$@" >&2
exit 1
}
trap 'echo Něco se nepovedlo :-/' ERR
# Vždycky chceme zajistit, že běžíme z rootu repozitáře
# TODO: chceme? Nechceme naopak umět to spouštět odkudkoliv, aspoň u většiny targetů?
test -e '.git' || die "Make skript spuštěn ve špatné složce, spusť ho z kořenového adresáře repozitáře."
function ensure_venv {
test -f "$VENV_PATH/bin/activate" || $VENV "$VENV_PATH"
. "$VENV_PATH/bin/activate"
# To ale není všechno Horste – ten venv nemusí fungovat, chceme to ověřit a případně spadnout.
local SPRAVNA_CESTA="$(readlink -f "$VENV_PATH/bin/python")"
local SKUTECNA_CESTA="$(readlink -f "$(which python)")"
test "$SPRAVNA_CESTA" == "$SKUTECNA_CESTA" || die "Venv asi nefunguje. Prosím smaž si ho a zkus to znovu."
python -c 'print()' > /dev/null || die "Python ve venvu je rozbitý. Prosím smaž venv a zkus to znovu."
}
function ensure_web_installed {
ensure_venv
python -c 'import django; print(django.__version__)' > /dev/null && return
echo >&2 "POZOR: Web nevypadá nainstalovaně, instaluji."
make/install_web
}
function gimli_only {
# Rovnou zkontrolujeme i uživatele
if test "$HOSTNAME" != gimli -o "$USER" != mam-web
then
echo "Tento příkaz se má spouštět na gimlim, chceš pokračovat? Pokud ne, sestřel tento skript."
read
fi
}
function only_in_directory {
local DIR="$1"
local CURRENT="$(readlink -f .)"
if test "$CURRENT" != "$DIR"
then
echo "Tento příkaz se má spouštět ve složce $DIR, chceš pokračovat? Pokud ne, sestřel tento skript."
read
fi
}
function safe_checkout_branch {
if test "$#" -ne 1
then
echo >&2 "Použití: $0 <branch>"
return 1
fi
local BRANCH="$1"
local SCRIPT="$0"
git fetch --all
# Od teď si musíme dát pozor, abychom nezměnili kód, který právě běží.
# Zkontrolujeme, že se nemění tahle knihovna a skript, který běží.
# `git rev-parse` dává SHA-1 hashe objektů, vizte manuálovou stránku pro pochopení.
# Pozor: tohle porovnává jen verze commitnuté do gitu. Lokální změny udělají něco náhodného…
if test "$(git rev-parse @:make/lib.sh)" != "$(git rev-parse "$BRANCH@{u}":make/lib.sh)"
then
echo >&2 "Změna v make/lib.sh, prosím pullni manuálně"
exit 1
fi
if test "$(git rev-parse @:"$SCRIPT")" != "$(git rev-parse "$BRANCH@{u}":"$SCRIPT")"
then
echo >&2 "Změna v $SCRIPT, prosím pullni manuálně"
exit 1
fi
git checkout "$BRANCH"
git pull
git clean -f
}
function install_everything {
# Skoro celé nasazení webu je stejné pro testweb i pro produkci, tak je to tady dohromady
gimli_only
ensure_venv
make/install
./manage.py migrate
./manage.py collectstatic --noinput
setfacl -R -m default:u:www-data:rX media static
setfacl -R -m u:www-data:rX media static
chown -R :mam . || true
chmod -R g+rX,go-w . || true
}

14
make/push_compiled_vue_to_test

@ -0,0 +1,14 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
scp vue_frontend/webpack-stats.json "$GIMLI_LOGIN:$TESTWEB/vue_frontend/"
rsync -ave ssh seminar/static/seminar/vue "$GIMLI_LOGIN:$TESTWEB/seminar/static/seminar/"
ssh "$GIMLI_LOGIN" "
set -euxo pipefail
cd $TESTWEB
. make/lib.sh
ensure_venv
./manage.py collectstatic --noinput
"

8
make/run

@ -0,0 +1,8 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
ensure_web_installed
./manage.py runserver "$@"

9
make/schema

@ -0,0 +1,9 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
ensure_web_installed
./manage.py graph_models seminar | dot -Tpdf > schema_seminar.pdf
./manage.py graph_models -a -g | dot -Tpdf > schema_all.pdf

20
make/sync_prod_flatpages

@ -0,0 +1,20 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
ensure_web_installed
# TODO: This is very ugly, will fix in a future PR (hopefully)
ssh "$GIMLI_LOGIN" "
set -euxo pipefail
cd $PRODWEB
. make/lib.sh
ensure_venv
./manage.py dumpdata flatpages --indent=2 > flat.json
./fix_json.py flat.json flat_fixed.json
"
rsync -ave ssh "$GIMLI_LOGIN:$PRODWEB/flat_fixed.json" data/flat.json
./manage.py loaddata data/flat.json

11
make/sync_test

@ -0,0 +1,11 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
# Prerekvizity nekontrolujeme, dokud voláme další make skripty, tak by akorát
# vedly k víc dotazům na stejnou věc a bylo by to otravné. Pokud tu někdy bude
# něco jiného, tak pak ať tu prerekvizity zmíněné jsou.
make/sync_test_db_aggressive
make/sync_test_media

18
make/sync_test_db_aggressive

@ -0,0 +1,18 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
gimli_only
# Teoreticky není potřeba, ale stejně jinde make skripty nejsou a pouštět to z
# produkce nezní jako běžný stav, kromě toho to aktuálně vyrábí pomocné soubory
# v aktuální složce (FIXME do budoucna) a to na produkci nechceme
only_in_directory "$TESTWEB"
pg_dump mam_test > "dump-test-$(date +"%Y%m%d_%H%M").sql"
pg_dump -Fc mam_prod > dump-prod.sql
psql mam_test -c 'DROP OWNED BY "mam-web";'
pg_restore -c --if-exists -d mam_test 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"

9
make/sync_test_media

@ -0,0 +1,9 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
gimli_only
only_in_directory "$TESTWEB"
rsync -av --delete "$PRODWEB/media/" ./media

9
make/test

@ -0,0 +1,9 @@
#!/bin/bash
set -exuo pipefail
. make/lib.sh
ensure_web_installed
trap - ERR # Testy nejspíš selžou, ale nechceme kolem toho dělat další chybovou hlášku.
./manage.py test -v2

32
mamweb/admin.py

@ -17,14 +17,14 @@ from ckeditor_uploader.widgets import CKEditorUploadingWidget
class FlatpageForm(FlatpageFormOld): class FlatpageForm(FlatpageFormOld):
content = forms.CharField(widget=CKEditorUploadingWidget()) content = forms.CharField(widget=CKEditorUploadingWidget())
class Meta: class Meta:
model = FlatPage # this is not automatically inherited from FlatpageFormOld model = FlatPage # this is not automatically inherited from FlatpageFormOld
exclude = [] exclude = []
class FlatPageAdmin(FlatPageAdminOld): class FlatPageAdmin(FlatPageAdminOld):
form = FlatpageForm form = FlatpageForm
# We have to unregister the normal admin, and then reregister ours # We have to unregister the normal admin, and then reregister ours
@ -36,19 +36,19 @@ locale.setlocale(locale.LC_COLLATE, 'cs_CZ.UTF-8')
# https://books.agiliq.com/projects/django-admin-cookbook/en/latest/set_ordering.html # https://books.agiliq.com/projects/django-admin-cookbook/en/latest/set_ordering.html
# FIXME zpraseno pomocí toho, že Python umí bez problému přepisovat funkce # FIXME zpraseno pomocí toho, že Python umí bez problému přepisovat funkce
def get_app_list(self, request): def get_app_list(self, request):
""" """
Return a sorted list of all the installed apps that have been Return a sorted list of all the installed apps that have been
registered in this site. registered in this site.
""" """
app_dict = self._build_app_dict(request) app_dict = self._build_app_dict(request)
# Sort the apps alphabetically. # Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower())) app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower()))
# Sort the models alphabetically within each app. # Sort the models alphabetically within each app.
for app in app_list: for app in app_list:
app['models'].sort(key=lambda x: locale.strxfrm('žž' + x['name'].lower()) if (x['name'].endswith("(Node)")) else locale.strxfrm(x['name'].lower())) app['models'].sort(key=lambda x: locale.strxfrm('žž' + x['name'].lower()) if (x['name'].endswith("(Node)")) else locale.strxfrm(x['name'].lower()))
return app_list return app_list
AdminSite.get_app_list = get_app_list AdminSite.get_app_list = get_app_list

152
mamweb/middleware.py

@ -6,83 +6,83 @@ from django.http import HttpResponse, HttpResponseRedirect
class LoggedInHintCookieMiddleware(object): class LoggedInHintCookieMiddleware(object):
"""Middleware to securely help with 'logged-in' detection for dual HTTP/HTTPS sites. """Middleware to securely help with 'logged-in' detection for dual HTTP/HTTPS sites.
On insecure requests: Checks for a (non-secure) cookie settings.LOGGED_IN_HINT_COOKIE_NAME On insecure requests: Checks for a (non-secure) cookie settings.LOGGED_IN_HINT_COOKIE_NAME
and if present, redirects to HTTPS (same adress). and if present, redirects to HTTPS (same adress).
Note this usually breaks non-GET (POST) requests. Note this usually breaks non-GET (POST) requests.
On secure requests: Updates cookie settings.LOGGED_IN_HINT_COOKIE_NAME to reflect On secure requests: Updates cookie settings.LOGGED_IN_HINT_COOKIE_NAME to reflect
whether an user is logged in in the current session (cookie set to 'True' or cleared). whether an user is logged in in the current session (cookie set to 'True' or cleared).
The cookie is set to expire at the same time as the sessionid cookie. The cookie is set to expire at the same time as the sessionid cookie.
By default, LOGGED_IN_HINT_COOKIE_NAME = 'logged_in_hint'. By default, LOGGED_IN_HINT_COOKIE_NAME = 'logged_in_hint'.
""" """
def __init__(self): def __init__(self):
if hasattr(settings, 'LOGGED_IN_HINT_COOKIE_NAME'): if hasattr(settings, 'LOGGED_IN_HINT_COOKIE_NAME'):
self.cookie_name = settings.LOGGED_IN_HINT_COOKIE_NAME self.cookie_name = settings.LOGGED_IN_HINT_COOKIE_NAME
else: self.cookie_name = 'logged_in_hint' else: self.cookie_name = 'logged_in_hint'
self.cookie_value = 'True' self.cookie_value = 'True'
def cookie_correct(self, request): def cookie_correct(self, request):
return self.cookie_name in request.COOKIES and request.COOKIES[self.cookie_name] == self.cookie_value return self.cookie_name in request.COOKIES and request.COOKIES[self.cookie_name] == self.cookie_value
def process_request(self, request): def process_request(self, request):
if not request.is_secure(): if not request.is_secure():
if self.cookie_correct(request): if self.cookie_correct(request):
# redirect insecure (assuming http) requests with hint cookie to https # redirect insecure (assuming http) requests with hint cookie to https
url = request.build_absolute_uri() url = request.build_absolute_uri()
assert url[:5] == 'http:' assert url[:5] == 'http:'
return HttpResponseRedirect('https:' + url[5:]) return HttpResponseRedirect('https:' + url[5:])
return None return None
def process_response(self, request, response): def process_response(self, request, response):
if request.is_secure(): if request.is_secure():
# assuming full session info (as the conn. is secure) # assuming full session info (as the conn. is secure)
try: try:
user = request.user user = request.user
except AttributeError: # no user - ajax or other special request except AttributeError: # no user - ajax or other special request
return response return response
if user.is_authenticated(): if user.is_authenticated():
if not self.cookie_correct(request): if not self.cookie_correct(request):
expiry = None if request.session.get_expire_at_browser_close() else request.session.get_expiry_date() expiry = None if request.session.get_expire_at_browser_close() else request.session.get_expiry_date()
response.set_cookie(self.cookie_name, value=self.cookie_value, expires=expiry, secure=False) response.set_cookie(self.cookie_name, value=self.cookie_value, expires=expiry, secure=False)
else: else:
if self.cookie_name in request.COOKIES: if self.cookie_name in request.COOKIES:
response.delete_cookie(self.cookie_name) response.delete_cookie(self.cookie_name)
return response return response
class vzhled: class vzhled:
def process_request(self, request): def process_request(self, request):
return None return None
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
#print "====== process_request ======" #print "====== process_request ======"
#print view_func #print view_func
#print view_args #print view_args
#print view_kwargs #print view_kwargs
#print "=============================" #print "============================="
return None return None
def process_template_response(self, request, response): def process_template_response(self, request, response):
hodin = datetime.now().hour hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 14): # TODO 20 if (hodin <= 6) or (hodin >= 14): # TODO 20
response.context_data['noc'] = True response.context_data['noc'] = True
else: else:
response.context_data['noc'] = False response.context_data['noc'] = False
return response return response
def process_response(self, request, response): def process_response(self, request, response):
#hodin = datetime.now().hour #hodin = datetime.now().hour
#if (hodin <= 6) or (hodin >= 14): # TODO 20 #if (hodin <= 6) or (hodin >= 14): # TODO 20
#response.context_data['noc'] = True #response.context_data['noc'] = True
#else: #else:
#response.context_data['noc'] = False #response.context_data['noc'] = False
return response return response
##def process_exception(request, exception): ##def process_exception(request, exception):
#pass #pass

385
mamweb/settings_common.py

@ -40,8 +40,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
) )
# Where redirect for login required services # Where redirect for login required services
@ -57,41 +57,41 @@ DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
# Modules configuration # Modules configuration
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
MIDDLEWARE = ( MIDDLEWARE = (
# 'reversion.middleware.RevisionMiddleware', # 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
# FIXME: rozbilo se při přechodu na Django 2.0, nevím, jestli # FIXME: rozbilo se při přechodu na Django 2.0, nevím, jestli
# se to dá zahodit bez náhrady # se to dá zahodit bez náhrady
# 'mamweb.middleware.LoggedInHintCookieMiddleware', # 'mamweb.middleware.LoggedInHintCookieMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
) )
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': ( 'context_processors': (
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.template.context_processors.request', 'django.template.context_processors.request',
'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.rozliseni',
'various.context_processors.april', 'various.context_processors.april',
) )
}, },
}, },
] ]
@ -99,59 +99,59 @@ TEMPLATES = [
INSTALLED_APPS = ( INSTALLED_APPS = (
# Basic # Basic
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.auth', 'django.contrib.auth',
# Utilities # Utilities
'sekizai', 'sekizai',
'reversion', 'reversion',
'django_countries', 'django_countries',
'solo', 'solo',
'ckeditor', 'ckeditor',
'ckeditor_uploader', 'ckeditor_uploader',
'taggit', 'taggit',
'dal', 'dal',
'dal_select2', 'dal_select2',
'crispy_forms', 'crispy_forms',
'django_comments', 'django_comments',
'django.contrib.flatpages', 'django.contrib.flatpages',
'django.contrib.humanize', 'django.contrib.humanize',
'sitetree', 'sitetree',
'imagekit', 'imagekit',
'polymorphic', 'polymorphic',
'webpack_loader', 'webpack_loader',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
# MaMweb # MaMweb
'mamweb', 'mamweb',
'seminar', 'seminar',
'galerie', 'galerie',
'korektury', 'korektury',
'prednasky', 'prednasky',
'header_fotky', 'header_fotky',
'various', 'various',
'various.autentizace', 'various.autentizace',
'api', 'api',
'aesop', 'aesop',
'odevzdavatko', 'odevzdavatko',
'vysledkovky', 'vysledkovky',
'personalni', 'personalni',
'soustredeni', 'soustredeni',
'treenode', 'treenode',
# Admin upravy: # Admin upravy:
# 'material', # 'material',
# 'material.admin', # 'material.admin',
@ -159,73 +159,76 @@ INSTALLED_APPS = (
# 'admin_tools.theming', # 'admin_tools.theming',
# 'admin_tools.menu', # 'admin_tools.menu',
# 'admin_tools.dashboard', # 'admin_tools.dashboard',
'django.contrib.admin', 'django.contrib.admin',
# Nechat na konci (INSTALLED_APPS je uspořádané):
'django_cleanup.apps.CleanupConfig', # Uklízí media/
) )
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
'SHOW_COLLAPSED': True, 'SHOW_COLLAPSED': True,
} }
SUMMERNOTE_CONFIG = { SUMMERNOTE_CONFIG = {
'iframe': False, 'iframe': False,
'airMode': False, 'airMode': False,
'attachment_require_authentication': True, 'attachment_require_authentication': True,
'width': '80%', 'width': '80%',
# 'height': '30em', # 'height': '30em',
'toolbar': [ 'toolbar': [
['style', ['style']], ['style', ['style']],
['font', ['bold', 'italic', 'superscript', 'subscript', 'clear']], ['font', ['bold', 'italic', 'superscript', 'subscript', 'clear']],
['color', ['color']], ['color', ['color']],
['para', ['ul', 'ol', 'paragraph']], ['para', ['ul', 'ol', 'paragraph']],
['table', ['table']], ['table', ['table']],
['insert', ['link', 'picture', 'hr']], ['insert', ['link', 'picture', 'hr']],
['view', ['fullscreen', 'codeview']], ['view', ['fullscreen', 'codeview']],
['help', ['help']], ['help', ['help']],
] ]
} }
CKEDITOR_UPLOAD_PATH = "uploads/" CKEDITOR_UPLOAD_PATH = "uploads/"
CKEDITOR_IMAGE_BACKEND = 'pillow' CKEDITOR_IMAGE_BACKEND = 'pillow'
#CKEDITOR_JQUERY_URL = '//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js' #CKEDITOR_JQUERY_URL = '//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js'
CKEDITOR_CONFIGS = { CKEDITOR_CONFIGS = {
'default': { 'default': {
'entities': False, 'entities': False,
'toolbar': [ 'toolbar': [
['Source', 'ShowBlocks', '-', 'Maximize'], ['Source', 'ShowBlocks', '-', 'Maximize'],
['Bold', 'Italic', 'Subscript', 'Superscript', '-', 'RemoveFormat'], ['Bold', 'Italic', 'Subscript', 'Superscript', '-', 'RemoveFormat'],
['NumberedList','BulletedList','-','Blockquote','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'], ['NumberedList','BulletedList','-','Blockquote','-','JustifyLeft','JustifyCenter','JustifyRight','JustifyBlock'],
['Link', 'Unlink', 'Anchor', '-', 'Image', 'Table', 'HorizontalRule'], ['Link', 'Unlink', 'Anchor', '-', 'Image', 'Table', 'HorizontalRule'],
['Format'], ['Format'],
], ],
# 'toolbar': 'full', # 'toolbar': 'full',
'height': '40em', 'height': '40em',
'width': '100%', 'width': '100%',
'toolbarStartupExpanded': False, 'toolbarStartupExpanded': False,
'allowedContent' : True, 'allowedContent' : True,
}, },
} }
# Webpack loader # Webpack loader
VUE_FRONTEND_DIR = os.path.join(BASE_DIR, 'vue_frontend') VUE_FRONTEND_DIR = os.path.join(BASE_DIR, 'vue_frontend')
WEBPACK_LOADER = { WEBPACK_LOADER = {
'DEFAULT': { 'DEFAULT': {
'CACHE': False, 'CACHE': False,
'BUNDLE_DIR_NAME': 'vue/', # must end with slash 'BUNDLE_DIR_NAME': 'vue/', # must end with slash
'STATS_FILE': os.path.join(VUE_FRONTEND_DIR, 'webpack-stats.json'), 'STATS_FILE': os.path.join(VUE_FRONTEND_DIR, 'webpack-stats.json'),
'POLL_INTERVAL': 0.1, 'POLL_INTERVAL': 0.1,
'TIMEOUT': None, 'TIMEOUT': None,
'IGNORE': [r'.+\.hot-update.js', r'.+\.map'] 'IGNORE': [r'.+\.hot-update.js', r'.+\.map']
} }
} }
# Dajngo REST Framework # Dajngo REST Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 100 'PAGE_SIZE': 100
} }
@ -233,22 +236,22 @@ REST_FRAMEWORK = {
# Create file 'django.secret' in every install (it is not kept in git) # Create file 'django.secret' in every install (it is not kept in git)
try: try:
with open(os.path.join(os.path.dirname(__file__), '..', 'django.secret')) as f: with open(os.path.join(os.path.dirname(__file__), '..', 'django.secret')) as f:
SECRET_KEY = f.readline().strip() SECRET_KEY = f.readline().strip()
except: except:
SECRET_KEY = '12345zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzdqwkdlkj' SECRET_KEY = '12345zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzdqwkdlkj'
# Logging # Logging
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': False, 'disable_existing_loggers': False,
'formatters': { 'formatters': {
'verbose': { 'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s (logger %(name)s): %(message)s' 'format': '%(levelname)s %(asctime)s %(module)s (logger %(name)s): %(message)s'
}, },
}, },
'filters': { 'filters': {
'Http404AsInfo': { 'Http404AsInfo': {
@ -259,76 +262,76 @@ LOGGING = {
}, },
}, },
'loggers': { 'loggers': {
'django': { 'django': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['StripSensitiveFormData'], 'filters': ['StripSensitiveFormData'],
}, },
'django.security.csrf': { 'django.security.csrf': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['StripSensitiveFormData'], 'filters': ['StripSensitiveFormData'],
}, },
'django.request': { 'django.request': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['Http404AsInfo'], 'filters': ['Http404AsInfo'],
}, },
'seminar.prihlaska.form':{ 'seminar.prihlaska.form':{
'handlers': ['console','registration_logfile'], 'handlers': ['console','registration_logfile'],
'level': 'INFO' 'level': 'INFO'
}, },
'seminar.prihlaska.problem':{ 'seminar.prihlaska.problem':{
'handlers': ['console','mail_registration','registration_error_log'], 'handlers': ['console','mail_registration','registration_error_log'],
'level': 'INFO' 'level': 'INFO'
}, },
# Catch-all logger # Catch-all logger
'': { '': {
'handlers': ['console'], # Add 'mail_admins' in prod and test 'handlers': ['console'], # Add 'mail_admins' in prod and test
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['StripSensitiveFormData'], 'filters': ['StripSensitiveFormData'],
}, },
}, },
'handlers': { 'handlers': {
'console': { 'console': {
'level': 'WARNING', ## Set to 'DEBUG' in local 'level': 'WARNING', ## Set to 'DEBUG' in local
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'verbose', 'formatter': 'verbose',
}, },
'mail_admins': { 'mail_admins': {
'level': 'WARNING', 'level': 'WARNING',
'class': 'django.utils.log.AdminEmailHandler', 'class': 'django.utils.log.AdminEmailHandler',
'formatter': 'verbose', 'formatter': 'verbose',
'filters': ['StripSensitiveFormData'], 'filters': ['StripSensitiveFormData'],
}, },
'mail_registration': { 'mail_registration': {
'level': 'WARNING', 'level': 'WARNING',
'class': 'django.utils.log.AdminEmailHandler', 'class': 'django.utils.log.AdminEmailHandler',
'formatter': 'verbose', 'formatter': 'verbose',
}, },
'registration_logfile':{ 'registration_logfile':{
'level': 'INFO', 'level': 'INFO',
'class': 'logging.FileHandler', 'class': 'logging.FileHandler',
# filename declared in specific configuration files # filename declared in specific configuration files
'formatter': 'verbose', 'formatter': 'verbose',
}, },
'registration_error_log':{ 'registration_error_log':{
'level': 'INFO', 'level': 'INFO',
'class': 'logging.FileHandler', 'class': 'logging.FileHandler',
# filename declared in specific configuration files # filename declared in specific configuration files
'formatter': 'verbose', 'formatter': 'verbose',
}, },
}, },
} }
# Permissions for uploads # Permissions for uploads
FILE_UPLOAD_PERMISSIONS = 0o0644 FILE_UPLOAD_PERMISSIONS = 0o0644
@ -349,14 +352,14 @@ POSLI_MAILOVOU_NOTIFIKACI = False
# Logování chyb # Logování chyb
class InvalidTemplateVariable(str): class InvalidTemplateVariable(str):
def __mod__(self, variable): def __mod__(self, variable):
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
for line in traceback.walk_stack(None): for line in traceback.walk_stack(None):
if 'context' in line[0].f_locals and 'request' in line[0].f_locals['context']: if 'context' in line[0].f_locals and 'request' in line[0].f_locals['context']:
logger.warning("Proměnná '%s' neexistuje: %s" % (variable, line[0].f_locals['context']['request'])) logger.warning("Proměnná '%s' neexistuje: %s" % (variable, line[0].f_locals['context']['request']))
break break
return '' return ''
TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidTemplateVariable('%s') TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidTemplateVariable('%s')
# Django 3.2 vyžaduje explicitní nastavení autoklíče, zatím nechápu proč # Django 3.2 vyžaduje explicitní nastavení autoklíče, zatím nechápu proč

96
mamweb/settings_local.py

@ -11,16 +11,16 @@ import os.path
from .settings_common import * from .settings_common import *
MIDDLEWARE += ( MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
) )
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += ( INSTALLED_APPS += (
'debug_toolbar', 'debug_toolbar',
'django_extensions', 'django_extensions',
) )
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@ -37,10 +37,10 @@ ALLOWED_HOSTS.append('localhost')
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db-local.sqlite3'), 'NAME': os.path.join(BASE_DIR, 'db-local.sqlite3'),
} }
} }
#DATABASES = { #DATABASES = {
# 'default': { # 'default': {
@ -52,46 +52,46 @@ DATABASES = {
# LOGGING # LOGGING
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,
'filters': { 'filters': {
'require_debug_false': { 'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse' '()': 'django.utils.log.RequireDebugFalse'
} }
}, },
'formatters': { 'formatters': {
'simple': { 'simple': {
'format': '%(asctime)s - %(name)s - %(levelname)-8s - %(message)s', 'format': '%(asctime)s - %(name)s - %(levelname)-8s - %(message)s',
}, },
}, },
'handlers': { 'handlers': {
'dummy': { 'dummy': {
'class': 'logging.NullHandler', 'class': 'logging.NullHandler',
}, },
'console': { 'console': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'simple', 'formatter': 'simple',
}, },
}, },
'loggers': { 'loggers': {
# Vypisovani databazovych dotazu do konzole # Vypisovani databazovych dotazu do konzole
#'django.db.backends': { #'django.db.backends': {
# 'level': 'DEBUG', # 'level': 'DEBUG',
# 'handlers': ['console'], # 'handlers': ['console'],
# 'propagate': False, # 'propagate': False,
#}, #},
'werkzeug': { 'werkzeug': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': True, 'propagate': True,
}, },
'': { '': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': False, 'propagate': False,
}, },
}, },
} }
# set to 'DEBUG' for EXTRA verbose output # set to 'DEBUG' for EXTRA verbose output

20
mamweb/settings_prod.py

@ -16,8 +16,8 @@ from .settings_common import *
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += ( INSTALLED_APPS += (
'django_extensions', 'django_extensions',
) )
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
assert not SECRET_KEY.startswith('12345') assert not SECRET_KEY.startswith('12345')
@ -34,14 +34,14 @@ ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cun
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'mam_prod', 'NAME': 'mam_prod',
'USER': 'mam-web', 'USER': 'mam-web',
'TEST': { 'TEST': {
'NAME': 'mam-prod-testdb', 'NAME': 'mam-prod-testdb',
}, },
}, },
} }
import os import os

28
mamweb/settings_test.py

@ -13,16 +13,16 @@ import os.path
from .settings_common import * # zatim nutne, casem snad vyresime # noqa from .settings_common import * # zatim nutne, casem snad vyresime # noqa
MIDDLEWARE += ( MIDDLEWARE += (
'debug_toolbar.middleware.DebugToolbarMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware',
) )
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
INSTALLED_APPS += ( INSTALLED_APPS += (
'debug_toolbar', 'debug_toolbar',
'django_extensions', 'django_extensions',
) )
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = ')^u=i65*zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzv9l+zo)n' SECRET_KEY = ')^u=i65*zmr_k53a*@f4q_+ji^o@!pgpef*5&8c7zzv9l+zo)n'
@ -38,21 +38,21 @@ ALLOWED_HOSTS = ['*.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', 'mam.mff.cuni.
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'mam_test', 'NAME': 'mam_test',
'USER': 'mam-web', 'USER': 'mam-web',
'TEST': { 'TEST': {
'NAME': 'mam-test-testdb', 'NAME': 'mam-test-testdb',
}, },
}, },
} }
import os import os
SERVER_EMAIL = 'mamweb-test-errors@mam.mff.cuni.cz' SERVER_EMAIL = 'mamweb-test-errors@mam.mff.cuni.cz'
ADMINS = [ ADMINS = [
('M&M ERRORs', 'mam-errors@mam.mff.cuni.cz'), ('M&M ERRORs', 'mam-errors@mam.mff.cuni.cz'),
] ]

17
mamweb/static/css/mamweb.css

@ -425,6 +425,13 @@ textarea.feedback {
margin: 5px; margin: 5px;
} }
/* td obsahující křížek v detailu řešení se nesmí smrštit na 0 */
/* FIXME až bude firefox příčetný, nahradit td:has(.smazat_hodnoceni) */
.has_smazat_hodnoceni {
min-width: 20px;
padding: 3px;
}
/* titulni stranka */ /* titulni stranka */
@ -1152,8 +1159,7 @@ div.zadani_termin .datum {
/* posune kotvu obrázku v galerii o oranžový pruh dolu, aby se pod ním obrázek neschovával */ /* 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 */ /* https://stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjust-for-fixed-header */
.kotva_obrazku { .kotva_obrazku {
display: block; position: absolute;
position: relative;
width: 0; width: 0;
height: 55px; /* viz #title */ height: 55px; /* viz #title */
margin-top: -55px; /* viz #title */ margin-top: -55px; /* viz #title */
@ -1210,6 +1216,13 @@ div.gdpr {
} }
/* tabulka odevzdaných a došlých řešení */ /* tabulka odevzdaných a došlých řešení */
/* Roztáhne obsah z containeru na celou šířku obrazovky: */
.full_width {
width: 100vw;
margin-left: calc(-50vw + 485px);
}
.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;

2
odevzdavatko/forms.py

@ -98,7 +98,7 @@ class JednoHodnoceniForm(forms.ModelForm):
fields = ('problem', 'body', 'deadline_body', 'feedback',) fields = ('problem', 'body', 'deadline_body', 'feedback',)
widgets = { widgets = {
'problem': autocomplete.ModelSelect2( 'problem': autocomplete.ModelSelect2(
url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší? url='autocomplete_problem',
), ),
'feedback': forms.Textarea(attrs={'rows': 1, 'cols': 30, 'class': 'feedback'}), 'feedback': forms.Textarea(attrs={'rows': 1, 'cols': 30, 'class': 'feedback'}),
} }

6
odevzdavatko/templates/odevzdavatko/detail.html

@ -56,14 +56,14 @@
<td>{{ subform.body }}</td> <td>{{ subform.body }}</td>
<td>{{ subform.deadline_body }}</td> <td>{{ subform.deadline_body }}</td>
<td>{{ subform.feedback }}</td> <td>{{ subform.feedback }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> <td class="has_smazat_hodnoceni"><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove" title="Smazat hodnocení"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</tbody> </tbody>
{% endfor %} {% endfor %}
</table> </table>
<a href="#"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> <br/> <a href="#" title="Přidat hodnocení"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> <br/>
<input type=submit value="Uložit"></form> <input type=submit value="Uložit"></form>
<table id="empty_form" style="display: none;"> <table id="empty_form" style="display: none;">
@ -72,7 +72,7 @@
<td>{{ form.empty_form.body }}</td> <td>{{ form.empty_form.body }}</td>
<td>{{ form.empty_form.deadline_body }}</td> <td>{{ form.empty_form.deadline_body }}</td>
<td>{{ form.empty_form.feedback }}</td> <td>{{ form.empty_form.feedback }}</td>
<td><a href="#" class="smazat_hodnoceni" id="id_{{form.empty_form.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> <td class="has_smazat_hodnoceni"><a href="#" class="smazat_hodnoceni" id="id_{{form.empty_form.prefix}}-jsremove" title="Smazat hodnocení"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td>
</tr> </tr>
</table> </table>

2
odevzdavatko/templates/odevzdavatko/tabulka.html

@ -4,6 +4,7 @@
{% block content %} {% block content %}
<div class="full_width">
<form method=get action=.> <form method=get action=.>
{{ filtr.resitele }} {{ filtr.resitele }}
{{ filtr.problemy }} {{ filtr.problemy }}
@ -64,4 +65,5 @@ Do data (včetně): {{ filtr.reseni_do }}
location.assign(redirect); location.assign(redirect);
} }
</script> </script>
</div>
{% endblock %} {% endblock %}

1
requirements.txt

@ -21,6 +21,7 @@ django-sekizai
django-countries django-countries
django-solo django-solo
django-ckeditor django-ckeditor
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.0rc1

18
seminar/models/base.py

@ -4,18 +4,18 @@ from django.db import models
class SeminarModelBase(models.Model): class SeminarModelBase(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def verejne(self): def verejne(self):
return False return False
# def get_absolute_url(self): # def get_absolute_url(self):
# return "https://" + str(get_current_site(None)) + self.verejne_url() # return "https://" + str(get_current_site(None)) + self.verejne_url()
def admin_url(self): def admin_url(self):
model_name = self.__class__.__name__.lower() model_name = self.__class__.__name__.lower()
return reverse('admin:seminar_{}_change'.format(model_name), args=(self.id, )) return reverse('admin:seminar_{}_change'.format(model_name), args=(self.id, ))
# def verejne_url(self): # def verejne_url(self):
# return None # return None

42
seminar/models/novinky.py

@ -9,30 +9,30 @@ from . import personalni as pm
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Novinky(models.Model): class Novinky(models.Model):
class Meta: class Meta:
verbose_name = 'Novinka' verbose_name = 'Novinka'
verbose_name_plural = 'Novinky' verbose_name_plural = 'Novinky'
ordering = ['-datum'] ordering = ['-datum']
datum = models.DateField(auto_now_add=True) datum = models.DateField(auto_now_add=True)
text = models.TextField('Text novinky', blank=True, null=True) text = models.TextField('Text novinky', blank=True, null=True)
obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/', obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/',
null=True, blank=True) null=True, blank=True)
obrazek_maly = ImageSpecField(source='obrazek', obrazek_maly = ImageSpecField(source='obrazek',
processors=[ processors=[
ResizeToFit(350, 200, upscale=False) ResizeToFit(350, 200, upscale=False)
], ],
options={'quality': 95}) options={'quality': 95})
autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True, autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True,
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
zverejneno = models.BooleanField('Zveřejněno', default=False) zverejneno = models.BooleanField('Zveřejněno', default=False)
def __str__(self): def __str__(self):
if self.text: if self.text:
return '[' + str(self.datum) + '] ' + self.text[0:50] return '[' + str(self.datum) + '] ' + self.text[0:50]
else: else:
return '[' + str(self.datum) + '] ' return '[' + str(self.datum) + '] '

234
seminar/models/odevzdavatko.py

@ -18,68 +18,68 @@ from seminar.models import base as bm
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Reseni(bm.SeminarModelBase): class Reseni(bm.SeminarModelBase):
class Meta: class Meta:
db_table = 'seminar_reseni' db_table = 'seminar_reseni'
verbose_name = 'Řešení' verbose_name = 'Řešení'
verbose_name_plural = 'Řešení' verbose_name_plural = 'Řešení'
#ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to.
ordering = ['-cas_doruceni'] ordering = ['-cas_doruceni']
# Interní ID # Interní ID
id = models.AutoField(primary_key = True) id = models.AutoField(primary_key = True)
# Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby.
problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém',
through='Hodnoceni') through='Hodnoceni')
resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení', resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení',
help_text='Seznam autorů řešení', through='Reseni_Resitele') help_text='Seznam autorů řešení', through='Reseni_Resitele')
cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True)
FORMA_PAPIR = 'papir' FORMA_PAPIR = 'papir'
FORMA_EMAIL = 'email' FORMA_EMAIL = 'email'
FORMA_UPLOAD = 'upload' FORMA_UPLOAD = 'upload'
FORMA_CHOICES = [ FORMA_CHOICES = [
(FORMA_PAPIR, 'Papírové řešení'), (FORMA_PAPIR, 'Papírové řešení'),
(FORMA_EMAIL, 'Emailem'), (FORMA_EMAIL, 'Emailem'),
(FORMA_UPLOAD, 'Upload přes web'), (FORMA_UPLOAD, 'Upload přes web'),
] ]
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL) default=FORMA_EMAIL)
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set", blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.PROTECT) on_delete=models.PROTECT)
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)')
zverejneno = models.BooleanField('řešení zveřejněno', default=False, zverejneno = models.BooleanField('řešení zveřejněno', default=False,
help_text='Udává, zda je řešení zveřejněno') help_text='Udává, zda je řešení zveřejněno')
def verejne_url(self): def verejne_url(self):
return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id]))
def absolute_url(self): def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url() return "https://" + str(get_current_site(None)) + self.verejne_url()
# má OneToOneField s: # má OneToOneField s:
# Konfera # Konfera
# má ForeignKey s: # má ForeignKey s:
# Hodnoceni # Hodnoceni
def sum_body(self): def sum_body(self):
return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"]
def __str__(self): def __str__(self):
return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all()))
# NOTE: Potenciální DB HOG (bez select_related) # NOTE: Potenciální DB HOG (bez select_related)
def deadline_reseni(self): def deadline_reseni(self):
return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first() return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
## Pravdepodobne uz nebude potreba: ## Pravdepodobne uz nebude potreba:
# def save(self, *args, **kwargs): # def save(self, *args, **kwargs):
@ -89,112 +89,112 @@ class Reseni(bm.SeminarModelBase):
# super(Reseni, self).save(*args, **kwargs) # super(Reseni, self).save(*args, **kwargs)
class Hodnoceni(bm.SeminarModelBase): class Hodnoceni(bm.SeminarModelBase):
class Meta: class Meta:
db_table = 'seminar_hodnoceni' db_table = 'seminar_hodnoceni'
verbose_name = 'Hodnocení' verbose_name = 'Hodnocení'
verbose_name_plural = 'Hodnocení' verbose_name_plural = 'Hodnocení'
# Interní ID # Interní ID
id = models.AutoField(primary_key = True) id = models.AutoField(primary_key = True)
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=True, null=True) blank=True, null=True)
cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body',
related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT)
# V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body # V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body
deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body', deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body',
related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
problem = models.ForeignKey(am.Problem, verbose_name='problém', problem = models.ForeignKey(am.Problem, verbose_name='problém',
related_name='hodnoceni', on_delete=models.PROTECT) related_name='hodnoceni', on_delete=models.PROTECT)
feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)') feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)')
def __str__(self): def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body) return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def generate_filename(self, filename): def generate_filename(self, filename):
return os.path.join( return os.path.join(
settings.SEMINAR_RESENI_DIR, settings.SEMINAR_RESENI_DIR,
am.aux_generate_filename(self, filename) am.aux_generate_filename(self, filename)
) )
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class PrilohaReseni(bm.SeminarModelBase): class PrilohaReseni(bm.SeminarModelBase):
class Meta: class Meta:
db_table = 'seminar_priloha_reseni' db_table = 'seminar_priloha_reseni'
verbose_name = 'Příloha řešení' verbose_name = 'Příloha řešení'
verbose_name_plural = 'Přílohy řešení' verbose_name_plural = 'Přílohy řešení'
ordering = ['reseni', 'vytvoreno'] ordering = ['reseni', 'vytvoreno']
# Interní ID # Interní ID
id = models.AutoField(primary_key = True) id = models.AutoField(primary_key = True)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy',
on_delete=models.CASCADE) on_delete=models.CASCADE)
vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False)
soubor = models.FileField('soubor', upload_to = generate_filename) soubor = models.FileField('soubor', upload_to = generate_filename)
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 příloze řešení (plain text), např. o původu') help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu')
res_poznamka = models.TextField('poznámka řešitele', blank=True, res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
def __str__(self): def __str__(self):
return str(self.soubor) return str(self.soubor)
def split(self): def split(self):
"Vrátí cestu rozsekanou po složkách. To se hodí v templatech" "Vrátí cestu rozsekanou po složkách. To se hodí v templatech"
# Věřím, že tohle funguje, případně použít os.path nebo pathlib. # Věřím, že tohle funguje, případně použít os.path nebo pathlib.
return self.soubor.url.split('/') return self.soubor.url.split('/')
# Vazebna tabulka. Mozna se generuje automaticky. # Vazebna tabulka. Mozna se generuje automaticky.
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Reseni_Resitele(models.Model): class Reseni_Resitele(models.Model):
class Meta: class Meta:
db_table = 'seminar_reseni_resitele' db_table = 'seminar_reseni_resitele'
verbose_name = 'Řešení řešitelů' verbose_name = 'Řešení řešitelů'
verbose_name_plural = 'Řešení řešitelů' verbose_name_plural = 'Řešení řešitelů'
ordering = ['reseni', 'resitele'] ordering = ['reseni', 'resitele']
# Interní ID # Interní ID
id = models.AutoField(primary_key = True) id = models.AutoField(primary_key = True)
resitele = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) resitele = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE)
# podil - jakou merou se ktery resitel podilel na danem reseni # podil - jakou merou se ktery resitel podilel na danem reseni
# - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni
def __str__(self): def __str__(self):
return '{} od {}'.format(self.reseni, self.resitel) return '{} od {}'.format(self.reseni, self.resitel)
# NOTE: Poteciální DB HOG bez select_related # NOTE: Poteciální DB HOG bez select_related
class ReseniNode(tm.TreeNode): class ReseniNode(tm.TreeNode):
class Meta: class Meta:
db_table = 'seminar_nodes_otistene_reseni' db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)' verbose_name = 'Otištěné řešení (Node)'
verbose_name_plural = 'Otištěná řešení (Node)' verbose_name_plural = 'Otištěná řešení (Node)'
reseni = models.ForeignKey(Reseni, reseni = models.ForeignKey(Reseni,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name = 'reseni') verbose_name = 'reseni')
def aktualizuj_nazev(self): def aktualizuj_nazev(self):
self.nazev = "ReseniNode: "+str(self.reseni) self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self): def getOdkazStr(self):
return str(self.reseni) return str(self.reseni)

3
seminar/templates/seminar/archiv/cislo.html

@ -40,8 +40,7 @@
<li><a href="obalky.pdf">Obálky (PDF)</a></li> <li><a href="obalky.pdf">Obálky (PDF)</a></li>
<li><a href="tituly.tex" download>Tituly (TeX)</a></li> <li><a href="tituly.tex" download>Tituly (TeX)</a></li>
<li><a href="vysledkovka.tex" download>Výsledkovka (TeX)</a></li> <li><a href="vysledkovka.tex" download>Výsledkovka (TeX)</a></li>
<li><a href="obalkovani">Obálkování</a></li> <li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
<li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

33
seminar/templates/seminar/archiv/cislo_obalkovani.html

@ -1,33 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>
{% block nadpis1a %}
Obálkování {{ cislo }}
{% endblock %}
</h1>
Obálkovat se budou tyto problémy:
<ul>
{% for p in problemy %}
<li> {{ p.kod_v_rocniku }} {{ p }}
{% endfor %}
</ul>
{% for r in reseni %}
{% ifchanged r.resitel %}
{% if not forloop.first %}
</ul>
{% endif %}
<h4>{{ r.resitel }}</h4>
<ul>
{% endifchanged %}
<li>
{{ r.problem.kod_v_rocniku }} {{ r.problem.nazev }} ({{ r.body }})
{% endfor %}
</ul>
{% endblock content %}

30
seminar/templates/seminar/org/obalkovani.html

@ -1,30 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>
{% block nadpis1a %}
Obálkování {{ cislo }}
{% endblock %}
</h1>
<ul>
{% for reseni in object_list %}
{% ifchanged reseni.resitele %}
{% if not forloop.first %}
</ul>
{% endif %}
<h4>{% for resitel in reseni.resitele.all %}{{resitel.osoba}},{% endfor %}</h4>
<ul>
{% endifchanged %}
<li>Celkem {{reseni.hodnoceni__body__sum}} bodů z {{reseni.hodnoceni__count}} hodnocení
<ul>
{% for h in reseni.hodnoceni_set.all %}
<li> {{ h.problem }}: {{ h.body }}b </li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
{% endblock content %}

18
seminar/testutils.py

@ -474,15 +474,15 @@ def get_text():
def gen_dlouhe_tema(rnd, organizatori, rocnik, nazev, obor, kod): def gen_dlouhe_tema(rnd, organizatori, rocnik, nazev, obor, kod):
tema = Tema.objects.create( tema = Tema.objects.create(
nazev=nazev, nazev=nazev,
stav=Problem.STAV_ZADANY, stav=Problem.STAV_ZADANY,
zamereni="M", zamereni="M",
autor=rnd.choice(organizatori), autor=rnd.choice(organizatori),
garant=rnd.choice(organizatori), garant=rnd.choice(organizatori),
kod=str(kod), kod=str(kod),
tema_typ=rnd.choice(Tema.TEMA_CHOICES)[0], tema_typ=rnd.choice(Tema.TEMA_CHOICES)[0],
rocnik=rocnik, rocnik=rocnik,
abstrakt = lorem.paragraph() abstrakt = lorem.paragraph()
) )
# Generování struktury k tématu # Generování struktury k tématu

6
seminar/urls.py

@ -23,7 +23,6 @@ Soubor sloužící jako „router“, tj. zde se definují url adresy a na co uk
- ``cislo/<int:rocnik>.<str:cislo>/obalky.pdf`` (seminar_cislo_obalky) :func:`~seminar.views.views_all.cisloObalkyView` - ``cislo/<int:rocnik>.<str:cislo>/obalky.pdf`` (seminar_cislo_obalky) :func:`~seminar.views.views_all.cisloObalkyView`
- ``cislo/<int:rocnik>.<str:cislo>/tituly.tex`` (seminar_cislo_titul) :func:`~seminar.views.views_all.TitulyView` - ``cislo/<int:rocnik>.<str:cislo>/tituly.tex`` (seminar_cislo_titul) :func:`~seminar.views.views_all.TitulyView`
- ``stav`` (stav_databaze) :func:`~seminar.views.views_all.StavDatabazeView` - ``stav`` (stav_databaze) :func:`~seminar.views.views_all.StavDatabazeView`
- ``cislo/<int:rocnik>.<str:cislo>/obalkovani`` (seminar_cislo_resitel_obalkovani) :class:`~seminar.views.views_all.ObalkovaniView`
- ``cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/`` (seminar_archiv_odmeny) :class:`~seminar.views.views_all.OdmenyView` - ``cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/`` (seminar_archiv_odmeny) :class:`~seminar.views.views_all.OdmenyView`
- Další - Další
- `` `` (titulni_strana) :class:`~seminar.views.views_all.TitulniStranaView` - `` `` (titulni_strana) :class:`~seminar.views.views_all.TitulniStranaView`
@ -102,11 +101,6 @@ urlpatterns = [
org_required(views.StavDatabazeView), org_required(views.StavDatabazeView),
name='stav_databaze' name='stav_databaze'
), ),
path(
'cislo/<int:rocnik>.<str:cislo>/obalkovani',
org_required(views.ObalkovaniView.as_view()),
name='seminar_cislo_resitel_obalkovani'
),
path( path(
'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/', 'cislo/<int:trocnik>.<str:tcislo>/odmeny/<int:frocnik>.<str:fcislo>/',
org_required(views.OdmenyView.as_view()), org_required(views.OdmenyView.as_view()),

41
seminar/views/views_all.py

@ -53,23 +53,6 @@ logger = logging.getLogger(__name__)
def get_problemy_k_tematu(tema): def get_problemy_k_tematu(tema):
return Problem.objects.filter(nadproblem = tema) return Problem.objects.filter(nadproblem = tema)
class ObalkovaniView(generic.ListView):
template_name = 'seminar/org/obalkovani.html'
def get_queryset(self):
rocnik = get_object_or_404(Rocnik,rocnik=self.kwargs['rocnik'])
cislo = get_object_or_404(Cislo,rocnik=rocnik,poradi=self.kwargs['cislo'])
self.cislo = cislo
self.hodnoceni = s.Hodnoceni.objects.filter(cislo_body=cislo)
self.reseni = Reseni.objects.filter(hodnoceni__in = self.hodnoceni).annotate(Sum('hodnoceni__body')).annotate(Count('hodnoceni')).order_by('resitele__osoba')
return self.reseni
def get_context_data(self, **kwargs):
context = super(ObalkovaniView, self).get_context_data(**kwargs)
print(self.cislo)
context['cislo'] = self.cislo
return context
# FIXME: Pozor, níž je ještě jeden ProblemView! # FIXME: Pozor, níž je ještě jeden ProblemView!
#class ProblemView(generic.DetailView): #class ProblemView(generic.DetailView):
@ -590,28 +573,6 @@ def obalkyView(request, resitele):
return response return response
def oldObalkovaniView(request, rocnik, cislo):
rocnik = Rocnik.objects.get(rocnik=rocnik)
cislo = Cislo.objects.get(rocnik=rocnik, cislo=cislo)
reseni = (
Reseni.objects.filter(cislo_body=cislo)
.order_by(
'resitel__prijmeni',
'resitel__jmeno',
'problem__typ',
'problem__kod'
)
)
problemy = sorted(set(r.problem for r in reseni), key=lambda p: (p.typ, p.kod))
return render(
request,
'seminar/archiv/cislo_obalkovani.html',
{'cislo': cislo, 'problemy': problemy, 'reseni': reseni}
)
### Tituly ### Tituly
def TitulyViewRocnik(request, rocnik): def TitulyViewRocnik(request, rocnik):
return TitulyView(request, rocnik, None) return TitulyView(request, rocnik, None)
@ -725,7 +686,7 @@ def formularOKView(request, text=''):
] ]
context = { context = {
'odkazy': odkazy, 'odkazy': odkazy,
'text': text, 'text': text,
} }
return render(request, template_name, context) return render(request, template_name, context)

50
soustredeni/admin.py

@ -6,38 +6,38 @@ from seminar.models import soustredeni as m
class SoustredeniUcastniciInline(admin.TabularInline): class SoustredeniUcastniciInline(admin.TabularInline):
model = m.Soustredeni_Ucastnici model = m.Soustredeni_Ucastnici
extra = 1 extra = 1
fields = ['resitel','poznamka'] fields = ['resitel','poznamka']
autocomplete_fields = ['resitel'] autocomplete_fields = ['resitel']
ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni'] ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni']
formfield_overrides = { formfield_overrides = {
models.TextField: {'widget': widgets.TextInput} models.TextField: {'widget': widgets.TextInput}
} }
def get_queryset(self,request): def get_queryset(self,request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.select_related('resitel','soustredeni') return qs.select_related('resitel','soustredeni')
class SoustredeniOrganizatoriInline(admin.TabularInline): class SoustredeniOrganizatoriInline(admin.TabularInline):
model = m.Soustredeni.organizatori.through model = m.Soustredeni.organizatori.through
extra = 1 extra = 1
fields = ['organizator','poznamka'] fields = ['organizator','poznamka']
autocomplete_fields = ['organizator'] autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni'] ordering = ['organizator__osoba__jmeno','organizator__prijmeni']
formfield_overrides = { formfield_overrides = {
models.TextField: {'widget': widgets.TextInput} models.TextField: {'widget': widgets.TextInput}
} }
def get_queryset(self,request): def get_queryset(self,request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.select_related('organizator', 'soustredeni') return qs.select_related('organizator', 'soustredeni')
@admin.register(m.Soustredeni) @admin.register(m.Soustredeni)
class SoustredeniAdmin(admin.ModelAdmin): class SoustredeniAdmin(admin.ModelAdmin):
model = m.Soustredeni model = m.Soustredeni
inline_type = 'tabular' inline_type = 'tabular'
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline] inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]

90
treenode/admin.py

@ -9,80 +9,80 @@ import seminar.models as m
@admin.register(m.TreeNode) @admin.register(m.TreeNode)
class TreeNodeAdmin(PolymorphicParentModelAdmin): class TreeNodeAdmin(PolymorphicParentModelAdmin):
base_model = m.TreeNode base_model = m.TreeNode
child_models = [ child_models = [
m.RocnikNode, m.RocnikNode,
m.CisloNode, m.CisloNode,
m.MezicisloNode, m.MezicisloNode,
m.TemaVCisleNode, m.TemaVCisleNode,
m.UlohaZadaniNode, m.UlohaZadaniNode,
m.PohadkaNode, m.PohadkaNode,
m.UlohaVzorakNode, m.UlohaVzorakNode,
m.TextNode, m.TextNode,
m.CastNode, m.CastNode,
m.OrgTextNode, m.OrgTextNode,
] ]
actions = ['aktualizuj_nazvy'] actions = ['aktualizuj_nazvy']
# XXX: nejspíš je to totální DB HOG, nechcete to použít moc často. # XXX: nejspíš je to totální DB HOG, nechcete to použít moc často.
def aktualizuj_nazvy(self, request, queryset): def aktualizuj_nazvy(self, request, queryset):
newqs = queryset.get_real_instances() newqs = queryset.get_real_instances()
for tn in newqs: for tn in newqs:
tn.aktualizuj_nazev() tn.aktualizuj_nazev()
tn.save() tn.save()
self.message_user(request, "Názvy aktualizovány.") self.message_user(request, "Názvy aktualizovány.")
aktualizuj_nazvy.short_description = "Aktualizuj vybraným TreeNodům názvy" aktualizuj_nazvy.short_description = "Aktualizuj vybraným TreeNodům názvy"
@admin.register(m.RocnikNode) @admin.register(m.RocnikNode)
class RocnikNodeAdmin(PolymorphicChildModelAdmin): class RocnikNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.RocnikNode base_model = m.RocnikNode
show_in_index = True show_in_index = True
@admin.register(m.CisloNode) @admin.register(m.CisloNode)
class CisloNodeAdmin(PolymorphicChildModelAdmin): class CisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CisloNode base_model = m.CisloNode
show_in_index = True show_in_index = True
@admin.register(m.MezicisloNode) @admin.register(m.MezicisloNode)
class MezicisloNodeAdmin(PolymorphicChildModelAdmin): class MezicisloNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.MezicisloNode base_model = m.MezicisloNode
show_in_index = True show_in_index = True
@admin.register(m.TemaVCisleNode) @admin.register(m.TemaVCisleNode)
class TemaVCisleNodeAdmin(PolymorphicChildModelAdmin): class TemaVCisleNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TemaVCisleNode base_model = m.TemaVCisleNode
show_in_index = True show_in_index = True
@admin.register(m.UlohaZadaniNode) @admin.register(m.UlohaZadaniNode)
class UlohaZadaniNodeAdmin(PolymorphicChildModelAdmin): class UlohaZadaniNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaZadaniNode base_model = m.UlohaZadaniNode
show_in_index = True show_in_index = True
@admin.register(m.PohadkaNode) @admin.register(m.PohadkaNode)
class PohadkaNodeAdmin(PolymorphicChildModelAdmin): class PohadkaNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.PohadkaNode base_model = m.PohadkaNode
show_in_index = True show_in_index = True
@admin.register(m.UlohaVzorakNode) @admin.register(m.UlohaVzorakNode)
class UlohaVzorakNodeAdmin(PolymorphicChildModelAdmin): class UlohaVzorakNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.UlohaVzorakNode base_model = m.UlohaVzorakNode
show_in_index = True show_in_index = True
@admin.register(m.TextNode) @admin.register(m.TextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin): class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.TextNode base_model = m.TextNode
show_in_index = True show_in_index = True
@admin.register(m.CastNode) @admin.register(m.CastNode)
class TextNodeAdmin(PolymorphicChildModelAdmin): class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.CastNode base_model = m.CastNode
show_in_index = True show_in_index = True
fields = ('nadpis',) fields = ('nadpis',)
@admin.register(m.OrgTextNode) @admin.register(m.OrgTextNode)
class TextNodeAdmin(PolymorphicChildModelAdmin): class TextNodeAdmin(PolymorphicChildModelAdmin):
base_model = m.OrgTextNode base_model = m.OrgTextNode
show_in_index = True show_in_index = True

4
treenode/permissions.py

@ -2,6 +2,6 @@ from rest_framework.permissions import BasePermission
class AllowWrite(BasePermission): class AllowWrite(BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
return request.user.has_perm('auth.org') return request.user.has_perm('auth.org')

24
various/autentizace/utils.py

@ -6,16 +6,16 @@ from django.utils.http import urlsafe_base64_encode
def posli_reset_hesla(u, request=None): def posli_reset_hesla(u, request=None):
uid = urlsafe_base64_encode(force_bytes(u.pk)) uid = urlsafe_base64_encode(force_bytes(u.pk))
token = PasswordResetTokenGenerator().make_token(u) token = PasswordResetTokenGenerator().make_token(u)
url = "https://%s%s" % ( url = "https://%s%s" % (
str(get_current_site(request)), str(get_current_site(request)),
str(reverse_lazy("reset_password_confirm", args=[uid, token])) str(reverse_lazy("reset_password_confirm", args=[uid, token]))
) )
u.email_user( u.email_user(
subject="Vítej mezi řešiteli M&M!", subject="Vítej mezi řešiteli M&M!",
message="""Milý řešiteli, milá řešitelko, message="""Milý řešiteli, milá řešitelko,
tvůj e-mail byl právě zaregistrován na mam.matfyz.cz. Heslo si prosím nastav na: %s tvůj e-mail byl právě zaregistrován na mam.matfyz.cz. Heslo si prosím nastav na: %s
@ -26,6 +26,6 @@ Organizátoři M&M
Tento e-mail byl vygenerován automaticky, chceš-li nás kontaktovat, napiš nám na adresu mam@matfyz.cz. Tento e-mail byl vygenerován automaticky, chceš-li nás kontaktovat, napiš nám na adresu mam@matfyz.cz.
""" % url, """ % url,
# TODO: templates/autentizace a django/contrib/auth/forms.py říkají, jak na to lépe # TODO: templates/autentizace a django/contrib/auth/forms.py říkají, jak na to lépe
from_email="registrace@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení? from_email="registrace@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení?
) )

8
various/log_filters.py

@ -2,10 +2,10 @@ from logging import Filter, INFO
from django.urls import reverse from django.urls import reverse
class Http404AsInfoFilter(Filter): class Http404AsInfoFilter(Filter):
def filter(self, record): def filter(self, record):
if record.name == 'django.request' and record.status_code == 404: if record.name == 'django.request' and record.status_code == 404:
record.levelno = INFO record.levelno = INFO
return 1 # Keep the log record return 1 # Keep the log record
class StripSensitiveFormDataFilter(Filter): class StripSensitiveFormDataFilter(Filter):
def filter(self, record): def filter(self, record):

32
vysledkovky/templates/vysledkovky/vysledkovka_cisla.html

@ -1,49 +1,49 @@
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">
<table class='vysledkovka'> <table class='vysledkovka'>
<tr class='border-b'> <tr class='border-b'>
<th class='border-r'># <th class='border-r'>#</th>
<th class='border-r'>Jméno <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 class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </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 class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}
{% endfor %} {% endfor %}
{% if vysledkovka.je_nejake_ostatni %}<th class='border-r' id='problem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }}'>Ostatní {% endif %} {% if vysledkovka.je_nejake_ostatni %}<th class='border-r' id='problem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }}'>Ostatní</th>{% endif %}
{# 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 class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}
<th class='border-r'>Za číslo <th class='border-r'>Za číslo</th>
<th class='border-r'>Za ročník <th class='border-r'>Za ročník</th>
<th class='border-r'>Odjakživa <th class='border-r'>Odjakživa</th>
{% for rv in vysledkovka.radky_vysledkovky %} {% for rv in vysledkovka.radky_vysledkovky %}
<tr> <tr>
<td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %} <td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %}</td>
<th class='border-r'> <th class='border-r'>
{% if rv.titul %} {% if rv.titul %}
{{ rv.titul }}<sup>MM</sup> {{ rv.titul }}<sup>MM</sup>
{% endif %} {% endif %}
{{ rv.resitel.osoba.plne_jmeno }} {{ rv.resitel.osoba.plne_jmeno }}</th>
{% for b in rv.body_za_temata_seznam %} {% for b in rv.body_za_temata_seznam %}
<td class='border-r'>{{ b }} <td class='border-r'>{{ b }}</td>
{% for body_podproblemu in rv.body_podproblemy_iter.next %} {% for body_podproblemu in rv.body_podproblemy_iter.next %}
<td class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{{ body_podproblemu }} <td class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{{ body_podproblemu }}</td>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
<td class='border-r'>{{ rv.body_cislo }} <td class='border-r'>{{ rv.body_cislo }}</td>
<td class='border-r'><b>{{ rv.body_rocnik }}</b> <td class='border-r'><b>{{ rv.body_rocnik }}</b></td>
<td class='border-r'>{{ rv.body_celkem_odjakziva }} <td class='border-r'>{{ rv.body_celkem_odjakziva }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

24
vysledkovky/templates/vysledkovky/vysledkovka_rocnik.html

@ -1,29 +1,29 @@
<table class='vysledkovka'> <table class='vysledkovka'>
<tr class='border-b'> <tr class='border-b'>
<th class='border-r'># <th class='border-r'>#</th>
<th class='border-r'>Jméno <th class='border-r'>Jméno</th>
<th class='border-r'>R. <th class='border-r'>R.</th>
<th class='border-r'>Odjakživa <th class='border-r'>Odjakživa</th>
{% for c in vysledkovka.cisla_rocniku %} {% for c in vysledkovka.cisla_rocniku %}
<th class='border-r'><a href="{{ c.verejne_url }}"> <th class='border-r'><a href="{{ c.verejne_url }}">
{{c.rocnik.rocnik}}.{{ c.poradi }}</a> {{c.rocnik.rocnik}}.{{ c.poradi }}</a></th>
{% endfor %} {% endfor %}
<th class='border-r'>Celkem <th class='border-r'>Celkem</th>
{% for rv in vysledkovka.radky_vysledkovky %} {% for rv in vysledkovka.radky_vysledkovky %}
<tr> <tr>
<td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %} <td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %}</td>
<th class='border-r'> <th class='border-r'>
{% if rv.titul %} {% if rv.titul %}
{{ rv.titul }}<sup>MM</sup> {{ rv.titul }}<sup>MM</sup>
{% endif %} {% endif %}
{{ rv.resitel.osoba.plne_jmeno }} {{ rv.resitel.osoba.plne_jmeno }}</th>
<td class='border-r'>{{ rv.rocnik_resitele }} <td class='border-r'>{{ rv.rocnik_resitele }}</td>
<td class='border-r'>{{ rv.body_celkem_odjakziva }} <td class='border-r'>{{ rv.body_celkem_odjakziva }}</td>
{% for b in rv.body_cisla_seznam %} {% for b in rv.body_cisla_seznam %}
<td class='border-r'>{{ b }} <td class='border-r'>{{ b }}</td>
{% endfor %} {% endfor %}
<td class='border-r'><b>{{ rv.body_rocnik }}</b> <td class='border-r'><b>{{ rv.body_rocnik }}</b></td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

Loading…
Cancel
Save