Browse Source

Merge remote-tracking branch 'gitea/master' into zadavatko_problemu

zadavatko_problemu
Pavel "LEdoian" Turinsky 2 years ago
parent
commit
e6f5bb4975
  1. 159
      Makefile
  2. 135
      README.md
  3. 11
      api/tests/test_skola_autocomplete.py
  4. 1
      api/urls.py
  5. 25
      api/views/autocomplete.py
  6. 10
      data/flat.json
  7. 1
      docs/april.rst
  8. 14
      docs/conf.py
  9. 19
      docs/index.rst
  10. 108
      docs/skripty.rst
  11. 4
      docs/sphinx.rst
  12. 31
      docs/struktura.rst
  13. 25
      docs/tabulka_prerekvizit.rst
  14. 200
      docs/vyvoj.rst
  15. 5
      docs/zapisy/zapisy.rst
  16. 84
      header_fotky/context_processors.py
  17. 18
      korektury/migrations/0019_auto_20221205_2014.py
  18. 2
      korektury/models.py
  19. 2
      korektury/templates/korektury/opraf.html
  20. 34
      korektury/views.py
  21. 8
      make/README.md
  22. 31
      make/deploy
  23. 25
      make/deploy_prod
  24. 10
      make/init_local
  25. 1
      make/install
  26. 16
      make/install_web
  27. 115
      make/lib.sh
  28. 14
      make/push_compiled_vue_to_test
  29. 8
      make/run
  30. 9
      make/schema
  31. 20
      make/sync_prod_flatpages
  32. 11
      make/sync_test
  33. 18
      make/sync_test_db_aggressive
  34. 9
      make/sync_test_media
  35. 9
      make/test
  36. 32
      mamweb/admin.py
  37. 152
      mamweb/middleware.py
  38. 386
      mamweb/settings_common.py
  39. 99
      mamweb/settings_local.py
  40. 20
      mamweb/settings_prod.py
  41. 28
      mamweb/settings_test.py
  42. 24
      mamweb/static/css/mamweb.css
  43. 16
      odevzdavatko/forms.py
  44. 22
      odevzdavatko/static/odevzdavatko/check_for_detail.js
  45. 56
      odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js
  46. 120
      odevzdavatko/templates/odevzdavatko/detail.html
  47. 51
      odevzdavatko/templates/odevzdavatko/detail_resitele.html
  48. 2
      odevzdavatko/templates/odevzdavatko/nahraj_reseni.html
  49. 2
      odevzdavatko/templates/odevzdavatko/tabulka.html
  50. 4
      odevzdavatko/urls.py
  51. 80
      odevzdavatko/views.py
  52. 2
      personalni/admin.py
  53. 19
      personalni/forms.py
  54. 1
      personalni/templates/personalni/udaje/edit.html
  55. 2
      personalni/templates/personalni/udaje/gdpr.html
  56. 1
      personalni/templates/personalni/udaje/prihlaska.html
  57. 2
      personalni/views.py
  58. 18
      seminar/migrations/0110_resitel_prezdivka.py
  59. 18
      seminar/migrations/0111_nikam2nezasilat_papirove.py
  60. 18
      seminar/models/base.py
  61. 42
      seminar/models/novinky.py
  62. 234
      seminar/models/odevzdavatko.py
  63. 4
      seminar/models/personalni.py
  64. 32
      seminar/models/tvorba.py
  65. 2
      seminar/templatetags/deadliny.py
  66. 18
      seminar/testutils.py
  67. 20
      seminar/views/views_all.py
  68. 50
      soustredeni/admin.py
  69. 90
      treenode/admin.py
  70. 4
      treenode/permissions.py
  71. 24
      various/autentizace/utils.py
  72. 8
      various/log_filters.py
  73. 0
      various/templatetags/__init__.py
  74. 48
      various/templatetags/mail.py
  75. 59
      various/tests.py
  76. 4
      vysledkovky/utils.py

159
Makefile

@ -1,154 +1,9 @@
PYTHON ?= python3
VENV ?= ${PYTHON} -m venv
# Všechny flagy, které se s venvem/virtualenvem/... mají volat patří sem. Volá se "${VENV} cesta"
VENV_PATH ?= env
# Musí být definovaná, i kdyby to měla být "."
# Existence tohohle Makefile je tu jen proto, aby fungovala svalová paměť. Pokud můžete, použijte rovnou `make/…`
%:
# 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:
make/$*
.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)
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'
.PHONY: default

135
README.md

@ -1,77 +1,58 @@
Basic commands for web development
==================================
After you clone this repository, run `make`. It will download, locally install
and setup virtualenv and pip, and then locally install all required packages
from `requirements.txt`.
When working with the code, always use the binaries in `./bin/`, such as
`bin/pip`, `bin/python`, ... never the global python, pip, ...
Use `make` and `./manage.py` for most things.
Use git :-)
Quickstart
----------
Run the following commands:
make install_venv
. env/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
After finishing development, run "deactivate".
Make commands
-------------
* `make install` (or `make`) - locally install and setup virtualpy, install
required packages. Ran again installs missing packages. Run after changing
`requirements.txt`.
* `make clean` - remove local python packages.
* `make veryclean` - remove local packages and virtualpy enviroment and
binaries.
* `make run` - runs "./manage.py runserver_plus"
* `make push_test` - pushes the last commited version to test location.
Only git-commited changes are pushed! Rest is re-generated from scratch.
At test server, the media data and database are kept the same.
Everything else not in .gitignore is deleted/overwritten on the test server.
* `make schema` - generates graph of seminar and all schemas as PDF. Supercool!
* `make sync_prod_flatpages` - downloads and applies static/flat pages from mamweb-prod
./manage.py commands
--------------------
* `./manage.py migrate` - update the database schema, initialise the database.
You need to run this in the beginning.
* `./manage.py runserver_plus` - run a debugging server for the web. Slightly
enhanced compared to `./manage.py runserver`.
Open [127.0.0.1:8000](127.0.0.1:8000).
* `./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`.
Web M&M
======
Tohle je repozitář s kódem M&Mího webu. Pokud zde hledáte web samotný nebo
informace o semináři, najdete je na <https://mam.matfyz.cz> (a upřímně nechápu,
jak jste se dostali k tomuhle textu :-D)
Pokud jste tu zůstali, tak vás beztak zajímá vývoj webu (a jestli ne, tak
budeme rádi, když začne :-)).
Co je M&Mweb uvnitř
------
Celý náš web je napsaný v [Pythonu](https://www.python.org) ve frameworku
[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
náš kód je uložený v [Gitu](https://git-scm.com/) na [téhle
gitee](https://gitea.ks.matfyz.cz/). Pro dokumentaci používáme
[Sphinx](https://www.sphinx-doc.org).
<!--TODO: Z odstavce výše by ideálně měla být zachována jen první věta a zbytek
by měl být někde v docs s podrobnějším popisem…-->
Jak si web pořídit
------
Prosím přečti si podrobnější návod v <docs/vyvoj.rst> (tady by bylo zbytečné
ho duplikovat).
Jak web vyvíjet
----
<!--TODO: Napsat obšírněji, asi zase do docs/-->
Na webu je mnoho věcí k dělání, některé ani nevyžadují kódění (třeba uhánění
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ě
obecný návod, tak tady je zhruba návod na úpravy kódu a pokud se něco z toho
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 :-))
1. Nejprve si stáhni repozitář a rozběhni si lokální web u sebe (viz <docs/vyvoj.rst>).
1. Najdi si problém v Kanboardu (klikni na „Issues“ na Gitee) a/nebo se domluv
s webaři, na čem bys tak mohl/a pracovat.
1. Najdi místo, kde se to dá opravit a zkus to tam opravit. Uznávám, že tenhle
bod je otravně obecný, pokud tápeš, zkus se zeptat zkušenějších webařů nebo
podívat do dokumentace.
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)
1. Po dohodě s webaři to vyzkoušej na testwebu
1. Pošli pull-request a případně zkus reagovat na komentáře
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 :-)
### Proč pull-requesty?
<!--FIXME: Tohle ale už úplně patří do docs a ne sem, jen je zatím nemám prozkoumané…-->
Účelů pull-requestů je několik. Jednak doufáme, že pomůže webařům se orientovat
v kódu, jednak tím umožňujeme dělat experimenty a dávat si zpětnou vazbu. V
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 :-))

11
api/tests/test_skola_autocomplete.py

@ -11,19 +11,20 @@ class OrgSkolyAutocompleteTestCase(TestCase):
sync_skoly('https://mam.mff.cuni.cz/')
# Správné školy podle toho, co orgové poslali: (prefix, ID školy)
# NOTE: Pozor, jedná se o databázové indexy. Pokud se to někdy rozbije, bude potřeba je přepsat nebo předělat na IZO
# TODO: Opravit zakomentované školy.
cls.spravna_data = [
('gymnázium kolín', 53),
('kolín', 53),
('gasoš', 96),
#('gasoš', 96),
('Rokycany', 96),
('gasoš Rokycany', 96),
('SPŠE Pardubice', 815),
#('gasoš Rokycany', 96),
#('SPŠE Pardubice', 815),
('Jaroše', 164),
("Gymnázium, Brno, tř. Kpt. Jaroše", 164),
#("Gymnázium, Brno, tř. Kpt. Jaroše", 164),
("Jírovcova", 157),
('České Budějovice', 157),
("Gymnázium, České Budějovice, Jírovcova 8", 157),
("první soukromé", 2),
#("první soukromé", 2),
("Gymnázium Elgartova", 147),
("Jihlava", 45),
('Milevsko', 223),

1
api/urls.py

@ -22,6 +22,7 @@ urlpatterns = [
# Autocomplete
path('api/autocomplete/skola/', views.SkolaAutocomplete.as_view(), name='autocomplete_skola'),
path('api/autocomplete/resitel/', org_required(views.ResitelAutocomplete.as_view()), name='autocomplete_resitel'),
path('api/autocomplete/resitel_public/', views.PublicResitelAutocomplete.as_view(), name='autocomplete_resitel_public'),
path('api/autocomplete/problem/odevzdatelny', views.OdevzdatelnyProblemAutocomplete.as_view(), name='autocomplete_problem_odevzdatelny'),
path('api/autocomplete/problem/vsechny', views.ProblemAutocomplete.as_view(), name='autocomplete_problem'),

25
api/views/autocomplete.py

@ -12,7 +12,7 @@ from .helpers import LoginRequiredAjaxMixin
class SkolaAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání škol hlavně při registraci. """
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
# Don't forget to filter out results depending on the visitor !
qs = m.Skola.objects.all()
if self.q:
words = self.q.split(' ') #TODO re split podle bileho znaku
@ -44,6 +44,29 @@ class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetVie
qs = qs.filter(query)
return qs
class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2QuerySetView):
"""
View k :mod:`dal.autocomplete` pro vyhledávání řešitelů podle přezdívky
především v odevzdávátku.
"""
def get_queryset(self):
letos = m.Nastaveni.get_solo().aktualni_rocnik
qs = m.Resitel.objects.filter(
rok_maturity__gte=letos.druhy_rok()
).filter(
prezdivka_resitele__isnull=False
).exclude(
prezdivka_resitele=""
).filter(
prezdivka_resitele__icontains=self.q
).all()
return qs
def get_result_label(self, result):
return result.prezdivka_resitele
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self):

10
data/flat.json

File diff suppressed because one or more lines are too long

1
docs/april.rst

@ -2,3 +2,4 @@ Aprílové nápad
==============
* aprílový easter-egg pro řešitele - vytvořit nějakou vtipnou testovací databázi a nasadit ji místo produkce
* změnit veškerý text na oranžovo

14
docs/conf.py

@ -85,3 +85,17 @@ source_suffix = {
'.rst': 'restructuredtext',
'.md': 'markdown',
}
# Autodoc má ignorovat některé moduly
# Ref: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#event-autodoc-skip-member
# Kudos: https://stackoverflow.com/a/21449475/
def ignorovat(app, what, name, obj, skip, options):
blacklist = (
# typ (what), name
('module', 'settings.mamweb_prod'),
)
ignore = (what, name) in blacklist
return True if ignore else None
def setup(app):
app.connect('autodoc-skip-member', ignorovat)

19
docs/index.rst

@ -6,14 +6,31 @@
Vítejte v dokumentaci M&Mího webu!
===================================
Tzv. produkce (tedy to, co vidí uživatelé) běží na `<mam.mff.cuni.cz>`_ (resp.
`<mam.matfyz.cz>`_), menu, obrázky v pozadí menu a spousta stránek (ty pouze se
statickým textem/obrázky) se mění přímo na produkci. Testovací verze běží na
`<https://mam-test.ks.matfyz.cz/>`_.
Abychom uměli web vyvíjet, musíme ho většinou nejdřív umět
:doc:`naklonovat a spustit lokálně <vyvoj>`.
:doc:`struktura mamwebu <struktura>` se řídí hlavně djangem, ale snažíme se
také o oddělení jednotlivých částí do :doc:`samostatných aplikací
<modules/modules>`.
Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve
:doc:`sphinxu <sphinx>`.
.. toctree::
:caption: M&M web
:maxdepth: 2
:titlesonly:
vyvoj
sphinx
dalsi_soubory
skripty
modules/modules
dalsi_soubory
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``.

4
docs/sphinx.rst

@ -8,12 +8,14 @@ Jinak všechny rst, co jsou ve složce ``doc`` a jejích podsložkách nezačín
Sphinx se píše v rst: `Návod na syntaxi rst`_ `Cheat sheet`_
To je snad vše, co je potřeba vědět k dokumentaci mamwebu. Následující sekce jsou o tom, co jsem provedl Sphinxu, aby to fungovalo:
.. _Návod na syntaxi rst: https://sphinx-tutorial.readthedocs.io/step-1/#sections
.. _Cheat sheet: https://sphinx-tutorial.readthedocs.io/cheatsheet/
make html
---------
Make html dělá následující: Vygenerují se rst soubory do modules z pythoní dokumentace pomocí::
``make html`` dělá následující: Vygenerují se rst soubory do modules z pythoní dokumentace pomocí::
sphinx-apidoc --module-first -o modules .. ../*/migrations --templatedir _templates -f

31
docs/struktura.rst

@ -0,0 +1,31 @@
Co kde najít (mamweb + django)
==============================
Nejdůležitější aplikace z pohledu djanga je ``mamweb``. Tu totiž django pouští
a obsahuje tedy nastavení (tam se přidávají ostatní aplikace, včetně těch
importovaných z djanga, a nastavují se tam různé věci jak v djangu, tak i naše,
například složky, kam se budou věci přidané uživateli ukládat). Dále obsahuje
základní urls, udávající, „na jaké adrese co je“. A nakonec obsahuje obecné
věci jako chybové hlášky a vzhled M&M stránek (menu, patička, atd.). Aktuálně
i veškeré csv.
Další jsou pak jednotlivé aplikace (pokud něco hledáte, tak zřejmě chcete najít
tu aplikaci, která tomu odpovídá, respektive se k ní dostat přes url), za
zmínku stojí seminar, kde jsou takové ty věci, co zbyly. Plus jsou tam aktuálně
téměř všechny modely, protože je těžké je přesunout jinam.
**TLDR: Nevšímejte si složky data/ a souborů přímo v kořenové složce.**
Kromě věcí potřebných ke gitu, :doc:`ke spuštění <vyvoj>` a fukci djanga,
dalších drobností, lokální databáze a již zmíněných aplikací jsou tu ``data``,
kde je takový ten obsah webu, co by se měl dát snadno měnit (tudíž musí být v
databázi), tj. statické stránky, menu a obrázky v pozadí menu. Ten je třeba
měnit hlavně na produkci a sekundárně tady (může to dělat i newebař a nechcete
přepsat jeho práci). Vše, co nejsou aplikace je popsáno :doc:`tady <dalsi_soubory>`.
Základy djanga
--------------
mamweb je psaný téměř čistě v djangu. Což znamená, že to „co je vidět na stránkách“
jsou views.

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
=====================
Stačí spustit::
## Nahradte svym gimli username
git clone USER@gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb.git mamweb
cd mamweb
## Instalace je trochu magická, spusť následující posloupnost příkazů:
make install_venv
. env/bin/activate
make install_web
## Vygeneruje nejaka testovaci data (spis chuda)
./manage.py testdata
## Nahraje statické stránky, menu a obrázky v pozadí menu
./manage.py loaddata data/*
## Spusti testovaci server na http://127.0.0.1:8000/
./manage.py runserver
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.
Asi hlavní část vývoje většiny webu probíhá lokálně. Každý tak může pracovat na
vlastních úpravách nezávisle na ostatních.
Potřebné vybavení
-------
Tento soupis cílí na Linuxáky. Jistě je
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
narazit na odlišné chování od Linuxu.)
Motivace cílení na Linux je to, že Gimli je Linuxový stroj, takže je vývojové
prostředí pak podobné produkci a zmenšuje to množství odlišného chování.
.. 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 :-)
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, …)

5
docs/zapisy/zapisy.rst

@ -2,4 +2,7 @@ Zápisy
======
.. toctree::
2021-12-06-testovani_dokumentace_codereview
:caption: Importy zápisů z Markdownu
:maxdepth: 1
2021-12-06-testovani_dokumentace_codereview

84
header_fotky/context_processors.py

@ -12,46 +12,46 @@ from header_fotky.models import FotkaUrlVazba
def vzhled(request):
"""
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.
url adresu nejprve vyzkouší celou, pak postupně odřezává věci za
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é
libovolnou. (Z více možných fotek pro 1 url a 1 dobu vybírá náhodně.)
"""
hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 20):
noc = True
nedoba = 'den'
doba = 'noc'
else:
noc = False
nedoba = 'noc'
doba = 'den'
url = request.path
fotky = FotkaUrlVazba.objects.exclude(denni_doba=nedoba)
fotka = None
# TODO rychlejší patternmatch?
while (fotka is None) and (url != ''):
presne = fotky.filter(url__exact=url)
if presne.count() > 0:
presne_doba = presne.filter(denni_doba=doba)
if presne_doba.count() > 0:
fotka = random.choice(presne_doba).url_fotky()
else:
fotka = random.choice(presne).url_fotky()
url = url[:-1]
index = url.rfind('/')
if index != -1:
url = url[:index+1]
if fotka is None:
fotka = settings.STATIC_URL + "images/header/vikendovka.jpg"
return {'noc': noc, 'fotka': fotka}
"""
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.
url adresu nejprve vyzkouší celou, pak postupně odřezává věci za
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é
libovolnou. (Z více možných fotek pro 1 url a 1 dobu vybírá náhodně.)
"""
hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 20):
noc = True
nedoba = 'den'
doba = 'noc'
else:
noc = False
nedoba = 'noc'
doba = 'den'
url = request.path
fotky = FotkaUrlVazba.objects.exclude(denni_doba=nedoba)
fotka = None
# TODO rychlejší patternmatch?
while (fotka is None) and (url != ''):
presne = fotky.filter(url__exact=url)
if presne.count() > 0:
presne_doba = presne.filter(denni_doba=doba)
if presne_doba.count() > 0:
fotka = random.choice(presne_doba).url_fotky()
else:
fotka = random.choice(presne).url_fotky()
url = url[:-1]
index = url.rfind('/')
if index != -1:
url = url[:index+1]
if fotka is None:
fotka = settings.STATIC_URL + "images/header/vikendovka.jpg"
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')
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)')

2
korektury/templates/korektury/opraf.html

@ -146,7 +146,7 @@
</button>
{% endif %}
{% if o.status != 'neni_chyba' %}
<button type='submit' name='action' value='wontfix' title='Označ jako irelevantní '>
<button type='submit' name='action' value='wontfix' title='Označ, že se nebude měnit'>
<img src="{% static "korektury/imgs/cross.png" %}"/>
</button>
{% endif %}

34
korektury/views.py

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

115
make/lib.sh

@ -0,0 +1,115 @@
#!/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}"
UPSTREAM_REMOTE='origin'
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
local UPSTREAM_BRANCH
if git rev-parse "$BRANCH@{u}" >/dev/null 2>/dev/null
then
UPSTREAM_BRANCH="$BRANCH@{u}" # Stačí symbolicky.
else
# Tohle je jediná možná záchrana.
UPSTREAM_BRANCH="$UPSTREAM_REMOTE/$BRANCH"
fi
git rev-parse "$UPSTREAM_BRANCH" || die "Vzdálená větev $UPSTREAM_BRANCH neexistuje?"
# 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 "$UPSTREAM_BRANCH":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 "$UPSTREAM_BRANCH":"$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 --keepdb "$@"

32
mamweb/admin.py

@ -17,14 +17,14 @@ from ckeditor_uploader.widgets import CKEditorUploadingWidget
class FlatpageForm(FlatpageFormOld):
content = forms.CharField(widget=CKEditorUploadingWidget())
class Meta:
model = FlatPage # this is not automatically inherited from FlatpageFormOld
exclude = []
content = forms.CharField(widget=CKEditorUploadingWidget())
class Meta:
model = FlatPage # this is not automatically inherited from FlatpageFormOld
exclude = []
class FlatPageAdmin(FlatPageAdminOld):
form = FlatpageForm
form = FlatpageForm
# 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
# FIXME zpraseno pomocí toho, že Python umí bez problému přepisovat funkce
def get_app_list(self, request):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower()))
app_dict = self._build_app_dict(request)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower()))
# Sort the models alphabetically within each app.
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()))
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(key=lambda x: locale.strxfrm('žž' + x['name'].lower()) if (x['name'].endswith("(Node)")) else locale.strxfrm(x['name'].lower()))
return app_list
return 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):
"""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
and if present, redirects to HTTPS (same adress).
Note this usually breaks non-GET (POST) requests.
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).
The cookie is set to expire at the same time as the sessionid cookie.
By default, LOGGED_IN_HINT_COOKIE_NAME = 'logged_in_hint'.
"""
def __init__(self):
if hasattr(settings, 'LOGGED_IN_HINT_COOKIE_NAME'):
self.cookie_name = settings.LOGGED_IN_HINT_COOKIE_NAME
else: self.cookie_name = 'logged_in_hint'
self.cookie_value = 'True'
def cookie_correct(self, request):
return self.cookie_name in request.COOKIES and request.COOKIES[self.cookie_name] == self.cookie_value
def process_request(self, request):
if not request.is_secure():
if self.cookie_correct(request):
# redirect insecure (assuming http) requests with hint cookie to https
url = request.build_absolute_uri()
assert url[:5] == 'http:'
return HttpResponseRedirect('https:' + url[5:])
return None
def process_response(self, request, response):
if request.is_secure():
# assuming full session info (as the conn. is secure)
try:
user = request.user
except AttributeError: # no user - ajax or other special request
return response
if user.is_authenticated():
if not self.cookie_correct(request):
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)
else:
if self.cookie_name in request.COOKIES:
response.delete_cookie(self.cookie_name)
return response
"""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
and if present, redirects to HTTPS (same adress).
Note this usually breaks non-GET (POST) requests.
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).
The cookie is set to expire at the same time as the sessionid cookie.
By default, LOGGED_IN_HINT_COOKIE_NAME = 'logged_in_hint'.
"""
def __init__(self):
if hasattr(settings, 'LOGGED_IN_HINT_COOKIE_NAME'):
self.cookie_name = settings.LOGGED_IN_HINT_COOKIE_NAME
else: self.cookie_name = 'logged_in_hint'
self.cookie_value = 'True'
def cookie_correct(self, request):
return self.cookie_name in request.COOKIES and request.COOKIES[self.cookie_name] == self.cookie_value
def process_request(self, request):
if not request.is_secure():
if self.cookie_correct(request):
# redirect insecure (assuming http) requests with hint cookie to https
url = request.build_absolute_uri()
assert url[:5] == 'http:'
return HttpResponseRedirect('https:' + url[5:])
return None
def process_response(self, request, response):
if request.is_secure():
# assuming full session info (as the conn. is secure)
try:
user = request.user
except AttributeError: # no user - ajax or other special request
return response
if user.is_authenticated():
if not self.cookie_correct(request):
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)
else:
if self.cookie_name in request.COOKIES:
response.delete_cookie(self.cookie_name)
return response
class vzhled:
def process_request(self, request):
return None
def process_view(self, request, view_func, view_args, view_kwargs):
#print "====== process_request ======"
#print view_func
#print view_args
#print view_kwargs
#print "============================="
return None
def process_template_response(self, request, response):
hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 14): # TODO 20
response.context_data['noc'] = True
else:
response.context_data['noc'] = False
return response
def process_response(self, request, response):
#hodin = datetime.now().hour
#if (hodin <= 6) or (hodin >= 14): # TODO 20
#response.context_data['noc'] = True
#else:
#response.context_data['noc'] = False
return response
##def process_exception(request, exception):
#pass
def process_request(self, request):
return None
def process_view(self, request, view_func, view_args, view_kwargs):
#print "====== process_request ======"
#print view_func
#print view_args
#print view_kwargs
#print "============================="
return None
def process_template_response(self, request, response):
hodin = datetime.now().hour
if (hodin <= 6) or (hodin >= 14): # TODO 20
response.context_data['noc'] = True
else:
response.context_data['noc'] = False
return response
def process_response(self, request, response):
#hodin = datetime.now().hour
#if (hodin <= 6) or (hodin >= 14): # TODO 20
#response.context_data['noc'] = True
#else:
#response.context_data['noc'] = False
return response
##def process_exception(request, exception):
#pass

386
mamweb/settings_common.py

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

99
mamweb/settings_local.py

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

28
mamweb/settings_test.py

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

24
mamweb/static/css/mamweb.css

@ -425,6 +425,13 @@ textarea.feedback {
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 */
@ -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 */
/* https://stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjust-for-fixed-header */
.kotva_obrazku {
display: block;
position: relative;
position: absolute;
width: 0;
height: 55px; /* viz #title */
margin-top: -55px; /* viz #title */
@ -1210,6 +1216,20 @@ div.gdpr {
}
/* 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);
}
/* Na úzkém displeji nechceme nic dělat. */
@media(max-width: 860px) {
.full_width{
margin-left: 0;
width: unset;
}
}
.dosla_reseni tr th, .dosla_reseni tr td {
padding: 1px 10px 1px 10px;
border-collapse: collapse;

16
odevzdavatko/forms.py

@ -63,7 +63,7 @@ class PosliReseniForm(forms.Form):
class NahrajReseniForm(forms.ModelForm):
class Meta:
model = m.Reseni
fields = ('problem',)
fields = ('problem', 'resitele')
help_texts = {'problem':''} # Nezobrazovat help text ve formuláři
widgets = {'problem':
@ -72,9 +72,23 @@ class NahrajReseniForm(forms.ModelForm):
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'},
),
'resitele':
autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel_public',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
'data-allow-clear': 'true'},
)
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
if 'resitele' in self.fields:
# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to.
self.fields['resitele'].required = False
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm,
fields = ('soubor','res_poznamka'),

22
odevzdavatko/static/odevzdavatko/check_for_detail.js

@ -0,0 +1,22 @@
// Kontrola, že org neposílá nějakou blbost v detail.html
function zkontroluj_hodnoceni() {
const pocet = $('.hodnoceni').length;
if (pocet === 1) { // vidím pouze plusko
const vysledek = confirm("Odstranil jsi všechny problémy tohoto řešení. Nepůjde tedy dohledat přes problémy, co řeší, tj. například v došlých řešeních. Přesto odeslat?");
if (!vysledek) {
event.preventDefault();
return false;
}
}
function problem_is_empty(elem, index, array) {return elem.firstElementChild.children.length !== 1 && elem.firstElementChild.children[1].textContent === "";}
if ($('.hodnoceni').toArray().some(problem_is_empty)) {
alert("Neuloženo! Nezadal jsi problém, ke kterému posíláš hodnocení. Pokud je toto hodnocení navíc, smaž ho prosím křížkem a znovu odešli.")
event.preventDefault()
return false;
}
return true;
}

56
odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js

@ -0,0 +1,56 @@
// FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function updateElementIndex(el, prefix, ndx) {
var id_regex = new RegExp('(' + prefix + '-\\d+)');
var replacement = prefix + '-' + ndx;
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
}
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function deleteForm(prefix, btn) {
var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
if (total >= 1){
btn.closest('tr').remove();
var forms = $('.hodnoceni');
var formCount = forms.length - 1; // There is one extra such form hidden as template!
$('#id_' + prefix + '-TOTAL_FORMS').val(formCount);
for (var i=0; i<formCount; i++) {
$(forms.get(i)).find(':input').each(function() {
updateElementIndex(this, prefix, i);
});
}
}
return false;
}
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
$(document).ready(function(){
$('#pridat_hodnoceni').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
$('#form_set').append(new_form);
// Newly created form has not the binding between remove button and remove function
// We need to add it manually
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
// Copy deadline
if (form_idx !== "0") {
$('#id_form-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value
}
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
});

120
odevzdavatko/templates/odevzdavatko/detail.html

@ -1,71 +1,32 @@
{% extends "base.html" %}
{% load static %}
{% load deadliny %}
{% load mail %}
{% block content %}
{# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #}
<script type='text/javascript'>
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function updateElementIndex(el, prefix, ndx) {
var id_regex = new RegExp('(' + prefix + '-\\d+)');
var replacement = prefix + '-' + ndx;
if ($(el).attr("for")) {
$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
}
if (el.id) {
el.id = el.id.replace(id_regex, replacement);
}
if (el.name) {
el.name = el.name.replace(id_regex, replacement);
}
}
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
function deleteForm(prefix, btn) {
var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
if (total >= 1){
btn.closest('tr').remove();
var forms = $('.hodnoceni');
var formCount = forms.length - 1; // There is one extra such form hidden as template!
$('#id_' + prefix + '-TOTAL_FORMS').val(formCount);
for (var i=0; i<formCount; i++) {
$(forms.get(i)).find(':input').each(function() {
updateElementIndex(this, prefix, i);
});
}
}
return false;
}
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
$(document).ready(function(){
$('#pridat_hodnoceni').click(function() {
var form_idx = $('#id_form-TOTAL_FORMS').val();
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
$('#form_set').append(new_form);
// Newly created form has not the binding between remove button and remove function
// We need to add it manually
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
// Copy deadline
if (form_idx !== "0") {
$('#id_form-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value
}
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
});
$('.smazat_hodnoceni').click(function(){
deleteForm("form",this);
});
});
</script>
{% if edit %}
<script src="{% static 'odevzdavatko/dynamic_formsets_for_detail.js' %}"></script>
<script src="{% static 'odevzdavatko/check_for_detail.js' %}"></script>
{% endif %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {% for r in object.resitele.all %} {{ r }} (<a href="mailto:{{ r.osoba.email }}?subject={{ "Oprava řešení M&M " | urlencode }}{{ object.problem.all.0.hlavni_problem | urlencode }}">{{ r.osoba.email }}</a>)
{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}</p>
{% if edit %}
<p>Řešitelé:
{% for r in object.resitele.all %}
{{ r }}
{# DjangoTemplates neumí spojovat řetězce (https://stackoverflow.com/q/4386168), tak si necháváme vyrobit subject mailu ve view. #}
({% maillink r.osoba.email to=r.osoba.email subject=predmetmailu %}){% if forloop.revcounter0 != 0 %}, {% endif %}
{% endfor %}
</p>
<p>
{% maillink "Poslat mail všem řešitelům" bcc=maily_vsech_resitelu subject=predmetmailu %}
</p>
{% else %}
<p>Řešitelé: {{ object.resitele.all | join:", " }}</p>
{% endif %}
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}</p>
@ -82,13 +43,13 @@ $(document).ready(function(){
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{% if edit %}
<form method=post onsubmit="return zkontroluj_hodnoceni();">
{# Poznámka #}
<h3>Neveřejná poznámka:</h3>
@ -109,14 +70,14 @@ $(document).ready(function(){
<td>{{ subform.body }}</td>
<td>{{ subform.deadline_body }}</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>
</tbody>
{% endfor %}
</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>
<table id="empty_form" style="display: none;">
@ -125,32 +86,23 @@ $(document).ready(function(){
<td>{{ form.empty_form.body }}</td>
<td>{{ form.empty_form.deadline_body }}</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>
</table>
{% else %}
<h3>Hodnocení:</h3>
<table class="dosla_reseni">
<tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th></tr>
{% for h in hodnoceni %}
<tr class="hodnoceni">
<td>{{ h.problem }}</td>
<td>{{ h.body }}</td>
<td>{{ h.feedback | linebreaks }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
<script type="text/javascript">
function zkontroluj_hodnoceni() {
const pocet = $('.hodnoceni').length;
if (pocet === 1) { {# vydím pouze plusko #}
const vysledek = confirm("Odstranil jsi všechny problémy tohoto řešení. Nepůjde tedy dohledat přes problémy, co řeší, tj. například v došlých řešeních. Přesto odeslat?");
if (!vysledek) {
event.preventDefault();
return false;
}
}
function problem_is_empty(elem, index, array) {return elem.firstElementChild.children.length !== 1 && elem.firstElementChild.children[1].textContent === "";}
if ($('.hodnoceni').toArray().some(problem_is_empty)) {
alert("Neuloženo! Nezadal jsi problém, ke kterému posíláš hodnocení. Pokud je toto hodnocení navíc, smaž ho prosím křížkem a znovu odešli.")
event.preventDefault()
return false;
}
return true;
}
</script>
{% endblock %}

51
odevzdavatko/templates/odevzdavatko/detail_resitele.html

@ -1,51 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% load deadliny %}
{% block content %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {% for r in object.resitele.all %} {{ r }}
{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}</p>
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}</p>
<p>Doručeno {{ object.cas_doruceni }}, deadline: {{object.deadline_reseni | deadline_html }}</p>
{# Soubory: #}
<h3>Přílohy:</h3>
{% if object.prilohy.all %}
<table class="dosla_reseni">
<tr><th>Soubor</th><th>Řešitelova poznámka</th><th>Datum</th></tr>
{% for priloha in object.prilohy.all %}
<tr>
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{#<h3>Poznámka:</h3>#}
{#<p>{{ poznamka }}</p>#}
{# Hodnocení: #}
<h3>Hodnocení:</h3>
<table id="form_set" class="dosla_reseni">
<tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th>{# <th>Deadline pro body</th> #}</tr>
{% for h in hodnoceni %}
<tr class="hodnoceni">
<td>{{ h.problem }}</td>
<td>{{ h.body }}</td>
<td>{{ h.feedback }}</td>
{# <td>{{ h.deadline_body }}</td>#}
</tr>
{% endfor %}
</table>
{% endblock %}

2
odevzdavatko/templates/odevzdavatko/nahraj_reseni.html

@ -13,6 +13,8 @@
<p style="text-align: justify">Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a&nbsp;třeba ti je i&nbsp;rychleji opravíme.</p>
<p>Pokud řešíte ve více lidech, je <strong>nutné</strong> přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
{% csrf_token %}
<table class='form' id="reseni">

2
odevzdavatko/templates/odevzdavatko/tabulka.html

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

4
odevzdavatko/urls.py

@ -26,9 +26,9 @@ urlpatterns = [
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.EditReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/all', org_required(views.SeznamReseniView.as_view())),
path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
path('resitel/reseni/<int:pk>', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
path('resitel/reseni/<int:pk>', resitel_or_org_required(views.DetailReseniView.as_view()), name='odevzdavatko_resitel_reseni'),
]

80
odevzdavatko/views.py

@ -60,7 +60,7 @@ class TabulkaOdevzdanychReseniView(ListView):
self.aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
if 'rocnik' in self.kwargs:
self.aktualni_rocnik = m.Rocnik.objects.get(rocnik=self.kwargs['rocnik'])
self.aktualni_rocnik = get_object_or_404(m.Rocnik, rocnik=self.kwargs['rocnik'])
form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik)
if form.is_valid():
@ -102,16 +102,21 @@ class TabulkaOdevzdanychReseniView(ListView):
)
#self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník....
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
self.problemy = self.problemy.non_polymorphic()
self.problemy = self.problemy.non_polymorphic().distinct()
self.reseni = self.reseni.filter(cas_doruceni__date__gt=reseni_od, cas_doruceni__date__lte=reseni_do)
if jen_neobodovane:
self.reseni = self.reseni.filter(hodnoceni__body__isnull=True)
self.jen_neobodovane = jen_neobodovane
def get_queryset(self):
self.inicializuj_osy_tabulky()
qs = super().get_queryset()
qs = qs.filter(problem__in=self.problemy, reseni__in=self.reseni, reseni__resitele__in=self.resitele).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba')
if self.jen_neobodovane:
qs = qs.filter(body__isnull=True)
qs = qs.filter(problem__in=self.problemy, reseni__in=self.reseni, reseni__resitele__in=self.resitele).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba').distinct()
# FIXME tohle je ošklivé, na špatném místě a pomalé. Ale moc mě štvalo, že musím hledat správná místa v tabulce.
self.problemy = self.problemy.filter(id__in=qs.values("problem__id"))
return qs
def get_context_data(self, *args, **kwargs):
@ -211,6 +216,7 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView(DetailView):
""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
model = m.Reseni
template_name = 'odevzdavatko/detail.html'
@ -227,18 +233,48 @@ class DetailReseniView(DetailView):
return result
def get_context_data(self, **kw):
self.check_access()
ctx = super().get_context_data(**kw)
ctx['form'] = f.OhodnoceniReseniFormSet(
initial = self.aktualni_hodnoceni()
)
detaily_hodnoceni = self.aktualni_hodnoceni()
ctx["hodnoceni"] = detaily_hodnoceni
# Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168)
ctx["predmetmailu"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev
ctx["maily_vsech_resitelu"] = [y for x in self.reseni.resitele.all().values_list('osoba__email') for y in x]
return ctx
def get(self, request, *args, **kwargs):
"""
Oproti :py:class:`django.views.generic.detail.BaseDetailView`
kontroluje přístup pomocí :py:meth:`check_access`
"""
response = super().get(self, request, *args, **kwargs)
self.check_access()
return response
def check_access(self):
""" Řešitel musí být součástí řešení, jinak se na něj nemá co dívat. Případně to může být org."""
if not self.object.resitele.filter(osoba__user=self.request.user).exists() and not self.request.user.je_org:
raise PermissionDenied()
class EditReseniView(DetailReseniView):
""" Editace (hlavně hodnocení) řešení. """
def get_context_data(self, **kw):
ctx = super().get_context_data(**kw)
ctx['form'] = f.OhodnoceniReseniFormSet(initial=ctx["hodnoceni"])
ctx['poznamka_form'] = f.PoznamkaReseniForm(instance=self.reseni)
ctx['edit'] = True
return ctx
def check_access(self):
# Na orga máme nároky už v urls.py ale better safe then sorry
if not self.request.user.je_org:
raise PermissionDenied()
def hodnoceniReseniView(request, pk, *args, **kwargs):
reseni = get_object_or_404(m.Reseni, pk=pk)
template_name = 'odevzdavatko/detail.html'
form_class = f.OhodnoceniReseniFormSet
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
@ -270,33 +306,6 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
return redirect(success_url)
class ResitelReseniView(DetailView):
model = m.Reseni
template_name = 'odevzdavatko/detail_resitele.html'
def aktualni_hodnoceni(self):
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
result = []
for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni):
result.append(
{
"problem": hodn.problem,
"body": hodn.body,
"feedback": hodn.feedback,
# "deadline_body": hodn.deadline_body,
}
)
return result
def get_context_data(self, **kw):
ctx = super().get_context_data(**kw)
hodnoceni = self.aktualni_hodnoceni()
if not self.reseni.resitele.filter(osoba__user=self.request.user).exists():
raise PermissionDenied()
# ctx['poznamka'] = f.PoznamkaReseniForm(instance=self.reseni)
ctx["hodnoceni"] = hodnoceni
return ctx
class PrehledOdevzdanychReseni(ListView):
@ -408,6 +417,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
with transaction.atomic():
self.object = form.save()
self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user))
self.object.resitele.add(*form.cleaned_data["resitele"])
self.object.cas_doruceni = timezone.now()
self.object.forma = m.Reseni.FORMA_UPLOAD
self.object.save()

2
personalni/admin.py

@ -43,7 +43,7 @@ class OrganizatorAdmin(ReverseModelAdmin):
@admin.register(m.Resitel)
class ResitelAdmin(ReverseModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
ordering = ('osoba__jmeno','osoba__prijmeni')
ordering = ('osoba__prijmeni', 'osoba__jmeno')
inline_type = 'stacked'
inline_reverse = ['osoba']

19
personalni/forms.py

@ -32,6 +32,7 @@ class PrihlaskaForm(PasswordResetForm):
help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři')
jmeno = forms.CharField(label='Jméno', max_length=256, required=True)
prezdivka_resitele = forms.CharField(label='Přezdívka (veřejná)', max_length=256, required=False)
prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True)
pohlavi_muz = forms.ChoiceField(label='Pohlaví',
choices = ((True,'muž'),(False,'žena')), required=True)
@ -105,6 +106,14 @@ class PrihlaskaForm(PasswordResetForm):
pass
return email
def clean_prezdivka_resitele(self):
prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele')
if prezdivka_resitele == '':
return prezdivka_resitele
if Resitel.objects.filter(prezdivka_resitele=prezdivka_resitele).count() > 0:
raise forms.ValidationError('Přezdívka je již použita')
return prezdivka_resitele
def clean_zasilat(self):
zasilat = self.cleaned_data.get('zasilat')
ulice = self.cleaned_data.get('ulice')
@ -138,6 +147,7 @@ class ProfileEditForm(forms.Form):
disabled=True)
jmeno = forms.CharField(label='Jméno', max_length=256, required=True)
prezdivka_resitele = forms.CharField(label='Přezdívka (veřejná)', max_length=256, required=False)
prijmeni = forms.CharField(label='Příjmení', max_length=256, required=True)
pohlavi_muz = forms.ChoiceField(label='Pohlaví',
choices = ((True,'muž'),(False,'žena')), required=True)
@ -190,6 +200,15 @@ class ProfileEditForm(forms.Form):
# pass
# return username
#
def clean_prezdivka_resitele(self):
prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele')
if prezdivka_resitele == '':
return prezdivka_resitele
if Resitel.objects.filter(prezdivka_resitele=prezdivka_resitele).exclude(osoba__user__username=self.username).count() > 0:
raise forms.ValidationError('Přezdívka je již použita')
return prezdivka_resitele
def clean_email(self):
err_logger = logging.getLogger('seminar.prihlaska.problem')
email = self.cleaned_data.get('email')

1
personalni/templates/personalni/udaje/edit.html

@ -44,6 +44,7 @@
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prezdivka_resitele %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "personalni/udaje/prihlaska_field.html" with field=form.email %}

2
personalni/templates/personalni/udaje/gdpr.html

@ -11,7 +11,7 @@ Získáváme od Tebe údaje vyplněné v přihlášce do semináře (jméno, př
Slibujeme Ti, že Tvá osobní data nezneužijeme k ničemu, co by nesouviselo s M&amp;M nebo s dalšími aktivitami Matfyzu, a nikdy je nepředáme nikomu cizímu. Údaje využíváme k zajištění chodu semináře a také je sdílíme s ostatními propagačními akcemi Matfyzu, abychom mohli vyhodnocovat úspěšnost akcí. Pokud budeš mít zájem, budeme Ti také posílat zajímavé zprávy a novinky týkajíci se Matfyzu.
</p>
<p class="gdpr">
Veřejně vystavujeme pouze výsledkové listiny, které také uchováváme pro archivní účely. Pokud ale z nějakého důvodu nebudeš chtít mít své jméno či školu uvedené ve výsledkové listině, není problém to zařídit, napiš nám. Z tištěných materiálů samozřejmě údaje už odstranit nemůžeme.
Veřejně vystavujeme pouze seznam přezdívek (pro výběr spoluřešitelů k řešení) a výsledkové listiny, které také uchováváme pro archivní účely. Pokud ale z nějakého důvodu nebudeš chtít mít své jméno či školu uvedené ve výsledkové listině, není problém to zařídit, napiš nám. Z tištěných materiálů samozřejmě údaje už odstranit nemůžeme.
</p>
<p class="gdpr">
Na soustředěních a dalších akcích semináře navíc pořizujeme fotografie a videozáznamy a používáme je ke zpravodajským a propagačním účelům. Pro propagační účely si od Tebe vyžádáme samostatný souhlas na začátku akce.

1
personalni/templates/personalni/udaje/prihlaska.html

@ -46,6 +46,7 @@
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.jmeno %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prezdivka_resitele %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.prijmeni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.pohlavi_muz%}
{% include "personalni/udaje/prihlaska_field.html" with field=form.email %}

2
personalni/views.py

@ -160,6 +160,7 @@ def resitelEditView(request):
if resitel_edit:
## Změny v řešiteli
resitel_edit.prezdivka_resitele = fcd['prezdivka_resitele'] if fcd['prezdivka_resitele'] != '' else None
resitel_edit.skola = fcd['skola']
resitel_edit.rok_maturity = fcd['rok_maturity']
resitel_edit.zasilat = fcd['zasilat']
@ -263,6 +264,7 @@ def prihlaskaView(request):
err_logger.warning(f'Zaregistrovala se osoba s kolizním jménem. ID osob: {[o.id for o in kolize]}')
r = s.Resitel(
prezdivka_resitele=fcd['prezdivka_resitele'],
rok_maturity = fcd['rok_maturity'],
zasilat = fcd['zasilat'],
zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']

18
seminar/migrations/0110_resitel_prezdivka.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2022-11-21 22:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0109_hodnoceni_feedback'),
]
operations = [
migrations.AddField(
model_name='resitel',
name='prezdivka_resitele',
field=models.CharField(blank=True, max_length=256, null=True, unique=True, verbose_name='přezdívka řešitele'),
),
]

18
seminar/migrations/0111_nikam2nezasilat_papirove.py

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-01-30 19:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seminar', '0110_resitel_prezdivka'),
]
operations = [
migrations.AlterField(
model_name='resitel',
name='zasilat',
field=models.CharField(choices=[('domu', 'Domů'), ('do_skoly', 'Do školy'), ('nikam', 'Nezasílat papírově')], default='domu', max_length=32, verbose_name='kam zasílat'),
),
]

18
seminar/models/base.py

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

42
seminar/models/novinky.py

@ -9,30 +9,30 @@ from . import personalni as pm
@reversion.register(ignore_duplicates=True)
class Novinky(models.Model):
class Meta:
verbose_name = 'Novinka'
verbose_name_plural = 'Novinky'
ordering = ['-datum']
class Meta:
verbose_name = 'Novinka'
verbose_name_plural = 'Novinky'
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)
obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/',
null=True, blank=True)
text = models.TextField('Text novinky', blank=True, null=True)
obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/',
null=True, blank=True)
obrazek_maly = ImageSpecField(source='obrazek',
processors=[
ResizeToFit(350, 200, upscale=False)
],
options={'quality': 95})
obrazek_maly = ImageSpecField(source='obrazek',
processors=[
ResizeToFit(350, 200, upscale=False)
],
options={'quality': 95})
autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True,
on_delete=models.SET_NULL)
autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True,
on_delete=models.SET_NULL)
zverejneno = models.BooleanField('Zveřejněno', default=False)
zverejneno = models.BooleanField('Zveřejněno', default=False)
def __str__(self):
if self.text:
return '[' + str(self.datum) + '] ' + self.text[0:50]
else:
return '[' + str(self.datum) + '] '
def __str__(self):
if self.text:
return '[' + str(self.datum) + '] ' + self.text[0:50]
else:
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)
class Reseni(bm.SeminarModelBase):
class Meta:
db_table = 'seminar_reseni'
verbose_name = 'Řešení'
verbose_name_plural = 'Řešení'
#ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to.
ordering = ['-cas_doruceni']
class Meta:
db_table = 'seminar_reseni'
verbose_name = 'Řešení'
verbose_name_plural = 'Řešení'
#ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to.
ordering = ['-cas_doruceni']
# Interní ID
id = models.AutoField(primary_key = True)
# Interní ID
id = models.AutoField(primary_key = True)
# 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',
through='Hodnoceni')
# 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',
through='Hodnoceni')
resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení',
help_text='Seznam autorů řešení', through='Reseni_Resitele')
resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení',
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_EMAIL = 'email'
FORMA_UPLOAD = 'upload'
FORMA_CHOICES = [
(FORMA_PAPIR, 'Papírové řešení'),
(FORMA_EMAIL, 'Emailem'),
(FORMA_UPLOAD, 'Upload přes web'),
]
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL)
FORMA_PAPIR = 'papir'
FORMA_EMAIL = 'email'
FORMA_UPLOAD = 'upload'
FORMA_CHOICES = [
(FORMA_PAPIR, 'Papírové řešení'),
(FORMA_EMAIL, 'Emailem'),
(FORMA_UPLOAD, 'Upload přes web'),
]
forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False,
default=FORMA_EMAIL)
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.PROTECT)
text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',
blank=True, null=True, related_name="reseni_cely_set",
on_delete=models.PROTECT)
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešení (plain text)')
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešení (plain text)')
zverejneno = models.BooleanField('řešení zveřejněno', default=False,
help_text='Udává, zda je řešení zveřejněno')
zverejneno = models.BooleanField('řešení zveřejněno', default=False,
help_text='Udává, zda je řešení zveřejněno')
def verejne_url(self):
return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id]))
def verejne_url(self):
return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id]))
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
# má OneToOneField s:
# Konfera
# má OneToOneField s:
# Konfera
# má ForeignKey s:
# Hodnoceni
# má ForeignKey s:
# Hodnoceni
def sum_body(self):
return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"]
def sum_body(self):
return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"]
def __str__(self):
return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all()))
# NOTE: Potenciální DB HOG (bez select_related)
def __str__(self):
return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all()))
# NOTE: Potenciální DB HOG (bez select_related)
def deadline_reseni(self):
return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
def deadline_reseni(self):
return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
## Pravdepodobne uz nebude potreba:
# def save(self, *args, **kwargs):
@ -89,112 +89,112 @@ class Reseni(bm.SeminarModelBase):
# super(Reseni, self).save(*args, **kwargs)
class Hodnoceni(bm.SeminarModelBase):
class Meta:
db_table = 'seminar_hodnoceni'
verbose_name = 'Hodnocení'
verbose_name_plural = 'Hodnocení'
class Meta:
db_table = 'seminar_hodnoceni'
verbose_name = 'Hodnocení'
verbose_name_plural = 'Hodnocení'
# Interní ID
id = models.AutoField(primary_key = True)
# Interní ID
id = models.AutoField(primary_key = True)
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=True, null=True)
body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',
blank=True, null=True)
cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body',
related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT)
cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body',
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
deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body',
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
deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body',
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',
related_name='hodnoceni', on_delete=models.PROTECT)
problem = models.ForeignKey(am.Problem, verbose_name='problém',
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):
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
def generate_filename(self, filename):
return os.path.join(
settings.SEMINAR_RESENI_DIR,
am.aux_generate_filename(self, filename)
)
return os.path.join(
settings.SEMINAR_RESENI_DIR,
am.aux_generate_filename(self, filename)
)
@reversion.register(ignore_duplicates=True)
class PrilohaReseni(bm.SeminarModelBase):
class Meta:
db_table = 'seminar_priloha_reseni'
verbose_name = 'Příloha řešení'
verbose_name_plural = 'Přílohy řešení'
ordering = ['reseni', 'vytvoreno']
class Meta:
db_table = 'seminar_priloha_reseni'
verbose_name = 'Příloha řešení'
verbose_name_plural = 'Přílohy řešení'
ordering = ['reseni', 'vytvoreno']
# Interní ID
id = models.AutoField(primary_key = True)
# Interní ID
id = models.AutoField(primary_key = True)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy',
on_delete=models.CASCADE)
reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy',
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,
help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu')
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')
res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
res_poznamka = models.TextField('poznámka řešitele', blank=True,
help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje')
def __str__(self):
return str(self.soubor)
def __str__(self):
return str(self.soubor)
def split(self):
"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.
return self.soubor.url.split('/')
def split(self):
"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.
return self.soubor.url.split('/')
# Vazebna tabulka. Mozna se generuje automaticky.
@reversion.register(ignore_duplicates=True)
class Reseni_Resitele(models.Model):
class Meta:
db_table = 'seminar_reseni_resitele'
verbose_name = 'Řešení řešitelů'
verbose_name_plural = 'Řešení řešitelů'
ordering = ['reseni', 'resitele']
class Meta:
db_table = 'seminar_reseni_resitele'
verbose_name = 'Řešení řešitelů'
verbose_name_plural = 'Řešení řešitelů'
ordering = ['reseni', 'resitele']
# Interní ID
id = models.AutoField(primary_key = True)
# Interní ID
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
# - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne 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
def __str__(self):
return '{} od {}'.format(self.reseni, self.resitel)
# NOTE: Poteciální DB HOG bez select_related
def __str__(self):
return '{} od {}'.format(self.reseni, self.resitel)
# NOTE: Poteciální DB HOG bez select_related
class ReseniNode(tm.TreeNode):
class Meta:
db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)'
verbose_name_plural = 'Otištěná řešení (Node)'
reseni = models.ForeignKey(Reseni,
on_delete=models.PROTECT,
verbose_name = 'reseni')
def aktualizuj_nazev(self):
self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self):
return str(self.reseni)
class Meta:
db_table = 'seminar_nodes_otistene_reseni'
verbose_name = 'Otištěné řešení (Node)'
verbose_name_plural = 'Otištěná řešení (Node)'
reseni = models.ForeignKey(Reseni,
on_delete=models.PROTECT,
verbose_name = 'reseni')
def aktualizuj_nazev(self):
self.nazev = "ReseniNode: "+str(self.reseni)
def getOdkazStr(self):
return str(self.reseni)

4
seminar/models/personalni.py

@ -211,6 +211,8 @@ class Resitel(SeminarModelBase):
# Interní ID
id = models.AutoField(primary_key = True)
prezdivka_resitele = models.CharField('přezdívka řešitele', blank=True, null=True, max_length=256, unique=True)
osoba = models.OneToOneField(Osoba, blank=False, null=False, verbose_name='osoba',
on_delete=models.PROTECT)
@ -227,7 +229,7 @@ class Resitel(SeminarModelBase):
ZASILAT_CHOICES = [
(ZASILAT_DOMU, 'Domů'),
(ZASILAT_DO_SKOLY, 'Do školy'),
(ZASILAT_NIKAM, 'Nikam'),
(ZASILAT_NIKAM, 'Nezasílat papírově'),
]
zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU)

32
seminar/models/tvorba.py

@ -270,21 +270,27 @@ class Cislo(SeminarModelBase):
'Vaše M&M\n'.format(odkaz)
# Prijemci e-mailu
emaily = map(lambda r: r.osoba.email, filter(lambda r: r.zasilat_cislo_emailem, aktivniResitele(self)))
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
return
resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True)
def posli(text, resitele):
emaily = map(lambda resitel: resitel.osoba.email, resitele)
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
return
email = EmailMessage(
subject=predmet,
body=text,
from_email=poslat_z_mailu,
bcc=list(emaily)
#bcc = příjemci skryté kopie
)
email = EmailMessage(
subject=predmet,
body=text_mailu,
from_email=poslat_z_mailu,
bcc=list(emaily)
#bcc = příjemci skryté kopie
)
email.send()
email.send()
posli(text_mailu, resitele_vsichni.filter(zasilat=pm.Resitel.ZASILAT_NIKAM))
posli(text_mailu + 'P. S. Také by vám brzy měla přijít papírová verze. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Děkujeme. (Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem.)\n',
resitele_vsichni.exclude(zasilat=pm.Resitel.ZASILAT_NIKAM))
def save(self, *args, **kwargs):
super().save(*args, **kwargs)

2
seminar/templatetags/deadliny.py

@ -26,7 +26,7 @@ def deadline_html(deadline: m.Deadline):
m.Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline',
m.Deadline.TYP_CISLA: 'final_deadline',
}
return mark_safe(f'<span class="{classes[deadline.typ]}">{text}</span>')
return mark_safe(f'<span class="{classes[deadline.typ]}" title="{deadline}">{text}</span>')
@register.filter(name='zkrat_nazev_problemu')
def zkrat_nazev_problemu(nazev,width):

18
seminar/testutils.py

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

20
seminar/views/views_all.py

@ -349,7 +349,7 @@ def resiteleRocnikuCsvExportView(request, rocnik):
assert request.method in ('GET', 'HEAD')
return dataResiteluCsvResponse(
utils.resi_v_rocniku(
m.Rocnik.objects.get(rocnik=rocnik)
get_object_or_404(m.Rocnik, rocnik=rocnik)
)
)
@ -442,17 +442,17 @@ class OdmenyView(generic.TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fromcislo = Cislo.objects.get(rocnik=self.kwargs.get('frocnik'), poradi=self.kwargs.get('fcislo'))
tocislo = Cislo.objects.get(rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo'))
fromcislo = get_object_or_404(Cislo, rocnik=self.kwargs.get('frocnik'), poradi=self.kwargs.get('fcislo'))
tocislo = get_object_or_404(Cislo, rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo'))
resitele = aktivniResitele(tocislo)
def get_diff(from_deadline: Deadline, to_deadline: Deadline):
frombody = body_resitelu(resitele=resitele, jen_verejne=False, do=from_deadline)
tobody = body_resitelu(resitele=resitele, jen_verejne=False, do=to_deadline)
outlist = []
for (aid, tbody) in tobody.items():
fbody = frombody.get(aid,0)
resitel = Resitel.objects.get(pk=aid)
for resitel in resitele:
fbody = frombody.get(resitel.id, 0)
tbody = tobody.get(resitel.id, 0)
ftitul = resitel.get_titul(fbody)
ttitul = resitel.get_titul(tbody)
if ftitul != ttitul:
@ -554,7 +554,7 @@ class RocnikVysledkovkaView(RocnikView):
#vypise na stranku textovy obsah vyTeXane vysledkovky k okopirovani
def cisloObalkyView(request, rocnik, cislo):
realne_cislo = Cislo.objects.get(poradi=cislo, rocnik__rocnik=rocnik)
realne_cislo = get_object_or_404(Cislo, poradi=cislo, rocnik__rocnik=rocnik)
return obalkyView(request, aktivniResitele(realne_cislo))
@ -580,14 +580,14 @@ def TitulyViewRocnik(request, rocnik):
def TitulyView(request, rocnik, cislo):
""" View pro stažení makra titulů v TeXu."""
rocnik_obj = Rocnik.objects.get(rocnik = rocnik)
rocnik_obj = get_object_or_404(Rocnik, rocnik = rocnik)
resitele = Resitel.objects.filter(rok_maturity__gte = rocnik_obj.prvni_rok)
asciijmena = []
jmenovci = False # detekuje, zda jsou dva řešitelé jmenovci (modulo nabodeníčka),
# pokud ano, vrátí se jako true
if cislo is not None:
cislo_obj = Cislo.objects.get(rocnik=rocnik_obj, poradi=cislo)
cislo_obj = get_object_or_404(Cislo, rocnik=rocnik_obj, poradi=cislo)
slovnik_s_body = body_resitelu(do=cislo_obj.zlomovy_deadline_pro_papirove_cislo(), jen_verejne=False)
else:
slovnik_s_body = body_resitelu(do=Deadline.objects.filter(cislo__rocnik=rocnik_obj).last(), jen_verejne=False)
@ -686,7 +686,7 @@ def formularOKView(request, text=''):
]
context = {
'odkazy': odkazy,
'text': text,
'text': text,
}
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):
model = m.Soustredeni_Ucastnici
extra = 1
fields = ['resitel','poznamka']
autocomplete_fields = ['resitel']
ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
model = m.Soustredeni_Ucastnici
extra = 1
fields = ['resitel','poznamka']
autocomplete_fields = ['resitel']
ordering = ['resitel__osoba__jmeno', 'resitel__osoba__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('resitel','soustredeni')
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('resitel','soustredeni')
class SoustredeniOrganizatoriInline(admin.TabularInline):
model = m.Soustredeni.organizatori.through
extra = 1
fields = ['organizator','poznamka']
autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
model = m.Soustredeni.organizatori.through
extra = 1
fields = ['organizator','poznamka']
autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni']
formfield_overrides = {
models.TextField: {'widget': widgets.TextInput}
}
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('organizator', 'soustredeni')
def get_queryset(self,request):
qs = super().get_queryset(request)
return qs.select_related('organizator', 'soustredeni')
@admin.register(m.Soustredeni)
class SoustredeniAdmin(admin.ModelAdmin):
model = m.Soustredeni
inline_type = 'tabular'
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]
model = m.Soustredeni
inline_type = 'tabular'
inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline]

90
treenode/admin.py

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

4
treenode/permissions.py

@ -2,6 +2,6 @@ from rest_framework.permissions import BasePermission
class AllowWrite(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('auth.org')
def has_permission(self, request, view):
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):
uid = urlsafe_base64_encode(force_bytes(u.pk))
token = PasswordResetTokenGenerator().make_token(u)
url = "https://%s%s" % (
str(get_current_site(request)),
str(reverse_lazy("reset_password_confirm", args=[uid, token]))
)
uid = urlsafe_base64_encode(force_bytes(u.pk))
token = PasswordResetTokenGenerator().make_token(u)
url = "https://%s%s" % (
str(get_current_site(request)),
str(reverse_lazy("reset_password_confirm", args=[uid, token]))
)
u.email_user(
subject="Vítej mezi řešiteli M&M!",
message="""Milý řešiteli, milá řešitelko,
u.email_user(
subject="Vítej mezi řešiteli M&M!",
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
@ -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.
""" % url,
# 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í?
)
# 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í?
)

8
various/log_filters.py

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

0
various/templatetags/__init__.py

48
various/templatetags/mail.py

@ -0,0 +1,48 @@
from django import template
from django.utils.safestring import mark_safe
from urllib.request import quote as urlencode
register = template.Library()
@register.simple_tag
def mailurl(*, subject=None, body=None, to=[], cc=[], bcc=[]):
"""Tag na vytváření správně zakódované mailto: adresy
Ref: RFC 6068, <https://en.wikipedia.org/wiki/Mailto>"""
if isinstance(to, str):
to = [to]
if isinstance(cc, str):
cc = [cc]
if isinstance(bcc, str):
bcc = [bcc]
assert isinstance(to, list)
assert isinstance(cc, list)
assert isinstance(bcc, list)
# FIXME: adresa není správně zakódovaná, rozbije se to na adresách s divnými znaky
parts = [
f'mailto:{str.join(",", to)}',
]
if len(to) + len(cc) + len(bcc) < 1:
raise ValueError('Cannot mail to empty set of people')
if subject:
parts.append(f'subject={urlencode(subject)}')
if body:
parts.append(f'body={urlencode(body)}')
if len(cc) > 0:
parts.append(f'cc={str.join(",", cc)}')
if len(bcc) > 0:
parts.append(f'bcc={str.join(",", bcc)}')
if len(parts) > 1:
url = parts[0] + '?' + str.join('&', parts[1:])
else:
url = parts[0]
return url
@register.simple_tag
def maillink(text, subject=None, body=None, to=[], cc=[], bcc=[], attrs=None):
url = mailurl(subject=subject, body=body, to=to, cc=cc, bcc=bcc)
if not attrs: attrs = ''
mezera = ' '*bool(attrs)
full_link = f'<a href="{url}"{mezera}{attrs}>{text}</a>'
return mark_safe(full_link)

59
various/tests.py

@ -1,3 +1,60 @@
from django.test import TestCase
# TODO: Možná vyrobit separátní soubory v tests/… než mít všechny testy v jednom souboru?
from various.templatetags.mail import maillink, mailurl
# Create your tests here.
class MailTagsTest(TestCase):
"""Testuje template tagy ohledně mailů."""
def test_maillink(self):
# Tohle nedává smysl dělit do víc funkcí, bylo by v nich víc boilerplatu než užitečného kódu.
self.assertEquals(maillink('Hello', to='some@body.test'), r'<a href="mailto:some@body.test">Hello</a>')
self.assertEquals(maillink('Hello', to=['some@body.test']), r'<a href="mailto:some@body.test">Hello</a>')
self.assertEquals(
maillink('Hello', to=['alice@test.test', 'bob@jinde.test']),
r'<a href="mailto:alice@test.test,bob@jinde.test">Hello</a>',
)
self.assertEquals(
maillink('Hello', to='some@body.test', attrs='class="trida" id="id"'),
r'<a href="mailto:some@body.test" class="trida" id="id">Hello</a>',
)
# Následující test toho testuje moc zároveň, měly by předcházet dedikované testy… (kašlu na ně :-P)
self.assertEquals(
maillink('Text odkazu', to='prijemce@wtf.test', subject="Předmět", body="Čau"),
r'<a href="mailto:prijemce@wtf.test?subject=P%C5%99edm%C4%9Bt&body=%C4%8Cau">Text odkazu</a>',
)
self.assertRaises(ValueError, lambda: maillink('Nemám příjemce'))
self.assertRaises(TypeError, lambda: maillink()) # Nemá text, takže to shodí python
def test_mailurl(self):
self.assertEquals(mailurl(to='some@body.test'), r'mailto:some@body.test')
self.assertEquals(mailurl(to=['some@body.test']), r'mailto:some@body.test')
self.assertEquals(mailurl(to=['alice@test.test', 'bob@jinde.test']), r'mailto:alice@test.test,bob@jinde.test')
self.assertEquals(
mailurl(to='some@body.test', body='Tělo', subject='Předmět'),
r'mailto:some@body.test?subject=P%C5%99edm%C4%9Bt&body=T%C4%9Blo',
)
self.assertRaises(ValueError, lambda: mailurl())
def test_render_in_template(self):
# Pomocná funkce: vykreslí template do stringu
# Ref: https://stackoverflow.com/a/1690879
def render_template(template, context=None):
from django.template import Template, Context
context = context or {}
context = Context(context)
return Template(template).render(context)
template = (
r'{% load mail %}'
# TODO: Vyzkoušet i víc adresátů. (Nepamatuji si z hlavy syntaxi…)
r'{% maillink "Text" to="alice@test.test" subject="Oprava řešení" %}'
)
self.assertEquals(
render_template(template),
r'<a href="mailto:alice@test.test?subject=Oprava%20%C5%99e%C5%A1en%C3%AD">Text</a>',
)
mailurltemplate = (
r'{% load mail %}'
r'{% mailurl to="alice@test.test" subject="Čau Alice" %}'
)
self.assertEquals(render_template(mailurltemplate), r'mailto:alice@test.test?subject=%C4%8Cau%20Alice')

4
vysledkovky/utils.py

@ -1,6 +1,6 @@
import abc
from functools import cached_property
from typing import Union # TODO: s pythonem 3.10 přepsat na '|'
from typing import Union, Iterable # TODO: s pythonem 3.10 přepsat na '|'
import seminar.models as m
from django.db.models import Q, Sum
@ -22,7 +22,7 @@ def body_resitelu(
do: m.Deadline = None,
od: m.Deadline = None,
jen_verejne: bool = True,
resitele=None,
resitele: Iterable[m.Resitel] = None,
null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body
) -> dict[int, int]:
filtr = Q()

Loading…
Cancel
Save