Merge branch 'develop' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into develop
This commit is contained in:
		
						commit
						ea3b1a7791
					
				
					 36 changed files with 485 additions and 50 deletions
				
			
		
							
								
								
									
										20
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								Makefile
									
									
									
									
									
								
							|  | @ -102,10 +102,10 @@ deploy_prod: venv_check | ||||||
| sync_prod_flatpages: venv_check  | sync_prod_flatpages: venv_check  | ||||||
| 	@echo Downloading current version of flatpages from mamweb-prod. | 	@echo Downloading current version of flatpages from mamweb-prod. | ||||||
| 	ssh mam-web@gimli.ms.mff.cuni.cz \
 | 	ssh mam-web@gimli.ms.mff.cuni.cz \
 | ||||||
| 	"cd /akce/mam/www/mamweb-prod; . env/bin/activate; ./manage.py dumpdata flatpages --indent=2 > flat.json" | 	"cd /akce/mam/www/mamweb-prod; . env/bin/activate; ./manage.py dumpdata flatpages --indent=2 > flat.json; ./fix_json.py flat.json flat_fixed.json" | ||||||
| 	rsync -ave ssh mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/flat.json ./flat.json | 	rsync -ave ssh mam-web@gimli.ms.mff.cuni.cz:/akce/mam/www/mamweb-prod/flat_fixed.json data/flat.json | ||||||
| 	@echo "Applying downloaded flatpages." | 	@echo "Applying downloaded flatpages." | ||||||
| 	./manage.py loaddata flat.json | 	./manage.py loaddata data/flat.json | ||||||
| 	@echo "Done." | 	@echo "Done." | ||||||
| 
 | 
 | ||||||
| # Sync test media directory with production
 | # Sync test media directory with production
 | ||||||
|  | @ -114,24 +114,16 @@ sync_test_media: | ||||||
| 	@if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-test" ]; then echo "Only possible in /akce/mam/www/mamweb-test"; exit 1; fi | 	@if [ `readlink -f .` != "/aux/akce/mam/www/mamweb-test" ]; then echo "Only possible in /akce/mam/www/mamweb-test"; exit 1; fi | ||||||
| 	rsync -av --delete /akce/mam/www/mamweb-prod/media/ ./media | 	rsync -av --delete /akce/mam/www/mamweb-prod/media/ ./media | ||||||
| 
 | 
 | ||||||
| # Sync test database with production database
 | # Sync (with drop) test database with production database
 | ||||||
| sync_test_db: |  | ||||||
| 	@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi |  | ||||||
| 	pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql |  | ||||||
| 	pg_dump -Fc mam_prod > dump-prod.sql |  | ||||||
| 	pg_restore -c --if-exists -d mam_test dump-prod.sql |  | ||||||
| 	rm dump-prod.sql |  | ||||||
| 	@echo Done. |  | ||||||
| 	 |  | ||||||
| # Aggresive variant: destroy original mam_test db with 'DROP OWNED BY "mam-web";'
 |  | ||||||
| sync_test_db_aggressive: | sync_test_db_aggressive: | ||||||
| 	@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi | 	@if [ ${USER} != "mam-web" ]; then echo "Only possible by user mam-web"; exit 1; fi | ||||||
| 	pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql | 	pg_dump mam_test > dump-test-`date +"%Y%m%d_%H%M"`.sql | ||||||
| 	pg_dump -Fc mam_prod > dump-prod.sql | 	pg_dump -Fc mam_prod > dump-prod.sql | ||||||
| 	@# I am not sure which shell is used, so I am calling bash to make sure | 	@# I am not sure which shell is used, so I am calling bash to make sure | ||||||
| 	bash -c "psql mam_test <<< 'DROP OWNED BY \"mam-web\";'" | 	psql mam_test -c 'DROP OWNED BY "mam-web";' | ||||||
| 	pg_restore -c --if-exists -d mam_test dump-prod.sql | 	pg_restore -c --if-exists -d mam_test dump-prod.sql | ||||||
| 	rm dump-prod.sql | 	rm dump-prod.sql | ||||||
|  | 	psql mam_test -c "UPDATE django_site SET name='MaMweb (test)', domain='mam-test.ks.matfyz.cz' WHERE id=1" | ||||||
| 	@echo Done. | 	@echo Done. | ||||||
| 
 | 
 | ||||||
| # Sync test with production
 | # Sync test with production
 | ||||||
|  |  | ||||||
|  | @ -15,4 +15,4 @@ cd "$tmp" | ||||||
| wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" || true # wget nejspíš skončí s chybou, že něco nestáhl… | wget --spider -o "$logfile" -r -p -X '/soustredeni/*/fotogalerie/' "$@" || true # wget nejspíš skončí s chybou, že něco nestáhl… | ||||||
| 
 | 
 | ||||||
| echo "Result: (a last few lines of the file $logfile)" | echo "Result: (a last few lines of the file $logfile)" | ||||||
| sed -ne '/^Found [0-9]* broken links/,$ p' "$logfile" | sed -ne '/^Found [0-9]* broken link/,$ p' "$logfile" | ||||||
|  |  | ||||||
|  | @ -91,7 +91,7 @@ | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		"fields": { | 		"fields": { | ||||||
| 			"content": "<h2>Podzimní soustředění</h2>\r\n\r\n<p>se uskuteční 16. - 24. října 2021.</p>", | 			"content": "<h2>Jarní soustředění</h2>\r\n\r\n<p>se uskuteční 2. – 10. dubna 2022.</p>", | ||||||
| 			"enable_comments": false, | 			"enable_comments": false, | ||||||
| 			"registration_required": false, | 			"registration_required": false, | ||||||
| 			"sites": [ | 			"sites": [ | ||||||
|  | @ -136,7 +136,7 @@ | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		"fields": { | 		"fields": { | ||||||
| 			"content": "<h2>Zážitkové akce</h2>\r\n\r\n<h6>Letní a Zimní Škola Matematiky a Fyziky</h6>\r\n\r\n<p style=\"text-align: justify;\">ŠMFko je zážitková akce určená středoškolákům se zájmem o další sebevzdělání. Krom populárně naučných přednášek se na ŠMFku proběhneš venku, vyřádíš se ve sněhu, užiješ si veselý vnitřní program a taky se pobavíš během společného šarádění, lenošení nebo hraní na kytaru.</p>\r\n\r\n<h6>InterSoB</h6>\r\n\r\n<p style=\"text-align: justify;\">InterSoB je zábavná a poučná jednodenní soutěž středoškolských studentů, při které máte možnost podívat se netradičním způsobem do zákulisí Masarykovy univerzity, vyzkoušet si své schopnosti v mnoha různých oblastech, udělat si s kamarády zajímavý výlet po Brně a v neposlední řadě také poměřit svoje síly s dalšími týmy.</p>\r\n\r\n<h2>Další semináře</h2>\r\n\r\n<h6>Korespondenční Seminář z Programování</h6>\r\n\r\n<p style=\"text-align: justify;\">KSP je seminář určený pro studenty středních a základních škol, kteří mají zájem naučit se něco z oblasti algoritmů, logických úloh, programování a informatiky vůbec. Na své si však přijdou i příznivci matematiky (a vlastně libovolného přemýšlení), ježto oba obory mají mnoho společného.</p>\r\n\r\n<h6>FYzikální KOrespondenční Seminář</h6>\r\n\r\n<p style=\"text-align: justify;\">FYKOS pro vás představuje možnost si zajímavým způsobem rozšířit chápání fyziky a proniknout do dalších, dosud nepoznaných, oblastí této vědy. Cílem FYKOSu je rozvíjet fyzikální myšlení, protože člověk, který se umí nad (nejen fyzikálními) problémy zamyslet a cítí touhu dobrat se k nějakému řešení, se uplatní všude, kde si schopností lidského mozku cení.</p>\r\n\r\n<h6>Matematický korespondenční seminář PraSe (PRAžský SEminář)</h6>\r\n\r\n<p style=\"text-align: justify;\">Řešením úloh tohoto semináře získáš mnoho matematických znalostí a naučíš přesněji a srozumitelněji formulovat své myšlenky a závěry. Seminář je dobrou přípravou pro účast v nejrůznějších matematických soutěžích i pro další studium matematiky, ale schopnost logického myšlení, kterou si můžeš procvičit, se ti v životě bude hodit, i když se v něm třeba právě matematice věnovat nehodláš.</p>\r\n\r\n<h2>Pro mladší sourozence</h2>\r\n\r\n<h6>Pikomat</h6>\r\n\r\n<p style=\"text-align: justify;\">Pikomat je matematický korespondenční seminář určený žákům šestých až devátých tříd základních škol a studentům odpovídajících ročníků víceletých gymnázií. Spočívá v řešení několika úloh propojených příběhem. Na jaře se koná soustředění pro nejlepší řešitele, v létě pak tábor pro všechny zájemce.</p>\r\n\r\n<h6>Výfuk (VÝpočty Fyzikálních UKolů)</h6>\r\n\r\n<p style=\"text-align: justify;\">Výfuk je samostatný korespondenční seminář Matfyzu, který spadá pod Katedru didaktiky fyziky. Během školního roku kromě šesti sérií semináře organizátoři připravují i podzimní a jarní setkání, letní tábor a Náboj junior.</p>\r\n\r\n<h1>Databáze mimoškolních aktivit</h1>\r\n\r\n<p style=\"text-align: justify;\">Je-li ti výčet aktivit výše málo nebo tě žádná z nich nezaujala, doporučujeme navštívit web organizace <a href=\"https://prostredoskolaky.cz\">ProStředoškoláky</a>, jež zde připravila rozsáhlou databázi mimoškolních aktivit a akcí. Krom toho organizace pořádá soutěž <strong>Středoškolák roku</strong>, ve které každý rok oceňuje nejaktivnější středoškoláky. Věnuješ-li se tedy mimoškolně něčemu ve větším měřítku, neváhej se do soutěže přihlásit.</p>", | 			"content": "<h2>Zážitkové akce</h2>\r\n\r\n<a href=\"https://smf.mff.cuni.cz/\"><h6>Letní a Zimní Škola Matematiky a Fyziky</h6></a>\r\n\r\n<p style=\"text-align: justify;\">ŠMFko je zážitková akce určená středoškolákům se zájmem o další sebevzdělání. Krom populárně naučných přednášek se na ŠMFku proběhneš venku, vyřádíš se ve sněhu, užiješ si veselý vnitřní program a taky se pobavíš během společného šarádění, lenošení nebo hraní na kytaru.</p>\r\n\r\n<a href=\"https://intersob.math.muni.cz/\"><h6>InterSoB</h6></a>\r\n\r\n<p style=\"text-align: justify;\">InterSoB je zábavná a poučná jednodenní soutěž středoškolských studentů, při které máte možnost podívat se netradičním způsobem do zákulisí Masarykovy univerzity, vyzkoušet si své schopnosti v mnoha různých oblastech, udělat si s kamarády zajímavý výlet po Brně a v neposlední řadě také poměřit svoje síly s dalšími týmy.</p>\r\n\r\n<h2>Další semináře</h2>\r\n\r\n<a href=\"https://ksp.mff.cuni.cz/\"><h6>Korespondenční Seminář z Programování</h6></a>\r\n\r\n<p style=\"text-align: justify;\">KSP je seminář určený pro studenty středních a základních škol, kteří mají zájem naučit se něco z oblasti algoritmů, logických úloh, programování a informatiky vůbec. Na své si však přijdou i příznivci matematiky (a vlastně libovolného přemýšlení), ježto oba obory mají mnoho společného.</p>\r\n\r\n<a href=\"https://fykos.cz/\"><h6>FYzikální KOrespondenční Seminář</h6></a>\r\n\r\n<p style=\"text-align: justify;\">FYKOS pro vás představuje možnost si zajímavým způsobem rozšířit chápání fyziky a proniknout do dalších, dosud nepoznaných, oblastí této vědy. Cílem FYKOSu je rozvíjet fyzikální myšlení, protože člověk, který se umí nad (nejen fyzikálními) problémy zamyslet a cítí touhu dobrat se k nějakému řešení, se uplatní všude, kde si schopností lidského mozku cení.</p>\r\n\r\n\r\n<a href=\"https://prase.cz/\"><h6>Matematický korespondenční seminář PraSe (PRAžský SEminář)</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Řešením úloh tohoto semináře získáš mnoho matematických znalostí a naučíš přesněji a srozumitelněji formulovat své myšlenky a závěry. Seminář je dobrou přípravou pro účast v nejrůznějších matematických soutěžích i pro další studium matematiky, ale schopnost logického myšlení, kterou si můžeš procvičit, se ti v životě bude hodit, i když se v něm třeba právě matematice věnovat nehodláš.</p>\r\n\r\n<h2>Pro mladší sourozence</h2>\r\n\r\n\r\n<a href=\"https://pikomat.mff.cuni.cz/\"><h6>Pikomat</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Pikomat je matematický korespondenční seminář určený žákům šestých až devátých tříd základních škol a studentům odpovídajících ročníků víceletých gymnázií. Spočívá v řešení několika úloh propojených příběhem. Na jaře se koná soustředění pro nejlepší řešitele, v létě pak tábor pro všechny zájemce.</p>\r\n\r\n\r\n<a href=\"https://vyfuk.mff.cuni.cz/\"><h6>Výfuk (VÝpočty Fyzikálních UKolů)</h6></a>\r\n\r\n<p style=\"text-align: justify;\">Výfuk je samostatný korespondenční seminář Matfyzu, který spadá pod Katedru didaktiky fyziky. Během školního roku kromě šesti sérií semináře organizátoři připravují i podzimní a jarní setkání, letní tábor a Náboj junior.</p>\r\n\r\n<h1>Databáze mimoškolních aktivit</h1>\r\n\r\n<p style=\"text-align: justify;\">Je-li ti výčet aktivit výše málo nebo tě žádná z nich nezaujala, doporučujeme navštívit web organizace <a href=\"https://prostredoskolaky.cz\">ProStředoškoláky</a>, jež zde připravila rozsáhlou databázi mimoškolních aktivit a akcí. Krom toho organizace pořádá soutěž <strong>Středoškolák roku</strong>, ve které každý rok oceňuje nejaktivnější středoškoláky. Věnuješ-li se tedy mimoškolně něčemu ve větším měřítku, neváhej se do soutěže přihlásit.</p>", | ||||||
| 			"enable_comments": false, | 			"enable_comments": false, | ||||||
| 			"registration_required": false, | 			"registration_required": false, | ||||||
| 			"sites": [ | 			"sites": [ | ||||||
|  | @ -178,5 +178,20 @@ | ||||||
| 		}, | 		}, | ||||||
| 		"model": "flatpages.flatpage", | 		"model": "flatpages.flatpage", | ||||||
| 		"pk": 29 | 		"pk": 29 | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		"fields": { | ||||||
|  | 			"content": "<div class=\"content\">\r\n<h1>Nápověda ke korekturovátku</h1>\r\n\r\n<p>Korekturovátko slouží k přidávání korektur do PDF souborů. Umožňuje přidávat a komentovat korektury a označovat je jako k zanesení, zanesené nebo irelevantní. Rovněž umožňuje o PDF říci, že jsou právě zanášeny korektury nebo že je zastaralé.</p>\r\n\r\n<h2>Použití</h2>\r\n\r\n<p>Kliknu do PDF tam, kam chci zadat korekturu, napíši text a kliknu na Oprav! (nebo Ctrl-Enter). Korektura se zobrazí na pravé straně červeně. Pokud chci korekturu okomentovat, kliknu na ikonu <img src=\"http://127.0.0.1:8000/static/korektury/imgs/comment.png\" />, napíši komentář a kliknu na Oprav! (nebo Ctrl-Enter). Komentář se zobrazí pod původní korekturou.</p>\r\n\r\n<h2>Tlačítka u korektury</h2>\r\n\r\n<ul>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/delete.png\" /> – smazat korekturu</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/check.png\" /> – označt koreturu jako zanesenou</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/cross.png\" /> – označit korekturu jako irelevantní (není to chyba, nebude zaneseno)</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/tex.png\" /> – označt koreturu jako připravenou k zanesení</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/edit.png\" /> – upravit text korektury</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/comment.png\" /> – okomentovat korekturu</li>\r\n\t<li><img src=\"http://127.0.0.1:8000/static/korektury/imgs/hide.png\" /> – srolovat korekturu</li>\r\n</ul>\r\n\r\n<h2>Stavy</h2>\r\n\r\n<h3>Korektura</h3>\r\n\r\n<ul>\r\n\t<li>K vyřešení (červená) – bug report či návrh úpravy, probíhá diskuze, zatím nerozhodnuto</li>\r\n\t<li>Zanesená (modrá) – zanesená v TeXu</li>\r\n\t<li>Irelevantní (šedá) – není to chyba, nebude zanesena</li>\r\n\t<li>K zanesení (zelená) – rozhodnuto, čeká na zanesení do TeXu</li>\r\n</ul>\r\n\r\n<h3>PDF</h3>\r\n\r\n<ul>\r\n\t<li>Přidávání – probíhá přidávání korektur</li>\r\n\t<li>Zanášení (žluté pozadí) – probíhá zanášení korektur do TeXu</li>\r\n\t<li>Zastaralé (červené pozadí) – PDF je zastaralé, nepřidávat nové korektury</li>\r\n</ul>\r\n</div>", | ||||||
|  | 			"enable_comments": false, | ||||||
|  | 			"registration_required": false, | ||||||
|  | 			"sites": [ | ||||||
|  | 				1 | ||||||
|  | 			], | ||||||
|  | 			"template_name": "", | ||||||
|  | 			"title": "Nápověda ke korekturovátku", | ||||||
|  | 			"url": "/korektury/help/" | ||||||
|  | 		}, | ||||||
|  | 		"model": "flatpages.flatpage", | ||||||
|  | 		"pk": 30 | ||||||
| 	} | 	} | ||||||
| ] | ] | ||||||
|  |  | ||||||
							
								
								
									
										173
									
								
								docs/zapisy/2021-12-06-testovani_dokumentace_codereview.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								docs/zapisy/2021-12-06-testovani_dokumentace_codereview.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,173 @@ | ||||||
|  | # Postup zkrášlení kódu M&Mího webu | ||||||
|  | 
 | ||||||
|  | ## Obecně o webu | ||||||
|  | 
 | ||||||
|  | - Python, Django, spousta nějakých rozšíření, frontend HTML + CSS + trocha JS | ||||||
|  | - Velké břímě historie, kterou nejspíš nechceme zahodit | ||||||
|  |     - Změny v M&M někdy dost zamotají potřebný kód (tituly) | ||||||
|  | - Občas je potřeba dělat opravy rychle | ||||||
|  | 
 | ||||||
|  | ## Aktuální stav | ||||||
|  | 
 | ||||||
|  | - Zběsile zbastlený kód | ||||||
|  |     - „Co je to ‚single responsibility principle‘?“ ☺ | ||||||
|  | - Dost netriviální množství objektů a jejich vazeb | ||||||
|  |     - Dost možná do velké míry inherentní složitost | ||||||
|  | - Webaři aktuálně relativně zběhlí v programování (ale je potřeba myslet na to, že to tak být nemusí) | ||||||
|  | - Webaři stárnou (Jethro, Kristý, Anet) a mizí (Pavel, Káťa), je potřeba web připravit na předání | ||||||
|  | - Kód je rozdělený mezi orgy a jeden „nerozumí“ tomu, co druhý napsal (musí to vyčíst z kódu, nezná souvislosti, …) | ||||||
|  |     - Noví orgové se aktuálně musí ptát, což je jim nepříjemné a nutí je umět formulovat dotazy | ||||||
|  | 
 | ||||||
|  | ### Invarianty | ||||||
|  | 
 | ||||||
|  | - Není to práce, ale zábava-ish → libovolný proces nesmí být (moc) na obtíž. | ||||||
|  |     - I malé nepohodlí je potřeba vyvážit relativně velkým přínosem | ||||||
|  |         - nebo dostatečně zřejmou vidinou budoucího pohodlí / minimalizace nepohodlí | ||||||
|  | - Nejsme programátoři, spíš jsme bastlíři kódu (kteří znají rozumnou podmnožinu syntaxe Pythonu) | ||||||
|  |     - Zvlášť noví orgové | ||||||
|  |     - Nechceme cílit na mega-profi kód, je to nedosažitelný cíl | ||||||
|  |         - Nejspíš to do nějaké míry ubastlené bude pořád, ta míra závisí na zkušenosti aktuálních webařů | ||||||
|  |     - Nástroje nás nesmí moc mást. | ||||||
|  | - Děláme to zadarmo jeden večer v týdnu | ||||||
|  |     - Vývoj jde pomalu, často pomaleji než vývoj knihoven | ||||||
|  |         - → kód se rozbíjí i sám | ||||||
|  | - Běží to na Gimlim | ||||||
|  |     - Debian (old)stable → nemůžeme používat moc nové featury Pythonu | ||||||
|  |         - Aktuálně Python 3.7.3 | ||||||
|  | - Webaři jsou náhodně vzniklá skupina lidí. | ||||||
|  |     - Různé nástroje, různé operační systémy | ||||||
|  |     - Nechceme vynucovat konkrétní metody, multiplatformní nástroje asi požadovat můžeme | ||||||
|  |     - Na serveru může běžet cokoliv, co tam jde rozběhnout | ||||||
|  |     - Kód by neměl být moc složitý / matoucí / kompaktní (?) | ||||||
|  |         - případně fakt hodně okomentovaný | ||||||
|  |      | ||||||
|  | ## O co se snažíme | ||||||
|  | 
 | ||||||
|  | - Zpřístupnit vývoj novým webařům | ||||||
|  | - Umožnit chápání i cizího kódu co nejjednodušeji | ||||||
|  | - Nevzít si s sebou implementační tajemství ~~do hrobu~~ pryč z M&Mka | ||||||
|  | 
 | ||||||
|  | ```graphviz | ||||||
|  | digraph "Závislosti věcí" { | ||||||
|  |     ss -> sdil -> doku -> cs; | ||||||
|  |     ss -> cit ->ref -> cs; | ||||||
|  |     ref -> nrt -> tst -> cs; | ||||||
|  |     sdil -> cr -> cs | ||||||
|  |     ss -> nrt; | ||||||
|  |     ss[label="Současný stav",shape=box]; | ||||||
|  |     cit[label="čitelný kód"]; | ||||||
|  |     cs[label="Cílový stav",shape=box]; | ||||||
|  |     ref[label="refactoring"]; | ||||||
|  |     nrt[label="nerozbít to"]; | ||||||
|  |     tst[label="testy"]; | ||||||
|  |     doku[label="dokumentace (vývojářská)"]; | ||||||
|  |     sdil[label="chápání kódu ostatních / stávajícího"]; | ||||||
|  |     cr[label="code review"]; | ||||||
|  |     nrt,sdil,cit[shape=hexagon]; | ||||||
|  |     tst,doku,cr[color=blue]; | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Code review | ||||||
|  | 
 | ||||||
|  | Aktuálně: Pavel občas z rozmaru čte diffy; párkrát jsme zkoušeli [programovat v páru](https://mam.mff.cuni.cz/wiki/Web/Tipy/PairProgramming), je to relativně časově náročné. | ||||||
|  | 
 | ||||||
|  | - Nevynucovat | ||||||
|  | - Primární motivace je umožnit nějak vidět změny a případně k nim dávat komentáře, jak stylu „tohle mi není jasné“, tak i „tohle by chtělo přepsat“. | ||||||
|  | - Chceme hlavně vytvořit příležitost ke čtení cizího kódu a seznamování se s ním (i v zájmu zaučování nových webařů) | ||||||
|  | 
 | ||||||
|  | ### Názory | ||||||
|  | 
 | ||||||
|  | - spíš post-hoc | ||||||
|  | - Možná code-review toho, co jde na produkci | ||||||
|  | - Chceme umět komentovat konkrétní řádky kódu | ||||||
|  | 
 | ||||||
|  | - Gitea/gitlab? | ||||||
|  |     - Klikátko a barevný řádky a vyhledávání jsou fajn (gitlab, gitea) | ||||||
|  |         - Jethro: je fajn umět skočit na definici | ||||||
|  |     - Kombinace s CI | ||||||
|  |     - Pokud bude gitea v něčem nedostatečná, tak hrozí, že bude potřeba migrovat znovu, což je nepraktické | ||||||
|  |     - Gitlab je asi častější než gitea → je lepší názor si na to zvyknout | ||||||
|  |      | ||||||
|  | __Závěr:__ Zkusíme to hodit do GitLabu (nejspíš veřejného\*), zavedeme pull-requesty do `master` větve, náhodně se budeme přiřazovat a používat ho nějak intuitivně. | ||||||
|  | 
 | ||||||
|  | \*: Ve fakultním neumíme přidávat další uživatele, v KAMím zas možná není CI. | ||||||
|  | 
 | ||||||
|  | ## Testy | ||||||
|  | 
 | ||||||
|  | Aktuálně: pár testů na dohromady řádově 4 funkce, drobný pokus o TDD, jenž narazil na úskalí reálného světa. Lokální spuštění testů trvá relativně dlouho, nejspíš kvůli spoustě migrací. | ||||||
|  | 
 | ||||||
|  | - Potřebujeme ověřit, že to funguje a že to _pořád_ funguje | ||||||
|  | 
 | ||||||
|  | - CI? Coverage? | ||||||
|  | 
 | ||||||
|  | ### Názory | ||||||
|  | 
 | ||||||
|  | - PyTest je fajn | ||||||
|  | - Testovat frontend? | ||||||
|  |     - Jethro: při nasazení by se mohly dělat screenshoty celých vybraných stránek přes Selenium (nebo obdobné) a hlásit rozdíly | ||||||
|  | 
 | ||||||
|  | __Závěr:__ Backend testujeme PyTestem (`./manage.py test`), u frontendu výhledově zkusíme ty vizuální diffy a pak se uvidí, CI podle webového klikátka na code review. Bylo by dobré mít rozumná testdata, ať se nemusí mockovat moc věcí.  | ||||||
|  | 
 | ||||||
|  | ## Formát kódu | ||||||
|  | 
 | ||||||
|  | Aktuálně: Jakýsi coding style zhruba existuje, není popsaný, šíří se lidovou slovesností. | ||||||
|  | 
 | ||||||
|  | - Nesmí být striktně vynucovaný | ||||||
|  | - Musel by být hodně nastavitelný | ||||||
|  |     - Nechceme mít kód plný `#NOQA: WTF42` | ||||||
|  | - Nejspíš vždycky bude mít false positives (`seminar.utils.roman_numerals`) i false negatives (`seminar.models.tvorba.Cislo.posli_cislo_mailem`) | ||||||
|  |     - Možná dobrý sluha, ale určitě špatný pán (also: špatná zkušenost ☺) | ||||||
|  | - __Důsledek:__ Hrozí, že těch falešných varování bude moc, čímž to ztratí smysl úplně | ||||||
|  |     - Potenciálně by šlo aplikovat jen lokálně na změny? | ||||||
|  | 
 | ||||||
|  | ### Názory | ||||||
|  | 
 | ||||||
|  | - P: nemyslím si, že zvládneme mít „průhledný kód“ (dostatečně konzistentní kód, aby ho člověk přestal vnímat a spíš viděl myšlenky). | ||||||
|  | 
 | ||||||
|  | - P: Kecadla do kódu trochu zavání větším peklem než užitkem (takže black a flake8 jsou ze hry); isort možná dává smysl; je otázka, jestli použít mypy, ale typové značky v kódu spíš chceme (zvlášť u věcí, které nejsou prostě django view – typicky utils.) | ||||||
|  |     - J: Divné (=Pavlovo) groupování importů je spíš matoucí, spíš moc nepomáhalo | ||||||
|  | 
 | ||||||
|  | - P (doplněno zpětně): pydocstyle vynucuje PEP-257, který se možná tluče se sphinxem… | ||||||
|  | - P (též zpětně): Možná by se mohl dát použít pylint, tomu jde aspoň vysvětlit coding style a nerazí tupě PEP-8, ale nastavovat ho je asi větší porod, než jak moc pomůže… | ||||||
|  | 
 | ||||||
|  | __Závěr:__ Kecadla na formát spíš nechceme; isort by mohl být fajn, ale bylo by dobré mít rozdělené bloky „náš kód“, „standardní knihovna“ a „Django a další balíčky z PyPA“; u našeho kódu (utils, obecně ne-Django věci) chceme držet záměr (na úrovni dohody) psát signatury funkcí a v případě jejich přítomnosti se nechat varovat při porušení signatury. | ||||||
|  | 
 | ||||||
|  | - P (večerní prokrastinace): Bandit vypadá, že hlásí jen strašně obvious věci by default, nepřijde mi jako moc úžasné vylepšení. Do CI možná dobrý | ||||||
|  | 
 | ||||||
|  | ## Dokumentace | ||||||
|  | 
 | ||||||
|  | Aktuálně: něco málo je na wiki (`/Web/`), občas má nějaká funkce docstring, obecně je toho málo | ||||||
|  | 
 | ||||||
|  | ### Požadavky | ||||||
|  | 
 | ||||||
|  | - Jedno autoritativní místo a dá se najít | ||||||
|  | - Dostatečně „blízko“ kódu | ||||||
|  |     - nastavit mindset na psaní dokumentace „rovnou“? | ||||||
|  | - Umožnit i obecný text, ne jen komentáře kódu (modulů, funkcí) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Bonusy | ||||||
|  | 
 | ||||||
|  | - Zajišťování konzistence s uživatelskou dokumentací | ||||||
|  |     - aktuálně wiki | ||||||
|  | - Podpora i dalších jazyků (Vue, Javascript, CSS, možná django-templates) | ||||||
|  | 
 | ||||||
|  | ### Názory | ||||||
|  | 
 | ||||||
|  | - Jde to dělat sphinxem | ||||||
|  |     - Stínovlas by mohl vědět v CZ.NICu se sphinx jede. Případně zkusit zjistit, co umí jejich Akademie | ||||||
|  | - Free-textová dokumentace (architektura ap.) má být nezávislá na dokumentaci konkrétního kódu, ale je fajn, když se to pak spojí dohromady, jde-li to. | ||||||
|  |     - Lepší, když se obojí dá dělat stejným nástrojem | ||||||
|  | - Káťa má někdy pocit, že tráví spoustu času tím, že hledá který soubor vůbec upravit; to má v naší dokumentaci být. | ||||||
|  | 
 | ||||||
|  | __Závěr:__ Zkusíme použít sphinx (z nedostatku vlastních zkušeností a kvůli jeho popularitě), když se nám to nebude líbit, tak budeme řešit dál. Bylo by fajn najít nějaký workshop, bude to rychlejší nalejvárna než číst dokumentaci. V krátkodobém výhledu stáhneme vývojovou dokumentaci z webu do repozitáře a zprovozníme někde automatické buildy (aby se dala číst jako člověk a ne jako stroj). | ||||||
|  | 
 | ||||||
|  | ## Uživatelská dokumentace | ||||||
|  | 
 | ||||||
|  | - K: Dělat ji ve spolupráci s těma uživatelema | ||||||
|  | - P: Dávný Hedgedoc může případně sloužit jako základ osnovy | ||||||
|  | - K: Dost možná nevíme, co bolí víc a co míň | ||||||
|  | - Jethro: Musí být na wiki, jinak tím zmateme oržstvo | ||||||
|  | 
 | ||||||
|  | __Závěr:__ Dokumentace bude na wiki (<https://mam.mff.cuni.cz/wiki/Web_user/Dokumentace>, výhledově možná ve stejné složce v dalších souborech), bude vznikat podle našich pocitů a dotazů od ostatních; výhledově bude schůzka na ukázání featur nového webu a tam se dosbírají náměty na to, co sepsat. | ||||||
|  | @ -62,11 +62,12 @@ | ||||||
|         </div> |         </div> | ||||||
|       {% endwith %} |       {% endwith %} | ||||||
|     {% endif%} |     {% endif%} | ||||||
|  |     <span id="nahoru" class="kotva_obrazku"></span> | ||||||
|     <img src="{{obrazek.obrazek_stredni.url}}" |     <img src="{{obrazek.obrazek_stredni.url}}" | ||||||
|          height="{{vyska}}" |          height="{{vyska}}" | ||||||
|          width="{{sirka}}" |          width="{{sirka}}" | ||||||
|          alt="{{obrazek.popis}}" |          alt="{{obrazek.popis}}" | ||||||
|          class="obrazek" id="nahoru"> |          class="obrazek"> | ||||||
|           |           | ||||||
|     {% if obrazky_dalsi %} |     {% if obrazky_dalsi %} | ||||||
|       {% with obrazky_dalsi|first as dalsi_obrazek %} |       {% with obrazky_dalsi|first as dalsi_obrazek %} | ||||||
|  |  | ||||||
|  | @ -2,6 +2,8 @@ from django.contrib import admin | ||||||
| from reversion.admin import VersionAdmin | from reversion.admin import VersionAdmin | ||||||
| from korektury.models import KorekturovanePDF | from korektury.models import KorekturovanePDF | ||||||
| 
 | 
 | ||||||
|  | from django.core.mail import send_mail | ||||||
|  | from django.urls import reverse | ||||||
| 
 | 
 | ||||||
| # Register your models here. | # Register your models here. | ||||||
| class KorekturovanePDFAdmin(VersionAdmin): | class KorekturovanePDFAdmin(VersionAdmin): | ||||||
|  | @ -16,11 +18,31 @@ class KorekturovanePDFAdmin(VersionAdmin): | ||||||
| 	fieldsets = [ | 	fieldsets = [ | ||||||
| 			(None, | 			(None, | ||||||
| 				{'fields': | 				{'fields': | ||||||
| 					['pdf', 'cas', 'org', 'stran', 'nazev', 'komentar']}), | 					['pdf', 'cas', 'org', 'stran', 'nazev', 'komentar', 'poslat_mail']}), | ||||||
| 			# (u'PDF',       {'fields': ['pdf']}), | 			# (u'PDF',       {'fields': ['pdf']}), | ||||||
| 				] | 				] | ||||||
| 	list_display = ['nazev', 'cas', 'stran', 'org'] | 	list_display = ['nazev', 'cas', 'stran', 'org'] | ||||||
| 	list_filter = [] | 	list_filter = [] | ||||||
| 	search_fields = [] | 	search_fields = [] | ||||||
| 
 | 
 | ||||||
|  | 	def save_model(self, request, obj, form, change): | ||||||
|  | 		super().save_model(request, obj, form, change) | ||||||
|  | 		if not change and obj.poslat_mail: # Je nový a má se poslat mail | ||||||
|  | 			odkaz = request.build_absolute_uri(reverse('korektury', kwargs={'pdf': obj.id})) | ||||||
|  | 			odesilatel = 'korekturovatko-nove-pdf@mam.mff.cuni.cz' | ||||||
|  | 			prijemce = 'org@mam.mff.cuni.cz' | ||||||
|  | 			predmet = f'Nové korektury: {obj.nazev}' | ||||||
|  | 			text = f'''\ | ||||||
|  | V korekturovátku se objevil nový soubor: {obj.nazev} | ||||||
|  | {odkaz} | ||||||
|  | 
 | ||||||
|  | Popis souboru: | ||||||
|  | {obj.komentar} | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | S pozdravem a korekturám zdar! | ||||||
|  | Korekturovátko | ||||||
|  | ''' | ||||||
|  | 			send_mail(predmet,text,odesilatel,[prijemce]) | ||||||
|  | 
 | ||||||
| admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin) | admin.site.register(KorekturovanePDF, KorekturovanePDFAdmin) | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								korektury/migrations/0018_korekturovanepdf_poslat_mail.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								korektury/migrations/0018_korekturovanepdf_poslat_mail.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | # Generated by Django 2.2.24 on 2021-12-05 23:09 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations, models | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('korektury', '0017_auto_20190610_2358'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name='korekturovanepdf', | ||||||
|  |             name='poslat_mail', | ||||||
|  |             field=models.BooleanField(default=True, help_text='Určuje, zda se má o nově nahraném PDF poslat e-mail do mam-org. Při upravování existujícího souboru už nemá žádný vliv.', verbose_name='Poslat mail o novém PDF'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  | @ -63,6 +63,10 @@ class KorekturovanePDF(models.Model): | ||||||
| 	status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False, | 	status = models.CharField(u'stav PDF',max_length=16, choices=STATUS_CHOICES, blank=False, | ||||||
| 			default = STATUS_PRIDAVANI) | 			default = STATUS_PRIDAVANI) | ||||||
| 
 | 
 | ||||||
|  | 	poslat_mail = models.BooleanField('Poslat mail o novém PDF', default=True, | ||||||
|  | 			help_text='Určuje, zda se má o nově nahraném PDF poslat e-mail do mam-org. Při upravování existujícího souboru už nemá žádný vliv.', | ||||||
|  | 			) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 	#TODO Nepovinný foreign key k číslu | 	#TODO Nepovinný foreign key k číslu | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -89,6 +89,7 @@ form { | ||||||
| 	border: 4px solid red; | 	border: 4px solid red; | ||||||
| 	border-radius: 10px; | 	border-radius: 10px; | ||||||
| 	background-color: white; | 	background-color: white; | ||||||
|  | 	opacity: 80%; | ||||||
| } | } | ||||||
| .close-button{ | .close-button{ | ||||||
| 	background-color: yellow;	 | 	background-color: yellow;	 | ||||||
|  |  | ||||||
|  | @ -4,10 +4,11 @@ | ||||||
| <head> | <head> | ||||||
| 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | ||||||
|   <link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}" /> |   <link rel="stylesheet" title="opraf-css" type="text/css" media="screen, projection" href="{% static "korektury/opraf.css"%}" /> | ||||||
|  |     <link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet"> | ||||||
|   <script src="{% static "korektury/opraf.js"%}"></script> |   <script src="{% static "korektury/opraf.js"%}"></script> | ||||||
| 	<title>Korektury {{pdf.nazev}}</title> | 	<title>Korektury {{pdf.nazev}}</title> | ||||||
| </head> | </head> | ||||||
| <body {% if pdf.status == 'zanaseni'%} class="comitting" {% elif pdf.status == 'zastarale' %} class="deprecated" {% endif %} onload='place_comments()'> | <body class="{{ LOCAL_TEST_PROD }}web{% if pdf.status == 'zanaseni'%} comitting{% elif pdf.status == 'zastarale' %} deprecated{% endif %}" onload='place_comments()'> | ||||||
| 	<h1>Korektury {{pdf.nazev}}</h1> | 	<h1>Korektury {{pdf.nazev}}</h1> | ||||||
| 	{% if pdf.status == 'zanaseni' %} <h2> Probíhá zanášení korektur, zvažte, zda chcete přidávat nové </h2> {% endif %} | 	{% if pdf.status == 'zanaseni' %} <h2> Probíhá zanášení korektur, zvažte, zda chcete přidávat nové </h2> {% endif %} | ||||||
| 	{% if pdf.status == 'zastarale' %} <h2> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2> {% endif %} | 	{% if pdf.status == 'zastarale' %} <h2> Toto PDF je již zastaralé, nepřidávejte nové korektury </h2> {% endif %} | ||||||
|  |  | ||||||
|  | @ -22,9 +22,8 @@ | ||||||
| <ul> | <ul> | ||||||
|   {% for pdf in skupina.list %} |   {% for pdf in skupina.list %} | ||||||
| 	  <li><span {% if pdf.status == 'zanaseni'%} class="comitting-text" {% elif pdf.status == 'zastarale' %} class="deprecated-text" {% endif %}> | 	  <li><span {% if pdf.status == 'zanaseni'%} class="comitting-text" {% elif pdf.status == 'zastarale' %} class="deprecated-text" {% endif %}> | ||||||
| 	  	<b>{{ pdf.nazev }}</b> |         <b><a href="/korektury/{{pdf.id}}">{{ pdf.nazev }}</a></b> | ||||||
| 		<i>{{pdf.komentar}}</i> | 		<i>{{pdf.komentar}}</i> | ||||||
| 		<a href="/korektury/{{pdf.id}}">{{pdf.pdf.name}}</a> |  | ||||||
| 		(k opravě: {{pdf.k_oprave_cnt}},  | 		(k opravě: {{pdf.k_oprave_cnt}},  | ||||||
| 		opraveno: {{pdf.opraveno_cnt}}, | 		opraveno: {{pdf.opraveno_cnt}}, | ||||||
| 		není chyba: {{pdf.neni_chyba_cnt}}, | 		není chyba: {{pdf.neni_chyba_cnt}}, | ||||||
|  |  | ||||||
|  | @ -6,5 +6,4 @@ urlpatterns = [ | ||||||
| 	path('korektury/', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'), | 	path('korektury/', org_required(views.KorekturySeskupeneListView.as_view()), name='korektury_list'), | ||||||
| 	path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'), | 	path('korektury/zastarale/', org_required(views.KorekturyZastaraleListView.as_view()), name='korektury_stare_list'), | ||||||
| 	path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'), | 	path('korektury/<int:pdf>/', org_required(views.KorekturyView.as_view()), name='korektury'), | ||||||
| 	path('korektury/help/', org_required(views.KorekturyHelpView.as_view()), name='korektury-help'), |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -87,7 +87,7 @@ class KorekturyView(generic.TemplateView): | ||||||
| 
 | 
 | ||||||
| 			op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf) | 			op = Oprava(x=x,y=y, autor=autor, text=text, strana=strana,pdf = pdf) | ||||||
| 			op.save() | 			op.save() | ||||||
| 			self.send_email_notification_komentar(op, autor, text) | 			self.send_email_notification_komentar(op,autor) | ||||||
| 		elif (action == 'del'): | 		elif (action == 'del'): | ||||||
| 			id = int(q.get('id')) | 			id = int(q.get('id')) | ||||||
| 			op = Oprava.objects.get(id=id) | 			op = Oprava.objects.get(id=id) | ||||||
|  | @ -125,7 +125,7 @@ class KorekturyView(generic.TemplateView): | ||||||
| 			text = q.get('txt') | 			text = q.get('txt') | ||||||
| 			kom = Komentar(oprava=op,autor=autor,text=text) | 			kom = Komentar(oprava=op,autor=autor,text=text) | ||||||
| 			kom.save() | 			kom.save() | ||||||
| 			self.send_email_notification_komentar(op, autor, text) | 			self.send_email_notification_komentar(op,autor) | ||||||
| 		elif (action == 'update-comment'): | 		elif (action == 'update-comment'): | ||||||
| 			id = int(q.get('id')) | 			id = int(q.get('id')) | ||||||
| 			kom = Komentar.objects.get(id=id) | 			kom = Komentar.objects.get(id=id) | ||||||
|  | @ -151,21 +151,25 @@ class KorekturyView(generic.TemplateView): | ||||||
| 		context['autor'] = autor | 		context['autor'] = autor | ||||||
| 		return render(request, 'korektury/opraf.html',context) | 		return render(request, 'korektury/opraf.html',context) | ||||||
| 
 | 
 | ||||||
| 	def send_email_notification_komentar(self, oprava, autor, text): | 	def send_email_notification_komentar(self, oprava, autor): | ||||||
| 		''' Rozesle e-mail pri pridani komentare, | 		''' Rozesle e-mail pri pridani komentare / opravy, | ||||||
| 			ktery obsahuje text komentare. | 			ktery obsahuje text vlakna opravy. | ||||||
| 		''' | 		''' | ||||||
| 
 | 
 | ||||||
| 		# parametry e-mailu | 		# parametry e-mailu | ||||||
| 		#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk) | 		#odkaz = "https://mam.mff.cuni.cz/korektury/{}/".format(oprava.pdf.pk) | ||||||
| 		from django.urls import reverse | 		from django.urls import reverse | ||||||
| 		odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk})) | 		odkaz = self.request.build_absolute_uri(reverse('korektury', kwargs={'pdf': oprava.pdf.pk})) | ||||||
|  | 		odkaz = f"{odkaz}#op{oprava.id}-pointer" | ||||||
| 		from_email = 'korekturovatko@mam.mff.cuni.cz' | 		from_email = 'korekturovatko@mam.mff.cuni.cz' | ||||||
| 		subject = 'Nová korektura od {} v {}'.format(autor, | 		subject = 'Nová korektura od {} v {}'.format(autor, oprava.pdf.nazev) | ||||||
| 													 oprava.pdf.nazev) | 		texty = [(oprava.autor.osoba.plne_jmeno(),oprava.text)] | ||||||
|  | 		for kom in Komentar.objects.filter(oprava=oprava): | ||||||
|  | 			texty.append((kom.autor.osoba.plne_jmeno(),kom.text)) | ||||||
|  | 		optext = "\n\n\n".join([": ".join(t) for t in texty]) | ||||||
| 		text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\ | 		text = u"Text komentáře:\n\n{}\n\n=== Konec textu komentáře ===\n\ | ||||||
| 				\nodkaz do korekturovátka: {}\n\ | 				\nodkaz do korekturovátka: {}\n\ | ||||||
| 				\nVaše korekturovátko\n".format(text, odkaz) | 				\nVaše korekturovátko\n".format(optext, odkaz) | ||||||
| 
 | 
 | ||||||
| 		# Prijemci e-mailu | 		# Prijemci e-mailu | ||||||
| 		emails = set() | 		emails = set() | ||||||
|  | @ -193,6 +197,9 @@ class KorekturyView(generic.TemplateView): | ||||||
| 
 | 
 | ||||||
| 		if not settings.POSLI_MAILOVOU_NOTIFIKACI: | 		if not settings.POSLI_MAILOVOU_NOTIFIKACI: | ||||||
| 			print("Poslal bych upozornění na tyto adresy: ", " ".join(emails)) | 			print("Poslal bych upozornění na tyto adresy: ", " ".join(emails)) | ||||||
|  | 			print("---- Upozornění:") | ||||||
|  | 			print(text) | ||||||
|  | 			print("---- Konec upozornění") | ||||||
| 			return | 			return | ||||||
| 
 | 
 | ||||||
| 		send_mail(subject, text, from_email, list(emails)) | 		send_mail(subject, text, from_email, list(emails)) | ||||||
|  |  | ||||||
|  | @ -79,6 +79,7 @@ TEMPLATES = [ | ||||||
|                 'django.contrib.messages.context_processors.messages', |                 'django.contrib.messages.context_processors.messages', | ||||||
|                 'sekizai.context_processors.sekizai', |                 'sekizai.context_processors.sekizai', | ||||||
|                 'header_fotky.context_processors.vzhled', |                 'header_fotky.context_processors.vzhled', | ||||||
|  |                 'various.context_processors.rozliseni', | ||||||
|                 'various.context_processors.april', |                 'various.context_processors.april', | ||||||
|             ) |             ) | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|  | @ -28,7 +28,9 @@ INTERNAL_IPS = ['127.0.0.1'] | ||||||
| 
 | 
 | ||||||
| TEMPLATES[0]['OPTIONS']['debug'] = True | TEMPLATES[0]['OPTIONS']['debug'] = True | ||||||
| 
 | 
 | ||||||
| ALLOWED_HOSTS = ['127.0.0.1', '192.168.43.34'] | from ipaddress import ip_network | ||||||
|  | ALLOWED_HOSTS = [str(ip) for ip in ip_network('192.168.0.0/16')] | ||||||
|  | ALLOWED_HOSTS.append('127.0.0.1') | ||||||
| 
 | 
 | ||||||
| # Database | # Database | ||||||
| # https://docs.djangoproject.com/en/1.7/ref/settings/#databases | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases | ||||||
|  | @ -97,3 +99,4 @@ LOGGING = { | ||||||
| # E-maily posílat chceme, ale do terminálu :-) | # E-maily posílat chceme, ale do terminálu :-) | ||||||
| EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' | ||||||
| SEND_EMAIL_NOTIFICATIONS = True | SEND_EMAIL_NOTIFICATIONS = True | ||||||
|  | LOCAL_TEST_PROD = "local" | ||||||
|  |  | ||||||
|  | @ -67,3 +67,4 @@ LOGGING['handlers']['registration_error_log']['filename'] = '/home/mam-web/logs/ | ||||||
| 
 | 
 | ||||||
| # E-MAIL NOTIFICATIONS | # E-MAIL NOTIFICATIONS | ||||||
| POSLI_MAILOVOU_NOTIFIKACI = True | POSLI_MAILOVOU_NOTIFIKACI = True | ||||||
|  | LOCAL_TEST_PROD = "prod" | ||||||
|  |  | ||||||
|  | @ -76,3 +76,4 @@ EMAIL_BACKEND = 'various.mail_prefixer.PrefixingMailBackend' | ||||||
| # TODO Pouze na otestování testu… Zvolit konferu! | # TODO Pouze na otestování testu… Zvolit konferu! | ||||||
| # XXX: Je to pole, protože implementační detail backendu. | # XXX: Je to pole, protože implementační detail backendu. | ||||||
| TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz'] | TESTOVACI_EMAILOVA_KONFERENCE = ['betatest@mam.mff.cuni.cz'] | ||||||
|  | LOCAL_TEST_PROD = "test" | ||||||
|  |  | ||||||
|  | @ -1,3 +1,5 @@ | ||||||
|  | @import url("rozliseni.css"); | ||||||
|  | 
 | ||||||
| @font-face { | @font-face { | ||||||
| font-family: 'OpenSans'; | font-family: 'OpenSans'; | ||||||
| src: url("../fonts/OpenSans/OpenSans-Regular.ttf"); | src: url("../fonts/OpenSans/OpenSans-Regular.ttf"); | ||||||
|  | @ -1111,6 +1113,21 @@ div.zadani_termin .datum { | ||||||
|     float: right; |     float: right; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /* posune kotvu obrázku v galerii o oranžový pruh dolu, aby se pod ním obrázek neschovával */ | ||||||
|  | /* https://stackoverflow.com/questions/10732690/offsetting-an-html-anchor-to-adjust-for-fixed-header */ | ||||||
|  | .kotva_obrazku { | ||||||
|  | 	display: block; | ||||||
|  | 	position: relative; | ||||||
|  | 	width: 0; | ||||||
|  | 	height: 55px; /* viz #title */ | ||||||
|  | 	margin-top: -55px; /* viz #title */ | ||||||
|  | } | ||||||
|  | @media(max-width: 860px) { | ||||||
|  | 	.kotva_obrazku { | ||||||
|  | 		height: 3em; /* #FIXME nemám páru, jak zjistit výšku toho elementu */ | ||||||
|  | 		margin-top: -3em; /* #FIXME */ | ||||||
|  | 	} | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /**/ | /**/ | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										29
									
								
								mamweb/static/css/rozliseni.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								mamweb/static/css/rozliseni.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | /* Rozlišení mezi lokálním, test a produkčním webem */ | ||||||
|  | 
 | ||||||
|  | .localweb { | ||||||
|  |     border-left: 20px solid greenyellow; | ||||||
|  |     border-right: 20px solid greenyellow; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .localweb .login-bar { | ||||||
|  |     margin-left: -20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .testweb { | ||||||
|  |     border-left: 20px solid darkorange; | ||||||
|  |     border-right: 20px solid darkorange; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .testweb .login-bar { | ||||||
|  |     margin-left: -20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* Produkční web z pohledu superuživatele */ | ||||||
|  | .suprodweb { | ||||||
|  |     border-left: 20px solid red; | ||||||
|  |     border-right: 20px solid red; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .suprodweb .login-bar { | ||||||
|  |     margin-left: -20px; | ||||||
|  | } | ||||||
|  | @ -4,8 +4,11 @@ | ||||||
| {% block extrahead %} | {% block extrahead %} | ||||||
| <link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> | <link rel="shortcut icon" href="{% static 'favicon.ico' %}" type="image/x-icon"> | ||||||
| <script src="{% static 'js/jquery-1.11.1.js' %}"></script> | <script src="{% static 'js/jquery-1.11.1.js' %}"></script> | ||||||
|  | <link href="{% static 'css/rozliseni.css' %}?version=1" rel="stylesheet"> | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
|  | {% block bodyclass %}{{ LOCAL_TEST_PROD }}web{% endblock %} | ||||||
|  | 
 | ||||||
| {% block branding %} | {% block branding %} | ||||||
| <h1 id="site-name"><a href="/"> M&M GWP web </a></h1> | <h1 id="site-name"><a href="/"> M&M GWP web </a></h1> | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ | ||||||
|     {% block script %}{% endblock %} |     {% block script %}{% endblock %} | ||||||
| 
 | 
 | ||||||
|   </head> |   </head> | ||||||
|   <body class='{% if user.is_staff %}org-logged-in{% endif %}'> |   <body class='{{ LOCAL_TEST_PROD }}web{% if user.is_staff %} org-logged-in{% endif %}'> | ||||||
| 
 | 
 | ||||||
|     {% if user.is_staff %} |     {% if user.is_staff %} | ||||||
|       <div class="login-bar" > |       <div class="login-bar" > | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ urlpatterns = [ | ||||||
| 	path('comments_dj/', include('django_comments.urls')), | 	path('comments_dj/', include('django_comments.urls')), | ||||||
| 
 | 
 | ||||||
| 	# REST API | 	# REST API | ||||||
| 	path('api/', include(router.urls)), | #	path('api/', include(router.urls)), | ||||||
| 
 | 
 | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
| 
 | 
 | ||||||
| <form method=get action=../odevzdavatko> | <form method=get action=.> | ||||||
| {{ filtr.resitele }} | {{ filtr.resitele }} | ||||||
| {{ filtr.problemy }} | {{ filtr.problemy }} | ||||||
| Od: {{ filtr.reseni_od }} | Od: {{ filtr.reseni_od }} | ||||||
|  |  | ||||||
|  | @ -312,12 +312,15 @@ class PrehledOdevzdanychReseni(ListView): | ||||||
| 		resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first() | 		resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first() | ||||||
| 		qs = super().get_queryset() | 		qs = super().get_queryset() | ||||||
| 		qs = qs.filter(reseni__resitele__in=[resitel]) | 		qs = qs.filter(reseni__resitele__in=[resitel]) | ||||||
|  | 		# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení | ||||||
|  | 		qs = qs.order_by('reseni__cas_doruceni') | ||||||
| 		return qs | 		return qs | ||||||
| 	 | 	 | ||||||
| 	def get_context_data(self, *args, **kwargs): | 	def get_context_data(self, *args, **kwargs): | ||||||
| 		ctx = super().get_context_data(*args, **kwargs) | 		ctx = super().get_context_data(*args, **kwargs) | ||||||
| 		# Ročník určujeme podle čísla, do jehož deadlinu došlo řešení. | 		# Ročník určujeme podle čísla, do jehož deadlinu došlo řešení. | ||||||
| 		# Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/ | 		# Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/ | ||||||
|  | 		# TODO: Funkce deadline vrací deadliny v jiném ročníku, zvlášť pokud se vyrobí řešení až po deadlinu (třeba při poslání mailem) | ||||||
| 		podle_rocniku = [] | 		podle_rocniku = [] | ||||||
| 		for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: deadline(ho.reseni.cas_doruceni)[1].rocnik if deadline(ho.reseni.cas_doruceni) is not None else None): | 		for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: deadline(ho.reseni.cas_doruceni)[1].rocnik if deadline(ho.reseni.cas_doruceni) is not None else None): | ||||||
| 			podle_rocniku.append((rocnik, list(hodnoceni))) | 			podle_rocniku.append((rocnik, list(hodnoceni))) | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ from django.views.generic.base import TemplateView | ||||||
| from django.contrib.auth.models import User, Permission, Group | from django.contrib.auth.models import User, Permission, Group | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
|  | from django.http import HttpResponse | ||||||
| 
 | 
 | ||||||
| import seminar.models as s | import seminar.models as s | ||||||
| import seminar.models as m | import seminar.models as m | ||||||
|  | @ -14,6 +15,7 @@ from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm | ||||||
| 
 | 
 | ||||||
| from datetime import date | from datetime import date | ||||||
| import logging | import logging | ||||||
|  | import csv | ||||||
| 
 | 
 | ||||||
| from seminar.views import formularOKView | from seminar.views import formularOKView | ||||||
| from various.autentizace.views import LoginView | from various.autentizace.views import LoginView | ||||||
|  | @ -304,3 +306,71 @@ def profilView(request): | ||||||
| 		return ResitelView.as_view()(request) | 		return ResitelView.as_view()(request) | ||||||
| 	else: | 	else: | ||||||
| 		return LoginView.as_view()(request) | 		return LoginView.as_view()(request) | ||||||
|  | 
 | ||||||
|  | def dataResiteluCsvResponse(queryset, columns=None, with_header=True): | ||||||
|  | 	"""Pomocná funkce pro vracení dat řešitelů jako CSV. Musí dostat správný QuerySet, který dává Řešitele""" | ||||||
|  | 	# TODO: Možná nějak zobecnit i na Osoby? | ||||||
|  | 	# TODO: Nemá to spíš být class-based? Tohle je efektivně metoda "get", které ale chybí "get_queryset"… | ||||||
|  | 	 | ||||||
|  | 	default_columns = ( | ||||||
|  | 		'id', | ||||||
|  | 		'osoba__jmeno', | ||||||
|  | 		'osoba__prijmeni', | ||||||
|  | 		'osoba__prezdivka', | ||||||
|  | 		'osoba__email', | ||||||
|  | 		'osoba__telefon', | ||||||
|  | 		'osoba__user__username', | ||||||
|  | 		'osoba__datum_narozeni', | ||||||
|  | 		'osoba__pohlavi_muz', | ||||||
|  | 		'osoba__ulice', | ||||||
|  | 		'osoba__mesto', | ||||||
|  | 		'osoba__psc', | ||||||
|  | 		'osoba__stat', | ||||||
|  | 		'skola',	#FIXME: dává jen ID | ||||||
|  | 		'poznamka', | ||||||
|  | 		'osoba__poznamka', | ||||||
|  | 		'rok_maturity', | ||||||
|  | 		'zasilat', | ||||||
|  | 		'zasilat_cislo_emailem', | ||||||
|  | 		'osoba__datum_registrace', | ||||||
|  | 		'osoba__datum_souhlasu_udaje', | ||||||
|  | 		'osoba__datum_souhlasu_zasilani', | ||||||
|  | 		) | ||||||
|  | 	if columns is None: columns = default_columns | ||||||
|  | 
 | ||||||
|  | 	field_name_overrides = { | ||||||
|  | 		# Zrušení prefixu "osoba__" | ||||||
|  | 		'osoba__jmeno':                  'jmeno', | ||||||
|  | 		'osoba__prijmeni':               'prijmeni', | ||||||
|  | 		'osoba__prezdivka':              'prezdivka', | ||||||
|  | 		'osoba__email':                  'email', | ||||||
|  | 		'osoba__telefon':                'telefon', | ||||||
|  | 		'osoba__user__username':         'user', | ||||||
|  | 		'osoba__datum_narozeni':         'datum_narozeni', | ||||||
|  | 		'osoba__pohlavi_muz':            'pohlavi_muz', | ||||||
|  | 		'osoba__ulice':                  'ulice', | ||||||
|  | 		'osoba__mesto':                  'mesto', | ||||||
|  | 		'osoba__psc':                    'psc', | ||||||
|  | 		'osoba__stat':                   'stat', | ||||||
|  | 		'osoba__datum_registrace':       'datum_registrace', | ||||||
|  | 		'osoba__datum_souhlasu_udaje':   'datum_souhlasu_udaje', | ||||||
|  | 		'osoba__datum_souhlasu_zasilani':'datum_souhlasu_zasilani', | ||||||
|  | 		} | ||||||
|  | 	 | ||||||
|  | 	def get_field_name(column_name): | ||||||
|  | 		if column_name in field_name_overrides: | ||||||
|  | 			return field_name_overrides[column_name] | ||||||
|  | 		return column_name | ||||||
|  | 	 | ||||||
|  | 	response = HttpResponse(content_type='text/csv') | ||||||
|  | 	writer = csv.writer(response) | ||||||
|  | 	 | ||||||
|  | 	# První řádek je záhlaví | ||||||
|  | 	if with_header: | ||||||
|  | 		writer.writerow(map(get_field_name, columns)) | ||||||
|  | 	 | ||||||
|  | 	# Data: | ||||||
|  | 	queryset_list = queryset.values_list(*columns) | ||||||
|  | 	writer.writerows(queryset_list) | ||||||
|  | 
 | ||||||
|  | 	return response | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ django-solo | ||||||
| django-ckeditor | django-ckeditor | ||||||
| django-flat-theme | django-flat-theme | ||||||
| django-taggit | django-taggit | ||||||
| django-autocomplete-light | django-autocomplete-light>=3.9.0rc1 | ||||||
| django-crispy-forms | django-crispy-forms | ||||||
| django-imagekit | django-imagekit | ||||||
| django-polymorphic | django-polymorphic | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								seminar/migrations/0100_auto_20211129_2354.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								seminar/migrations/0100_auto_20211129_2354.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | # Generated by Django 2.2.24 on 2021-11-29 22:54 | ||||||
|  | 
 | ||||||
|  | from django.db import migrations, models | ||||||
|  | import seminar.models.tvorba | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  | 
 | ||||||
|  |     dependencies = [ | ||||||
|  |         ('seminar', '0099_auto_20210916_1509'), | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='cislo', | ||||||
|  |             name='pdf', | ||||||
|  |             field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=seminar.models.tvorba.OverwriteStorage(), upload_to=seminar.models.tvorba.cislo_pdf_filename, verbose_name='pdf'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| import logging | import logging | ||||||
|  | import os | ||||||
| from django.db import models | from django.db import models | ||||||
| 
 | 
 | ||||||
| from .base import SeminarModelBase | from .base import SeminarModelBase | ||||||
|  | @ -65,3 +66,5 @@ class Obrazek(SeminarModelBase): | ||||||
| 		upload_to='obrazky/%Y/%m/%d/', blank=True, null=True) | 		upload_to='obrazky/%Y/%m/%d/', blank=True, null=True) | ||||||
| 
 | 
 | ||||||
| 	# TODO placement hint - chci ho tady / pred textem / za textem | 	# TODO placement hint - chci ho tady / pred textem / za textem | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ from django.conf import settings | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.core.exceptions import ObjectDoesNotExist, ValidationError | from django.core.exceptions import ObjectDoesNotExist, ValidationError | ||||||
|  | from django.core.files.storage import FileSystemStorage | ||||||
| from django.utils.text import get_valid_filename | from django.utils.text import get_valid_filename | ||||||
| from django.utils.functional import cached_property | from django.utils.functional import cached_property | ||||||
| 
 | 
 | ||||||
|  | @ -37,6 +38,13 @@ from .base import SeminarModelBase | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
|  | class OverwriteStorage(FileSystemStorage): | ||||||
|  | 	""" Varianta FileSystemStorage, která v případě, že soubor cílového | ||||||
|  | 	jména již existuje, ho smaže a místo něj uloží soubor nový""" | ||||||
|  | 	def get_available_name(self,name, max_length=None): | ||||||
|  | 		if self.exists(name): | ||||||
|  | 			os.remove(os.path.join(self.location,name)) | ||||||
|  | 		return super().get_available_name(name,max_length) | ||||||
| 
 | 
 | ||||||
| @reversion.register(ignore_duplicates=True) | @reversion.register(ignore_duplicates=True) | ||||||
| class Rocnik(SeminarModelBase): | class Rocnik(SeminarModelBase): | ||||||
|  | @ -173,7 +181,7 @@ class Cislo(SeminarModelBase): | ||||||
| 		help_text='Neveřejná poznámka k číslu (plain text)') | 		help_text='Neveřejná poznámka k číslu (plain text)') | ||||||
| 
 | 
 | ||||||
| 	pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, | 	pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, | ||||||
| 		help_text='PDF čísla, které si mohou řešitelé stáhnout') | 		help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage()) | ||||||
| 
 | 
 | ||||||
| 	titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, | 	titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, | ||||||
| 		help_text='Obrázek titulní strany, generuje se automaticky') | 		help_text='Obrázek titulní strany, generuje se automaticky') | ||||||
|  | @ -390,10 +398,11 @@ class Problem(SeminarModelBase,PolymorphicModel): | ||||||
| 	# Implicitini implementace, jednotlivé dědící třídy si přepíšou | 	# Implicitini implementace, jednotlivé dědící třídy si přepíšou | ||||||
| 	@cached_property | 	@cached_property | ||||||
| 	def kod_v_rocniku(self): | 	def kod_v_rocniku(self): | ||||||
| 		if self.stav == 'zadany': | 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||||
| 			if self.nadproblem: | 			if self.nadproblem: | ||||||
| 				return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) | 				return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) | ||||||
| 			return str(self.kod) | 			return str(self.kod) | ||||||
|  | 		logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") | ||||||
| 		return '<Není zadaný>' | 		return '<Není zadaný>' | ||||||
| 
 | 
 | ||||||
| #	def verejne(self): | #	def verejne(self): | ||||||
|  | @ -467,10 +476,11 @@ class Tema(Problem): | ||||||
| 
 | 
 | ||||||
| 	@cached_property | 	@cached_property | ||||||
| 	def kod_v_rocniku(self): | 	def kod_v_rocniku(self): | ||||||
| 		if self.stav == 'zadany': | 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||||
| 			if self.nadproblem: | 			if self.nadproblem: | ||||||
| 				return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) | 				return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) | ||||||
| 			return "t{}".format(self.kod) | 			return "t{}".format(self.kod) | ||||||
|  | 		logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") | ||||||
| 		return '<Není zadaný>' | 		return '<Není zadaný>' | ||||||
| 
 | 
 | ||||||
| 	def save(self, *args, **kwargs): | 	def save(self, *args, **kwargs): | ||||||
|  | @ -501,11 +511,12 @@ class Clanek(Problem): | ||||||
| 
 | 
 | ||||||
| 	@cached_property | 	@cached_property | ||||||
| 	def kod_v_rocniku(self): | 	def kod_v_rocniku(self): | ||||||
| 		if self.stav == 'zadany': | 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||||
| # Nemělo by být potřeba | # Nemělo by být potřeba | ||||||
| #			if self.nadproblem: | #			if self.nadproblem: | ||||||
| #				return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) | #				return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) | ||||||
| 			return "c{}".format(self.kod) | 			return "c{}".format(self.kod) | ||||||
|  | 		logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") | ||||||
| 		return '<Není zadaný>' | 		return '<Není zadaný>' | ||||||
| 	 | 	 | ||||||
| 	def node(self): | 	def node(self): | ||||||
|  | @ -538,11 +549,12 @@ class Uloha(Problem): | ||||||
| 
 | 
 | ||||||
| 	@cached_property | 	@cached_property | ||||||
| 	def kod_v_rocniku(self): | 	def kod_v_rocniku(self): | ||||||
| 		if self.stav == 'zadany': | 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||||
| 			name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) | 			name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) | ||||||
| 			if self.nadproblem: | 			if self.nadproblem: | ||||||
| 				return self.nadproblem.kod_v_rocniku+name | 				return self.nadproblem.kod_v_rocniku+name | ||||||
| 			return name | 			return name | ||||||
|  | 		logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") | ||||||
| 		return '<Není zadaný>' | 		return '<Není zadaný>' | ||||||
| 
 | 
 | ||||||
| 	def save(self, *args, **kwargs): | 	def save(self, *args, **kwargs): | ||||||
|  |  | ||||||
|  | @ -119,7 +119,9 @@ | ||||||
| 
 | 
 | ||||||
|   {% if user.je_org %} |   {% if user.je_org %} | ||||||
|     <div class='mam-org-only'> |     <div class='mam-org-only'> | ||||||
|     <a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a> |     <p><a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a></p> | ||||||
|  |     {# FIXME: Sice to sem asi nepatří sémanticky, ale bylo to nejjednodušší… #} | ||||||
|  |     <p><a href='{% url 'seminar_rocnik_resitele_csv' rocnik=rocnik.rocnik %}' download>CSV export řešitelů</a></p> | ||||||
|     <h2>Výsledková listina včetně neveřejných bodů</h2> |     <h2>Výsledková listina včetně neveřejných bodů</h2> | ||||||
|         {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %} |         {% include "vysledkovky/vysledkovka_rocnik_neverejna.html" %} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  | @ -36,6 +36,11 @@ urlpatterns = [ | ||||||
| 		org_required(views.RocnikVysledkovkaView.as_view()), | 		org_required(views.RocnikVysledkovkaView.as_view()), | ||||||
| 		name='seminar_rocnik_vysledkovka' | 		name='seminar_rocnik_vysledkovka' | ||||||
| 	), | 	), | ||||||
|  | 	path( | ||||||
|  | 		'rocnik/<int:rocnik>/resitele.csv', | ||||||
|  | 		org_required(views.resiteleRocnikuCsvExportView), | ||||||
|  | 		name='seminar_rocnik_resitele_csv' | ||||||
|  | 	), | ||||||
| 	path( | 	path( | ||||||
| 		'cislo/<int:rocnik>.<str:cislo>/vysledkovka.tex', | 		'cislo/<int:rocnik>.<str:cislo>/vysledkovka.tex', | ||||||
| 		org_required(views.CisloVysledkovkaView.as_view()), | 		org_required(views.CisloVysledkovkaView.as_view()), | ||||||
|  |  | ||||||
|  | @ -217,10 +217,10 @@ def aktivniResitele(cislo, pouze_letosni=False): | ||||||
| 		zacatek_rocniku = False | 		zacatek_rocniku = False | ||||||
| 
 | 
 | ||||||
| 	if not zacatek_rocniku: | 	if not zacatek_rocniku: | ||||||
| 		return resi_v_rocniku(letos, cislo) | 		return resi_v_rocniku(letos, cislo).filter(rok_maturity__gte=letos.druhy_rok()) | ||||||
| 	else: | 	else: | ||||||
| 		# spojíme querysety s řešiteli loni a letos do daného čísla | 		# spojíme querysety s řešiteli loni a letos do daného čísla | ||||||
| 		return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct() | 		return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct().filter(rok_maturity__gte=letos.druhy_rok()) | ||||||
| 
 | 
 | ||||||
| def viewMethodSwitch(get, post): | def viewMethodSwitch(get, post): | ||||||
| 	""" | 	""" | ||||||
|  | @ -309,6 +309,15 @@ def podproblemy_v_cislu(cislo, problemy=None, hlavni_problemy=None): | ||||||
| 		else: | 		else: | ||||||
| 			podproblemy[-1].append(problem) | 			podproblemy[-1].append(problem) | ||||||
| 
 | 
 | ||||||
|  | 	for podproblem in podproblemy.keys(): | ||||||
|  | 		def int_or_zero(p): | ||||||
|  | 			try: | ||||||
|  | 				return int(p.kod) | ||||||
|  | 			except ValueError: | ||||||
|  | 				return 0 | ||||||
|  | 
 | ||||||
|  | 		podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero) | ||||||
|  | 
 | ||||||
| 	return podproblemy | 	return podproblemy | ||||||
| 
 | 
 | ||||||
| class TypDeadline(Enum): | class TypDeadline(Enum): | ||||||
|  |  | ||||||
|  | @ -390,6 +390,15 @@ class RocnikView(generic.DetailView): | ||||||
| 
 | 
 | ||||||
| 		return context | 		return context | ||||||
| 
 | 
 | ||||||
|  | def resiteleRocnikuCsvExportView(request, rocnik): | ||||||
|  | 	from personalni.views import dataResiteluCsvResponse | ||||||
|  | 	assert request.method in ('GET', 'HEAD') | ||||||
|  | 	return dataResiteluCsvResponse( | ||||||
|  | 		utils.resi_v_rocniku( | ||||||
|  | 			m.Rocnik.objects.get(rocnik=rocnik) | ||||||
|  | 		) | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| # FIXME: Pozor, výš je ještě jeden ProblemView! | # FIXME: Pozor, výš je ještě jeden ProblemView! | ||||||
| #class ProblemView(generic.DetailView): | #class ProblemView(generic.DetailView): | ||||||
|  |  | ||||||
|  | @ -1,3 +1,6 @@ | ||||||
|  | from django.conf import settings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def april(req): | def april(req): | ||||||
| 	if 'X-April' in req.headers: | 	if 'X-April' in req.headers: | ||||||
| 		try: | 		try: | ||||||
|  | @ -12,3 +15,10 @@ def april(req): | ||||||
| 		return {'april': today.year} | 		return {'april': today.year} | ||||||
| 	return {} | 	return {} | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | def rozliseni(request): | ||||||
|  | 	ltp = settings.LOCAL_TEST_PROD | ||||||
|  | 	if request.user.is_superuser and ltp == "prod": | ||||||
|  | 		ltp = "su" + ltp | ||||||
|  | 	return {"LOCAL_TEST_PROD": ltp} | ||||||
|  | 
 | ||||||
|  |  | ||||||
|  | @ -49,17 +49,21 @@ | ||||||
|             {% endfor %} |             {% endfor %} | ||||||
| </table> | </table> | ||||||
| 
 | 
 | ||||||
|  | <p>Po kliknutí na políčko v záhlaví tabulky se u daného problému zobrazí (/skryje) detailní rozpis, za které podproblémy řešitelé dostali body.</p> | ||||||
|  | 
 | ||||||
| {# TODELETE #} | {# TODELETE #} | ||||||
| <script> | <script> | ||||||
|     {% for p in problemy %} |     {% for p in problemy %} | ||||||
|  |         diplayed{{ forloop.counter }} = false; | ||||||
|         $(".podproblem{{ forloop.counter }}").css("display", "none") |         $(".podproblem{{ forloop.counter }}").css("display", "none") | ||||||
|         $("#problem{{ forloop.counter }}")[0].addEventListener('mouseover', podproblem{{ forloop.counter }}) |         $("#problem{{ forloop.counter }}")[0].addEventListener('click', podproblem{{ forloop.counter }}); | ||||||
|         $("#problem{{ forloop.counter }}")[0].addEventListener('mouseout', podproblem{{ forloop.counter }}end) |  | ||||||
|         function podproblem{{ forloop.counter }}(event) { |         function podproblem{{ forloop.counter }}(event) { | ||||||
|             $(".podproblem{{ forloop.counter }}").css("display", "") |             diplayed{{ forloop.counter }} = !diplayed{{ forloop.counter }}; | ||||||
|         } |             if (diplayed{{ forloop.counter }}) { | ||||||
|         function podproblem{{ forloop.counter }}end(event) { |                 $(".podproblem{{ forloop.counter }}").css("display", ""); | ||||||
|             $(".podproblem{{ forloop.counter }}").css("display", "none") |             } else { | ||||||
|  |                 $(".podproblem{{ forloop.counter }}").css("display", "none"); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -196,6 +196,7 @@ def data_vysledkovky_rocniku(rocnik, jen_verejne=True): | ||||||
| 	end = time.time() | 	end = time.time() | ||||||
| 	print("Vysledkovka rocniku",end-start) | 	print("Vysledkovka rocniku",end-start) | ||||||
| 
 | 
 | ||||||
|  | 	radky_vysledkovky = [radek for radek in radky_vysledkovky if radek.body_rocnik > 0] | ||||||
| 	return radky_vysledkovky, cisla | 	return radky_vysledkovky, cisla | ||||||
| 
 | 
 | ||||||
| class RadekVysledkovkyCisla(object): | class RadekVysledkovkyCisla(object): | ||||||
|  | @ -451,6 +452,7 @@ def data_vysledkovky_cisla(cislo): | ||||||
| 
 | 
 | ||||||
| 	# vytahané informace předáváme do kontextu | 	# vytahané informace předáváme do kontextu | ||||||
| 	pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] | 	pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] | ||||||
|  | 	radky_vysledkovky = [radek for radek in radky_vysledkovky if radek.body_rocnik > 0] | ||||||
| 	return ( | 	return ( | ||||||
| 		radky_vysledkovky, | 		radky_vysledkovky, | ||||||
| 		temata_a_spol, | 		temata_a_spol, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue
	
	 Kateřina Č
						Kateřina Č