diff --git a/Makefile b/Makefile index 94a377c4..fb967112 100644 --- a/Makefile +++ b/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 , 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 diff --git a/README.md b/README.md index 369dd30e..94b8cf2d 100644 --- a/README.md +++ b/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 , 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 (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). + + + +Jak si web pořídit +------ +Prosím přečti si podrobnější návod v (tady by bylo zbytečné +ho duplikovat). + +Jak web vyvíjet +---- + + +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 ). +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? + + +Úč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 :-)) diff --git a/api/tests/test_skola_autocomplete.py b/api/tests/test_skola_autocomplete.py index 91e5a82b..9fc4aee6 100644 --- a/api/tests/test_skola_autocomplete.py +++ b/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), diff --git a/api/urls.py b/api/urls.py index 2570ded9..a3b5a4aa 100644 --- a/api/urls.py +++ b/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'), diff --git a/api/views/autocomplete.py b/api/views/autocomplete.py index 33e083eb..601f4e35 100644 --- a/api/views/autocomplete.py +++ b/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): diff --git a/data/flat.json b/data/flat.json index 9dba5402..e621e266 100644 --- a/data/flat.json +++ b/data/flat.json @@ -16,7 +16,7 @@ }, { "fields": { - "content": "

Co je M&M?

\r\n\r\n

M&M je mezioborový korespondenční seminář pro středoškoláky

\r\n\r\n

Ptáš se, co že je to ten korespondenční seminář? Je to jednoduché, zamysli se nad zadanou úlohou či problémem, odevzdej řešení, my ti jej opravíme a pošleme zpět. Nabízíme témata z různých oblastí matematiky, fyziky, informatiky i dalších oborů, takže si u nás určitě najdeš to svoje. Při řešení si můžeš vyzkoušet, jak vypadá vědecká práce, a mnoho zajímavého se naučit. Podívej se na tipy v sekci Jak řešit a pusť se do toho! Můžeš se připojit kdykoliv během roku.

\r\n\r\n

M&M jsou témata

\r\n\r\n

Jádro M&M tvoří takzvaná témata, tedy texty doplněné o úlohy a náměty vyzývající k přemýšlení a experimentování. Přijímáme i originální přístup k problémům, takže můžeš například vyřešit fyzikální problém naprogramováním simulace. V minulých letech řešitelé sestavovali spektrometry, pomocí laserových ukazovátek měřili index lomu různých látek, zkoumali křivky popisující vývoj počtu cestujících v tramvaji v průběhu cesty nebo programovali strategie v jednoduché hře a pak je nechávali soupeřit v turnajích. Podívej se, jaká témata nabízíme právě teď.

\r\n\r\n

M&M je časopis

\r\n\r\n

Zadání a texty od organizátorů vydáváme formou časopisu. Kromě toho zde rovněž otiskujeme řešení a příspěvky našich řešitelů k tématům, články shrnující výsledky konfer – projektů, kterými se zabývali na soustředění, i výsledky jejich vlastního výzkumu. Díky tomu jsme si například mohli přečíst o Lichtenbergových obrazcích či gravitaci v placatém světě. Během ročníku obvykle vyjde šest čísel časopisu, prohlédnout si je můžeš v Archivu.

\r\n\r\n

M&M je soutěž

\r\n\r\n

Ke všem příspěvkům posíláme řešitelům zpětnou vazbu a udělujeme za ně body. Vítězové semináře se mohou těšit na zajímavé knihy a deskové hry, autora nejlepšího otištěného článku každoročně odměňujeme dortem. Úspěšným řešitelům jsou navíc prominuty přijímací zkoušky na MFF UK.

\r\n\r\n

M&M jsou soustředění

\r\n\r\n

Aktivní řešitelé mají možnost s námi za odměnu jet dvakrát do roka na soustředění. Pojeď taky! Budeš mít možnost strávit týden v přírodě plný odborného programu a nejrůznější zábavy s partou kamarádů. Čekají tě malé vědecké projekty, přednášky, workshopy, šifrovačka, výlet a mnoho dalšího. Chceš-li se dozvědět víc, přečti si stránku Soustředění nebo se podívej na fotky.

\r\n\r\n

M&M jsme my

\r\n\r\n

Spolu s řešiteli jsme tu i my, organizátoři. Většina z nás jsou studenti Matfyzu, tedy Matematicko-fyzikální fakulty Univerzity Karlovy. Společně se snažíme předávat dál radost z poznávání zajímavých zákoutí matematiky, fyziky a informatiky a budovat komunitu aktivních lidí, kteří se budou rádi scházet, ať už na soustředěních, víkendovkách či jiných akcích. Chceš nás poznat blíž? Podívej se na stránku Organizátoři nebo rovnou něco vyřeš a pojeď s námi na soustředění!

\r\n\r\n

Zaujali jsme tě? Zapoj se! Přečti si jak řešit a podívej se na aktuální zadání. Už se těšíme na tvé příspěvky!

", + "content": "

Co je M&M?

\r\n\r\n

M&M je mezioborový korespondenční seminář pro středoškoláky

\r\n\r\n

Ptáš se, co že je to ten korespondenční seminář? Je to jednoduché, zamysli se nad zadanou úlohou či problémem, odevzdej řešení, my ti jej opravíme a pošleme zpět. Nabízíme témata z různých oblastí matematiky, fyziky, informatiky i dalších oborů, takže si u nás určitě najdeš to svoje. Při řešení si můžeš vyzkoušet, jak vypadá vědecká práce, a mnoho zajímavého se naučit. Podívej se na tipy v sekci Jak řešit a pusť se do toho! Můžeš se připojit kdykoliv během roku.

\r\n\r\n

M&M jsou témata

\r\n\r\n

Jádro M&M tvoří takzvaná témata, tedy texty doplněné o úlohy a náměty vyzývající k přemýšlení a experimentování. Přijímáme i originální přístup k problémům, takže můžeš například vyřešit fyzikální problém naprogramováním simulace. V minulých letech řešitelé sestavovali spektrometry, pomocí laserových ukazovátek měřili index lomu různých látek, zkoumali křivky popisující vývoj počtu cestujících v tramvaji v průběhu cesty nebo programovali strategie v jednoduché hře a pak je nechávali soupeřit v turnajích. Podívej se, jaká témata nabízíme právě teď.

\r\n\r\n

M&M je časopis

\r\n\r\n

Zadání a texty od organizátorů vydáváme formou časopisu. Kromě toho zde rovněž otiskujeme řešení a příspěvky našich řešitelů k tématům, články shrnující výsledky konfer – projektů, kterými se zabývali na soustředění, i výsledky jejich vlastního výzkumu. Díky tomu jsme si například mohli přečíst o Lichtenbergových obrazcích či gravitaci v placatém světě. Během ročníku obvykle vyjde šest čísel časopisu, prohlédnout si je můžeš v Archivu.

\r\n\r\n

M&M je soutěž

\r\n\r\n

Ke všem příspěvkům posíláme řešitelům zpětnou vazbu a udělujeme za ně body. Vítězové semináře se mohou těšit na zajímavé knihy a deskové hry, autora nejlepšího otištěného článku každoročně odměňujeme dortem. Úspěšným řešitelům jsou navíc prominuty přijímací zkoušky na MFF UK.

\r\n\r\n

M&M jsou soustředění

\r\n\r\n

Aktivní řešitelé mají možnost s námi za odměnu jet dvakrát do roka na soustředění. Pojeď taky! Budeš mít možnost strávit týden v přírodě plný odborného programu a nejrůznější zábavy s partou kamarádů. Čekají tě malé vědecké projekty, přednášky, workshopy, šifrovačka, výlet a mnoho dalšího. Chceš-li se dozvědět víc, přečti si stránku Soustředění nebo se podívej na fotky.

\r\n\r\n

M&M jsme my

\r\n\r\n

Spolu s řešiteli jsme tu i my, organizátoři. Většina z nás jsou studenti Matfyzu, tedy Matematicko-fyzikální fakulty Univerzity Karlovy. Společně se snažíme předávat dál radost z poznávání zajímavých zákoutí matematiky, fyziky a informatiky a budovat komunitu aktivních lidí, kteří se budou rádi scházet, ať už na soustředěních, víkendovkách či jiných akcích. Chceš nás poznat blíž? Podívej se na stránku Organizátoři nebo rovnou něco vyřeš a pojeď s námi na soustředění!

\r\n\r\n

Zaujali jsme tě? Zapoj se! Přečti si jak řešit a podívej se na aktuální zadání. Už se těšíme na tvé příspěvky!

", "enable_comments": false, "registration_required": false, "sites": [ @@ -91,7 +91,7 @@ }, { "fields": { - "content": "

Jarní soustředění

\r\n\r\n

se uskuteční 2. – 10. dubna 2022.

", + "content": "

Jarní soustředění

\r\n\r\n

se uskuteční 25. března – 2. dubna 2023. Bližší informace sdělíme přihlášeným účastníkům.

\r\n\r\n

 

\r\n\r\n

Další akce

\r\n\r\n

Odkaz na M&M Google kalendář: https://calendar.google.com/calendar/embed?src=casopis.mam%40gmail.com&ctz=Europe%2FPrague

", "enable_comments": false, "registration_required": false, "sites": [ @@ -121,7 +121,7 @@ }, { "fields": { - "content": "

Odměny

\r\n\r\n

Největší odměnou za řešení M&M je účast na soustředění, kromě toho však každý rok oceňujeme pět nejlepších řešitelů knihou a deskovou hrou dle jejich výběru. Letos můžeš být mezi nimi i ty, stačí pilně řešit!

\r\n\r\n

\"\"

\r\n\r\n

Tituly

\r\n\r\n

Za pilné řešení semináře můžeš postupně získat různé titulyMM. Titul u tvého jména v časopisu značí, jakých úspěchů jsi za celou svoji kariéru v M&M zatím dosáhl/a. Kromě toho se s jeho dosažením vždy pojí nějaká drobná či větší odměna.

\r\n\r\n
    \r\n\t
  • Bc.MM (20 bodů) – propiska
  • \r\n\t
  • Mgr.MM (50 bodů) – reflexní páska
  • \r\n\t
  • Dr.MM (100 bodů) – hrneček
  • \r\n\t
  • Doc.MM (200 bodů) – deka
  • \r\n\t
  • Prof.MM (500 bodů) – mikina
  • \r\n\t
  • Akad.MM (1000 bodů) – tabule s nápisem Jsi fakt borec podepsaná všemi organizátory
  • \r\n
\r\n\r\n

\"\"

\r\n\r\n

Přijímací zkoušky na MFF

\r\n\r\n

Matematicko-fyzikální fakulta Univerzity Karlovy se rozhodla úspěšným řešitelům našeho korespondenčního semináře odpustit přijímací zkoušky. Konkrétně se to týká těch řešitelů, kteří získají za rok alespoň 100 bodů. Ti od nás dostanou „osvědčení úspěšného řešitele“, které pak mohou předložit fakultě.

\r\n\r\n

Dort za článek

\r\n\r\n

Autorovi nebo autorům nejlepšího otištěného článku v každém ročníku upečeme lahodný dort.

\r\n\r\n

 

\r\n\r\n

\"\"

", + "content": "

Odměny

\r\n\r\n

Největší odměnou za řešení M&M je účast na soustředění, kromě toho však každý rok oceňujeme pět nejlepších řešitelů knihou a deskovou hrou dle jejich výběru. Letos můžeš být mezi nimi i ty, stačí pilně řešit!

\r\n\r\n

\"\"

\r\n\r\n

Tituly

\r\n\r\n

Za pilné řešení semináře můžeš postupně získat různé titulyMM. Titul u tvého jména v časopisu značí, jakých úspěchů jsi za celou svoji kariéru v M&M zatím dosáhl/a. Kromě toho se s jeho dosažením vždy pojí nějaká drobná či větší odměna.

\r\n\r\n
    \r\n\t
  • Bc.MM (20 bodů) – propiska
  • \r\n\t
  • Mgr.MM (50 bodů) – reflexní páska
  • \r\n\t
  • Dr.MM (100 bodů) – hrneček
  • \r\n\t
  • Doc.MM (200 bodů) – deka
  • \r\n\t
  • Prof.MM (500 bodů) – mikina
  • \r\n\t
  • Akad.MM (1000 bodů) – tabule s nápisem Jsi fakt borec podepsaná všemi organizátory
  • \r\n
\r\n\r\n

\"\"

\r\n\r\n

Přijímací zkoušky na MFF

\r\n\r\n

Matematicko-fyzikální fakulta Univerzity Karlovy se rozhodla úspěšným řešitelům našeho korespondenčního semináře odpustit přijímací zkoušky. Konkrétně se to týká těch řešitelů, kteří získají za rok alespoň 100 bodů. Ti od nás dostanou „osvědčení úspěšného řešitele“, které pak mohou předložit fakultě.

\r\n\r\n

Dort za článek

\r\n\r\n

Autorovi nebo autorům nejlepšího otištěného článku v každém ročníku upečeme lahodný dort.

\r\n\r\n

 

\r\n\r\n

\"\"

", "enable_comments": false, "registration_required": false, "sites": [ @@ -166,7 +166,7 @@ }, { "fields": { - "content": "

Jak řešit

\r\n\r\n

V našem semináři simulujeme vědeckou práci. Vydáváme časopis, v němž se věnujeme různým tématům z oblasti matematiky, fyziky, informatiky a občas i jiných oborů.

\r\n\r\n

Témata

\r\n\r\n

Naším cílem je vás prostřednictvím témat seznámit se zajímavými zákoutími matematiky, fyziky a informatiky a inspirovat vás, abyste sami objevovali jejich krásy. Témata většinou obsahují průvodní text od organizátorů. Na něj navazují různé otázky, nad kterými můžeš v průběhu školního roku přemýšlet a bádat. o své výsledky se pak podělíš s námi, my tvou práci ohodnotíme a pošleme ti zpětnou vazbu. Vybrané příspěvky také otiskujeme, takže si je mohou přečíst ostatní řešitelé a reagovat na ně.

\r\n\r\n

Každý ročník vypisujeme zhruba čtyři až šest témat. Pokud si mezi nimi nevybereš, můžeš si položit vlastní otázku, přemýšlet, experimentovat a o svých výsledcích nám napsat článek. Nabízíme ti pár tipů, jak jej napsat pěkně a přehledně. Autora nejlepšího otištěného článku každoročně odměňujeme dortem.

\r\n\r\n

Otázky u jednotlivých témat jsou dvou druhů. Podle toho se bude lišit i formát tvého řešení, jak je popsáno níže.

\r\n\r\n

Úlohy

\r\n\r\n

Jak vypadají?

\r\n\r\n

V rámci témat jsou zadány uzavřené úlohy. Tyto úlohy bývají trochu těžší než obvyklé školní a jejich řešení často vyžaduje hlubší zamyšlení nebo nějaký trik. k jejich vyřešení by ti však měly stačit středoškolské znalosti a pochopení průvodního textu, který tyto znalosti rozvíjí.

\r\n\r\n

Jak na to?

\r\n\r\n

Vyřeš úlohu a pošli nám řešení. Nezapomeň na postup! U každé úlohy je uveden počet bodů za správné řešení, přiměřenou část však lze získat i za řešení neúplné. Neboj se proto poslat své úvahy, i když nedojdeš ke zdárnému konci. Částečné řešení je lepší než žádné. Pokud je naopak tvé řešení velmi elegantní, můžeš získat body navíc.

\r\n\r\n

Používáš-li v řešení nějaké pojmy, které nebyly v rámci témátka definovány a nepatří mezi běžné středoškolské znalosti, nezapomeň jen krátce definovat.

\r\n\r\n

Termín odevzdání

\r\n\r\n

Odevzdávání úloh je omezené termínem odeslání, poté je zveřejněno vzorové řešení. Tím se může stát i tvé pěkně sepsané řešení.

\r\n\r\n

Problémy

\r\n\r\n

Jak vypadají?

\r\n\r\n

Témata poskytují příležitost pro rozsáhlejší zamyšlení a podrobnější rozbor zadaných otázek. Řešení proto obvykle vyžaduje o něco víc prostoru než řešení úlohy. Zároveň je v závislosti na kvalitě štědřeji bodově hodnoceno.

\r\n\r\n

Jak na to?

\r\n\r\n

Řešením problému může být popis (případně videozáznam) provedeného experimentu a diskuze výsledků, popis teoretického výpočtu či napsaného programu, úvaha založená na dosud známých informacích, souhrn dalších (podle tebe zajímavých a nezodpovězených) otázek z daného okruhu nebo jakýkoliv další přístup, který příspívá k řešení problému. Můžeš navrhnout i vlastní problém týkající se tématu a jeho řešení. Řešení konkrétního problému jdoucí do hloubky je bodově hodnoceno výrazně lépe než souhrn krátce zmiňující kdeco.

\r\n\r\n

Řešení problému můžeš sepsat jako článek nebo krátký příspěvek. Napsat článek je trochu náročnější a je tedy lépe bodově hodnocen. Nabízíme ti pár tipů, jak jej pěkně a přehledně napsat. Neboj se však k tématu poslat i méně propracovaný příspěvek, klidně jeden odstavec shrnující tvé myšlenky.

\r\n\r\n

Zaslané řešení vedoucí tématu ohodnotí, případně zkoriguje – probere s tebou, co by bylo vhodné vylepšit, doplnit či upřesnit. Na závěr článek publikujeme na webu či dokonce v čísle.

\r\n\r\n

Inspiruj se a reaguj!

\r\n\r\n

Velmi důležitou vlastností témat je, že můžeš na články ostatních reagovat – rozvíjet je, nebo naopak bořit autorovy představy. Přesně to se děje i při skutečné vědecké práci. Proto se hodí poslat také částečná řešení, postřehy nebo nápady ohledně dalších otázek k tématu, které třeba nezvládneš vyřešit sám. Takovéto příspěvky dávají prostor ostatním a mohou je dál inspirovat a přivést k novým nápadům – ostatní zase svými příspěvky mohou inspirovat či navést k zajímavému nápadu tebe.

\r\n\r\n

Termín odevzdání

\r\n\r\n

Nad problémy k tématům sice můžeš většinou přemýšlet celý rok až do termínu odevzdání poslední série úloh, ale čím dříve nám článek pošleš, tím dříve na něj ostatní budou moci zareagovat. Zároveň se ti nestane, že ti tvůj nápad někdo vyfoukne.

\r\n\r\n

Jak poslat řešení

\r\n\r\n

Řešení jednotlivých témat sepiš samostatně, aby si je mohli vedoucí rozdělit. Nezapomeň uvést své jméno a číslo tématu i jednotlivých úloh a problémů. Svá řešení odevzdej elektronicky v odevzdávátku. Textové řešení ve formátu PDF můžeš doplnit libovolnou přílohou, například videem nebo kódem. Pokud bys měl s odevzdáním nějaký problém, tak nám neváhej napsat na mam@matfyz.cz.

\r\n\r\n

Termíny odevzdání

\r\n\r\n

Každé číslo má dva deadliny pro odevzdání řešení úloh, které jsou v něm otištěné. Pokud pošleš řešení do 1. deadlinu, stihneme ti poslat opravená řešení již s následujícím číslem časopisu, ve kterém se zároveň objeví body, které jsi za toto řešení dostal.

\r\n\r\n

Když tedy v prvním čísle zašleš řešení úlohy do 1. deadlinu, pak se body za něj získané objeví ve druhém čísle. Opravené řešení ti přijde e-mailem před vydáním druhého čísla nebo v obálce s druhým číslem.

\r\n\r\n

Tvá řešení se snažíme opravit co nejdříve a rovnou ti je poslat opravená e-mailem. Pokud ti přijde opravené řešení před 2. deadlinem, máš možnost si jej ještě vylepšit a poslat nám další verzi. Čím dříve své řešení pošleš, tím větší je šance, že jej stihneme opravit s předstihem. Nemůžeme ti to však zcela slíbit.

\r\n\r\n

Úlohy poslané mezi 1. a 2. deadlinem pravděpodobně do následujícího čísla opravit nestihneme, výjimečně se to ale stát může. 2. deadline je finální a úlohy, které přijdou po něm, už nebudou hodnoceny.

\r\n\r\n

Příklad: Pokud tedy úlohu zadanou v prvním čísle pošleš mezi 1. a 2. deadlinem, opravené řešení a body typicky dostaneš až se třetím číslem.

\r\n\r\n

Články

\r\n\r\n

Pokud se rozhodneš poslat řešení formou článku, budeme rádi, pokud to uděláš co nejdřív. Může se totiž stát, že bude potřeba abychom jej před vydáním společně ještě trochu upravili. Na to se hodí, abychom měli my i ty dostatek času. Vždy záleží na konkrétním případu, ale pokud pošleš článek až po 1. deadlinu, je velmi nepravděpodobné, že by se stihl objevit už v následujícím čísle.

\r\n\r\n

Spolupracuj

\r\n\r\n

Protože víc hlav víc ví, můžeš na tématech spolupracovat s ostatními řešiteli. i opravdové vědecké práce často publikuje kolektiv autorů. Pokud se na zaslaném řešení podílí n autorů, dostane každý z nich 3b/(n + 2) bodů, kde b je počet bodů, které by řešení získalo, pokud by mělo jednoho autora.

\r\n\r\n

Ke spolupráci můžeš využít e-mailové konference. Pokud pošleš e-mail na její adresu, přijde všem vedoucím tématu a řešitelům, kteří se jím také zabývají. Pošleš-li k danému tématu alespoň nějaké řešení, automaticky tě přidáme do příslušné konference. Pokud bys chtěl do konference přidat, i když jsi zatím nic neposlal, neboj se nám ozvat.

", + "content": "

Jak řešit

\r\n\r\n

V našem semináři simulujeme vědeckou práci. Vydáváme časopis, v němž se věnujeme různým tématům z oblasti matematiky, fyziky, informatiky a občas i jiných oborů.

\r\n\r\n

Témata

\r\n\r\n

Naším cílem je vás prostřednictvím témat seznámit se zajímavými zákoutími matematiky, fyziky a informatiky a inspirovat vás, abyste sami objevovali jejich krásy. Témata většinou obsahují průvodní text od organizátorů. Na něj navazují různé otázky, nad kterými můžeš v průběhu školního roku přemýšlet a bádat. o své výsledky se pak podělíš s námi, my tvou práci ohodnotíme a pošleme ti zpětnou vazbu. Vybrané příspěvky také otiskujeme, takže si je mohou přečíst ostatní řešitelé a reagovat na ně.

\r\n\r\n

Každý ročník vypisujeme zhruba čtyři až šest témat. Pokud si mezi nimi nevybereš, můžeš si položit vlastní otázku, přemýšlet, experimentovat a o svých výsledcích nám napsat článek. Nabízíme ti pár tipů, jak jej napsat pěkně a přehledně. Autora nejlepšího otištěného článku každoročně odměňujeme dortem.

\r\n\r\n

Otázky u jednotlivých témat jsou dvou druhů. Podle toho se bude lišit i formát tvého řešení, jak je popsáno níže.

\r\n\r\n

Úlohy

\r\n\r\n

Jak vypadají?

\r\n\r\n

V rámci témat jsou zadány uzavřené úlohy. Tyto úlohy bývají trochu těžší než obvyklé školní a jejich řešení často vyžaduje hlubší zamyšlení nebo nějaký trik. k jejich vyřešení by ti však měly stačit středoškolské znalosti a pochopení průvodního textu, který tyto znalosti rozvíjí.

\r\n\r\n

Jak na to?

\r\n\r\n

Vyřeš úlohu a pošli nám řešení. Nezapomeň na postup! U každé úlohy je uveden počet bodů za správné řešení, přiměřenou část však lze získat i za řešení neúplné. Neboj se proto poslat své úvahy, i když nedojdeš ke zdárnému konci. Částečné řešení je lepší než žádné. Pokud je naopak tvé řešení velmi elegantní, můžeš získat body navíc.

\r\n\r\n

Používáš-li v řešení nějaké pojmy, které nebyly v rámci témátka definovány a nepatří mezi běžné středoškolské znalosti, nezapomeň jen krátce definovat.

\r\n\r\n

Termín odevzdání

\r\n\r\n

Odevzdávání úloh je omezené termínem odeslání, poté je zveřejněno vzorové řešení. Tím se může stát i tvé pěkně sepsané řešení.

\r\n\r\n

Problémy

\r\n\r\n

Jak vypadají?

\r\n\r\n

Témata poskytují příležitost pro rozsáhlejší zamyšlení a podrobnější rozbor zadaných otázek. Řešení proto obvykle vyžaduje o něco víc prostoru než řešení úlohy. Zároveň je v závislosti na kvalitě štědřeji bodově hodnoceno.

\r\n\r\n

Jak na to?

\r\n\r\n

Řešením problému může být popis (případně videozáznam) provedeného experimentu a diskuze výsledků, popis teoretického výpočtu či napsaného programu, úvaha založená na dosud známých informacích, souhrn dalších (podle tebe zajímavých a nezodpovězených) otázek z daného okruhu nebo jakýkoliv další přístup, který příspívá k řešení problému. Můžeš navrhnout i vlastní problém týkající se tématu a jeho řešení. Řešení konkrétního problému jdoucí do hloubky je bodově hodnoceno výrazně lépe než souhrn krátce zmiňující kdeco.

\r\n\r\n

Řešení problému můžeš sepsat jako článek nebo krátký příspěvek. Napsat článek je trochu náročnější a je tedy lépe bodově hodnocen. Nabízíme ti pár tipů, jak jej pěkně a přehledně napsat. Neboj se však k tématu poslat i méně propracovaný příspěvek, klidně jeden odstavec shrnující tvé myšlenky.

\r\n\r\n

Zaslané řešení vedoucí tématu ohodnotí, případně zkoriguje – probere s tebou, co by bylo vhodné vylepšit, doplnit či upřesnit. Na závěr článek publikujeme na webu či dokonce v čísle.

\r\n\r\n

Inspiruj se a reaguj!

\r\n\r\n

Velmi důležitou vlastností témat je, že můžeš na články ostatních reagovat – rozvíjet je, nebo naopak bořit autorovy představy. Přesně to se děje i při skutečné vědecké práci. Proto se hodí poslat také částečná řešení, postřehy nebo nápady ohledně dalších otázek k tématu, které třeba nezvládneš vyřešit sám. Takovéto příspěvky dávají prostor ostatním a mohou je dál inspirovat a přivést k novým nápadům – ostatní zase svými příspěvky mohou inspirovat či navést k zajímavému nápadu tebe.

\r\n\r\n

Termín odevzdání

\r\n\r\n

Nad problémy k tématům sice můžeš většinou přemýšlet celý rok až do termínu odevzdání poslední série úloh, ale čím dříve nám článek pošleš, tím dříve na něj ostatní budou moci zareagovat. Zároveň se ti nestane, že ti tvůj nápad někdo vyfoukne.

\r\n\r\n

Jak poslat řešení

\r\n\r\n

Řešení jednotlivých témat sepiš samostatně, aby si je mohli vedoucí rozdělit. Nezapomeň uvést své jméno a číslo tématu i jednotlivých úloh a problémů. Svá řešení odevzdej elektronicky v odevzdávátku. Textové řešení ve formátu PDF můžeš doplnit libovolnou přílohou, například videem nebo kódem. Pokud bys měl s odevzdáním nějaký problém, tak nám neváhej napsat na mam@matfyz.cz.

\r\n\r\n

Termíny odevzdání

\r\n\r\n

Každé číslo má dva deadliny pro odevzdání řešení úloh, které jsou v něm otištěné. Pokud pošleš řešení do 1. deadlinu, stihneme ti poslat opravená řešení již s následujícím číslem časopisu, ve kterém se zároveň objeví body, které jsi za toto řešení dostal.

\r\n\r\n

Když tedy v prvním čísle zašleš řešení úlohy do 1. deadlinu, pak se body za něj získané objeví ve druhém čísle. Opravené řešení ti přijde e-mailem před vydáním druhého čísla nebo v obálce s druhým číslem.

\r\n\r\n

Tvá řešení se snažíme opravit co nejdříve a rovnou ti je poslat opravená e-mailem. Pokud ti přijde opravené řešení před 2. deadlinem, máš možnost si jej ještě vylepšit a poslat nám další verzi. Čím dříve své řešení pošleš, tím větší je šance, že jej stihneme opravit s předstihem. Nemůžeme ti to však zcela slíbit.

\r\n\r\n

Úlohy poslané mezi 1. a 2. deadlinem pravděpodobně do následujícího čísla opravit nestihneme, výjimečně se to ale stát může. 2. deadline je finální a úlohy, které přijdou po něm, už nebudou hodnoceny.

\r\n\r\n

Příklad: Pokud tedy úlohu zadanou v prvním čísle pošleš mezi 1. a 2. deadlinem, opravené řešení a body typicky dostaneš až se třetím číslem.

\r\n\r\n

Články

\r\n\r\n

Pokud se rozhodneš poslat řešení formou článku, budeme rádi, pokud to uděláš co nejdřív. Může se totiž stát, že bude potřeba abychom jej před vydáním společně ještě trochu upravili. Na to se hodí, abychom měli my i ty dostatek času. Vždy záleží na konkrétním případu, ale pokud pošleš článek až po 1. deadlinu, je velmi nepravděpodobné, že by se stihl objevit už v následujícím čísle.

\r\n\r\n

Spolupracuj

\r\n\r\n

Protože víc hlav víc ví, můžeš na tématech spolupracovat s ostatními řešiteli. i opravdové vědecké práce často publikuje kolektiv autorů. Pokud se na zaslaném řešení podílí n autorů, dostane každý z nich 3b/(n + 2) bodů, kde b je počet bodů, které by řešení získalo, pokud by mělo jednoho autora.

\r\n\r\n

Ke komunikaci s ostatními řešiteli i organizátory můžeš využít náš Discord.

", "enable_comments": false, "registration_required": false, "sites": [ @@ -181,7 +181,7 @@ }, { "fields": { - "content": "
\r\n

Nápověda ke korekturovátku

\r\n\r\n

Korekturovátko slouží k přidávání korektur do PDF souborů. Umožňuje přidávat a komentovat korektury a označovat je jako k zanesení, zanesené nebo irelevantní. Rovněž umožňuje o PDF říci, že jsou právě zanášeny korektury nebo že je zastaralé.

\r\n\r\n

Použití

\r\n\r\n

Kliknu do PDF tam, kam chci zadat korekturu, napíši text a kliknu na Oprav! (nebo Ctrl-Enter). Korektura se zobrazí na pravé straně červeně. Pokud chci korekturu okomentovat, kliknu na ikonu , napíši komentář a kliknu na Oprav! (nebo Ctrl-Enter). Komentář se zobrazí pod původní korekturou.

\r\n\r\n

Tlačítka u korektury

\r\n\r\n
    \r\n\t
  • – smazat korekturu
  • \r\n\t
  • – označt koreturu jako zanesenou
  • \r\n\t
  • – označit korekturu jako irelevantní (není to chyba, nebude zaneseno)
  • \r\n\t
  • – označt koreturu jako připravenou k zanesení
  • \r\n\t
  • – upravit text korektury
  • \r\n\t
  • – okomentovat korekturu
  • \r\n\t
  • – srolovat korekturu
  • \r\n
\r\n\r\n

Stavy

\r\n\r\n

Korektura

\r\n\r\n
    \r\n\t
  • K vyřešení (červená) – bug report či návrh úpravy, probíhá diskuze, zatím nerozhodnuto
  • \r\n\t
  • Zanesená (modrá) – zanesená v TeXu
  • \r\n\t
  • Irelevantní (šedá) – není to chyba, nebude zanesena
  • \r\n\t
  • K zanesení (zelená) – rozhodnuto, čeká na zanesení do TeXu
  • \r\n
\r\n\r\n

PDF

\r\n\r\n
    \r\n\t
  • Přidávání – probíhá přidávání korektur
  • \r\n\t
  • Zanášení (žluté pozadí) – probíhá zanášení korektur do TeXu
  • \r\n\t
  • Zastaralé (červené pozadí) – PDF je zastaralé, nepřidávat nové korektury
  • \r\n
\r\n
", + "content": "
\r\n

Nápověda ke korekturovátku

\r\n\r\n

Korekturovátko slouží k přidávání korektur do PDF souborů. Umožňuje přidávat a komentovat korektury a označovat je jako k zanesení, zanesené nebo že se nebude text měnit. Rovněž umožňuje o PDF říci, že jsou právě zanášeny korektury nebo že je zastaralé.

\r\n\r\n

Použití

\r\n\r\n

Kliknu do PDF tam, kam chci zadat korekturu, napíši text a kliknu na Oprav! (nebo Ctrl-Enter). Korektura se zobrazí na pravé straně červeně. Pokud chci korekturu okomentovat, kliknu na ikonu , napíši komentář a kliknu na Oprav! (nebo Ctrl-Enter). Komentář se zobrazí pod původní korekturou.

\r\n\r\n

Tlačítka u korektury

\r\n\r\n
    \r\n\t
  • – smazat korekturu
  • \r\n\t
  • – označt koreturu jako zanesenou
  • \r\n\t
  • – označit, že se text nemá měnit (není to chyba)
  • \r\n\t
  • – označt koreturu jako připravenou k zanesení
  • \r\n\t
  • – upravit text korektury
  • \r\n\t
  • – okomentovat korekturu
  • \r\n\t
  • – srolovat korekturu
  • \r\n
\r\n\r\n

Stavy

\r\n\r\n

Korektura

\r\n\r\n
    \r\n\t
  • K vyřešení (červená) – bug report či návrh úpravy, probíhá diskuze, zatím nerozhodnuto
  • \r\n\t
  • Zanesená (modrá) – zanesená v TeXu
  • \r\n\t
  • Irelevantní (šedá) – není to chyba, nebude zanesena
  • \r\n\t
  • K zanesení (zelená) – rozhodnuto, čeká na zanesení do TeXu
  • \r\n
\r\n\r\n

PDF

\r\n\r\n
    \r\n\t
  • Přidávání – probíhá přidávání korektur
  • \r\n\t
  • Zanášení (žluté pozadí) – probíhá zanášení korektur do TeXu
  • \r\n\t
  • Zastaralé (červené pozadí) – PDF je zastaralé, nepřidávat nové korektury
  • \r\n
\r\n
", "enable_comments": false, "registration_required": false, "sites": [ diff --git a/docs/april.rst b/docs/april.rst index 5f103357..8a479b20 100644 --- a/docs/april.rst +++ b/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 diff --git a/docs/conf.py b/docs/conf.py index d597faf2..75bca8d3 100644 --- a/docs/conf.py +++ b/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) diff --git a/docs/index.rst b/docs/index.rst index 10d6016f..5481bb88 100644 --- a/docs/index.rst +++ b/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 ``_ (resp. +``_), 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 +``_. + +Abychom uměli web vyvíjet, musíme ho většinou nejdřív umět +:doc:`naklonovat a spustit lokálně `. + +:doc:`struktura mamwebu ` se řídí hlavně djangem, ale snažíme se +také o oddělení jednotlivých částí do :doc:`samostatných aplikací +`. + +Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve +:doc:`sphinxu `. + .. toctree:: :caption: M&M web :maxdepth: 2 + :titlesonly: vyvoj sphinx - dalsi_soubory + skripty modules/modules + dalsi_soubory zapisy/zapisy diff --git a/docs/skripty.rst b/docs/skripty.rst new file mode 100644 index 00000000..8f9e1064 --- /dev/null +++ b/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 +``/management/commands/``, případně vestavěné, a volají se pomocí +``./manage.py ``. 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/``. + +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 + ``, 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 `` + Otestuje, že skript běží z konkrétní složky. Zejména použitelné s ``gimli_only`` a ``$TESTWEB`` +``safe_checkout_branch `` + 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 `_. +``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``. diff --git a/docs/sphinx.rst b/docs/sphinx.rst index fea2c1a7..6a9b7a53 100644 --- a/docs/sphinx.rst +++ b/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 diff --git a/docs/struktura.rst b/docs/struktura.rst new file mode 100644 index 00000000..439e39e9 --- /dev/null +++ b/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í ` 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 `. + +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. diff --git a/docs/tabulka_prerekvizit.rst b/docs/tabulka_prerekvizit.rst new file mode 100644 index 00000000..9dcce4c5 --- /dev/null +++ b/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." + diff --git a/docs/vyvoj.rst b/docs/vyvoj.rst index 438e8199..0d23972a 100644 --- a/docs/vyvoj.rst +++ b/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. \ No newline at end of file + +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 `_ +- `Python `_ + + - 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 `_, kde +bydlí gitový repozitář s kódem. + +.. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce + prerekvizit `. + +Doporučené +^^^^^^^^^^ + +- Python wheel (možná řeší problémy s potřebou ``-dev`` balíčků…) +- Editor / IDE podporující `Editorconfig `_ +- Uživatelská zkušenost s `produkční verzí webu `_ +- Účet v `Kanci `_ + +.. 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 +`_.) 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 ``_. + +Časté problémy +^^^^^^ + +- ``make/install_web`` vypíše ``Error: pg_config executable not found.``: + Chybí ``libpq-dev`` +- Chybová hláška obsahuje ``#include ``: 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 `` nahraje data ze souborů do databáze +- ``./manage.py dumpdata `` 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 +`_ + +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`` až ``o3:o`` +- Řešitelské účty: ``r:r``, ``r1:r`` až ``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, …) + diff --git a/docs/zapisy/zapisy.rst b/docs/zapisy/zapisy.rst index 1150a65c..7839449f 100644 --- a/docs/zapisy/zapisy.rst +++ b/docs/zapisy/zapisy.rst @@ -2,4 +2,7 @@ Zápisy ====== .. toctree:: - 2021-12-06-testovani_dokumentace_codereview \ No newline at end of file + :caption: Importy zápisů z Markdownu + :maxdepth: 1 + + 2021-12-06-testovani_dokumentace_codereview \ No newline at end of file diff --git a/header_fotky/context_processors.py b/header_fotky/context_processors.py index 0040cb5f..7f73faa7 100644 --- a/header_fotky/context_processors.py +++ b/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 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 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} diff --git a/korektury/migrations/0019_auto_20221205_2014.py b/korektury/migrations/0019_auto_20221205_2014.py new file mode 100644 index 00000000..6860e626 --- /dev/null +++ b/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'), + ), + ] diff --git a/korektury/models.py b/korektury/models.py index 34caba9a..ac82c14e 100644 --- a/korektury/models.py +++ b/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)') diff --git a/korektury/templates/korektury/opraf.html b/korektury/templates/korektury/opraf.html index c7a97317..d2ae11aa 100644 --- a/korektury/templates/korektury/opraf.html +++ b/korektury/templates/korektury/opraf.html @@ -146,7 +146,7 @@ {% endif %} {% if o.status != 'neni_chyba' %} - {% endif %} diff --git a/korektury/views.py b/korektury/views.py index d8d78c24..efeab19d 100644 --- a/korektury/views.py +++ b/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' diff --git a/make/README.md b/make/README.md new file mode 100644 index 00000000..16b77901 --- /dev/null +++ b/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 + diff --git a/make/deploy b/make/deploy new file mode 100755 index 00000000..b615f73f --- /dev/null +++ b/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 diff --git a/make/deploy_prod b/make/deploy_prod new file mode 100755 index 00000000..ec3110c6 --- /dev/null +++ b/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 diff --git a/make/init_local b/make/init_local new file mode 100755 index 00000000..75ee1ccf --- /dev/null +++ b/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 diff --git a/make/install b/make/install new file mode 120000 index 00000000..53e51b50 --- /dev/null +++ b/make/install @@ -0,0 +1 @@ +install_web \ No newline at end of file diff --git a/make/install_web b/make/install_web new file mode 100755 index 00000000..5ebf963d --- /dev/null +++ b/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' diff --git a/make/lib.sh b/make/lib.sh new file mode 100644 index 00000000..dd56ef73 --- /dev/null +++ b/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 " + 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 +} + diff --git a/make/push_compiled_vue_to_test b/make/push_compiled_vue_to_test new file mode 100755 index 00000000..99495a7b --- /dev/null +++ b/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 + " diff --git a/make/run b/make/run new file mode 100755 index 00000000..c3caf58d --- /dev/null +++ b/make/run @@ -0,0 +1,8 @@ +#!/bin/bash + +set -exuo pipefail +. make/lib.sh + +ensure_web_installed + +./manage.py runserver "$@" diff --git a/make/schema b/make/schema new file mode 100755 index 00000000..05e84b61 --- /dev/null +++ b/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 diff --git a/make/sync_prod_flatpages b/make/sync_prod_flatpages new file mode 100755 index 00000000..ca32c95b --- /dev/null +++ b/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 + diff --git a/make/sync_test b/make/sync_test new file mode 100755 index 00000000..68c6020d --- /dev/null +++ b/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 diff --git a/make/sync_test_db_aggressive b/make/sync_test_db_aggressive new file mode 100755 index 00000000..9acce93b --- /dev/null +++ b/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" + diff --git a/make/sync_test_media b/make/sync_test_media new file mode 100755 index 00000000..44b0e830 --- /dev/null +++ b/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 diff --git a/make/test b/make/test new file mode 100755 index 00000000..45de21f1 --- /dev/null +++ b/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 "$@" diff --git a/mamweb/admin.py b/mamweb/admin.py index 2ad1aaaa..b6924468 100644 --- a/mamweb/admin.py +++ b/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 diff --git a/mamweb/middleware.py b/mamweb/middleware.py index c1014257..7109423e 100644 --- a/mamweb/middleware.py +++ b/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 diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 5a610795..139190fa 100644 --- a/mamweb/settings_common.py +++ b/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č diff --git a/mamweb/settings_local.py b/mamweb/settings_local.py index 4cf76dc2..4d98cd38 100644 --- a/mamweb/settings_local.py +++ b/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 diff --git a/mamweb/settings_prod.py b/mamweb/settings_prod.py index 6a20ff8c..3a81c8c4 100644 --- a/mamweb/settings_prod.py +++ b/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 diff --git a/mamweb/settings_test.py b/mamweb/settings_test.py index 365664d2..eac5a7b4 100644 --- a/mamweb/settings_test.py +++ b/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'), ] diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 53e06967..63c5f527 100644 --- a/mamweb/static/css/mamweb.css +++ b/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; diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 65a8b7ac..223d807f 100644 --- a/odevzdavatko/forms.py +++ b/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'), diff --git a/odevzdavatko/static/odevzdavatko/check_for_detail.js b/odevzdavatko/static/odevzdavatko/check_for_detail.js new file mode 100644 index 00000000..b694dc88 --- /dev/null +++ b/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; +} diff --git a/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js b/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js new file mode 100644 index 00000000..a14c9f8f --- /dev/null +++ b/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 -// 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 + {% if edit %} + + + {% endif %}

Řešené problémy: {{ object.problem.all | join:", " }}

-

Řešitelé: {% for r in object.resitele.all %} {{ r }} ({{ r.osoba.email }}) -{% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

+{% if edit %} +

Ř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 %} +

+

+ {% maillink "Poslat mail všem řešitelům" bcc=maily_vsech_resitelu subject=predmetmailu %} +

+{% else %} +

Řešitelé: {{ object.resitele.all | join:", " }}

+{% endif %} {# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}

Forma: {{ object.get_forma_display }}

@@ -82,13 +43,13 @@ $(document).ready(function(){ {{ priloha.split | last }} {{ priloha.res_poznamka }} {{ priloha.vytvoreno }} - {# TODO: Orgo-poznámka, ideálně jako formulář #} {% endfor %} {% else %}

Žádné přílohy

{% endif %} + {% if edit %}
{# Poznámka #}

Neveřejná poznámka:

@@ -109,14 +70,14 @@ $(document).ready(function(){ {{ subform.body }} {{ subform.deadline_body }} {{ subform.feedback }} - Smazat + Smazat {% endfor %} - Přidat hodnocení
+ Přidat hodnocení
@@ -125,32 +86,23 @@ $(document).ready(function(){ - + + {% else %} +

Hodnocení:

+ + +{% for h in hodnoceni %} + + + + + +{% endfor %} +
ProblémBodyZpětná vazba od opravovatele
{{ h.problem }}{{ h.body }}{{ h.feedback | linebreaks }}
+ {% endif %} - {% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/detail_resitele.html b/odevzdavatko/templates/odevzdavatko/detail_resitele.html deleted file mode 100644 index fb0cb5fb..00000000 --- a/odevzdavatko/templates/odevzdavatko/detail_resitele.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends "base.html" %} -{% load static %} -{% load deadliny %} - -{% block content %} - -

Řešené problémy: {{ object.problem.all | join:", " }}

- -

Řešitelé: {% for r in object.resitele.all %} {{ r }} - {% if forloop.revcounter0 != 0 %}, {% endif %} {% endfor %}

- -{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #} -

Forma: {{ object.get_forma_display }}

- -

Doručeno {{ object.cas_doruceni }}, deadline: {{object.deadline_reseni | deadline_html }}

- -{# Soubory: #} -

Přílohy:

-{% if object.prilohy.all %} - - -{% for priloha in object.prilohy.all %} - - - - - {# TODO: Orgo-poznámka, ideálně jako formulář #} -{% endfor %} -
SouborŘešitelova poznámkaDatum
{{ priloha.split | last }}{{ priloha.res_poznamka }}{{ priloha.vytvoreno }}
-{% else %} -

Žádné přílohy

-{% endif %} - -{#

Poznámka:

#} -{#

{{ poznamka }}

#} - -{# Hodnocení: #} -

Hodnocení:

- -{# #} -{% for h in hodnoceni %} - - - - -{# #} - -{% endfor %} -
ProblémBodyZpětná vazba od opravovateleDeadline pro body
{{ h.problem }}{{ h.body }}{{ h.feedback }}{{ h.deadline_body }}
- -{% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html index 64ef92c1..739340c3 100644 --- a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html @@ -13,6 +13,8 @@

Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a třeba ti je i rychleji opravíme.

+

Pokud řešíte ve více lidech, je nutné 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 jednou (ne každý sám).

+
{% csrf_token %} diff --git a/odevzdavatko/templates/odevzdavatko/tabulka.html b/odevzdavatko/templates/odevzdavatko/tabulka.html index 419b4473..6d1232d2 100644 --- a/odevzdavatko/templates/odevzdavatko/tabulka.html +++ b/odevzdavatko/templates/odevzdavatko/tabulka.html @@ -4,6 +4,7 @@ {% block content %} +
{{ filtr.resitele }} {{ filtr.problemy }} @@ -64,4 +65,5 @@ Do data (včetně): {{ filtr.reseni_do }} location.assign(redirect); } +
{% endblock %} diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py index e15b3807..8c53de6b 100644 --- a/odevzdavatko/urls.py +++ b/odevzdavatko/urls.py @@ -26,9 +26,9 @@ urlpatterns = [ path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/rocnik//', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni///', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), - path('org/reseni/', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), + path('org/reseni/', 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/', resitel_or_org_required(views.ResitelReseniView.as_view()), name='odevzdavatko_resitel_reseni'), + path('resitel/reseni/', resitel_or_org_required(views.DetailReseniView.as_view()), name='odevzdavatko_resitel_reseni'), ] diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 6c232172..2390d27f 100644 --- a/odevzdavatko/views.py +++ b/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() diff --git a/personalni/admin.py b/personalni/admin.py index 8befe589..fc3cadd4 100644 --- a/personalni/admin.py +++ b/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'] diff --git a/personalni/forms.py b/personalni/forms.py index ea200267..f9d90182 100644 --- a/personalni/forms.py +++ b/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') diff --git a/personalni/templates/personalni/udaje/edit.html b/personalni/templates/personalni/udaje/edit.html index 5ec690d9..9091925d 100644 --- a/personalni/templates/personalni/udaje/edit.html +++ b/personalni/templates/personalni/udaje/edit.html @@ -44,6 +44,7 @@
{% 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 %} diff --git a/personalni/templates/personalni/udaje/gdpr.html b/personalni/templates/personalni/udaje/gdpr.html index 3e85de78..5d9af535 100644 --- a/personalni/templates/personalni/udaje/gdpr.html +++ b/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&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.

-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.

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. diff --git a/personalni/templates/personalni/udaje/prihlaska.html b/personalni/templates/personalni/udaje/prihlaska.html index 5e6434bf..33adba03 100644 --- a/personalni/templates/personalni/udaje/prihlaska.html +++ b/personalni/templates/personalni/udaje/prihlaska.html @@ -46,6 +46,7 @@

{% 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 %} diff --git a/personalni/views.py b/personalni/views.py index 94b90dea..a52f7f5f 100644 --- a/personalni/views.py +++ b/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'] diff --git a/seminar/migrations/0110_resitel_prezdivka.py b/seminar/migrations/0110_resitel_prezdivka.py new file mode 100644 index 00000000..51d25822 --- /dev/null +++ b/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'), + ), + ] diff --git a/seminar/migrations/0111_nikam2nezasilat_papirove.py b/seminar/migrations/0111_nikam2nezasilat_papirove.py new file mode 100644 index 00000000..683d71d7 --- /dev/null +++ b/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'), + ), + ] diff --git a/seminar/models/base.py b/seminar/models/base.py index 77c857a3..1069f165 100644 --- a/seminar/models/base.py +++ b/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 diff --git a/seminar/models/novinky.py b/seminar/models/novinky.py index f6ce4161..cee674a8 100644 --- a/seminar/models/novinky.py +++ b/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) + '] ' diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index 9ae161c5..c286558c 100644 --- a/seminar/models/odevzdavatko.py +++ b/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) diff --git a/seminar/models/personalni.py b/seminar/models/personalni.py index 28deec4d..9ac85e51 100644 --- a/seminar/models/personalni.py +++ b/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) diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index 7e04b213..f2c10e3e 100644 --- a/seminar/models/tvorba.py +++ b/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) diff --git a/seminar/templatetags/deadliny.py b/seminar/templatetags/deadliny.py index 95db664b..199a1eef 100644 --- a/seminar/templatetags/deadliny.py +++ b/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'{text}') + return mark_safe(f'{text}') @register.filter(name='zkrat_nazev_problemu') def zkrat_nazev_problemu(nazev,width): @@ -35,4 +35,4 @@ def zkrat_nazev_problemu(nazev,width): nazev = nazev[:width-1] + "..." else: nazev = nazev[:width] + "..." - return nazev \ No newline at end of file + return nazev diff --git a/seminar/testutils.py b/seminar/testutils.py index a13e82c8..7076d5f0 100644 --- a/seminar/testutils.py +++ b/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 diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index c5631430..4627989e 100644 --- a/seminar/views/views_all.py +++ b/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) diff --git a/soustredeni/admin.py b/soustredeni/admin.py index 11cb8d1d..091f9c59 100644 --- a/soustredeni/admin.py +++ b/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] diff --git a/treenode/admin.py b/treenode/admin.py index d2ff4409..92c85cd5 100644 --- a/treenode/admin.py +++ b/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 diff --git a/treenode/permissions.py b/treenode/permissions.py index 5503832f..b4022e19 100644 --- a/treenode/permissions.py +++ b/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') diff --git a/various/autentizace/utils.py b/various/autentizace/utils.py index 2341fbdd..d8bea060 100644 --- a/various/autentizace/utils.py +++ b/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í? - ) \ No newline at end of file + # 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í? + ) \ No newline at end of file diff --git a/various/log_filters.py b/various/log_filters.py index f20e032c..f037dcde 100644 --- a/various/log_filters.py +++ b/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): diff --git a/various/templatetags/__init__.py b/various/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/various/templatetags/mail.py b/various/templatetags/mail.py new file mode 100644 index 00000000..ecbb2a39 --- /dev/null +++ b/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, """ + 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'{text}' + return mark_safe(full_link) diff --git a/various/tests.py b/various/tests.py index 7ce503c2..0abf4e26 100644 --- a/various/tests.py +++ b/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'Hello') + self.assertEquals(maillink('Hello', to=['some@body.test']), r'Hello') + self.assertEquals( + maillink('Hello', to=['alice@test.test', 'bob@jinde.test']), + r'Hello', + ) + self.assertEquals( + maillink('Hello', to='some@body.test', attrs='class="trida" id="id"'), + r'Hello', + ) + # 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'Text odkazu', + ) + 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'Text', + ) + + 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') diff --git a/vysledkovky/utils.py b/vysledkovky/utils.py index f363abfd..3ff59fb1 100644 --- a/vysledkovky/utils.py +++ b/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()