Merge branch 'master' into korekturovatko
This commit is contained in:
		
						commit
						9ccacaecb5
					
				
					 39 changed files with 860 additions and 278 deletions
				
			
		
							
								
								
									
										144
									
								
								data/groups.json
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								data/groups.json
									
									
									
									
									
								
							|  | @ -14,12 +14,12 @@ | |||
| 					"flatpage" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_flatpage", | ||||
| 					"change_flatpage", | ||||
| 					"flatpages", | ||||
| 					"flatpage" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_flatpage", | ||||
| 					"delete_flatpage", | ||||
| 					"flatpages", | ||||
| 					"flatpage" | ||||
| 				], | ||||
|  | @ -34,12 +34,12 @@ | |||
| 					"galerie" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_galerie", | ||||
| 					"change_galerie", | ||||
| 					"galerie", | ||||
| 					"galerie" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_galerie", | ||||
| 					"delete_galerie", | ||||
| 					"galerie", | ||||
| 					"galerie" | ||||
| 				], | ||||
|  | @ -54,12 +54,12 @@ | |||
| 					"obrazek" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_obrazek", | ||||
| 					"change_obrazek", | ||||
| 					"galerie", | ||||
| 					"obrazek" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_obrazek", | ||||
| 					"delete_obrazek", | ||||
| 					"galerie", | ||||
| 					"obrazek" | ||||
| 				], | ||||
|  | @ -104,12 +104,12 @@ | |||
| 					"komentar" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_komentar", | ||||
| 					"change_komentar", | ||||
| 					"korektury", | ||||
| 					"komentar" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_komentar", | ||||
| 					"delete_komentar", | ||||
| 					"korektury", | ||||
| 					"komentar" | ||||
| 				], | ||||
|  | @ -124,12 +124,12 @@ | |||
| 					"korekturovanepdf" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_korekturovanepdf", | ||||
| 					"change_korekturovanepdf", | ||||
| 					"korektury", | ||||
| 					"korekturovanepdf" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_korekturovanepdf", | ||||
| 					"delete_korekturovanepdf", | ||||
| 					"korektury", | ||||
| 					"korekturovanepdf" | ||||
| 				], | ||||
|  | @ -144,12 +144,12 @@ | |||
| 					"oprava" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_oprava", | ||||
| 					"change_oprava", | ||||
| 					"korektury", | ||||
| 					"oprava" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_oprava", | ||||
| 					"delete_oprava", | ||||
| 					"korektury", | ||||
| 					"oprava" | ||||
| 				], | ||||
|  | @ -164,12 +164,12 @@ | |||
| 					"novinky" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_novinky", | ||||
| 					"change_novinky", | ||||
| 					"novinky", | ||||
| 					"novinky" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_novinky", | ||||
| 					"delete_novinky", | ||||
| 					"novinky", | ||||
| 					"novinky" | ||||
| 				], | ||||
|  | @ -204,12 +204,12 @@ | |||
| 					"prijemce" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_prijemce", | ||||
| 					"change_prijemce", | ||||
| 					"personalni", | ||||
| 					"prijemce" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_prijemce", | ||||
| 					"delete_prijemce", | ||||
| 					"personalni", | ||||
| 					"prijemce" | ||||
| 				], | ||||
|  | @ -234,12 +234,12 @@ | |||
| 					"skola" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_skola", | ||||
| 					"change_skola", | ||||
| 					"personalni", | ||||
| 					"skola" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_skola", | ||||
| 					"delete_skola", | ||||
| 					"personalni", | ||||
| 					"skola" | ||||
| 				], | ||||
|  | @ -248,38 +248,28 @@ | |||
| 					"personalni", | ||||
| 					"skola" | ||||
| 				], | ||||
| 				[ | ||||
| 					"add_hlasovani", | ||||
| 					"prednasky", | ||||
| 					"hlasovani" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_hlasovani", | ||||
| 					"prednasky", | ||||
| 					"hlasovani" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_hlasovani", | ||||
| 					"prednasky", | ||||
| 					"hlasovani" | ||||
| 				], | ||||
| 				[ | ||||
| 					"view_hlasovani", | ||||
| 					"prednasky", | ||||
| 					"hlasovani" | ||||
| 				], | ||||
| 				[ | ||||
| 					"view_hlasovanioznalostech", | ||||
| 					"prednasky", | ||||
| 					"hlasovanioznalostech" | ||||
| 				], | ||||
| 				[ | ||||
| 					"add_prednaska", | ||||
| 					"prednasky", | ||||
| 					"prednaska" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_prednaska", | ||||
| 					"change_prednaska", | ||||
| 					"prednasky", | ||||
| 					"prednaska" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_prednaska", | ||||
| 					"delete_prednaska", | ||||
| 					"prednasky", | ||||
| 					"prednaska" | ||||
| 				], | ||||
|  | @ -294,12 +284,12 @@ | |||
| 					"seznam" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_seznam", | ||||
| 					"change_seznam", | ||||
| 					"prednasky", | ||||
| 					"seznam" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_seznam", | ||||
| 					"delete_seznam", | ||||
| 					"prednasky", | ||||
| 					"seznam" | ||||
| 				], | ||||
|  | @ -308,18 +298,38 @@ | |||
| 					"prednasky", | ||||
| 					"seznam" | ||||
| 				], | ||||
| 				[ | ||||
| 					"add_znalost", | ||||
| 					"prednasky", | ||||
| 					"znalost" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_znalost", | ||||
| 					"prednasky", | ||||
| 					"znalost" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_znalost", | ||||
| 					"prednasky", | ||||
| 					"znalost" | ||||
| 				], | ||||
| 				[ | ||||
| 					"view_znalost", | ||||
| 					"prednasky", | ||||
| 					"znalost" | ||||
| 				], | ||||
| 				[ | ||||
| 					"add_konfera", | ||||
| 					"soustredeni", | ||||
| 					"konfera" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_konfera", | ||||
| 					"change_konfera", | ||||
| 					"soustredeni", | ||||
| 					"konfera" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_konfera", | ||||
| 					"delete_konfera", | ||||
| 					"soustredeni", | ||||
| 					"konfera" | ||||
| 				], | ||||
|  | @ -334,12 +344,12 @@ | |||
| 					"konfery_ucastnici" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_konfery_ucastnici", | ||||
| 					"change_konfery_ucastnici", | ||||
| 					"soustredeni", | ||||
| 					"konfery_ucastnici" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_konfery_ucastnici", | ||||
| 					"delete_konfery_ucastnici", | ||||
| 					"soustredeni", | ||||
| 					"konfery_ucastnici" | ||||
| 				], | ||||
|  | @ -354,12 +364,12 @@ | |||
| 					"soustredeni" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_soustredeni", | ||||
| 					"change_soustredeni", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_soustredeni", | ||||
| 					"delete_soustredeni", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni" | ||||
| 				], | ||||
|  | @ -374,12 +384,12 @@ | |||
| 					"soustredeni_organizatori" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_soustredeni_organizatori", | ||||
| 					"change_soustredeni_organizatori", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni_organizatori" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_soustredeni_organizatori", | ||||
| 					"delete_soustredeni_organizatori", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni_organizatori" | ||||
| 				], | ||||
|  | @ -394,12 +404,12 @@ | |||
| 					"soustredeni_ucastnici" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_soustredeni_ucastnici", | ||||
| 					"change_soustredeni_ucastnici", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni_ucastnici" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_soustredeni_ucastnici", | ||||
| 					"delete_soustredeni_ucastnici", | ||||
| 					"soustredeni", | ||||
| 					"soustredeni_ucastnici" | ||||
| 				], | ||||
|  | @ -414,12 +424,12 @@ | |||
| 					"tag" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_tag", | ||||
| 					"change_tag", | ||||
| 					"taggit", | ||||
| 					"tag" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_tag", | ||||
| 					"delete_tag", | ||||
| 					"taggit", | ||||
| 					"tag" | ||||
| 				], | ||||
|  | @ -434,12 +444,12 @@ | |||
| 					"taggeditem" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_taggeditem", | ||||
| 					"change_taggeditem", | ||||
| 					"taggit", | ||||
| 					"taggeditem" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_taggeditem", | ||||
| 					"delete_taggeditem", | ||||
| 					"taggit", | ||||
| 					"taggeditem" | ||||
| 				], | ||||
|  | @ -454,12 +464,12 @@ | |||
| 					"cislo" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_cislo", | ||||
| 					"change_cislo", | ||||
| 					"tvorba", | ||||
| 					"cislo" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_cislo", | ||||
| 					"delete_cislo", | ||||
| 					"tvorba", | ||||
| 					"cislo" | ||||
| 				], | ||||
|  | @ -474,12 +484,12 @@ | |||
| 					"clanek" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_clanek", | ||||
| 					"change_clanek", | ||||
| 					"tvorba", | ||||
| 					"clanek" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_clanek", | ||||
| 					"delete_clanek", | ||||
| 					"tvorba", | ||||
| 					"clanek" | ||||
| 				], | ||||
|  | @ -509,12 +519,12 @@ | |||
| 					"pohadka" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_pohadka", | ||||
| 					"change_pohadka", | ||||
| 					"tvorba", | ||||
| 					"pohadka" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_pohadka", | ||||
| 					"delete_pohadka", | ||||
| 					"tvorba", | ||||
| 					"pohadka" | ||||
| 				], | ||||
|  | @ -529,12 +539,12 @@ | |||
| 					"problem" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_problem", | ||||
| 					"change_problem", | ||||
| 					"tvorba", | ||||
| 					"problem" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_problem", | ||||
| 					"delete_problem", | ||||
| 					"tvorba", | ||||
| 					"problem" | ||||
| 				], | ||||
|  | @ -549,12 +559,12 @@ | |||
| 					"rocnik" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_rocnik", | ||||
| 					"change_rocnik", | ||||
| 					"tvorba", | ||||
| 					"rocnik" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_rocnik", | ||||
| 					"delete_rocnik", | ||||
| 					"tvorba", | ||||
| 					"rocnik" | ||||
| 				], | ||||
|  | @ -569,12 +579,12 @@ | |||
| 					"tema" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_tema", | ||||
| 					"change_tema", | ||||
| 					"tvorba", | ||||
| 					"tema" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_tema", | ||||
| 					"delete_tema", | ||||
| 					"tvorba", | ||||
| 					"tema" | ||||
| 				], | ||||
|  | @ -589,12 +599,12 @@ | |||
| 					"uloha" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_uloha", | ||||
| 					"change_uloha", | ||||
| 					"tvorba", | ||||
| 					"uloha" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_uloha", | ||||
| 					"delete_uloha", | ||||
| 					"tvorba", | ||||
| 					"uloha" | ||||
| 				], | ||||
|  | @ -609,12 +619,12 @@ | |||
| 					"nastaveni" | ||||
| 				], | ||||
| 				[ | ||||
| 					"delete_nastaveni", | ||||
| 					"change_nastaveni", | ||||
| 					"various", | ||||
| 					"nastaveni" | ||||
| 				], | ||||
| 				[ | ||||
| 					"change_nastaveni", | ||||
| 					"delete_nastaveni", | ||||
| 					"various", | ||||
| 					"nastaveni" | ||||
| 				], | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ extensions = [ | |||
|     'sphinx.ext.intersphinx', | ||||
|     'sphinx.ext.autosectionlabel', | ||||
|     'myst_parser', | ||||
| 	'sphinxcontrib_django', | ||||
| ] | ||||
| 
 | ||||
| # Add any paths that contain templates here, relative to this directory. | ||||
|  |  | |||
|  | @ -95,7 +95,7 @@ function safe_checkout_branch { | |||
| 		echo >&2 "Změna v $SCRIPT, prosím pullni manuálně" | ||||
| 		exit 1 | ||||
| 	fi | ||||
| 	git checkout "$BRANCH" | ||||
| 	git checkout "$BRANCH" -- | ||||
| 	git pull | ||||
| 	git clean -f | ||||
| } | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600  # rok | |||
| CSRF_FAILURE_VIEW = 'various.views.csrf.csrf_error' | ||||
| 
 | ||||
| # Modules configuration | ||||
| FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer" | ||||
| 
 | ||||
| AUTHENTICATION_BACKENDS = ( | ||||
| 	'django.contrib.auth.backends.ModelBackend', | ||||
|  |  | |||
|  | @ -435,6 +435,7 @@ body.localweb, body.testweb, body.suprodweb { | |||
| 		height: 100%; | ||||
| 		top: 0; | ||||
| 		z-index: -1000; | ||||
| 		opacity: 0.7; | ||||
| 	} | ||||
| 
 | ||||
| 	&:before { left: 0; } | ||||
|  |  | |||
|  | @ -503,5 +503,10 @@ label[for=id_skola] { | |||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| /* Přednášky */ | ||||
| .textznalosti, .textprednasky { | ||||
| 	font-style: italic; | ||||
| } | ||||
| 
 | ||||
| /*******************/ | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										20
									
								
								mamweb/static/css/rozliseni.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								mamweb/static/css/rozliseni.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| /**** ROZLIŠENÍ MEZI LOKÁLNÍM, TESTOVACÍM A PRODUKČNÍM WEBEM ****/ | ||||
| body.localweb, body.testweb, body.suprodweb { | ||||
| 	&:before, &:after { | ||||
| 		content: ""; | ||||
| 		position: fixed; | ||||
| 		width: 20px; | ||||
| 		height: 100%; | ||||
| 		top: 0; | ||||
| 		z-index: -1000; | ||||
| 		opacity: 0.7; | ||||
| 	} | ||||
| 
 | ||||
| 	&:before { left: 0; } | ||||
| 	&:after { right: 0; } | ||||
| } | ||||
| 
 | ||||
| body.localweb { &:before, &:after { background: greenyellow; } } | ||||
| body.testweb { &:before, &:after { background: darkorange; } } | ||||
| body.suprodweb { &:before, &:after { background: red; } } | ||||
| /****************************************************************/ | ||||
|  | @ -1,6 +1,6 @@ | |||
| # Tento soubor slouží pouze pro shell a podobné. Nikde neimportovat v kódu! | ||||
| 
 | ||||
| print("Naimportoval jsi `seminar.models`. Pevně věřím, že to nebylo nikde v kódu. Díky.") | ||||
| print("Naimportoval jsi `mamweb.vsechno`. Pevně věřím, že to nebylo nikde v kódu. Díky.") | ||||
| 
 | ||||
| from galerie.models import * | ||||
| from header_fotky.models import * | ||||
|  |  | |||
|  | @ -65,6 +65,9 @@ class Reseni(SeminarModelBase): | |||
| 	def absolute_url(self): | ||||
| 		return "https://" + str(get_current_site(None)) + self.verejne_url() | ||||
| 
 | ||||
| 	def resitel_url(self): | ||||
| 		return f'https://{get_current_site(None)}{reverse_lazy("odevzdavatko_resitel_reseni", args=[self.id])}' | ||||
| 
 | ||||
| 	# má OneToOneField s: | ||||
| 	# Konfera | ||||
| 
 | ||||
|  |  | |||
|  | @ -191,7 +191,7 @@ Sloupce: | |||
|     </ul> | ||||
|   </li> | ||||
|   <li>Pokud nemáš důvod, deadline neměň. Sloupeček s deadlinem znamená, do kterého deadlinu se započítají body (nemusí se shodovat s deadlinem řešení).</li> | ||||
|   <li>Poslední sloupec je na zpětnou vazbu řešiteli, tedy (na rozdíl od Neveřejné poznámky, která je určena pro synchronizaci orgů) ji uvidí řešitelé. Zatím jen pasivně (nechodí e-mail). Pohled řešitele si můžete prohlédnout <a href="{% url 'odevzdavatko_resitel_reseni' reseni.id %}">zde</a>. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.</li> | ||||
|   <li>Poslední sloupec je na zpětnou vazbu řešiteli, tedy (na rozdíl od Neveřejné poznámky, která je určena pro synchronizaci orgů) ji uvidí řešitelé. Změníte-li u nějakého hodnocení toto políčko, řešitel bude upozorněn emailem, pokud si tuto možnost nevypl ve svém profilu. Pohled řešitele si můžete prohlédnout <a href="{% url 'odevzdavatko_resitel_reseni' reseni.id %}">zde</a>. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.</li> | ||||
| </ol> | ||||
| 
 | ||||
| Další poznámky | ||||
|  |  | |||
|  | @ -222,6 +222,17 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi | |||
| 		ctx["problem_id"] = self.kwargs['problem'] | ||||
| 		return ctx | ||||
| 
 | ||||
| HODNOCENI_INITIAL_DATA = [ | ||||
| 	"problem", | ||||
| 	"body", | ||||
| 	"body_celkem", | ||||
| 	"body_neprepocitane", | ||||
| 	"body_neprepocitane_celkem", | ||||
| 	"body_max", | ||||
| 	"body_neprepocitane_max", | ||||
| 	"deadline_body", | ||||
| 	"feedback", | ||||
| ] | ||||
| ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex | ||||
| class DetailReseniView(DetailView): | ||||
| 	""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """ | ||||
|  | @ -232,18 +243,7 @@ class DetailReseniView(DetailView): | |||
| 		self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk']) | ||||
| 		result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet | ||||
| 		for hodn in Hodnoceni.objects.filter(reseni=self.reseni): | ||||
| 			seznam_atributu = [ | ||||
| 				"problem", | ||||
| 				"body", | ||||
| 				"body_celkem", | ||||
| 				"body_neprepocitane", | ||||
| 				"body_neprepocitane_celkem", | ||||
| 				"body_max", | ||||
| 				"body_neprepocitane_max", | ||||
| 				"deadline_body", | ||||
| 				"feedback", | ||||
| 			] | ||||
| 			result.append({attr: getattr(hodn, attr) for attr in seznam_atributu}) | ||||
| 			result.append({attr: getattr(hodn, attr) for attr in HODNOCENI_INITIAL_DATA}) | ||||
| 		return result | ||||
| 
 | ||||
| 	def get_context_data(self, **kw): | ||||
|  | @ -291,9 +291,9 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | |||
| 	reseni = get_object_or_404(Reseni, pk=pk) | ||||
| 	success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) | ||||
| 
 | ||||
| 	# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově | ||||
| 	# Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm | ||||
| 	formset = f.OhodnoceniReseniFormSet(request.POST) | ||||
| 	formset = f.OhodnoceniReseniFormSet(request.POST, initial=[ | ||||
| 		{k: getattr(h, k) for k in HODNOCENI_INITIAL_DATA} for h in Hodnoceni.objects.filter(reseni=reseni) | ||||
| 	]) | ||||
| 	poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni) | ||||
| 	# TODO: Napsat validaci formuláře a formsetu | ||||
| 	if not (formset.is_valid() and poznamka_form.is_valid()): | ||||
|  | @ -309,7 +309,9 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | |||
| 		qs.delete() | ||||
| 
 | ||||
| 		# Vyrobíme nová podle formsetu | ||||
| 		notifikace = False | ||||
| 		for form in formset: | ||||
| 			notifikace |= 'feedback' in form.changed_data | ||||
| 			data_for_hodnoceni = form.cleaned_data | ||||
| 			data_for_body = data_for_hodnoceni.copy() | ||||
| 			del(data_for_hodnoceni["body_celkem"]) | ||||
|  | @ -320,16 +322,44 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | |||
| 					**form.cleaned_data, | ||||
| 					) | ||||
| 			logger.info(f"Creating Hodnoceni: {hodnoceni}") | ||||
| 			# FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak | ||||
| 			zmeny_bodu = [it for it in form.changed_data if it.startswith("body")] | ||||
| 			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 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.") | ||||
| 			if len(zmeny_bodu) != 0: | ||||
| 				body_nastaveny: None | tuple[str, object] = None | ||||
| 				def nastav_body(jake, na_kolik): | ||||
| 					nonlocal body_nastaveny | ||||
| 					if body_nastaveny is not None: | ||||
| 						logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.") | ||||
| 						hodnoceni.body = -0.1 | ||||
| 					else: | ||||
| 						body_nastaveny = (jake, na_kolik) | ||||
| 						hodnoceni.__setattr__(jake, na_kolik) | ||||
| 
 | ||||
| 				for key, value in data_for_body.items(): | ||||
| 					if key.startswith("body") and value is not None: | ||||
| 						nastav_body(key, value) | ||||
| 
 | ||||
| 				# Něco se změnilo, ale nic není nastavené = něco bylo smazáno | ||||
| 				if body_nastaveny is None: | ||||
| 					hodnoceni.body = None | ||||
| 			hodnoceni.save() | ||||
| 
 | ||||
| 		adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True) | ||||
| 		if notifikace and adresati: | ||||
| 			email = EmailMessage( | ||||
| 					subject='Změna hodnocení odevzdaného řešení', | ||||
| 					body=f"""Milá řešitelko, milý řešiteli, | ||||
| 
 | ||||
| došlo ke změně zpětné vazby k Tebou odevzdanému řešení. Zobrazit si ji můžeš na {reseni.resitel_url()}. | ||||
| 
 | ||||
| Tvoji organizátoři M&M | ||||
| --- | ||||
| Nechceš-li tato upozornění dostávat, můžeš si to nastavit ve svém profilu.""", | ||||
| 					from_email='odevzdavatko@mam.mff.cuni.cz', | ||||
| 					bcc=adresati, | ||||
| 					) | ||||
| 			email.send() | ||||
| 
 | ||||
| 	return redirect(success_url) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -71,6 +71,8 @@ class UdajeForm(forms.Form): | |||
| 	zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False, initial=True) | ||||
| 	spam = forms.BooleanField(label='Souhlasím se zasíláním propagačních materiálů od MFF UK', required=False) | ||||
| 
 | ||||
| 	upozornovat_na_opravy_reseni = forms.BooleanField(label='Chci dostávat emailová upozornění na změnu zpětné vazby k mým řešením', required=False, initial=True) | ||||
| 
 | ||||
| 	def clean_prezdivka_resitele(self): | ||||
| 		prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele') | ||||
| 		if prezdivka_resitele == '': | ||||
|  |  | |||
							
								
								
									
										22
									
								
								personalni/migrations/0018_resitel_upozorneni.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								personalni/migrations/0018_resitel_upozorneni.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,22 @@ | |||
| # Generated by Django 4.2.16 on 2024-12-03 19:08 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| def vypnuti_upozorneni_na_opravy_reseni(apps, schema_editor): | ||||
|     Resitel = apps.get_model('personalni', 'Resitel') | ||||
|     Resitel.objects.update(upozorneni=False) | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('personalni', '0017_odstrel_treenode_post'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='resitel', | ||||
|             name='upozorneni', | ||||
|             field=models.BooleanField(default=True, verbose_name='zasílat upozornění na změnu zpětné vazby k řešení emailem'), | ||||
|         ), | ||||
|         migrations.RunPython(vypnuti_upozorneni_na_opravy_reseni), | ||||
|     ] | ||||
|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 4.2.18 on 2025-01-14 19:48 | ||||
| 
 | ||||
| from django.db import migrations | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('personalni', '0018_resitel_upozorneni'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name='resitel', | ||||
|             old_name='upozorneni', | ||||
|             new_name='upozornovat_na_opravy_reseni', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -250,6 +250,8 @@ class Resitel(SeminarModelBase): | |||
| 	poznamka = models.TextField('neveřejná poznámka', blank=True, | ||||
| 		help_text='Neveřejná poznámka k řešiteli (plain text)') | ||||
| 
 | ||||
| 	upozornovat_na_opravy_reseni = models.BooleanField('zasílat upozornění na změnu zpětné vazby k řešení emailem', default=True) | ||||
| 
 | ||||
| 
 | ||||
| 	def export_row(self): | ||||
| 		"Slovnik pro pouziti v AESOP exportu" | ||||
|  |  | |||
							
								
								
									
										34
									
								
								personalni/static/personalni/jak_se_dozvedeli.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								personalni/static/personalni/jak_se_dozvedeli.css
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| .seznam { | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	gap: 0.3em; | ||||
| } | ||||
| 
 | ||||
| .hint { | ||||
| 	border: 1px solid #ccc; | ||||
| 	padding: 0.3em 1em; | ||||
| 	border-radius: 5px; | ||||
| 	margin-bottom: 1em; | ||||
| } | ||||
| 
 | ||||
| .osoba { | ||||
| 	display: flex; | ||||
| 	justify-content: space-between; | ||||
| 	gap: 0.5em; | ||||
| 
 | ||||
| 	.uno { | ||||
| 		flex: 2; | ||||
| 	} | ||||
| 
 | ||||
| 	.dos { | ||||
| 		flex: 2; | ||||
| 	} | ||||
| 
 | ||||
| 	.tres { | ||||
| 		flex: 1; | ||||
| 	} | ||||
| 
 | ||||
| 	.grey { | ||||
| 		opacity: 0.5; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										28
									
								
								personalni/templates/personalni/jak_se_dozvedeli.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								personalni/templates/personalni/jak_se_dozvedeli.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| {% block custom_css %} | ||||
| {% load static %} | ||||
| <link href="{% static 'personalni/jak_se_dozvedeli.css' %}" rel="stylesheet"> | ||||
| {% endblock %} | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="seznam"> | ||||
|   <div class="osoba hint"> | ||||
|     <div class="uno">Jméno</div> | ||||
|     <div class="dos">Jak se dozvěděli</div> | ||||
|     <div class="tres">Datum registrace</div> | ||||
|   </div> | ||||
| 
 | ||||
|   {% for osoba in object_list %} | ||||
|   <div class="osoba"> | ||||
|     <div class="uno">{{ osoba.jmeno }} {{ osoba.prijmeni }}</div> | ||||
|     <div class="dos {% if not osoba.jak_se_dozvedeli %}grey{% endif %}">{% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}</div> | ||||
|     <div class="tres">{{ osoba.datum_registrace }}</div> | ||||
|   </div> | ||||
|   {% endfor %} | ||||
| </div> | ||||
| {% endblock%} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -51,6 +51,7 @@ | |||
| </h4> | ||||
| <table class="form"> | ||||
|   {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %} | ||||
|   {% include "personalni/udaje/prihlaska_field.html" with field=form.upozornovat_na_opravy_reseni %} | ||||
|   {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_papirove %} | ||||
|   {% include "personalni/udaje/prihlaska_field.html" with field=form.spam %} | ||||
|   {% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %} | ||||
|  |  | |||
|  | @ -33,4 +33,11 @@ urlpatterns = [ | |||
| 		name='stari_organizatori' | ||||
| 	), | ||||
| 
 | ||||
| 	# Zpřístupnění dat z "jak jste se o nás dozvěděli" pro orgy propagace | ||||
|     path( | ||||
|         'org/propagace/jak-se-dozvedeli/', | ||||
|         org_required(views.JakSeDozvedeliView.as_view()), | ||||
|         name='jak_se_dozvedeli' | ||||
| 	) | ||||
| 
 | ||||
| ] | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ from various.autentizace.utils import posli_reset_hesla | |||
| 
 | ||||
| from django.forms.models import model_to_dict | ||||
| 
 | ||||
| from .models import Organizator | ||||
| from .models import Organizator, Osoba  | ||||
| 
 | ||||
| 
 | ||||
| def aktivniOrganizatori(datum=timezone.now()): | ||||
|  | @ -62,6 +62,11 @@ class CojemamOrganizatoriStariView(generic.ListView): | |||
| 		id__in=aktivniOrganizatori() | ||||
| 	).order_by('-organizuje_do') | ||||
| 
 | ||||
| class JakSeDozvedeliView(generic.ListView): | ||||
| 	model = Osoba | ||||
| 	template_name = 'personalni/jak_se_dozvedeli.html' | ||||
| 	queryset = Osoba.objects.order_by('-datum_registrace') | ||||
| 
 | ||||
| 
 | ||||
| def obalkyView(request, resitele): | ||||
| 	if len(resitele) == 0: | ||||
|  | @ -230,6 +235,7 @@ def resitelEditView(request): | |||
| 				resitel_edit.zasilat = fcd['zasilat'] | ||||
| 				resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem'] | ||||
| 				resitel_edit.zasilat_cislo_papirove = fcd['zasilat_cislo_papirove'] | ||||
| 				resitel_edit.upozornovat_na_opravy_reseni = fcd['upozornovat_na_opravy_reseni'] | ||||
| 				if fcd.get('skola'): | ||||
| 					resitel_edit.skola = fcd['skola'] | ||||
| 				else: | ||||
|  |  | |||
|  | @ -0,0 +1,3 @@ | |||
| """ | ||||
| Aplikace umožňující orgům vypisovat si přednášky a účastníkům o nich hlasovat. | ||||
| """ | ||||
|  | @ -4,11 +4,15 @@ from reversion.admin import VersionAdmin | |||
| from django.utils.safestring import mark_safe | ||||
| from django.utils.html import escape | ||||
| 
 | ||||
| from .models import Prednaska, Seznam, STAV_NAVRH | ||||
| from .models import Prednaska, Seznam, Znalost | ||||
| from soustredeni.models import Soustredeni | ||||
| 
 | ||||
| 
 | ||||
| class Seznam_PrednaskaInline(admin.TabularInline): | ||||
| 	""" | ||||
| 		Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Přednášky <prednasky.models.Prednaska>` | ||||
| 		v adminu :py:class:`Seznamu <prednasky.models.Seznam>`. | ||||
| 	""" | ||||
| 	model = Prednaska.seznamy.through | ||||
| 	extra = 0 | ||||
| 
 | ||||
|  | @ -54,24 +58,57 @@ class Seznam_PrednaskaInline(admin.TabularInline): | |||
| 	def has_add_permission(self, req, obj): return False | ||||
| 
 | ||||
| 
 | ||||
| class Seznam_ZnalostInline(admin.TabularInline): | ||||
| 	""" | ||||
| 		Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 		v adminu :py:class:`Seznamu <prednasky.models.Seznam>`. | ||||
| 	""" | ||||
| 	model = Znalost.seznamy.through | ||||
| 	extra = 0 | ||||
| 
 | ||||
| 	def znalost__nazev(self, obj): | ||||
| 		return mark_safe( | ||||
| 			f"<a href='/admin/prednasky/znalost/{obj.znalost.id}'>{obj.znalost.nazev}</a>" | ||||
| 		) | ||||
| 
 | ||||
| 	def znalost__text(self, obj): | ||||
| 		return mark_safe( | ||||
| 			f"<div style='width: 200px'>{escape(obj.znalost.text)}</div>" | ||||
| 		) | ||||
| 
 | ||||
| 	znalost__nazev.short_description = u'Přednáška' | ||||
| 	znalost__text.short_description = u'Popis pro orgy' | ||||
| 
 | ||||
| 	readonly_fields = [ | ||||
| 		'znalost__nazev', | ||||
| 		'znalost__text', | ||||
| 	] | ||||
| 	exclude = ['znalost'] | ||||
| 
 | ||||
| 	def has_add_permission(self, req, obj): return False | ||||
| 
 | ||||
| 
 | ||||
| class SeznamAdmin(VersionAdmin): | ||||
| 	""" Admin pro :py:class:`Seznam <prednasky.models.Seznam>` """ | ||||
| 	list_display = ['soustredeni', 'stav'] | ||||
| 	inlines = [Seznam_PrednaskaInline] | ||||
| 	inlines = [Seznam_PrednaskaInline, Seznam_ZnalostInline] | ||||
| 
 | ||||
| admin.site.register(Seznam, SeznamAdmin) | ||||
| 
 | ||||
| 
 | ||||
| class PrednaskaAdmin(VersionAdmin): | ||||
| 	""" Admin pro :py:class:`Přednášku <prednasky.models.Prednaska> """ | ||||
| 	list_display = ['nazev', 'org', 'obor'] | ||||
| 	list_filter = ['org', 'obor'] | ||||
| 	search_fields = [] | ||||
| 	search_fields = ['nazev'] | ||||
| 	filter_horizontal = ('seznamy', ) | ||||
| 
 | ||||
| 	actions = ['move_to_soustredeni'] | ||||
| 
 | ||||
| 	def move_to_soustredeni(self, request, queryset): | ||||
| 		""" Přidá dané přednášky do seznamu, o kterém se právě hlasuje """ | ||||
| 		sous = Soustredeni.objects.first() | ||||
| 		seznam = Seznam.objects.filter(soustredeni=sous, stav=STAV_NAVRH) | ||||
| 		seznam = Seznam.objects.filter(soustredeni=sous, stav=Seznam.Stav.NAVRH) | ||||
| 		if len(seznam) == 0: | ||||
| 			self.message_user( | ||||
| 				request, | ||||
|  | @ -97,3 +134,14 @@ class PrednaskaAdmin(VersionAdmin): | |||
| 
 | ||||
| 
 | ||||
| admin.site.register(Prednaska, PrednaskaAdmin) | ||||
| 
 | ||||
| 
 | ||||
| class ZnalostAdmin(PrednaskaAdmin): # Trochu hack, ať nemusím vypisovat všechno znovu | ||||
| 	""" | ||||
| 		Admin pro :py:class:`Znalost <prednasky.models.Znalost> | ||||
| 		TODO předělat, aby nedědila z :py:class:`prednasky.admin.PrednaskaAdmin`, ale společné věci byly zvlášť | ||||
| 	""" | ||||
| 	list_display = ("__str__",) | ||||
| 	list_filter = () | ||||
| 
 | ||||
| admin.site.register(Znalost, ZnalostAdmin) | ||||
|  |  | |||
|  | @ -1,7 +1,31 @@ | |||
| from django import forms | ||||
| 
 | ||||
| class NewPrednaskyForm(forms.Form): | ||||
| 	ucastnik = forms.CharField(label = 'Tvoje jméno', max_length = 100) | ||||
| from .models import Hlasovani, HlasovaniOZnalostech | ||||
| 
 | ||||
| class HlasovaniPrednaskaForm(forms.Form): | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o jedné :py:class:`Přednášce <prednasky.models.Prednaska>` | ||||
| 	(neobsahuje téměř nic, většina se musí doplnit jiným způsobem) | ||||
| 	""" | ||||
| 
 | ||||
| 	#: ID :py:class:`Přednášky <prednasky.models.Prednaska>`, o které se hlasuje | ||||
| 	prednaska_id = forms.IntegerField(widget=forms.HiddenInput) | ||||
| 	#: :py:class:`Hodnocení (Body) <prednasky.models.Hlasovani.Body>` této přednášky | ||||
| 	body = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=Hlasovani.Body.choices, initial=Hlasovani.Body.JEDNO) | ||||
| 
 | ||||
| #: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniPrednaskaFormů <prednasky.forms.HlasovaniPrednaskaForm>`) | ||||
| #: pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o množině :py:class:`Přednášek <prednasky.models.Prednaska>` | ||||
| HlasovaniPrednaskaFormSet = forms.formset_factory(HlasovaniPrednaskaForm, extra=0) | ||||
| 
 | ||||
| class HlasovaniZnalostiForm(forms.Form): | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o jedné :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 	(neobsahuje téměř nic, většina se musí doplnit jiným způsobem) | ||||
| 	""" | ||||
| 
 | ||||
| 	#: ID :py:class:`Znalosti <prednasky.models.Znalost>`, o které hlasujeme | ||||
| 	znalost_id = forms.IntegerField(widget=forms.HiddenInput) | ||||
| 	#: :py:class:`Odpověď <prednasky.models.HlasovaniOZnalostech.Odpoved>` na tuto znalost | ||||
| 	odpoved = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=HlasovaniOZnalostech.Odpoved.choices) | ||||
| 
 | ||||
| #: Množina formulářů (:py:class:`formset <django.forms.formsets.BaseFormSet>` :py:class:`HlasovaniZnalostiFormů <prednasky.forms.HlasovaniZnalostiForm>`) | ||||
| #: pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o množině :py:class:`Znalostí <prednasky.models.Znalost>` | ||||
| HlasovaniZnalostiFormSet = forms.formset_factory(HlasovaniZnalostiForm, extra=0) | ||||
|  |  | |||
							
								
								
									
										39
									
								
								prednasky/migrations/0019_znalost_hlasovanioznalostech.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								prednasky/migrations/0019_znalost_hlasovanioznalostech.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| # Generated by Django 4.2.16 on 2025-01-24 13:41 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'), | ||||
|         ('prednasky', '0018_post_split_soustredeni'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Znalost', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('nazev', models.CharField(help_text='Např. Neuronové sítě', max_length=200, verbose_name='Nadpis')), | ||||
|                 ('text', models.TextField(blank=True, help_text='Např. Perceptron, vrstevnatá síť, forward a backward propagation', null=True, verbose_name='Detailní popis')), | ||||
|                 ('seznamy', models.ManyToManyField(to='prednasky.seznam')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Znalost k přednáškám', | ||||
|                 'verbose_name_plural': 'Znalosti k přednáškám', | ||||
|                 'db_table': 'prednasky_znalost', | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name='HlasovaniOZnalostech', | ||||
|             fields=[ | ||||
|                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('odpoved', models.CharField(choices=[(-1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (1, 'Tohle vůbec neznám')], max_length=16, verbose_name='odpověď')), | ||||
|                 ('seznam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='prednasky.seznam')), | ||||
|                 ('ucastnik', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personalni.osoba')), | ||||
|                 ('znalost', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='prednasky.znalost')), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										18
									
								
								prednasky/migrations/0020_alter_hlasovani_body.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								prednasky/migrations/0020_alter_hlasovani_body.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 4.2.16 on 2025-01-24 20:04 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('prednasky', '0019_znalost_hlasovanioznalostech'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='hlasovani', | ||||
|             name='body', | ||||
|             field=models.IntegerField(choices=[(-1, 'rozhodně nechci'), (0, 'je mi to jedno'), (1, 'rozhodně chci')], default=0, verbose_name='Body'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -0,0 +1,24 @@ | |||
| # Generated by Django 4.2.16 on 2025-02-04 20:09 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| def zmena_bodu(apps, _schema_editor): | ||||
|     HlasovaniOZnalostech = apps.get_model('prednasky','HlasovaniOZnalostech') | ||||
|     for h in HlasovaniOZnalostech.objects.all(): | ||||
|         h.odpoved = -int(h.odpoved) | ||||
|         h.save() | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('prednasky', '0020_alter_hlasovani_body'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='hlasovanioznalostech', | ||||
|             name='odpoved', | ||||
|             field=models.IntegerField(choices=[(1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím'), (-1, 'Tohle vůbec neznám')], verbose_name='odpověď'), | ||||
|         ), | ||||
|         migrations.RunPython(zmena_bodu, reverse_code=zmena_bodu), | ||||
|     ] | ||||
|  | @ -0,0 +1,18 @@ | |||
| # Generated by Django 4.2.16 on 2025-02-09 21:10 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('prednasky', '0021_alter_hlasovanioznalostech_odpoved'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name='hlasovanioznalostech', | ||||
|             name='odpoved', | ||||
|             field=models.IntegerField(choices=[(1, 'Tohle celkem umím'), (0, 'Už jsem o tom slyšel, ale neřekl bych, že to úplně umím'), (-1, 'Tohle vůbec neznám')], verbose_name='odpověď'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -1,81 +1,134 @@ | |||
| from django.db import models | ||||
| 
 | ||||
| from soustredeni.models import Soustredeni | ||||
| from personalni.models import Organizator | ||||
| 
 | ||||
| STAV_NAVRH = 1 | ||||
| STAV_BUDE = 2 | ||||
| 
 | ||||
| 
 | ||||
| STAV_CHOICES = ( | ||||
| (STAV_NAVRH, 'Návrh'), | ||||
| (STAV_BUDE, 'Bude') | ||||
| ) | ||||
| from personalni.models import Organizator, Osoba | ||||
| 
 | ||||
| 
 | ||||
| class Seznam(models.Model): | ||||
| 	class Meta: | ||||
| 		db_table = 'prednasky_seznam' | ||||
| 		verbose_name = 'Seznam přednášek' | ||||
| 		verbose_name_plural = 'Seznamy přednášek' | ||||
| 		ordering = ['soustredeni', 'stav'] | ||||
| 	""" | ||||
| 		Spojuje :py:class:`Přednášky <prednasky.models.Prednaska>` | ||||
| 		se :py:class:`Soustředěními <soustredeni.models.Soustredeni>`, | ||||
| 		kde by mohly zaznít, nebo zazní/zazněly. | ||||
| 	""" | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key = True)  | ||||
| 	soustredeni = models.ForeignKey(Soustredeni,null = True, default = None,  | ||||
| 		on_delete=models.PROTECT) | ||||
| 	stav = models.IntegerField('Stav',choices=STAV_CHOICES,default = STAV_NAVRH) | ||||
| 	class Meta: | ||||
| 		db_table = "prednasky_seznam" | ||||
| 		verbose_name = "Seznam přednášek" | ||||
| 		verbose_name_plural = "Seznamy přednášek" | ||||
| 		ordering = ["soustredeni", "stav"] | ||||
| 
 | ||||
| 	class Stav(models.IntegerChoices): | ||||
| 		""" Stav seznamu přednášek (NAVRH se používá k hlasování viz :py:func:`daný view <prednasky.views.newPrednaska>`). """ | ||||
| 		NAVRH = 1, "Návrh" | ||||
| 		BUDE = 2, "Bude" | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key=True) | ||||
| 	soustredeni = models.ForeignKey(Soustredeni, null=True, default=None, on_delete=models.PROTECT) | ||||
| 	stav = models.IntegerField("Stav", choices=Stav.choices, default=Stav.NAVRH) #: :py:class:`Stav <prednasky.models.Seznam.Stav>` Seznamu | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 			return "Seznam {}přednášek na {}".format("návrhů "  | ||||
| 					if self.stav == STAV_NAVRH else "", self.soustredeni) | ||||
| 		return f"Seznam {'návrhů ' if self.stav == Seznam.Stav.NAVRH else ''}přednášek na {self.soustredeni}" | ||||
| 
 | ||||
| 
 | ||||
| CHOICES_OBTIZNOST = ( | ||||
| 				(1, 'Lehká'), | ||||
| 				(2, 'Střední'), | ||||
| 				(3, 'Těžká'), | ||||
| 				) | ||||
| 
 | ||||
| CHOICES_BODY = ( | ||||
| 		(-1, '-1'), | ||||
| 		(0, '0'), | ||||
| 		(1, '1'), | ||||
| 		) | ||||
| 
 | ||||
| class Prednaska(models.Model): | ||||
| 	""" | ||||
| 		Reprezentuje přednášku, kterou si org může vypsat a účastník o ní hlasovat. | ||||
| 		(Viz :py:class:`Hlasování <prednasky.models.Hlasovani>`.) | ||||
| 	""" | ||||
| 	class Meta: | ||||
| 		db_table = 'prednasky_prednaska' | ||||
| 		verbose_name = 'Přednáška' | ||||
| 		verbose_name_plural = 'Přednášky' | ||||
| 		ordering = ['org', 'nazev'] | ||||
| 		db_table = "prednasky_prednaska" | ||||
| 		verbose_name = "Přednáška" | ||||
| 		verbose_name_plural = "Přednášky" | ||||
| 		ordering = ["org", "nazev"] | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key = True)  | ||||
| 	nazev = models.CharField('Název', max_length = 300) | ||||
| 	class Obtiznost(models.IntegerChoices): | ||||
| 		LEHKA = 1, "Lehká" | ||||
| 		STREDNI = 2, "Střední" | ||||
| 		TEZKA = 3, "Těžká" | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key=True) | ||||
| 	nazev = models.CharField("Název", max_length=300) | ||||
| 	org = models.ForeignKey(Organizator, on_delete=models.PROTECT) | ||||
| 	popis = models.TextField('Popis pro orgy',null = True, blank = True,help_text = 'Neveřejný popis pro ostatní orgy') | ||||
| 	anotace = models.TextField('Anotace',null = True, blank = True, help_text = 'Veřejná anotace v hlasování') | ||||
| 	obtiznost = models.IntegerField('Obtížnost', choices=CHOICES_OBTIZNOST) | ||||
| 	obor = models.CharField('Obor', max_length = 5, help_text = 'Podmnožina MFIOB') | ||||
| 	klicova = models.CharField('Klíčová slova', max_length = 200, null = True, blank = True) | ||||
| 	popis = models.TextField("Popis pro orgy", null=True, blank=True, help_text="Neveřejný popis pro ostatní orgy") | ||||
| 	anotace = models.TextField("Anotace", null=True, blank=True, help_text="Veřejná anotace v hlasování") | ||||
| 	obtiznost = models.IntegerField("Obtížnost", choices=Obtiznost.choices) #: :py:class:`Obtížnost <prednasky.models.Prednaska.Obtiznost>` Přednášky | ||||
| 	obor = models.CharField("Obor", max_length=5, help_text="Podmnožina MFIOB") | ||||
| 	klicova = models.CharField("Klíčová slova", max_length=200, null=True, blank=True) | ||||
| 	seznamy = models.ManyToManyField(Seznam) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return "{} ({})".format(self.nazev, self.org) | ||||
| 		return f"{self.nazev} ({self.org})" | ||||
| 
 | ||||
| 
 | ||||
| class Hlasovani(models.Model): | ||||
| 	""" | ||||
| 		Reprezentuje hlasování jednoho účastníka | ||||
| 		o jedné :py:class:`Přednášce <prednasky.models.Prednaska>` | ||||
| 		v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit) | ||||
| 	""" | ||||
| 	class Meta: | ||||
| 		db_table = 'prednasky_hlasovani' | ||||
| 		verbose_name = 'Hlasování' | ||||
| 		verbose_name_plural = 'Hlasování' | ||||
| 		ordering = ['ucastnik', 'prednaska'] | ||||
| 	id = models.AutoField(primary_key = True)  | ||||
| 		db_table = "prednasky_hlasovani" | ||||
| 		verbose_name = "Hlasování" | ||||
| 		verbose_name_plural = "Hlasování" | ||||
| 		ordering = ["ucastnik", "prednaska"] | ||||
| 
 | ||||
| 	class Body(models.IntegerChoices): | ||||
| 		""" Ohodnocení přednášky v daném Hlasování (větší číslo = víc chci) """ | ||||
| 		NECHCI = -1, "rozhodně nechci" | ||||
| 		JEDNO = 0, "je mi to jedno" | ||||
| 		CHCI = 1, "rozhodně chci" | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key=True) | ||||
| 	prednaska = models.ForeignKey(Prednaska, on_delete=models.CASCADE) | ||||
| 	body = models.IntegerField('Body', default = 0, choices = CHOICES_BODY) | ||||
| 	ucastnik = models.CharField('Účastník', max_length = 100) | ||||
| 	seznam = models.ForeignKey(Seznam,null=True,on_delete=models.SET_NULL) | ||||
| 	#: Příslušné hlasování: :py:class:`Body <prednasky.models.Hlasovani.Body>` | ||||
| 	body = models.IntegerField("Body", default=Body.JEDNO, choices=Body.choices) | ||||
| 
 | ||||
| 	#: Účastník, který hlasoval. Pouze string: | ||||
| 	#: *(přechod z jména na objekt Osoby nějak kape na tom, | ||||
| 	#: že všechna předchozí hlasování zde mají náhodný string…) | ||||
| 	#: TODO Změnit to na Osobu* | ||||
| 	ucastnik = models.CharField("Účastník", max_length=100) | ||||
| 	seznam = models.ForeignKey(Seznam, null=True, on_delete=models.SET_NULL) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return "{} dal {} bodů {} v seznamu {}".format(self.ucastnik,  | ||||
| 					self.body, self.prednaska, self.seznam) | ||||
| 		return f"{self.ucastnik} dal {self.body} bodů {self.prednaska} v seznamu {self.seznam}" | ||||
| 
 | ||||
| 
 | ||||
| class Znalost(models.Model): | ||||
| 	""" | ||||
| 		Reprezentuje znalost, na kterou se můžeme účastníka ptát (nechat je hlasovat). | ||||
| 		(Viz :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`.) | ||||
| 	""" | ||||
| 	class Meta: | ||||
| 		db_table = "prednasky_znalost" | ||||
| 		verbose_name = "Znalost k přednáškám" | ||||
| 		verbose_name_plural = "Znalosti k přednáškám" | ||||
| 
 | ||||
| 	nazev = models.CharField("Nadpis", max_length=200, blank=False, null=False, help_text="Např. Neuronové sítě") | ||||
| 	text = models.TextField("Detailní popis", blank=True, null=True, help_text="Např. Perceptron, vrstevnatá síť, forward a backward propagation") | ||||
| 	seznamy = models.ManyToManyField(Seznam) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return self.nazev | ||||
| 
 | ||||
| 
 | ||||
| class HlasovaniOZnalostech(models.Model): | ||||
| 	""" | ||||
| 		Reprezentuje hlasování jednoho účastníka | ||||
| 		o jedné :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 		v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit) | ||||
| 	""" | ||||
| 	class Odpoved(models.IntegerChoices): | ||||
| 		""" Na kolik danou znalost účastník ovládá v daném Hlasování (větší číslo = víc zná) """ | ||||
| 		UMIM = 1, "Tohle celkem umím" | ||||
| 		CIRCA = 0, "Už jsem o tom slyšel, ale neřekl bych, že to úplně umím" | ||||
| 		NEUMIM = -1, "Tohle vůbec neznám" | ||||
| 
 | ||||
| 	odpoved = models.IntegerField(u"odpověď", choices=Odpoved.choices, blank=False, null=False) #: :py:class:`Odpověď <prednasky.models.Prednaska.Odpoved>` na HlasováníOZnalostech | ||||
| 	znalost = models.ForeignKey(Znalost, on_delete=models.CASCADE, blank=False, null=False) | ||||
| 	ucastnik = models.ForeignKey(Osoba, on_delete=models.CASCADE, blank=False, null=False) | ||||
| 	seznam = models.ForeignKey(Seznam, on_delete=models.SET_NULL, blank=True, null=True) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return f"{self.ucastnik} dal {self.znalost} bodů {self.znalost} v seznamu {self.seznam}" | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,34 +5,36 @@ | |||
| 
 | ||||
| 
 | ||||
| {% block content %} | ||||
| <h1> | ||||
| {% block nadpis1a %}Hlasování o přednáškách{% endblock %} | ||||
| </h1> | ||||
| 
 | ||||
| <p> | ||||
| Jak moc by ses chtěl(a) zúčastnit následujících přednášek? | ||||
| <br> | ||||
| <span style="font-size: 75%">Obtížnost 1 je nejlehčí, 3 nejtěžší.</span> | ||||
| </p> | ||||
| <h1>{% block nadpis1a %}Hlasování o přednáškách{% endblock %}</h1> | ||||
| 
 | ||||
| <form enctype="multipart/form-data" action="." method="post"> | ||||
|   {% csrf_token %} | ||||
|   <table> | ||||
|     {% for p, h in prednasky %} | ||||
|     <tr><td><label>{{p.org}}: <span style="font-size: 175%">{{p.nazev}}</span></label></td></tr> | ||||
|     <tr><td><p><i>{{p.anotace}}</i></p></td></tr> | ||||
|     <tr><td><label>Obor: </label> {{p.obor}}</td></tr> | ||||
|     <tr><td><label>Obtížnost: </label> {{p.obtiznost}}</td>   </tr> | ||||
|     {% if p.klicova %}<tr><td><label>Klíčová slova: </label> {{p.klicova}}</td></tr>{% endif%} | ||||
|     <tr><td>Hodnocení: | ||||
|         <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="-1" {% if h == -1 %} CHECKED="checked" {% endif %} > rozhodně nechci | ||||
|         <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="0" {% if h == 0 %} CHECKED="checked" {% endif %}> je mi to jedno | ||||
|         <INPUT TYPE="radio" NAME="q{{p.pk}}" VALUE="1" {% if h == 1 %} CHECKED="checked" {% endif %}> rozhodně chci | ||||
|     </td></tr> | ||||
|     <tr><td> </td></tr> | ||||
| 
 | ||||
| <h3>Jak moc by ses chtěl(a) zúčastnit následujících přednášek?</h3> | ||||
| <p>Obtížnost 1 je nejlehčí, 3 nejtěžší.</p> | ||||
|   {{ form_set_prednasky.management_form }} | ||||
|   {% for f, p in formy_a_prednasky %} | ||||
|     <h4>{{p.nazev}} ({{p.org}})</h4> | ||||
|     <p class="textprednasky">{{p.anotace | linebreaksbr}}</p> | ||||
|     <label>Obor: </label> {{p.obor}}<br> | ||||
|     <label>Obtížnost: </label> {{p.obtiznost}}<br> | ||||
|     {% if p.klicova %}<label>Klíčová slova: </label> {{p.klicova}}<br>{% endif%} | ||||
|     <br> | ||||
|     {{ f }} | ||||
|     <br> | ||||
|   {% empty %} | ||||
|     Nejsou žádné přednášky o kterých by šlo hlasovat. | ||||
|   {% endfor %} | ||||
|     <tr><td><input name="odeslat" type="submit" value="Odeslat"></td><tr> | ||||
|   </table> | ||||
| 
 | ||||
|   {{ form_set_znalosti.management_form }} | ||||
|   {% for f, z in formy_a_znalosti %} | ||||
|     {% if forloop.first %}<hr/><h3>Jak moc znáš následující?</h3>{% endif %} | ||||
|     <h4>{{z.nazev}}</h4> | ||||
|     <p class="textznalosti">{{z.text | linebreaksbr}}</p> | ||||
|     {{ f }} | ||||
|     <br> | ||||
|   {% endfor %} | ||||
|   <input type="submit" value="Odeslat"/> | ||||
| </form> | ||||
|     | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -1,11 +0,0 @@ | |||
| {% extends 'base.html' %} | ||||
| 
 | ||||
| {% load humanize %} | ||||
| {% load static %} | ||||
| 
 | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
| <h1> Děkujeme. </h1> | ||||
|     | ||||
| {% endblock %} | ||||
|  | @ -14,7 +14,7 @@ | |||
|     {% else %} | ||||
|         <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Seznam přednášek na soustředění {{seznam.soustredeni.misto}} </a> | ||||
|     {% endif %} | ||||
|     <a href="/prednasky/seznam_prednasek/{{seznam.id}}/export">Export</a> | ||||
|     <a href="/prednasky/seznam_prednasek/{{seznam.id}}/hlasovani.csv">Export</a> | ||||
|     </li> | ||||
|   {% endfor %} | ||||
|   </ul> | ||||
|  |  | |||
|  | @ -12,10 +12,15 @@ urlpatterns = [ | |||
| 		'prednasky/metaseznam_prednasek', | ||||
| 		org_required(views.MetaSeznamListView.as_view()), | ||||
| 		name='metaseznam-list'), | ||||
| 	# path( | ||||
| 	# 	'prednasky/seznam_prednasek/<int:seznam>/export', | ||||
| 	# 	org_required(views.SeznamExportView), | ||||
| 	# 	name='seznam-export' | ||||
| 	# ), | ||||
| 	path( | ||||
| 		'prednasky/seznam_prednasek/<int:seznam>/export', | ||||
| 		org_required(views.SeznamExportView), | ||||
| 		name='seznam-export' | ||||
| 		'prednasky/seznam_prednasek/<int:seznam>/hlasovani.csv', | ||||
| 		org_required(views.PrednaskyExportView), | ||||
| 		name='seznam-export-csv' | ||||
| 	), | ||||
| 	path( | ||||
| 		'prednasky/seznam_prednasek/<int:seznam>/', | ||||
|  |  | |||
|  | @ -1,67 +1,142 @@ | |||
| import csv | ||||
| import http | ||||
| import logging | ||||
| 
 | ||||
| from django.http import HttpResponse, HttpRequest | ||||
| from django.shortcuts import render, get_object_or_404 | ||||
| from django.views import generic | ||||
| from django.shortcuts import HttpResponseRedirect | ||||
| from django.core.exceptions import ObjectDoesNotExist | ||||
| from django.db.models import Sum | ||||
| from django.forms import Form | ||||
| from django.db import transaction | ||||
| 
 | ||||
| from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH | ||||
| from various.views.pomocne import formularOKView | ||||
| from .forms import HlasovaniPrednaskaFormSet, HlasovaniZnalostiFormSet | ||||
| 
 | ||||
| from various.models import Nastaveni | ||||
| from prednasky.models import Prednaska, Hlasovani, Znalost, HlasovaniOZnalostech, Seznam | ||||
| from soustredeni.models import Soustredeni | ||||
| from personalni.models import Osoba | ||||
| 
 | ||||
| def newPrednaska(request): | ||||
| PREDNASKY_PREFIX = "prednasky" | ||||
| ZNALOSTI_PREFIX = "znalosti" | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| def newPrednaska(request: HttpRequest) -> HttpResponse: | ||||
| 	""" | ||||
| 		View zobrazující a ukládající účastnické hlasování | ||||
| 		(:py:class:`Hlasování <prednasky.models.Hlasovani>` | ||||
| 		a :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`) | ||||
| 		o :py:class:`Přednáškách <prednasky.models.Prednaska>` | ||||
| 		a :py:class:`Znalostech <prednasky.models.Znalost>` | ||||
| 	""" | ||||
| 	# hlasovani se vztahuje k nejnovejsimu soustredeni | ||||
| 	sous = Soustredeni.objects.first() | ||||
| 	seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first() | ||||
| 	sous = Nastaveni.get_solo().aktualni_sous | ||||
| 	seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first() | ||||
| 	if sous is None or seznam is None: | ||||
| 		return render(request, 'universal.html', { | ||||
| 			'title': "Nelze hlasovat", | ||||
| 			'text': "Není žádný seznam přednášek, o kterém by se dalo hlasovat.", | ||||
| 		}, status=http.HTTPStatus.NOT_FOUND) | ||||
| 
 | ||||
| 	osoba = Osoba.objects.filter(user=request.user).first() | ||||
| 	ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) | ||||
| 	# obsluha formulare | ||||
| 	if request.method == 'POST': | ||||
| 		form = Form(request.POST, request.FILES) | ||||
| 		if form.is_valid(): | ||||
| 			# id z důvodu duplicitních jmen (přechod z jména na objekt Osoby nějak kape na tom, | ||||
| 			# že všechna předchozí hlasování zde mají náhodný string…) | ||||
| 			# TODO Změnit to na Osobu | ||||
| 	ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) # id, kvůli kolizi jmen | ||||
| 
 | ||||
| 			# TODO v následujících řádcích je zbytečně mnoho dotazů na QuerySet (pokud účastník hlasoval, hlasoval u všech) | ||||
| 			for i in request.POST: | ||||
| 				if i[0] == 'q': | ||||
| 					prednaska = Prednaska.objects.filter(pk=int(i[1:]))[0] | ||||
| 					hlasovani = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first() | ||||
| 					if not hlasovani: | ||||
| 						hlasovani = Hlasovani() | ||||
| 						hlasovani.prednaska = prednaska | ||||
| 						hlasovani.ucastnik = ucastnik | ||||
| 						hlasovani.seznam = seznam | ||||
| 					hlasovani.body = int(request.POST[i]) | ||||
| 					hlasovani.save() | ||||
| 	if request.method == 'POST': # Když to byl POST, tak ukládáme. | ||||
| 		# Načteme data do formsetů | ||||
| 		form_set_prednasky = HlasovaniPrednaskaFormSet(request.POST, prefix=PREDNASKY_PREFIX) | ||||
| 		form_set_znalosti = HlasovaniZnalostiFormSet(request.POST, prefix=ZNALOSTI_PREFIX) | ||||
| 
 | ||||
| 		if form_set_prednasky.is_valid() and form_set_znalosti.is_valid(): | ||||
| 			with transaction.atomic(): | ||||
| 				# Místo updatování data prostě smažeme a vytvoříme nová | ||||
| 				seznam.hlasovani_set.filter(ucastnik=ucastnik).delete() | ||||
| 				seznam.hlasovanioznalostech_set.filter(ucastnik=osoba).delete() | ||||
| 
 | ||||
| 				for form in form_set_prednasky: | ||||
| 					prednaska_id = form.cleaned_data['prednaska_id'] | ||||
| 					prednaska = Prednaska.objects.filter(id=prednaska_id).first() | ||||
| 					if prednaska is None: | ||||
| 						logger.error(f"Účastník {ucastnik} hodnotil neexistující přednášku {prednaska_id} číslem {form.cleaned_data['body']}") | ||||
| 						continue | ||||
| 
 | ||||
| 					Hlasovani.objects.create( | ||||
| 						prednaska=prednaska, | ||||
| 						body=form.cleaned_data['body'], | ||||
| 						ucastnik=ucastnik, | ||||
| 						seznam=seznam, | ||||
| 					) | ||||
| 
 | ||||
| 				for form in form_set_znalosti: | ||||
| 					znalost_id = form.cleaned_data['znalost_id'] | ||||
| 					znalost = Znalost.objects.filter(id=znalost_id).first() | ||||
| 					if znalost is None: | ||||
| 						logger.error(f"Účastník {ucastnik} hodnotil neexistující znalost {znalost_id} číslem {form.cleaned_data['odpoved']}") | ||||
| 						continue | ||||
| 
 | ||||
| 					HlasovaniOZnalostech.objects.create( | ||||
| 						odpoved=form.cleaned_data['odpoved'], | ||||
| 						znalost=znalost, | ||||
| 						ucastnik=osoba, | ||||
| 						seznam=seznam, | ||||
| 					) | ||||
| 
 | ||||
| 			# presmerovani na prave vzniklou galerii | ||||
| 			return HttpResponseRedirect('./hotovo') | ||||
| 
 | ||||
| 	def prednaska_hodnoceni(prednaska): | ||||
| 		h = Hlasovani.objects.filter(ucastnik=ucastnik, prednaska=prednaska).first() | ||||
| 		if h: | ||||
| 			return prednaska, h.body | ||||
| 		else: | ||||
| 			return prednaska, 0 | ||||
| 		else: # Pokud je nějaký formset nevalidní, vracíme je k přepracování | ||||
| 			prednasky = seznam.prednaska_set.all() | ||||
| 			znalosti = seznam.znalost_set.all() | ||||
| 			# FIXME Spadnout, pokud nesedí přednáška/znalost s formulářem. (Nějak se mi to nepovedlo.) | ||||
| 			# Může se totiž stát, že se mezitím změnily přednášky (nějaká byla přidána/odebrána) | ||||
| 
 | ||||
| 	else: # Když to nebyl POST, tak inicializujeme (pokud už o přednášce/znalosti účastník hlasoval, předvyplníme mu to). | ||||
| 		def odpoved_prednasky(p: Prednaska) -> Hlasovani.Body: | ||||
| 			hlasovani = p.hlasovani_set.filter(ucastnik=ucastnik).first() | ||||
| 			return hlasovani.body if hlasovani else Hlasovani.Body.JEDNO | ||||
| 
 | ||||
| 		def odpoved_znalosti(z: Znalost) -> HlasovaniOZnalostech.Odpoved: | ||||
| 			hlasovani = z.hlasovanioznalostech_set.filter(ucastnik=osoba).first() | ||||
| 			return hlasovani.odpoved if hlasovani else HlasovaniOZnalostech.Odpoved.CIRCA | ||||
| 
 | ||||
| 		prednasky = seznam.prednaska_set.all() | ||||
| 		znalosti = seznam.znalost_set.all() | ||||
| 
 | ||||
| 		form_set_prednasky = HlasovaniPrednaskaFormSet(initial=[ | ||||
| 			{"prednaska_id": p.id, "body": odpoved_prednasky(p)} for p in prednasky | ||||
| 		], prefix=PREDNASKY_PREFIX) | ||||
| 
 | ||||
| 		form_set_znalosti = HlasovaniZnalostiFormSet(initial=[ | ||||
| 			{"znalost_id": z.id, "odpoved": odpoved_znalosti(z)} for z in znalosti | ||||
| 		], prefix=ZNALOSTI_PREFIX) | ||||
| 
 | ||||
| 
 | ||||
| 	# V případě nePOSTu nebo chyby při ukládání vracíme hlasování | ||||
| 	return render( | ||||
| 		request, | ||||
| 		'prednasky/base.html', | ||||
| 		{'prednasky': map(prednaska_hodnoceni, seznam.prednaska_set.all())} | ||||
| 		{ | ||||
| 			'form_set_prednasky': form_set_prednasky, 'form_set_znalosti': form_set_znalosti, | ||||
| 			'formy_a_prednasky': zip(form_set_prednasky, prednasky), | ||||
| 			'formy_a_znalosti': zip(form_set_znalosti, znalosti), | ||||
| 		} | ||||
| 	) | ||||
| 
 | ||||
| 
 | ||||
| def Prednaska_hotovo(request): | ||||
| 	return render(request, 'prednasky/hotovo.html') | ||||
| def Prednaska_hotovo(request: HttpRequest) -> HttpResponse: | ||||
| 	""" View po vyplnění :py:func:`hlasování <prednasky.views.newPrednaska>` """ | ||||
| 	return formularOKView(request, "Děkujeme za vyplnění hlasování o přednáškách a těšíme se na soustředění.") | ||||
| 
 | ||||
| class MetaSeznamListView(generic.ListView): | ||||
| 	""" Seznam všech :py:class:`Seznamů <prednasky.models.Seznam>` s odkazy na exporty """ | ||||
| 	model = Seznam | ||||
| 	template_name = 'prednasky/metaseznam_prednasek.html' | ||||
| 
 | ||||
| 
 | ||||
| class SeznamListView(generic.ListView): | ||||
| 	""" | ||||
| 		Náhled na to, kolik má která přednáška v :py:class:`Seznamu <prednasky.models.Seznam>` :py:class:`hlasů <prednasky.models.Hlasovani.Body>`. | ||||
| 		(Je otázka, zda tento View vůbec chceme. Pokud ano, hodilo by se do něj přidat i znalosti.) | ||||
| 	""" | ||||
| 	template_name = 'prednasky/seznam_prednasek.html' | ||||
| 
 | ||||
| 	def get_queryset(self): | ||||
|  | @ -77,7 +152,7 @@ class SeznamListView(generic.ListView): | |||
| 
 | ||||
| 		# hlasovani se vztahuje k nejnovejsimu soustredeni | ||||
| 		sous = Soustredeni.objects.first() | ||||
| 		seznam = Seznam.objects.filter(soustredeni = sous, stav = STAV_NAVRH).first() | ||||
| 		seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first() | ||||
| 	 | ||||
| 		for obj in self.object_list: | ||||
| 			hlasovani_set = obj.hlasovani_set.filter(seznam=seznam).only('body') | ||||
|  | @ -86,32 +161,86 @@ class SeznamListView(generic.ListView): | |||
| 		return context | ||||
| 
 | ||||
| 
 | ||||
| def SeznamExportView(request, seznam): | ||||
| 	"""Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor""" | ||||
| 	# TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro | ||||
| 	# lidi? | ||||
| 	hlasovani = Hlasovani.objects.filter(seznam=seznam) | ||||
| 	prednasky = Prednaska.objects.filter(seznamy=seznam) | ||||
| 	orgove = set(p.org for p in prednasky) | ||||
| 	ucastnici = set(h.ucastnik for h in hlasovani) | ||||
| # def SeznamExportView(request, seznam): | ||||
| # 	"""Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor""" | ||||
| # 	# TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro | ||||
| # 	# lidi? | ||||
| # 	hlasovani = Hlasovani.objects.filter(seznam=seznam) | ||||
| # 	prednasky = Prednaska.objects.filter(seznamy=seznam) | ||||
| # 	orgove = set(p.org for p in prednasky) | ||||
| # 	ucastnici = set(h.ucastnik for h in hlasovani) | ||||
| # | ||||
| # 	for p in prednasky: | ||||
| # 		p.body = [] | ||||
| # 		for u in ucastnici: | ||||
| # 			try: | ||||
| # 				p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body) | ||||
| # 			except ObjectDoesNotExist: | ||||
| # 				# účastník nehlasoval | ||||
| # 				p.body.append("?") | ||||
| # | ||||
| # 	for h in hlasovani: | ||||
| # 		h.ucastnik = hash(h.ucastnik) | ||||
| # | ||||
| # 	return render( | ||||
| # 		request, | ||||
| # 		'prednasky/seznam_prednasek_export.txt', | ||||
| # 		{"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove}, | ||||
| # 		content_type="text/plain" | ||||
| # 	) | ||||
| 
 | ||||
| 	for p in prednasky: | ||||
| 		p.body = [] | ||||
| 		for u in ucastnici: | ||||
| 			try: | ||||
| 				p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body) | ||||
| 			except ObjectDoesNotExist: | ||||
| 				# účastník nehlasoval | ||||
| 				p.body.append("?") | ||||
| 
 | ||||
| def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResponse: | ||||
| 	""" | ||||
| 		Vrátí všechna :py:class:`Hlasování <prednasky.models.Hlasovani>` | ||||
| 		i :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` | ||||
| 		v daném :py:class:`Seznamu <prednasky.models.Seznam>` | ||||
| 		jako csv soubor (řádky = účastníci, sloupce = přednášky&znalosti). | ||||
| 
 | ||||
| 		:param seznam: ID daného :py:class:`Seznamu <prednasky.models.Seznam>` | ||||
| 	""" | ||||
| 	hlasovani = Hlasovani.objects.filter(seznam=seznam).select_related("prednaska") | ||||
| 	hlasovani_o_znalostech = HlasovaniOZnalostech.objects.filter(seznam=seznam).select_related('ucastnik', 'znalost') | ||||
| 
 | ||||
| 	# Inicializujeme sloupce | ||||
| 	prednasky = list(Prednaska.objects.filter(seznamy=seznam)) | ||||
| 	znalosti = list(Znalost.objects.filter(seznamy=seznam)) | ||||
| 
 | ||||
| 	prednasky_map: dict[int, int] = {p.id: i for i, p in enumerate(prednasky, 1)} | ||||
| 	offset = len(prednasky_map) | ||||
| 	znalosti_map: dict[int, int] = {z.id: i for i, z in enumerate(znalosti, offset + 1)} | ||||
| 	width = offset + len(znalosti_map) | ||||
| 
 | ||||
| 	# A po inicializaci sloupců vyplníme tabulku | ||||
| 	table: [str, list[str|Prednaska|Znalost,]] = {} | ||||
| 
 | ||||
| 	for h in hlasovani: | ||||
| 		h.ucastnik = hash(h.ucastnik) | ||||
| 		if h.ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek | ||||
| 			table[h.ucastnik] = [h.ucastnik] + ([""] * width) | ||||
| 
 | ||||
| 	return render( | ||||
| 		request, | ||||
| 		'prednasky/seznam_prednasek_export.txt', | ||||
| 		{"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove}, | ||||
| 		content_type="text/plain" | ||||
| 	) | ||||
| 		if h.prednaska.id in prednasky_map: | ||||
| 			table[h.ucastnik][prednasky_map[h.prednaska.id]] = h.body | ||||
| 		else: | ||||
| 			pass # TODO Padat hlasitě? | ||||
| 
 | ||||
| 	for h in hlasovani_o_znalostech: | ||||
| 		ucastnik = str(h.ucastnik) + ' ' + str(h.ucastnik.id) # id, kvůli kolizi jmen | ||||
| 		if ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek | ||||
| 			table[ucastnik] = [ucastnik] + ([""] * width) | ||||
| 
 | ||||
| 		if h.znalost.id in znalosti_map: | ||||
| 			table[ucastnik][znalosti_map[h.znalost.id]] = h.odpoved | ||||
| 		else: | ||||
| 			pass # TODO Padat hlasitě? | ||||
| 
 | ||||
| 
 | ||||
| 	response = HttpResponse(content_type="text/csv", charset="utf-8") | ||||
| 	response["Content-Disposition"] = 'attachment; filename="hlasovani.csv"' | ||||
| 
 | ||||
| 	writer = csv.writer(response) | ||||
| 	writer.writerow(["jména \\ přednáška|znalost"] + list(map(str, prednasky + znalosti))) | ||||
| 	for row in table.values(): | ||||
| 		writer.writerow(list(map(str, row))) | ||||
| 	return response | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ django-solo # Singleton model (speciálně Nastavení) | |||
| django-ckeditor-5 # Editor htmlka (hlavně v adminu u flatpages) | ||||
| django-cleanup  # Uklízí media/ od smazaných „databázových“ souborů | ||||
| django-taggit # Taggy v djangu (speciálně zaměření problémů) | ||||
| django-autocomplete-light>=3.9.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích | ||||
| django-autocomplete-light>=3.9.0,<3.12.0 # Automatické doplňování (problémů, účastníků, …) ve formulářích | ||||
| django-imagekit # Všechny možné obrázky v Djangu | ||||
| django-polymorphic # Polymorfismus na django modelech (hlavně Problém nebo treenode) | ||||
| django-sitetree # Struktura stránek, hlavně pro meníčko | ||||
|  | @ -49,4 +49,5 @@ lorem | |||
| 
 | ||||
| sphinx | ||||
| sphinx_rtd_theme | ||||
| sphinxcontrib-django | ||||
| myst_parser | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| # Generated by Django 4.2.16 on 2025-01-21 20:54 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'), | ||||
|         ('tvorba', '0007_alter_deadline_typ'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.SeparateDatabaseAndState( | ||||
|             database_operations=[], | ||||
|             state_operations=[ | ||||
|                 migrations.AlterField( | ||||
|                     model_name='problem', | ||||
|                     name='opravovatele', | ||||
|                     field=models.ManyToManyField(blank=True, db_table='seminar_problemy_opravovatele', related_name='opravovatele_%(class)s', to='personalni.organizator', verbose_name='opravovatelé'), | ||||
|                 ), | ||||
|                 migrations.DeleteModel( | ||||
|                     name='Problemy_Opravovatele', | ||||
|                 ), | ||||
|             ] | ||||
|         ), | ||||
|     ] | ||||
|  | @ -393,20 +393,6 @@ class ZmrazenaVysledkovka(SeminarModelBase): | |||
| 
 | ||||
| 	html = models.TextField(null=False, blank=False) | ||||
| 
 | ||||
| class Problemy_Opravovatele(SeminarModelBase): | ||||
| 	"""Jen vazebná tabulka pro opravovatele. | ||||
| 
 | ||||
| 	Ona stejně existovala, při přesunu mezi aplikacemi jen potřebujeme zajistit nepřejmenování DB tabulky. | ||||
| 	Proto taky nepotřebuje žádná specifika, ze :py:class:SeminarModelBase: dědí ze zvyku než že by to k něčemu kdy měo být. | ||||
| 	""" | ||||
| 	class Meta: | ||||
| 		db_table = 'seminar_problemy_opravovatele' | ||||
| 	 | ||||
| 	id = models.AutoField(primary_key = True) | ||||
| 
 | ||||
| 	problem = models.ForeignKey('Problem', on_delete=models.CASCADE) | ||||
| 	organizator = models.ForeignKey(Organizator, on_delete=models.CASCADE) | ||||
| 
 | ||||
| @reversion.register(ignore_duplicates=True) | ||||
| # Pozor na následující řádek. *Nekrmit, asi kouše!* | ||||
| class Problem(SeminarModelBase,PolymorphicModel): | ||||
|  | @ -462,7 +448,7 @@ class Problem(SeminarModelBase,PolymorphicModel): | |||
| 		on_delete=models.SET_NULL) | ||||
| 
 | ||||
| 	opravovatele = models.ManyToManyField(Organizator, verbose_name='opravovatelé', | ||||
| 		blank=True, related_name='opravovatele_%(class)s', through=Problemy_Opravovatele) | ||||
| 		blank=True, related_name='opravovatele_%(class)s', db_table='seminar_problemy_opravovatele') | ||||
| 
 | ||||
| 	kod = models.CharField('lokální kód', max_length=32, blank=True, default='', | ||||
| 		help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') | ||||
|  |  | |||
|  | @ -27,7 +27,7 @@ class HromadnePridaniForm(Form): | |||
| 	""" Formulář pro hromadné přidání úložek a problémů """ | ||||
| 
 | ||||
| 	tema = CharField(label="Název tématu:") | ||||
| 	dil = IntegerField(label="Díl:", min_value=1) | ||||
| 	cislo = IntegerField(label="Číslo:", min_value=1) | ||||
| 	body = CharField(label="Počty bodů (0 pro problém) oddělené čárkami:") | ||||
| 
 | ||||
| 	def clean_tema(self): | ||||
|  | @ -41,7 +41,7 @@ class HromadnePridaniForm(Form): | |||
| 	def clean_body(self): | ||||
| 		""" Kontrola, že `body` je seznam čísel """ | ||||
| 		try: | ||||
| 			list(map(int, self.cleaned_data["body"].split(","))) | ||||
| 			list(map(float, self.cleaned_data["body"].split(","))) | ||||
| 		except ValueError: | ||||
| 			raise ValidationError("Špatný formát bodů") | ||||
| 		return self.cleaned_data['body'] | ||||
|  | @ -64,21 +64,21 @@ class HromadnePridaniView(FormView): | |||
| 		""" Upravený Pavlův skript na hromadné přidání úložek a problémů. """ | ||||
| 		cd = form.cleaned_data | ||||
| 		tema = cd["tema"] | ||||
| 		dil = cd["dil"] | ||||
| 		body = list(map(int, cd["body"].split(","))) | ||||
| 		cislo = cd["cislo"] | ||||
| 		body = list(map(float, cd["body"].split(","))) | ||||
| 
 | ||||
| 		t = Problem.objects.get(nazev__exact=tema, nadproblem=None) | ||||
| 		with transaction.atomic(): | ||||
| 			pfx = f"{t.nazev}, díl {dil}, " | ||||
| 			pfx = f"{t.nazev}, " | ||||
| 
 | ||||
| 			for k, b in enumerate(body, 1): | ||||
| 				u = Uloha.objects.create( | ||||
| 					nadproblem=t, | ||||
| 					nazev=pfx + f"{'úloha' if b > 0 else 'problém'} {k}", | ||||
| 					nazev=pfx + f"{'úloha' if b > 0 else 'problém'} {cislo}.{k}", | ||||
| 					autor=t.autor, | ||||
| 					garant=t.garant, | ||||
| 					max_body=b, | ||||
| 					cislo_zadani=Cislo.get(t.rocnik.rocnik, dil), | ||||
| 					cislo_zadani=Cislo.get(t.rocnik.rocnik, cislo), | ||||
| 					kod=k, | ||||
| 					stav=Problem.STAV_ZADANY, | ||||
| 				) | ||||
|  |  | |||
							
								
								
									
										20
									
								
								various/migrations/0007_nastaveni_aktualni_sous.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								various/migrations/0007_nastaveni_aktualni_sous.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # Generated by Django 4.2.16 on 2025-01-21 20:34 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('soustredeni', '0013_alter_soustredeni_kontaktnicek_pdf_and_more'), | ||||
|         ('various', '0006_tvorba_post'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='nastaveni', | ||||
|             name='aktualni_sous', | ||||
|             field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='soustredeni.soustredeni', verbose_name='Aktuálně připravovaný sous'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -26,6 +26,11 @@ class Nastaveni(SingletonModel): | |||
| 									verbose_name="Účastnický poplatek za soustředění", | ||||
| 									default=1000) | ||||
| 
 | ||||
| 	aktualni_sous = models.ForeignKey( | ||||
| 		"soustredeni.Soustredeni", verbose_name='Aktuálně připravovaný sous', | ||||
| 		null=True, blank=True, on_delete=models.PROTECT, | ||||
| 	) | ||||
| 
 | ||||
| 	@property | ||||
| 	def aktualni_rocnik(self): | ||||
| 		return self.aktualni_cislo.rocnik | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue