Merge branch 'master' into kontrola-prav-orgu
This commit is contained in:
		
						commit
						dbf9fc67b9
					
				
					 53 changed files with 973 additions and 227 deletions
				
			
		|  | @ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer | |||
| 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): | ||||
| 		nastaveni = get_object_or_404(m.Nastaveni) | ||||
| 		rocnik = nastaveni.aktualni_rocnik | ||||
| 		# Od tohoto místa dál jsem zkoušel spoustu variací podle https://django-polymorphic.readthedocs.io/en/stable/advanced.html | ||||
| 		temaQ = Q(Tema___rocnik = rocnik, stav=m.Problem.STAV_ZADANY) | ||||
| 		ulohaQ = Q(Uloha___cislo_zadani__rocnik = rocnik, stav=m.Problem.STAV_ZADANY) | ||||
| 		clanekQ = Q(Clanek___cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY) | ||||
| 		qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ) | ||||
| 		#print(temata, ulohy, clanky) | ||||
| 		#ulohy.union(temata, all=True) | ||||
| 		#print(ulohy) | ||||
| 		#ulohy.union(clanky, all=True) | ||||
| 		#print(ulohy) | ||||
| 		#qs = ulohy | ||||
| 		print(qs) | ||||
| 		qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY) | ||||
| 		if self.q: | ||||
| 			qs = qs.filter( | ||||
| 					Q(nazev__icontains=self.q)) | ||||
| 
 | ||||
| 		nadproblem_id = int(self.forwarded.get("nadproblem_id", -1)) | ||||
| 		if nadproblem_id != -1: | ||||
| 			# Seřadíme tak, aby ty s nadproblem==None byly dole (větší motivace tam naklikat konkrétní úlohy) a pak nějak rozumně. | ||||
| 			# Tohle je řazení pro odevzdávátko, kde je definován nadproblém, proto je to v tomto ifu. (Jinde si to netroufám řadit) | ||||
| 			qs = qs.order_by("nadproblem", "kod", "nazev") | ||||
| 			qs = list(filter(lambda problem: problem.hlavni_problem.id == nadproblem_id, qs)) | ||||
| 		return qs | ||||
| 
 | ||||
| class ProblemAutocomplete(autocomplete.Select2QuerySetView): | ||||
|  |  | |||
|  | @ -437,7 +437,7 @@ | |||
| 			"insitetree": true, | ||||
| 			"parent": 21, | ||||
| 			"sort_order": 36, | ||||
| 			"title": "Poslat řešení", | ||||
| 			"title": "Nahrát řešení", | ||||
| 			"tree": 1, | ||||
| 			"url": "seminar_nahraj_reseni", | ||||
| 			"urlaspattern": true | ||||
|  | @ -476,9 +476,9 @@ | |||
| 			"access_perm_type": 1, | ||||
| 			"access_permissions": [ | ||||
| 				[ | ||||
| 					"change_hodnoceni", | ||||
| 					"seminar", | ||||
| 					"hodnoceni" | ||||
| 					"org", | ||||
| 					"auth", | ||||
| 					"user" | ||||
| 				] | ||||
| 			], | ||||
| 			"access_restricted": true, | ||||
|  | @ -719,7 +719,7 @@ | |||
| 			"insitetree": true, | ||||
| 			"parent": 21, | ||||
| 			"sort_order": 36, | ||||
| 			"title": "Nahrát řešení", | ||||
| 			"title": "Vložit řešení", | ||||
| 			"tree": 1, | ||||
| 			"url": "seminar_vloz_reseni", | ||||
| 			"urlaspattern": true | ||||
|  | @ -1026,6 +1026,36 @@ | |||
| 		"model": "sitetree.treeitem", | ||||
| 		"pk": 51 | ||||
| 	}, | ||||
| 	{ | ||||
| 		"fields": { | ||||
| 			"access_guest": false, | ||||
| 			"access_loggedin": false, | ||||
| 			"access_perm_type": 1, | ||||
| 			"access_permissions": [ | ||||
| 				[ | ||||
| 					"resitel", | ||||
| 					"auth", | ||||
| 					"user" | ||||
| 				] | ||||
| 			], | ||||
| 			"access_restricted": true, | ||||
| 			"alias": null, | ||||
| 			"description": "", | ||||
| 			"hidden": false, | ||||
| 			"hint": "", | ||||
| 			"inbreadcrumbs": true, | ||||
| 			"inmenu": true, | ||||
| 			"insitetree": true, | ||||
| 			"parent": 23, | ||||
| 			"sort_order": 52, | ||||
| 			"title": "Nahrát řešení k nadproblému {{nadproblem_id}}", | ||||
| 			"tree": 1, | ||||
| 			"url": "seminar_nahraj_reseni nadproblem_id", | ||||
| 			"urlaspattern": true | ||||
| 		}, | ||||
| 		"model": "sitetree.treeitem", | ||||
| 		"pk": 52 | ||||
| 	}, | ||||
| 	{ | ||||
| 		"fields": { | ||||
| 			"access_guest": false, | ||||
|  | @ -1041,13 +1071,13 @@ | |||
| 			"inmenu": true, | ||||
| 			"insitetree": true, | ||||
| 			"parent": 28, | ||||
| 			"sort_order": 52, | ||||
| 			"sort_order": 53, | ||||
| 			"title": "Přidat PDF", | ||||
| 			"tree": 1, | ||||
| 			"url": "/admin/korektury/korekturovanepdf/add/", | ||||
| 			"urlaspattern": false | ||||
| 		}, | ||||
| 		"model": "sitetree.treeitem", | ||||
| 		"pk": 52 | ||||
| 		"pk": 53 | ||||
| 	} | ||||
| ] | ||||
| ] | ||||
|  |  | |||
|  | @ -64,6 +64,36 @@ | |||
| 		"ct_app_label": "galerie", | ||||
| 		"ct_model": "obrazek" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_komentar", | ||||
| 		"ct_app_label": "korektury", | ||||
|  | @ -224,6 +254,21 @@ | |||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "clanek" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_konfera", | ||||
| 		"ct_app_label": "seminar", | ||||
|  | @ -304,41 +349,21 @@ | |||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "novinky" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_organizator", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "organizator" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_organizator", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "organizator" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "delete_organizator", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "organizator" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_organizator", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "organizator" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_osoba", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "osoba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_osoba", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "osoba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "delete_osoba", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "osoba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_osoba", | ||||
| 		"ct_app_label": "seminar", | ||||
|  | @ -404,21 +429,11 @@ | |||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "problem" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_resitel", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "resitel" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_resitel", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "resitel" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "delete_resitel", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "resitel" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_resitel", | ||||
| 		"ct_app_label": "seminar", | ||||
|  | @ -603,50 +618,5 @@ | |||
| 		"codename": "view_taggeditem", | ||||
| 		"ct_app_label": "taggit", | ||||
| 		"ct_model": "taggeditem" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_fotkaheader", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaheader" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_fotkaurlvazba", | ||||
| 		"ct_app_label": "header_fotky", | ||||
| 		"ct_model": "fotkaurlvazba" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "add_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "change_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"codename": "view_deadline", | ||||
| 		"ct_app_label": "seminar", | ||||
| 		"ct_model": "deadline" | ||||
| 	} | ||||
| ] | ||||
| ] | ||||
|  | @ -28,7 +28,7 @@ Generuje se za pomocí:: | |||
| 
 | ||||
| nebo (v případě meníčka):: | ||||
| 
 | ||||
|     ./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json | ||||
|     ./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json | ||||
|     ./fix_json.py data/sitetree_new.json data/sitetree.json | ||||
| 
 | ||||
| deploy_v2 | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve | |||
|    :titlesonly: | ||||
| 
 | ||||
|    vyvoj | ||||
|    zavislosti | ||||
|    sphinx | ||||
|    skripty | ||||
|    modules/modules | ||||
|  |  | |||
|  | @ -1,25 +0,0 @@ | |||
| .. 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." | ||||
| 
 | ||||
|  | @ -37,7 +37,7 @@ Kromě toho je potřeba mít účet na `Gitee <https://gitea.ks.matfyz.cz>`_, kd | |||
| bydlí gitový repozitář s kódem. | ||||
| 
 | ||||
| .. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce | ||||
|    prerekvizit <Tabulka prerekvizit v různých distribucích>`. | ||||
|    prerekvizit <Alternativní jména balíčků>`. | ||||
| 
 | ||||
| Doporučené | ||||
| ^^^^^^^^^^ | ||||
|  |  | |||
							
								
								
									
										97
									
								
								docs/zavislosti.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								docs/zavislosti.rst
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,97 @@ | |||
| Závislosti webu | ||||
| @@@@@@@@@@@@@@@ | ||||
| 
 | ||||
| Web ke svému běhu potřebuje různé další programy. Tahle stránka se snaží je pokrýt. | ||||
| 
 | ||||
| Stránka je koncipována jako odrážkový seznam balíčků pro Ubuntu s případnými | ||||
| komentáři, na konci stránky jsou uvedena :ref:`jména balíčků <Alternativní jména | ||||
| balíčků>` v různých dalších distribucích. (Seznam mj. cílí na lokální | ||||
| rozchození, proto popisuji Ubuntu a ne Debian. I tak se ale snažíme popsat web | ||||
| v úplnosti.) | ||||
| 
 | ||||
| .. I use Arch, btw. | ||||
| 
 | ||||
| 
 | ||||
| Základ webu | ||||
| =========== | ||||
| 
 | ||||
| - ``python3`` – Ideálně Python 3.9, jenž je na Gimlim | ||||
| - ``python3-pip`` pro instalaci dalších Pythoních balíčků podle ``requirements.txt`` | ||||
| - ``python3-venv`` | ||||
| - ``gcc`` – kompilace Pythoních knihoven ze zdrojových distribucí (sdist), možná (neotestováno) jde jako alternativu použít ``python3-wheel`` a stahovat bdists | ||||
| - ``python3-dev`` – taktéž | ||||
| - ``libpq-dev`` do třetice… | ||||
| - ``ghostscript`` TODO konverze PDF v korekturovátku | ||||
| - ``pdflatex`` FIXME! generování obálek a stvrzenek | ||||
| - ``git`` – používán :ref:`Make skripty` | ||||
| - ``locales`` pro české formáty | ||||
| 
 | ||||
| Nasazení na produkci / testweb | ||||
| ============================== | ||||
| 
 | ||||
| (nejsou nutně potřeba k provozu lokální instance) | ||||
| 
 | ||||
| - ``rsync`` | ||||
| - ``pg_utils`` FIXME | ||||
| - ``htpasswd`` FIXME – aby testweb nepoužívali náhodní kolemjdoucí | ||||
| - ``postgresql-server`` TODO | ||||
| - ``acl`` pro nastavování práv přes ``setfacl`` | ||||
| 
 | ||||
| Pro testweb je potřeba i všechno pro :ref:`dokumentaci <Dokumentace>`, vizte níž. | ||||
| 
 | ||||
| Předpokládá se nasazení v uWSGI pod Nginxem a služba běžící pod systemd, nicméně to už je spíš záležitost infrastruktury a ne specifikum mamwebu. | ||||
| 
 | ||||
| Dokumentace | ||||
| =========== | ||||
| 
 | ||||
| - ``make`` pro zbuildění | ||||
| - Pythoní balíčky podle příslušné části ``requirements.txt`` | ||||
| 
 | ||||
| Vývojové nástroje | ||||
| ================= | ||||
| 
 | ||||
| (Nejsou nezbytně nutné, ale předpokládáme jejich užitečnost. Mohou se hodit i na produkci.) | ||||
| 
 | ||||
| - ``psql`` TODO pro manuální dotazy do PostgreSQL | ||||
| - ``sqlite3`` TODO totéž pro SQLite3 | ||||
| - ``ssh`` | ||||
| - ``graphviz`` pro vygenerování schématu | ||||
| - ``rsync`` | ||||
| - ``ipython3`` – hezčí interaktivní shell (stačí z ``requirements.txt``) | ||||
| 
 | ||||
| Potenciální usnadnění života | ||||
| ============================ | ||||
| 
 | ||||
| (Úplně zbytečné, ale sdílíme pozitivní zkušenosti :-)) | ||||
| 
 | ||||
| - ``tea`` – CLI klient pro Giteu, aby člověk nepotřeboval otevírat web pro založení PR | ||||
| 
 | ||||
| 
 | ||||
| Alternativní jména balíčků | ||||
| ========================== | ||||
| 
 | ||||
| Různé distribuce balí SW různě, takže to, co je v jedné distribuci jeden | ||||
| balíček může být v jiné rozděleno do víc. Pro usnadnění nasazení je tady | ||||
| přehled známých alternativních jmen. | ||||
| 
 | ||||
| TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte. | ||||
| 
 | ||||
| .. admonition:: Jak se pozná, že web funguje, pro účely tabulky? | ||||
| 
 | ||||
|    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 nefunguje synchronizace flatpages (nemáme SSH klíč), | ||||
|    ``make/init_local`` sestřelíme při pokusu o synchronizaci 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." | ||||
| 
 | ||||
							
								
								
									
										18
									
								
								korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 2.2.28 on 2023-06-19 19:38 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('korektury', '0019_auto_20221205_2014'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='korekturovanepdf', | ||||
|             name='nazev', | ||||
|             field=models.CharField(help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF', max_length=50, verbose_name='název PDF'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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 = False,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 | analyza v4` nebo `propagace | letacek v0`) 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)') | ||||
| 
 | ||||
|  |  | |||
|  | @ -54,6 +54,9 @@ LOGIN_REDIRECT_URL = 'profil' | |||
| SESSION_EXPIRE_AT_BROWSER_CLOSE = True | ||||
| DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600  # rok | ||||
| 
 | ||||
| # View pro chybu s CSRF tokenem (např. se sušenkami) | ||||
| CSRF_FAILURE_VIEW = 'various.views.csrf_error' | ||||
| 
 | ||||
| # Modules configuration | ||||
| 
 | ||||
| AUTHENTICATION_BACKENDS = ( | ||||
|  | @ -151,6 +154,7 @@ INSTALLED_APPS = ( | |||
| 	'soustredeni', | ||||
| 	'treenode', | ||||
| 	'vyroci', | ||||
| 	'sifrovacka', | ||||
| 
 | ||||
| 	# Admin upravy: | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,8 +27,9 @@ DEBUG = False | |||
| 
 | ||||
| TEMPLATE_DEBUG = False | ||||
| 
 | ||||
| ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz',  | ||||
| 		'mamweb.bezva.org','gimli.ms.mff.cuni.cz'] | ||||
| ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa | ||||
| 		'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny… | ||||
| 		] | ||||
| 
 | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/1.7/ref/settings/#databases | ||||
|  |  | |||
|  | @ -32,7 +32,10 @@ DEBUG = True | |||
| 
 | ||||
| TEMPLATES[0]['OPTIONS']['debug'] = True | ||||
| 
 | ||||
| ALLOWED_HOSTS = ['*.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', 'mam.mff.cuni.cz', 'mam-test.kam.mff.cuni.cz', 'gimli.ms.mff.cuni.cz', 'mam-test.ks.matfyz.cz'] | ||||
| ALLOWED_HOSTS = [ | ||||
| 	'mam-test.ks.matfyz.cz', | ||||
| 	'*.mam.mff.cuni.cz', # Asi se nikdy nepoužije… | ||||
| 	] | ||||
| 
 | ||||
| # Database | ||||
| # https://docs.djangoproject.com/en/1.7/ref/settings/#databases | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| @charset "utf-8"; /* vynuť utf-8 */ | ||||
| @import url("rozliseni.css"); | ||||
| 
 | ||||
| @font-face { | ||||
|  | @ -53,6 +54,17 @@ a.login-ref-admin { | |||
| 	color: #fffbf6; | ||||
| } | ||||
| 
 | ||||
| .napis-webarum { | ||||
| 	display: inline; | ||||
| 	color: #fffbf6; | ||||
| 	float: right; | ||||
| } | ||||
| 
 | ||||
| .napis-webarum a { | ||||
| 	color: #f9d59e; | ||||
| 	text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| /* odkazy a nadpisy */ | ||||
| 
 | ||||
| a { | ||||
|  | @ -1233,6 +1245,7 @@ div.gdpr { | |||
| .dosla_reseni tr th, .dosla_reseni tr td { | ||||
| 	padding: 1px 10px 1px 10px; | ||||
| 	border-collapse: collapse; | ||||
| 	min-width: 8em; /*Nastřeleno, aby se řádky s řešeními nezalamovaly. Teoreticky není potřeba pro th, ale whatever.*/ | ||||
| } | ||||
| 
 | ||||
| .dosla_reseni tr:nth-child(even) { | ||||
|  | @ -1260,3 +1273,31 @@ label[for=id_skola] { | |||
| .bodovani>input { | ||||
| 	width: 4em; | ||||
| } | ||||
| 
 | ||||
| .bodovani>input::placeholder { | ||||
| 	color: lightgray; | ||||
| 	opacity: 1; | ||||
| } | ||||
| 
 | ||||
| .bodovani>input::-webkit-input-placeholder { /* Edge */ | ||||
| 	color: lightgray; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */ | ||||
| /* Podle https://stackoverflow.com/a/48290544 */ | ||||
| /* U autocomplete.ModelSelect2Multiple vyžaduje 'data-dropdown-css-class': 's2m-se-zaskrtavatky' */ | ||||
| .s2m-se-zaskrtavatky .select2-results__option[aria-selected=true]:before { | ||||
| 	content: '☑ '; | ||||
| 	padding: 0 0 0 8px; | ||||
| } | ||||
| 
 | ||||
| .s2m-se-zaskrtavatky .select2-results__option[aria-selected=false]:before { | ||||
| 	content: '◻ '; | ||||
| 	padding: 0 0 0 8px; | ||||
| } | ||||
| 
 | ||||
| /* Oranžové zvýraznění v Select2 */ | ||||
| .select2-results__option--highlighted { | ||||
| 	background-color: #e84e10 !important; | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,10 @@ | |||
| {% load static %} | ||||
| 
 | ||||
| {% block errorheading %} | ||||
|       <br> {# Meníčko nedostaneme, protože dostáváme prázdný kontext. Tak alespoň ať se O-JO-JO-JO-JOJ neschovává pod ním #} | ||||
|     {% block nadpis1a %} | ||||
|       O-jo-jo-jo-joj | ||||
|     {% endblock %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block errortext %} | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang='cs'> | ||||
|   <head> | ||||
|     <meta charset="utf-8"> {# vynuť UTF-8. #} | ||||
|     <title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&M{% endblock title %}</title> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon"> | ||||
|  | @ -49,6 +50,8 @@ | |||
|           <a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a> | ||||
|         {% endif %} | ||||
|         <a class="login-ref-admin" href='/admin'>[admin mainpage]</a> | ||||
| 
 | ||||
|         <span class="napis-webarum">Něco ti nejde/nefunguje/mate tě? <a class="login-ref-admin" href='mailto:web@mam.mff.cuni.cz'>Napiš webařům!</a></span> | ||||
|       </div> | ||||
|     {% endif %} | ||||
| 
 | ||||
|  |  | |||
|  | @ -71,6 +71,8 @@ urlpatterns = [ | |||
| 	# Výroční sraz | ||||
| 	path('sraz/30-let/', include('vyroci.urls')), | ||||
| 
 | ||||
| 	# Miniapka na šifrovačku | ||||
| 	path('sifrovacka/', include('sifrovacka.urls')), | ||||
| ] | ||||
| 
 | ||||
| # This is only needed when using runserver. | ||||
|  |  | |||
|  | @ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš | |||
| Slovníček: | ||||
|     Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. | ||||
|     Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. | ||||
|     Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) | ||||
|     Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. | ||||
|     Nahrát řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) | ||||
|     Vlož řešení = Vložit řešení bez vztahu k aktuálnímu uživateli. | ||||
| 
 | ||||
| TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? | ||||
| """ | ||||
| """ | ||||
|  |  | |||
|  | @ -29,6 +29,8 @@ class PosliReseniForm(forms.Form): | |||
| 			attrs={ | ||||
| 				'data-placeholder--id': '-1', | ||||
| 				'data-placeholder--text': '---', | ||||
| 				'data-close-on-select': 'false', | ||||
| 				'data-dropdown-css-class': 's2m-se-zaskrtavatky', | ||||
| 				'data-allow-clear': 'true' | ||||
| 				}, | ||||
| 			), | ||||
|  | @ -43,6 +45,8 @@ class PosliReseniForm(forms.Form): | |||
| 			url='autocomplete_resitel', | ||||
| 			attrs = {'data-placeholder--id': '-1', | ||||
| 				'data-placeholder--text' : '---', | ||||
| 				'data-close-on-select': 'false', | ||||
| 				'data-dropdown-css-class': 's2m-se-zaskrtavatky', | ||||
| 				'data-allow-clear': 'true'}) | ||||
|     		) | ||||
| 
 | ||||
|  | @ -62,12 +66,6 @@ class PosliReseniForm(forms.Form): | |||
| 	#poznamka = models.TextField('neveřejná poznámka', blank=True, | ||||
| 	#	help_text='Neveřejná poznámka k řešení (plain text)') | ||||
| 
 | ||||
| 	#TODO body do cisla | ||||
| 	#TODO prilohy | ||||
| 
 | ||||
| 	##def __init__(self, *args, **kwargs): | ||||
| 	##	super().__init__(*args, **kwargs) | ||||
| 	##	#self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) | ||||
| 
 | ||||
| class NahrajReseniForm(forms.ModelForm): | ||||
| 	class Meta: | ||||
|  | @ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm): | |||
| 					url='autocomplete_problem_odevzdatelny', | ||||
| 					attrs = {'data-placeholder--id': '-1', | ||||
| 						'data-placeholder--text' : '---', | ||||
| 						'data-close-on-select': 'false', | ||||
| 						'data-dropdown-css-class': 's2m-se-zaskrtavatky', | ||||
| 						'data-allow-clear': 'true'}, | ||||
| 					forward=["nadproblem_id"], | ||||
| 				), | ||||
| 				'resitele': | ||||
| 				autocomplete.ModelSelect2Multiple( | ||||
| 					url='autocomplete_resitel_public', | ||||
| 					attrs = {'data-placeholder--id': '-1', | ||||
| 						'data-placeholder--text' : '---', | ||||
| 						'data-close-on-select': 'false', | ||||
| 						'data-dropdown-css-class': 's2m-se-zaskrtavatky', | ||||
| 						'data-allow-clear': 'true'}, | ||||
| 				) | ||||
| 		} | ||||
| 
 | ||||
| 	nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput()) | ||||
| 
 | ||||
| 	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 | ||||
| 			self.fields['resitele'].label = "Další autoři" | ||||
| 		if 'problem' in self.fields: | ||||
| 			self.fields['problem'].label = "Všechny řešené problémy" | ||||
| 
 | ||||
| 	def clean_problem(self): | ||||
| 		problem = self.cleaned_data.get('problem') | ||||
| 		for p in problem: | ||||
| 			if p.stav != m.Problem.STAV_ZADANY: | ||||
| 				raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!") | ||||
| 		return problem | ||||
| 
 | ||||
| ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,  | ||||
| 		form = NahrajReseniForm, | ||||
|  | @ -223,6 +238,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): | |||
| 			'reseni_od': terminy[-2] if rocnik is None else terminy[0], | ||||
| 			'reseni_do': terminy[-1], | ||||
| 			'neobodovane': False, | ||||
| 			'barvicky': True, | ||||
| 		} | ||||
| 		return initial | ||||
| 
 | ||||
|  | @ -247,3 +263,4 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): | |||
| 	reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) | ||||
| 	reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) | ||||
| 	neobodovane = forms.BooleanField(required=False) | ||||
| 	barvicky = forms.BooleanField(required=False) | ||||
|  |  | |||
|  | @ -7,19 +7,20 @@ | |||
| {% block content %} | ||||
| <h1> | ||||
|   {% block nadpis1a %} | ||||
|     Vložit řešení | ||||
|     Nahrát řešení | ||||
|   {% endblock %} | ||||
| </h1> | ||||
| 
 | ||||
| <p style="text-align: justify">Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a třeba ti je i rychleji opravíme.</p> | ||||
| 
 | ||||
|   <p>Pokud řešíte ve více lidech, je <strong>nutné</strong> přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p> | ||||
| 
 | ||||
| <form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();"> | ||||
| <form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();"> | ||||
|   {% csrf_token %} | ||||
|   <table class='form' id="reseni"> | ||||
|   <table class='form'> | ||||
|     <tr> | ||||
|       <td><label class="field-label field-required" for="tema">Téma:</label></td> | ||||
|       <td><input id="tema" disabled="" type="text" value="{{ nadproblem }}"></td> | ||||
|     </tr> | ||||
| 
 | ||||
|     {% with field=form.problem %} | ||||
|     <tr> | ||||
|       {% for field in form %} | ||||
|       <td> | ||||
|         <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> | ||||
|           {{ field.label }}: | ||||
|  | @ -28,15 +29,54 @@ | |||
|       <td> | ||||
|         {{ field }} | ||||
|       </td> | ||||
|       {% endfor %} | ||||
|     </tr> | ||||
| 
 | ||||
|     {% if field.errors %} | ||||
|       <tr> | ||||
|         <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> | ||||
|       </tr> | ||||
|     {% endif %} | ||||
| 
 | ||||
|     {% endwith %} | ||||
|   </table> | ||||
| 
 | ||||
|   {% for field in form.hidden_fields %} | ||||
|     {{ field }} | ||||
|   {% endfor %} | ||||
| 
 | ||||
|   <hr> | ||||
|   <h4>Spolupráce s dalšími řešiteli</h4> | ||||
| 
 | ||||
|   <p>Pokud řešíte ve více lidech, je <strong>potřeba</strong> přidat tyto lidi jako „Další autory“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p> | ||||
| 
 | ||||
|   <table class='form'> | ||||
|     {% with field=form.resitele %} | ||||
|       <tr> | ||||
|         <td> | ||||
|           <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> | ||||
|             {{ field.label }}: | ||||
|           </label> | ||||
|         </td> | ||||
|         <td> | ||||
|           {{ field }} | ||||
|         </td> | ||||
|       </tr> | ||||
| 
 | ||||
|       {% if field.errors %} | ||||
|         <tr> | ||||
|           <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> | ||||
|         </tr> | ||||
|       {% endif %} | ||||
| 
 | ||||
|     {% endwith %} | ||||
|   </table> | ||||
| 
 | ||||
| <hr> | ||||
| 
 | ||||
| {% include "odevzdavatko/prilohy.html" %} | ||||
| 
 | ||||
| {{form.non_field_errors}} | ||||
| 
 | ||||
| <hr> | ||||
| <h4>Odevzdat řešení</h4> | ||||
| <input type="submit" value="Odevzdat"> | ||||
|  |  | |||
|  | @ -0,0 +1,21 @@ | |||
| {% extends "base.html" %} | ||||
| {% load static %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <h1> | ||||
|   {% block nadpis1a %} | ||||
|     Nahrát řešení | ||||
|   {% endblock %} | ||||
| </h1> | ||||
| 
 | ||||
| <h4>Seznam témat k odevzdání</h4> | ||||
| 
 | ||||
| <ul> | ||||
|   {% for problem in object_list %} | ||||
|     <li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li> | ||||
|   {% empty %} | ||||
|     <li>Nelze nic odevzdávat.</li> | ||||
|   {% endfor %} | ||||
| </ul> | ||||
| 
 | ||||
| {% endblock %} | ||||
|  | @ -2,8 +2,9 @@ | |||
| 
 | ||||
| <h4>Soubory s řešením</h4> | ||||
| 
 | ||||
| <p style="text-align: justify">Maximální součet velikostí příloh je cca 49 MB. Pokud je to možné a dává to smysl, pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.</p> | ||||
| <p style="text-align: justify">Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p> | ||||
| <p style="text-align: justify">Pokud je to možné a dává to smysl (tj. není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu <strong>PDF</strong>, ostatní formáty nemusíme umět otevřít.</p> | ||||
| <p style="text-align: justify">Pokud svůj soubor <strong>rozumně pojmenuješ</strong>, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p> | ||||
| <p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49 MB</strong>.</p> | ||||
| 
 | ||||
| <div id="form_set"> | ||||
| {% for form in prilohy.forms %} | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} | ||||
| {% load barvy_reseni %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|  | @ -11,6 +12,7 @@ | |||
| Od data (vyjma): {{ filtr.reseni_od }} | ||||
| Do data (včetně): {{ filtr.reseni_do }} | ||||
| <span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }} | ||||
| <span title="Obarvit shodná řešení shodně">🎨?</span> {{ filtr.barvicky }} | ||||
| <input type=submit value="→"> | ||||
| </form> | ||||
| 
 | ||||
|  | @ -36,12 +38,15 @@ Do data (včetně): {{ filtr.reseni_do }} | |||
| 			{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #} | ||||
| 			{{ resitel }} | ||||
| 		</td> | ||||
| 		{% for hodn in hodnoty %} | ||||
| 		{% for soucet,bunka in hodnoty %} | ||||
| 			<td> | ||||
| 			{% if hodn %} | ||||
| 			<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}"> | ||||
| 				{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} | ||||
| 			</a> | ||||
| 			{% for reseni,hodnoceni in bunka %} | ||||
| 			<a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}"> | ||||
| 					{{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b) | ||||
| 				</a><br> | ||||
| 			{% endfor %} | ||||
| 			{% if bunka|length > 1 %} | ||||
| 				<b>Σ: {{soucet}} b</b> | ||||
| 			{% endif %} | ||||
| 			</td> | ||||
| 		{% endfor %} | ||||
|  |  | |||
							
								
								
									
										15
									
								
								odevzdavatko/templatetags/barvy_reseni.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								odevzdavatko/templatetags/barvy_reseni.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| from django import template | ||||
| register = template.Library() | ||||
| 
 | ||||
| from functools import cache | ||||
| import seminar.models as m | ||||
| 
 | ||||
| @register.filter | ||||
| @cache | ||||
| def barva_reseni(r: m.Reseni): | ||||
| 	"""Vrátí nějakou barvu pro daný problém, ve tvaru '#RRGGBB' | ||||
| 
 | ||||
| 	Efektivně hešujeme do barev.""" | ||||
| 
 | ||||
| 	#TODO: ne všechny barvy jsou dobře rozlišitelné a vidět… | ||||
| 	return f'#{hash(str(r.id)) & 0xffffff:06x}' | ||||
|  | @ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \ | |||
| from . import views | ||||
| 
 | ||||
| urlpatterns = [ | ||||
| 	path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), | ||||
| 	path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), | ||||
| 	path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'), | ||||
| 	path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'), | ||||
| 	path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), | ||||
| 	path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), | ||||
| 
 | ||||
| 	path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ from django.db.models import Q | |||
| 
 | ||||
| from dataclasses import dataclass | ||||
| import datetime | ||||
| from decimal import Decimal | ||||
| from itertools import groupby | ||||
| import logging | ||||
| 
 | ||||
|  | @ -37,14 +38,6 @@ logger = logging.getLogger(__name__) | |||
| # Taky se může hodit: | ||||
| # - Tabulka všech řešitelů x všech problémů? | ||||
| 
 | ||||
| @dataclass | ||||
| class SouhrnReseni: | ||||
| 	"""Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce.""" | ||||
| 	pocet_reseni : int | ||||
| 	posledni_odevzdani : datetime.datetime | ||||
| 	body : float | ||||
| 
 | ||||
| 
 | ||||
| class TabulkaOdevzdanychReseniView(ListView): | ||||
| 	template_name = 'odevzdavatko/tabulka.html' | ||||
| 	model = m.Hodnoceni | ||||
|  | @ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView): | |||
| 			reseni_od = fcd["reseni_od"] | ||||
| 			reseni_do = fcd["reseni_do"] | ||||
| 			jen_neobodovane = fcd["neobodovane"] | ||||
| 			self.barvicky = fcd["barvicky"] | ||||
| 		else: | ||||
| 			initial = FiltrForm.gen_initial(self.aktualni_rocnik) | ||||
| 			resitele = initial['resitele'] | ||||
|  | @ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView): | |||
| 			reseni_od = initial['reseni_od'][0] | ||||
| 			reseni_do = initial['reseni_do'][0] | ||||
| 			jen_neobodovane = initial["neobodovane"] | ||||
| 			self.barvicky = initial["barvicky"] | ||||
| 			 | ||||
| 
 | ||||
| 		# Chceme jen letošní problémy | ||||
|  | @ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView): | |||
| 		return qs | ||||
| 
 | ||||
| 	def get_context_data(self, *args, **kwargs): | ||||
| 		# TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl… | ||||
| 		# self.resitele, self.reseni a self.problemy jsou již nastavené | ||||
| 
 | ||||
| 		ctx = super().get_context_data(*args, **kwargs) | ||||
| 		ctx['problemy'] = self.problemy | ||||
| 		ctx['resitele'] = self.resitele | ||||
| 		tabulka = dict() | ||||
| 		tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict() | ||||
| 		soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict() | ||||
| 
 | ||||
| 		def pridej_reseni(problem, resitel, body, cas): | ||||
| 		def pridej_reseni(resitel, hodnoceni): | ||||
| 			problem = hodnoceni.problem | ||||
| 			body = hodnoceni.body | ||||
| 			cas = hodnoceni.reseni.cas_doruceni | ||||
| 			reseni = hodnoceni.reseni | ||||
| 			if problem not in tabulka: | ||||
| 				tabulka[problem] = dict() | ||||
| 				soucty[problem] = dict() | ||||
| 			if resitel not in tabulka[problem]: | ||||
| 				tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body) | ||||
| 				tabulka[problem][resitel] = [(reseni, hodnoceni)] | ||||
| 				soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme | ||||
| 			else: | ||||
| 				tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) | ||||
| 				# Zvětšení počtu bodů o aktuální počet, pokud se tam někde nevyskytuje None – pak je součet taky None ("Pozor, nezadané body") | ||||
| 				tabulka[problem][resitel].body = tabulka[problem][resitel].body + body if body is not None and tabulka[problem][resitel].body is not None else None | ||||
| 				tabulka[problem][resitel].pocet_reseni += 1 | ||||
| 			# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele | ||||
| 			tabulka[problem][resitel].problem_id = problem.id | ||||
| 			tabulka[problem][resitel].resitel_id = resitel.id | ||||
| 				tabulka[problem][resitel].append((reseni, hodnoceni)) | ||||
| 				soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme | ||||
| 		 | ||||
| 		for hodnoceni in self.get_queryset(): | ||||
| 			for resitel in hodnoceni.reseni.resitele.all(): | ||||
| 				pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) | ||||
| 				pridej_reseni(resitel, hodnoceni) | ||||
| 
 | ||||
| 		hodnoty = [] | ||||
| 		resitele_do_tabulky = [] | ||||
| 		hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému. | ||||
| 		resitele_do_tabulky: list[m.Resitel] = [] | ||||
| 		for resitel in self.resitele: | ||||
| 			dostal_body = False | ||||
| 			resiteluv_radek = [] | ||||
| 			resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy | ||||
| 			for problem in self.problemy: | ||||
| 				if problem in tabulka and resitel in tabulka[problem]: | ||||
| 					resiteluv_radek.append(tabulka[problem][resitel]) | ||||
| 					resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel])) | ||||
| 					dostal_body = True | ||||
| 				else: | ||||
| 					resiteluv_radek.append(None) | ||||
| 					resiteluv_radek.append((Decimal(0),[])) | ||||
| 			if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body: | ||||
| 				hodnoty.append(resiteluv_radek) | ||||
| 				resitele_do_tabulky.append(resitel) | ||||
|  | @ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView): | |||
| 		ctx['form'] = ctx['filtr'] | ||||
| 		# Pro maximum v přesměrovátku ročníků | ||||
| 		ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik | ||||
| 		ctx['barvicky'] = self.barvicky | ||||
| 		if 'rocnik' in self.kwargs: | ||||
| 			ctx['rocnik'] = self.kwargs['rocnik'] | ||||
| 		else: | ||||
|  | @ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView): | |||
| 
 | ||||
| # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? | ||||
| class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): | ||||
| 	"""Rozskok mezi více řešeními téhož problému od téhož řešitele. | ||||
| 	 | ||||
| 	Asi už bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-) | ||||
| 
 | ||||
| 	V případě, že takové řešení existuje jen jedno, tak na něj přesměruje.""" | ||||
| 	model = m.Reseni | ||||
| 	template_name = 'odevzdavatko/seznam.html' | ||||
| 	 | ||||
|  | @ -316,7 +320,8 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | |||
| 			if len(zmeny_bodu) == 1: | ||||
| 				hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]]) | ||||
| 			# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno | ||||
| 			if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4: | ||||
| 			if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2: | ||||
| 				# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo | ||||
| 				logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.") | ||||
| 				hodnoceni.body = -0.1 | ||||
| 			hodnoceni.save() | ||||
|  | @ -367,8 +372,8 @@ class SeznamAktualnichReseniView(SeznamReseniView): | |||
| 		return qs | ||||
| 
 | ||||
| 
 | ||||
| class PosliReseniView(LoginRequiredMixin, FormView): | ||||
| 	template_name = 'odevzdavatko/posli_reseni.html' | ||||
| class VlozReseniView(LoginRequiredMixin, FormView): | ||||
| 	template_name = 'odevzdavatko/vloz_reseni.html' | ||||
| 	form_class = f.PosliReseniForm | ||||
| 
 | ||||
| 	def form_valid(self, form): | ||||
|  | @ -399,12 +404,31 @@ class PosliReseniView(LoginRequiredMixin, FormView): | |||
| 		return data | ||||
| 
 | ||||
| 
 | ||||
| class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView): | ||||
| 	model = m.Problem | ||||
| 	template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html' | ||||
| 
 | ||||
| 	def get_queryset(self): | ||||
| 		return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True) | ||||
| 
 | ||||
| 
 | ||||
| class NahrajReseniView(LoginRequiredMixin, CreateView): | ||||
| 	model = m.Reseni | ||||
| 	template_name = 'odevzdavatko/nahraj_reseni.html' | ||||
| 	form_class = f.NahrajReseniForm | ||||
| 	nadproblem: m.Problem | ||||
| 
 | ||||
| 	def setup(self, request, *args, **kwargs): | ||||
| 		super().setup(request, *args, **kwargs) | ||||
| 		nadproblem_id = self.kwargs["nadproblem_id"] | ||||
| 		self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id) | ||||
| 
 | ||||
| 	def get(self, request, *args, **kwargs): | ||||
| 		# Zaříznutí nezadaných problémů | ||||
| 		if self.nadproblem.stav != m.Problem.STAV_ZADANY: | ||||
| 			raise PermissionDenied() | ||||
| 
 | ||||
| 
 | ||||
| 		# Zaříznutí starých řešitelů: | ||||
| 		# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… | ||||
| 		osoba = m.Osoba.objects.get(user=self.request.user) | ||||
|  | @ -417,12 +441,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): | |||
| 			}) | ||||
| 		return super().get(request, *args, **kwargs) | ||||
| 
 | ||||
| 	def get_initial(self): | ||||
| 		nadproblem_id = self.nadproblem.id | ||||
| 		return { | ||||
| 			"nadproblem_id": nadproblem_id, | ||||
| 			"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 	def get_context_data(self,**kwargs): | ||||
| 		data = super().get_context_data(**kwargs) | ||||
| 		if self.request.POST: | ||||
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) | ||||
| 		else: | ||||
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet() | ||||
| 
 | ||||
| 		data["nadproblem_id"] = self.nadproblem.id | ||||
| 		data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id) | ||||
| 		return data | ||||
| 
 | ||||
| 	# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni | ||||
|  | @ -474,4 +509,8 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): | |||
| 			to=list(prijemci), | ||||
| 		).send() | ||||
| 
 | ||||
| 		return formularOKView(self.request, text='Řešení úspěšně odevzdáno') | ||||
| 		return formularOKView( | ||||
| 			self.request, | ||||
| 			text='Řešení úspěšně odevzdáno', | ||||
| 			dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))], | ||||
| 		) | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| from django.contrib import admin | ||||
| from django.contrib.auth.models import Group | ||||
| from django_reverse_admin import ReverseModelAdmin | ||||
| from django.contrib.messages import WARNING, ERROR, SUCCESS | ||||
| import seminar.models as m | ||||
| from datetime import datetime | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(m.Osoba) | ||||
|  | @ -20,16 +22,24 @@ class OsobaAdmin(admin.ModelAdmin): | |||
| 
 | ||||
| 	def udelej_orgem(self,request,queryset): | ||||
| 		org_group = Group.objects.get(name='org') | ||||
| 		print(queryset) | ||||
| 		uspesne_vytvoreni_orgove = 0 | ||||
| 		for o in queryset: | ||||
| 			if m.Organizator.objects.filter(osoba=o).exists(): | ||||
| 				# Ref: https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.message_user | ||||
| 				self.message_user(request, f"Osoba {o} už je org, přeskakuji.", level=WARNING) | ||||
| 				continue | ||||
| 			user = o.user | ||||
| 			print(user) | ||||
| 			if user is None: | ||||
| 				self.message_user(request, f"Osoba {o} nemá uživatele! Přeskakuji.", level=ERROR) | ||||
| 				continue | ||||
| 			user.groups.add(org_group) | ||||
| 			user.is_staff = True | ||||
| 			user.save() | ||||
| 			org = m.Organizator.objects.create(osoba=o) | ||||
| 			org = m.Organizator.objects.create(osoba=o, organizuje_od=datetime.now()) | ||||
| 			org.save() | ||||
| 	udelej_orgem.short_description = "Udělej vybraných osob organizátory" | ||||
| 			uspesne_vytvoreni_orgove += 1 | ||||
| 		self.message_user(request, f'Úspěšně vytvořeno {uspesne_vytvoreni_orgove} orgů.', level=SUCCESS) | ||||
| 	udelej_orgem.short_description = "Udělej z vybraných osob organizátory" | ||||
| 
 | ||||
| class OsobaInline(admin.TabularInline): | ||||
| 	model = m.Osoba | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
| 
 | ||||
| <a href="{% url 'logout' %}">Odhlásit se</a><br> | ||||
| <a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br> | ||||
| <a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br> | ||||
| <a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br> | ||||
| <a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br> | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										63
									
								
								personalni/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								personalni/tests.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| from django.test import TestCase, RequestFactory | ||||
| 
 | ||||
| from django.contrib.auth.models import User, Group | ||||
| from django.contrib.admin.sites import AdminSite | ||||
| from personalni.admin import OsobaAdmin | ||||
| # Tohle bude peklo, až jednou ty modely fakt rozstřelíme… Možná vyrobit various.all_models, které půjdou importovat jako m? :-) | ||||
| import seminar.models as m | ||||
| 
 | ||||
| import logging | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| class DelaniOrguTest(TestCase): | ||||
| 	def setUp(self): | ||||
| 		# Admin musí mít instanci | ||||
| 		# Ref: https://www.argpar.se/posts/programming/testing-django-admin/ | ||||
| 		adm_site = AdminSite() | ||||
| 		self.admin = OsobaAdmin(m.Osoba, adm_site) | ||||
| 
 | ||||
| 		from django.contrib.messages.storage.cookie import CookieStorage | ||||
| 		self.request = RequestFactory().get('/admin') | ||||
| 		self.request._messages = CookieStorage(self.request) | ||||
| 
 | ||||
| 		self.org_group = Group.objects.get(name='org') | ||||
| 
 | ||||
| 		novy_user = User.objects.create(username='osoba') | ||||
| 		self.nova_osoba = m.Osoba.objects.create( | ||||
| 			jmeno='Milada', | ||||
| 			prijmeni='Von Kolej', | ||||
| 			user = novy_user, | ||||
| 			# Snad nic dalšího nepotřebujeme, kdyžtak se doplní… | ||||
| 			) | ||||
| 		stary_user = User.objects.create(username='stary_user') | ||||
| 		stara_osoba = m.Osoba.objects.create(user=stary_user) | ||||
| 		self.stary_org = m.Organizator.objects.create(osoba=stara_osoba) | ||||
| 
 | ||||
| 	def test_pridani_orga(self): | ||||
| 		# Nejdřív to není org… | ||||
| 		self.assertFalse(m.Organizator.objects.filter(osoba=self.nova_osoba).exists()) | ||||
| 		self.assertNotIn(self.org_group, self.nova_osoba.user.groups.all()) | ||||
| 		self.assertFalse(self.nova_osoba.user.has_perm('auth.org')) | ||||
| 		self.assertFalse(self.nova_osoba.user.is_staff) | ||||
| 
 | ||||
| 		# Pak orga uděláme… | ||||
| 		qs = m.Osoba.objects.filter(id=self.nova_osoba.id) | ||||
| 		self.admin.udelej_orgem(self.request, qs) | ||||
| 
 | ||||
| 		# A pak už to org má být. | ||||
| 		self.nova_osoba.refresh_from_db() | ||||
| 		self.assertTrue(self.nova_osoba.user.is_staff) | ||||
| 		# FIXME: V db nejsou práva. Nový org je sice ve skupině "org", ale ta nemá právo "auth.org" | ||||
| 		# Očekávané řešení: dodat fixture, která to přidá. | ||||
| 		#self.assertTrue(self.nova_osoba.user.has_perm('auth.org')) | ||||
| 		self.assertIn(self.org_group, self.nova_osoba.user.groups.all()) | ||||
| 		self.assertTrue(m.Organizator.objects.filter(osoba=self.nova_osoba).exists()) | ||||
| 		novy_org = m.Organizator.objects.get(osoba=self.nova_osoba) | ||||
| 		self.assertIsNotNone(novy_org.organizuje_od) | ||||
| 
 | ||||
| 	def test_pridani_stareho_orga(self): | ||||
| 		self.admin.udelej_orgem(self.request, m.Osoba.objects.filter(id=self.stary_org.osoba.id)) # Ugly | ||||
| 		# Když to spadne, tak jsem se to dozvěděl, takže už nepotřebuju nic kontrolovat. | ||||
| 		# Jestli to funguje správně má řešit jiný test. | ||||
| 
 | ||||
| 
 | ||||
|  | @ -173,7 +173,11 @@ def resitelEditView(request): | |||
| 					msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa']) | ||||
| 				resitel_edit.save() | ||||
| 			osoba_edit.save() | ||||
| 			return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>') | ||||
| 			return formularOKView( | ||||
| 				request, | ||||
| 				text='Údaje byly úspěšně uloženy.', | ||||
| 				dalsi_odkazy=[("Vrátit se zpět na profil", reverse("profil"))], | ||||
| 			) | ||||
| 
 | ||||
| 	return render(request, 'personalni/udaje/edit.html', {'form': form}) | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ psycopg2 | |||
| html5lib | ||||
| ipython | ||||
| Pillow | ||||
| pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0 | ||||
| pytz | ||||
| six | ||||
| pexpect | ||||
|  | @ -24,7 +25,7 @@ django-ckeditor | |||
| django-cleanup  # Uklízí media/ od smazaných „databázových“ souborů | ||||
| django-flat-theme | ||||
| django-taggit | ||||
| django-autocomplete-light>=3.9.0rc1 | ||||
| django-autocomplete-light>=3.9.0 | ||||
| django-crispy-forms | ||||
| django-imagekit | ||||
| django-polymorphic | ||||
|  | @ -52,9 +53,6 @@ Werkzeug | |||
| requests | ||||
| # requests-oauthlib | ||||
| 
 | ||||
| # uWSGI | ||||
| uWSGI | ||||
| 
 | ||||
| # Potřeba pro test data | ||||
| 
 | ||||
| lorem | ||||
|  |  | |||
|  | @ -12,15 +12,25 @@ from django.utils.safestring import mark_safe | |||
| import seminar.models as m | ||||
| 
 | ||||
| admin.site.register(m.Rocnik) | ||||
| 
 | ||||
| admin.site.register(m.Deadline) | ||||
| admin.site.register(m.ZmrazenaVysledkovka) | ||||
| 
 | ||||
| @admin.register(m.Deadline) | ||||
| class DeadlineAdmin(admin.ModelAdmin): | ||||
| 	actions = ['pregeneruj_vysledkovku'] | ||||
| 
 | ||||
| 	# Nikomu nezobrazovat, ale superuživatelům se může hodit :-) | ||||
| 	@admin.action(permissions=['bazmek'], description= 'Přegeneruj výsledkovky vybraných deadlinů') | ||||
| 	def pregeneruj_vysledkovku(self, req, qs): | ||||
| 		for deadline in qs: | ||||
| 			deadline.vygeneruj_vysledkovku() | ||||
| 	 | ||||
| 	def has_bazmek_permission(self, request): | ||||
| 		# Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu… | ||||
| 		return request.user.is_superuser | ||||
| 	 | ||||
| class DeadlineAdminInline(admin.TabularInline): | ||||
|     model = m.Deadline | ||||
|     extra = 0 | ||||
| 
 | ||||
| 	model = m.Deadline | ||||
| 	extra = 0 | ||||
| 
 | ||||
| class CisloForm(ModelForm): | ||||
| 	class Meta: | ||||
|  | @ -71,7 +81,7 @@ class CisloForm(ModelForm): | |||
| @admin.register(m.Cislo) | ||||
| class CisloAdmin(admin.ModelAdmin): | ||||
| 	form = CisloForm | ||||
| 	actions = ['force_publish'] | ||||
| 	actions = ['force_publish', 'pregeneruj_vysledkovky'] | ||||
| 	inlines = (DeadlineAdminInline,) | ||||
| 
 | ||||
| 	def force_publish(self,request,queryset): | ||||
|  | @ -111,6 +121,17 @@ class CisloAdmin(admin.ModelAdmin): | |||
| 
 | ||||
| 	force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' | ||||
| 
 | ||||
| 	# Jen pro superuživatele | ||||
| 	@admin.action(permissions=['bazmek'], description='Přegenerovat výsledkovky všech deadlinů vybraných čísel') | ||||
| 	def pregeneruj_vysledkovky(self, req, qs): | ||||
| 		for cislo in qs: | ||||
| 			for deadline in cislo.deadline_v_cisle.all(): | ||||
| 				deadline.vygeneruj_vysledkovku() | ||||
| 	 | ||||
| 	def has_bazmek_permission(self, request): | ||||
| 		# Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu… | ||||
| 		return request.user.is_superuser | ||||
| 
 | ||||
| 
 | ||||
| @admin.register(m.Problem) | ||||
| class ProblemAdmin(PolymorphicParentModelAdmin): | ||||
|  |  | |||
|  | @ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel): | |||
| 				return self.nadproblem.kod_v_rocniku+".{}".format(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 f'<Není zadaný: {self.kod}>' | ||||
| 
 | ||||
| #	def verejne(self): | ||||
| #		# aktuálně podle stavu problému | ||||
|  | @ -571,9 +571,9 @@ class Tema(Problem): | |||
| 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||
| 			if self.nadproblem: | ||||
| 				return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) | ||||
| 			return "t{}".format(self.kod) | ||||
| 			return 't'+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 f'<Není zadaný: {self.kod}>' | ||||
| 
 | ||||
| 	def save(self, *args, **kwargs): | ||||
| 		super().save(*args, **kwargs) | ||||
|  | @ -607,9 +607,9 @@ class Clanek(Problem): | |||
| # Nemělo by být potřeba | ||||
| #			if self.nadproblem: | ||||
| #				return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) | ||||
| 			return "c{}".format(self.kod) | ||||
| 			return "c" + 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 f'<Není zadaný: {self.kod}>' | ||||
| 	 | ||||
| 	def node(self): | ||||
| 		return None | ||||
|  | @ -642,12 +642,9 @@ class Uloha(Problem): | |||
| 	@cached_property | ||||
| 	def kod_v_rocniku(self): | ||||
| 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: | ||||
| 			name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) | ||||
| 			if self.nadproblem: | ||||
| 				return self.nadproblem.kod_v_rocniku+name | ||||
| 			return name | ||||
| 			return f"{self.cislo_zadani.poradi}.{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 f'<Není zadaný: {self.kod}>' | ||||
| 
 | ||||
| 	def save(self, *args, **kwargs): | ||||
| 		super().save(*args, **kwargs) | ||||
|  |  | |||
|  | @ -35,6 +35,7 @@ from django.conf import settings | |||
| import unicodedata | ||||
| import logging | ||||
| import time | ||||
| from collections.abc import Sequence | ||||
| 
 | ||||
| from seminar.utils import aktivniResitele | ||||
| 
 | ||||
|  | @ -534,7 +535,9 @@ class PosledniCisloVysledkovkaView(generic.DetailView): | |||
| 	def get_context_data(self, **kwargs): | ||||
| 		context = super(PosledniCisloVysledkovkaView, self).get_context_data() | ||||
| 		rocnik = context['rocnik'] | ||||
| 		cislo = rocnik.cisla.order_by("poradi").last() | ||||
| 		cislo = rocnik.cisla.order_by("poradi").filter(deadline_v_cisle__isnull=False).last() | ||||
| 		if cislo is None: | ||||
| 			raise Http404(f"Ročník {rocnik.rocnik} nemá číslo s deadlinem.") | ||||
| 		cislopred = cislo.predchozi() | ||||
| 		context['vysledkovka'] = VysledkovkaDoTeXu( | ||||
| 			cislo, | ||||
|  | @ -677,9 +680,9 @@ def StavDatabazeView(request): | |||
| 
 | ||||
| 
 | ||||
| # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) | ||||
| def formularOKView(request, text=''): | ||||
| def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()): | ||||
| 	template_name = 'seminar/formular_ok.html' | ||||
| 	odkazy = [ | ||||
| 	odkazy = list(dalsi_odkazy) + [ | ||||
| 		# (Text, odkaz) | ||||
| 		('Vrátit se na titulní stránku', reverse('titulni_strana')), | ||||
| 		('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')), | ||||
|  |  | |||
							
								
								
									
										0
									
								
								sifrovacka/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sifrovacka/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								sifrovacka/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								sifrovacka/admin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| from .models import OdpovedUcastnika, SpravnaOdpoved | ||||
| 
 | ||||
| # Register your models here. | ||||
| 
 | ||||
| admin.site.register(OdpovedUcastnika) | ||||
| admin.site.register(SpravnaOdpoved) | ||||
							
								
								
									
										5
									
								
								sifrovacka/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								sifrovacka/apps.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class SifrovackaConfig(AppConfig): | ||||
|     name = 'sifrovacka' | ||||
							
								
								
									
										18
									
								
								sifrovacka/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								sifrovacka/forms.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| from django.core.exceptions import ValidationError | ||||
| from django.forms import ModelForm, Textarea | ||||
| from .models import OdpovedUcastnika, SpravnaOdpoved | ||||
| 
 | ||||
| 
 | ||||
| class SifrovackaForm(ModelForm): | ||||
| 	class Meta: | ||||
| 		model = OdpovedUcastnika | ||||
| 		fields = ["sifra", "odpoved", ] | ||||
| 		widgets = { | ||||
| 			"odpoved": Textarea(attrs={'rows': 1, 'cols': 30}), | ||||
| 		} | ||||
| 
 | ||||
| 	def clean_sifra(self): | ||||
| 		sifra = self.cleaned_data.get('sifra') | ||||
| 		if SpravnaOdpoved.objects.filter(sifra=sifra).count() == 0: | ||||
| 			raise ValidationError("Tohle číslo šifry v databázi nemáme. Zkontrolujte si ho prosím.") | ||||
| 		return sifra | ||||
							
								
								
									
										34
									
								
								sifrovacka/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								sifrovacka/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| # Generated by Django 3.2.22 on 2023-10-14 09:20 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     initial = True | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('seminar', '0113_resitel_zasilat_cislo_papirove'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='SpravnaOdpoved', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('odpoved', models.TextField()), | ||||
|                 ('sifra', models.IntegerField()), | ||||
|                 ('skryty_text', models.TextField()), | ||||
|             ], | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='OdpovedUcastnika', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('odpoved', models.TextField(verbose_name='Tajenka')), | ||||
|                 ('sifra', models.IntegerField(verbose_name='Číslo šifry')), | ||||
|                 ('resitel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.resitel')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										28
									
								
								sifrovacka/migrations/0002_auto_20231015_1944.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								sifrovacka/migrations/0002_auto_20231015_1944.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| # Generated by Django 3.2.22 on 2023-10-15 17:44 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.utils.timezone | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('sifrovacka', '0001_initial'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name='odpoveducastnika', | ||||
|             options={'ordering': ['-timestamp']}, | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='odpoveducastnika', | ||||
|             name='timestamp', | ||||
|             field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp'), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name='odpoveducastnika', | ||||
|             name='odpoved', | ||||
|             field=models.TextField(verbose_name='Tajenka bez diakritiky'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								sifrovacka/migrations/0003_odpoveducastnika_uspech.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								sifrovacka/migrations/0003_odpoveducastnika_uspech.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 3.2.22 on 2023-10-16 17:51 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('sifrovacka', '0002_auto_20231015_1944'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='odpoveducastnika', | ||||
|             name='uspech', | ||||
|             field=models.BooleanField(default=False, verbose_name='Úspěch'), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								sifrovacka/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sifrovacka/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										27
									
								
								sifrovacka/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								sifrovacka/models.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| from django.db import models | ||||
| from django.utils import timezone | ||||
| 
 | ||||
| from seminar.models.personalni import Resitel | ||||
| 
 | ||||
| 
 | ||||
| # Create your models here. | ||||
| 
 | ||||
| 
 | ||||
| class OdpovedUcastnika(models.Model): | ||||
| 	class Meta: | ||||
| 		ordering = ["-timestamp"] | ||||
| 
 | ||||
| 	resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE) | ||||
| 	odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,) | ||||
| 	sifra = models.IntegerField("Číslo šifry", blank=False, null=False,) | ||||
| 	timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now) | ||||
| 	uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False) | ||||
| 
 | ||||
| 
 | ||||
| class SpravnaOdpoved(models.Model): | ||||
| 	odpoved = models.TextField(blank=False, null=False,) | ||||
| 	sifra = models.IntegerField(blank=False, null=False,) | ||||
| 	skryty_text = models.TextField(blank=False, null=False,) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return f"{self.sifra}: {self.odpoved}" | ||||
							
								
								
									
										25
									
								
								sifrovacka/templates/sifrovacka/odpovedi_list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								sifrovacka/templates/sifrovacka/odpovedi_list.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|   <h1>{% block nadpis1a %}Šifrovačka odpovědi{% endblock nadpis1a %}</h1> | ||||
| 
 | ||||
|   <table class="dosla_reseni"> | ||||
|     <tr> | ||||
|       <th>Timestamp</th> | ||||
|       <th>Řešitel</th> | ||||
|       <th>Šifra</th> | ||||
|       <th>Odpověď</th> | ||||
|     </tr> | ||||
| 
 | ||||
|     {% for u in object_list %} | ||||
|       <tr> | ||||
|         <td>{{ u.timestamp }}</td> | ||||
|         <td>{{ u.resitel }}</td> | ||||
|         <td>{{ u.sifra }}</td> | ||||
|         <td style="color: {% if u.uspech %}green{% else %}red{% endif %};">{{ u.odpoved }}</td> | ||||
|       </tr> | ||||
|     {% endfor %} | ||||
|   </table> | ||||
| 
 | ||||
| {% endblock content %} | ||||
							
								
								
									
										46
									
								
								sifrovacka/templates/sifrovacka/sifrovacka.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								sifrovacka/templates/sifrovacka/sifrovacka.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,46 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|   <br> | ||||
| 
 | ||||
|   <h1>{% block nadpis1a %}M&Mí šifrovačka{% endblock nadpis1a %}</h1> | ||||
| 
 | ||||
|   <br> | ||||
| 
 | ||||
|   <h2>Zadat tajenku šifry:</h2> | ||||
| 
 | ||||
|   <form action="{% url 'sifrovacka' %}" method="post"> | ||||
|     <table class="form"> | ||||
|       {{form.non_field_errors}} | ||||
|       {% for field in form %} | ||||
|         <tr> | ||||
|           <td> | ||||
|             <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> | ||||
|               {{ field.label }} | ||||
|             </label> | ||||
| 
 | ||||
|           </td> | ||||
| 
 | ||||
|           <td {% if field.help_text %} class="field-with-comment"{% endif %}> | ||||
|             {{ field }} | ||||
|             <span class="field-comment">{{ field.help_text|safe }}</span> | ||||
|           </td> | ||||
| 
 | ||||
|         </tr> | ||||
| 
 | ||||
| 
 | ||||
|         {% if field.errors %} | ||||
|           <tr> | ||||
|             <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> | ||||
|           </tr> | ||||
|         {% endif %} | ||||
|       {% endfor %} | ||||
|     </table> | ||||
| 
 | ||||
|     {% csrf_token %} | ||||
| 
 | ||||
|     <input type="submit" value="Tak pravím!"> | ||||
|   </form> | ||||
| 
 | ||||
| {% endblock content %} | ||||
							
								
								
									
										17
									
								
								sifrovacka/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sifrovacka/urls.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| from django.urls import path | ||||
| 
 | ||||
| from seminar.utils import org_required, resitel_or_org_required | ||||
| from .views import SifrovackaView, SifrovackaListView | ||||
| 
 | ||||
| urlpatterns = [ | ||||
| 	path( | ||||
| 		'', | ||||
| 		resitel_or_org_required(SifrovackaView.as_view()), | ||||
| 		name='sifrovacka' | ||||
| 	), | ||||
| 	path( | ||||
| 		'odpovedi/', | ||||
| 		org_required(SifrovackaListView.as_view()), | ||||
| 		name='sifrovacka_odpovedi' | ||||
| 	), | ||||
| ] | ||||
							
								
								
									
										33
									
								
								sifrovacka/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								sifrovacka/views.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| from django.urls import reverse | ||||
| from django.views.generic import FormView, ListView | ||||
| 
 | ||||
| from seminar.views import formularOKView | ||||
| from .forms import SifrovackaForm | ||||
| from .models import OdpovedUcastnika, SpravnaOdpoved | ||||
| from seminar.models.personalni import Resitel | ||||
| 
 | ||||
| 
 | ||||
| # Create your views here. | ||||
| 
 | ||||
| class SifrovackaView(FormView): | ||||
| 	template_name = 'sifrovacka/sifrovacka.html' | ||||
| 	form_class = SifrovackaForm | ||||
| 
 | ||||
| 	def form_valid(self, form): | ||||
| 		instance = form.save(commit=False) | ||||
| 		resitel = Resitel.objects.get(osoba__user=self.request.user) | ||||
| 		instance.resitel = resitel | ||||
| 		instance.save() | ||||
| 		sifra = SpravnaOdpoved.objects.filter(sifra=instance.sifra, odpoved__iexact=instance.odpoved.strip()).first() | ||||
| 		if sifra is None: | ||||
| 			return formularOKView(self.request, f'<h1>Bohužel vám hvězdy nebyly nakloněny. Rozumějte <i>máte to blbě</i>.</h1> <p><a href="{reverse("sifrovacka")}">Zkusit znovu.</a></p><br><br><br>') | ||||
| 
 | ||||
| 		instance.uspech = True | ||||
| 		instance.save() | ||||
| 		 | ||||
| 		return formularOKView(self.request, f'<h1>{sifra.skryty_text}</h1> <p><a href="{reverse("sifrovacka")}">Odevzdat další.</a></p><br><br><br>') | ||||
| 
 | ||||
| 
 | ||||
| class SifrovackaListView(ListView): | ||||
| 	template_name = 'sifrovacka/odpovedi_list.html' | ||||
| 	model = OdpovedUcastnika | ||||
							
								
								
									
										87
									
								
								various/static/various/img/zere_kostku.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								various/static/various/img/zere_kostku.svg
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| After Width: | Height: | Size: 32 KiB | 
							
								
								
									
										19
									
								
								various/templates/various/403_csrf.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								various/templates/various/403_csrf.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| {#{% extends "error_base.html" %} Z toho nedědíme, protože se nemá přecházet na titulní stránku. #} | ||||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% load static %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|   <h2>{% block nadpis1a %}O-jo-jo-jo-joj{% endblock nadpis1a %}</h2> | ||||
| 
 | ||||
|   <p> | ||||
|     Problém se sušenkami či něčím podobným. Zkuste {% if url %}to prosím znovu: <a href="{{ url }}">{{ url }}</a>. Případně můžete {% endif %}přejít na <a href="/">titulní stránku</a>. | ||||
|   </p> | ||||
| 
 | ||||
|   <p>Pokud problém přetrvává obraťte se na nás přes e-mail: <a href="mailto:mam@matfyz.cz">mailto:mam@matfyz.cz</a> a pošlete nám následující popis chyby: <code>{{ reason }}</code></p> | ||||
| 
 | ||||
|   <img src="{% static 'various/img/zere_kostku.svg' %}"> | ||||
| 
 | ||||
| 
 | ||||
| {% endblock %} | ||||
|  | @ -1,3 +1,13 @@ | |||
| from django.http import HttpResponseForbidden | ||||
| from django.shortcuts import render | ||||
| 
 | ||||
| # Create your views here. | ||||
| 
 | ||||
| 
 | ||||
| def csrf_error(request, reason=""): | ||||
| 	""" Jednoduchý „template view“ (třída to být nemůže) pro CSRF chyby """ | ||||
| 	return render( | ||||
| 		request, 'various/403_csrf.html', | ||||
| 		{"url": request.META.get("HTTP_REFERER", None), "reason": reason}, | ||||
| 		status=HttpResponseForbidden.status_code, | ||||
| 	) | ||||
|  |  | |||
|  | @ -4,11 +4,11 @@ | |||
|         <th class='border-r'>#</th> | ||||
|         <th class='border-r'>Jméno</th> | ||||
|             {% for p in vysledkovka.temata_a_spol%} | ||||
|                 <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}</th> | ||||
|                 <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}<span title="{{ p }}">{{ p.kod_v_rocniku }}</span>{# </a> #}</th> | ||||
| 
 | ||||
|                     {# TODELETE #} | ||||
|                     {% for podproblemy in vysledkovka.podproblemy_iter.next %} | ||||
|                         <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> | ||||
|                         <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> | ||||
|                     {% endfor %} | ||||
|                 {# TODELETE #} | ||||
| 
 | ||||
|  | @ -17,7 +17,7 @@ | |||
| 
 | ||||
|         {# TODELETE #} | ||||
|         {% for podproblemy in vysledkovka.podproblemy_iter.next %} | ||||
|             <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> | ||||
|             <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> | ||||
|         {% endfor %} | ||||
|         {# TODELETE #} | ||||
| 
 | ||||
|  |  | |||
|  | @ -257,7 +257,7 @@ class VysledkovkaCisla(Vysledkovka): | |||
| 		# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) | ||||
| 		hlavni_problemy = set() | ||||
| 		for p in self.problemy: | ||||
| 			hlavni_problemy.add(p.hlavni_problem) | ||||
| 			hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy… | ||||
| 
 | ||||
| 		# zunikátnění | ||||
| 		hlavni_problemy = list(hlavni_problemy) | ||||
|  | @ -313,7 +313,7 @@ class VysledkovkaCisla(Vysledkovka): | |||
| 
 | ||||
| 		# Sečteme hodnocení | ||||
| 		for hodnoceni in self.hodnoceni_do_cisla: | ||||
| 			prob = hodnoceni.problem | ||||
| 			prob = hodnoceni.problem.get_real_instance() | ||||
| 			nadproblem = prob.hlavni_problem.id | ||||
| 
 | ||||
| 			# Když nadproblém není "téma", pak je "Ostatní" | ||||
|  | @ -366,18 +366,12 @@ class VysledkovkaCisla(Vysledkovka): | |||
| 		for problem in self.problemy: | ||||
| 			h_problem = problem.hlavni_problem | ||||
| 			if h_problem in temata_a_spol: | ||||
| 				podproblemy[h_problem.id].append(problem) | ||||
| 				podproblemy[h_problem.id].append(problem.get_real_instance()) | ||||
| 			else: | ||||
| 				podproblemy[-1].append(problem) | ||||
| 				podproblemy[-1].append(problem.get_real_instance()) | ||||
| 
 | ||||
| 		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) | ||||
| 			podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku) | ||||
| 		return podproblemy | ||||
| 
 | ||||
| 	@cached_property | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Pavel "LEdoian" Turinsky
						Pavel "LEdoian" Turinsky