Dokumentace aplikace prednasky
				
					
				
			This commit is contained in:
		
							parent
							
								
									34f0dffd79
								
							
						
					
					
						commit
						5125525238
					
				
					 5 changed files with 106 additions and 20 deletions
				
			
		|  | @ -0,0 +1,3 @@ | |||
| """ | ||||
| Aplikace umožňující orgům vypisovat si přednášky a účastníkům o nich hlasovat. | ||||
| """ | ||||
|  | @ -9,6 +9,10 @@ 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 | ||||
| 
 | ||||
|  | @ -55,6 +59,7 @@ class Seznam_PrednaskaInline(admin.TabularInline): | |||
| 
 | ||||
| 
 | ||||
| class SeznamAdmin(VersionAdmin): | ||||
| 	""" Admin pro :py:class:`Seznam <prednasky.models.Seznam>` """ | ||||
| 	list_display = ['soustredeni', 'stav'] | ||||
| 	inlines = [Seznam_PrednaskaInline] | ||||
| 
 | ||||
|  | @ -62,6 +67,7 @@ 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 = ['nazev'] | ||||
|  | @ -70,6 +76,7 @@ class PrednaskaAdmin(VersionAdmin): | |||
| 	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=Seznam.Stav.NAVRH) | ||||
| 		if len(seznam) == 0: | ||||
|  | @ -100,6 +107,10 @@ 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 = () | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,13 +3,29 @@ from django import forms | |||
| 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) | ||||
|  |  | |||
|  | @ -5,6 +5,12 @@ from personalni.models import Organizator, Osoba | |||
| 
 | ||||
| 
 | ||||
| class Seznam(models.Model): | ||||
| 	""" | ||||
| 		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. | ||||
| 	""" | ||||
| 
 | ||||
| 	class Meta: | ||||
| 		db_table = "prednasky_seznam" | ||||
| 		verbose_name = "Seznam přednášek" | ||||
|  | @ -12,18 +18,23 @@ class Seznam(models.Model): | |||
| 		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) | ||||
| 	stav = models.IntegerField("Stav", choices=Stav.choices, default=Stav.NAVRH) #: :py:class:`Stav <prednasky.models.Seznam.Stav>` Seznamu | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return f"Seznam {'návrhů ' if self.stav == Seznam.Stav.NAVRH else ''}přednášek na {self.soustredeni}" | ||||
| 
 | ||||
| 
 | ||||
| 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" | ||||
|  | @ -40,7 +51,7 @@ class Prednaska(models.Model): | |||
| 	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=Obtiznost.choices) | ||||
| 	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) | ||||
|  | @ -50,6 +61,11 @@ class Prednaska(models.Model): | |||
| 
 | ||||
| 
 | ||||
| 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í" | ||||
|  | @ -57,17 +73,20 @@ class Hlasovani(models.Model): | |||
| 		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) | ||||
| 	#: Příslušné hlasování: :py:class:`Body <prednasky.models.Hlasovani.Body>` | ||||
| 	body = models.IntegerField("Body", default=Body.JEDNO, choices=Body.choices) | ||||
| 
 | ||||
| 	# (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 | ||||
| 	#: Úč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) | ||||
| 
 | ||||
|  | @ -76,6 +95,10 @@ class Hlasovani(models.Model): | |||
| 
 | ||||
| 
 | ||||
| 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" | ||||
|  | @ -90,12 +113,17 @@ class Znalost(models.Model): | |||
| 
 | ||||
| 
 | ||||
| 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): | ||||
| 		UMIM = -1, "Tohle celkem umím" | ||||
| 		CIRCA = 0, "Už jsem o tom slyšel, ale neřekl bychm, že to úplně umím" | ||||
| 		NEUMIM = 1, "Tohle vůbec neznám" | ||||
| 
 | ||||
| 	odpoved = models.CharField(u"odpověď", max_length=16, choices=Odpoved.choices, blank=False, null=False) | ||||
| 	odpoved = models.CharField(u"odpověď", max_length=16, 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) | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import csv | |||
| import http | ||||
| import logging | ||||
| 
 | ||||
| from django.http import HttpResponse | ||||
| 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 | ||||
|  | @ -22,7 +22,14 @@ ZNALOSTI_PREFIX = "znalosti" | |||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| def newPrednaska(request): | ||||
| 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 = Nastaveni.get_solo().aktualni_sous | ||||
| 	seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first() | ||||
|  | @ -35,12 +42,14 @@ def newPrednaska(request): | |||
| 	osoba = Osoba.objects.filter(user=request.user).first() | ||||
| 	ucastnik = osoba.plne_jmeno() + ' ' + str(osoba.id) # id, kvůli kolizi jmen | ||||
| 
 | ||||
| 	if request.method == 'POST': | ||||
| 	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() | ||||
| 
 | ||||
|  | @ -73,17 +82,19 @@ def newPrednaska(request): | |||
| 					) | ||||
| 
 | ||||
| 			return HttpResponseRedirect('./hotovo') | ||||
| 		else: | ||||
| 
 | ||||
| 		else: # Pokud je nějaký formset nevalidní, vracíme je k přepracování | ||||
| 			prednasky = seznam.prednaska_set.all() | ||||
| 			znalosti = seznam.znalost_set.all() | ||||
| 			# Spadnout, pokud nesedí přednáška/znalost s formulářem. (Nějak se mi to nepovedlo.) | ||||
| 			# 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: | ||||
| 		def odpoved_prednasky(p): | ||||
| 	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): | ||||
| 		def odpoved_znalosti(z: Znalost) -> Znalost.Odpoved: | ||||
| 			hlasovani = z.hlasovanioznalostech_set.filter(ucastnik=osoba).first() | ||||
| 			return hlasovani.odpoved if hlasovani else Znalost.Odpoved.CIRCA | ||||
| 
 | ||||
|  | @ -99,6 +110,7 @@ def newPrednaska(request): | |||
| 		], prefix=ZNALOSTI_PREFIX) | ||||
| 
 | ||||
| 
 | ||||
| 	# V případě nePOSTu nebo chyby při ukládání vracíme hlasování | ||||
| 	return render( | ||||
| 		request, | ||||
| 		'prednasky/base.html', | ||||
|  | @ -110,15 +122,21 @@ def newPrednaska(request): | |||
| 	) | ||||
| 
 | ||||
| 
 | ||||
| def Prednaska_hotovo(request): | ||||
| 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): | ||||
|  | @ -172,10 +190,19 @@ class SeznamListView(generic.ListView): | |||
| # 	) | ||||
| 
 | ||||
| 
 | ||||
| def PrednaskyExportView(request, seznam: int, **kwargs): | ||||
| 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)) | ||||
| 
 | ||||
|  | @ -184,26 +211,27 @@ def PrednaskyExportView(request, seznam: int, **kwargs): | |||
| 	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: | ||||
| 		if h.ucastnik not in table: | ||||
| 		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) | ||||
| 
 | ||||
| 		if h.prednaska.id in prednasky_map: | ||||
| 			table[h.ucastnik][prednasky_map[h.prednaska.id]] = h.body | ||||
| 		else: | ||||
| 			pass # Padat hlasitě? | ||||
| 			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: | ||||
| 		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 # Padat hlasitě? | ||||
| 			pass # TODO Padat hlasitě? | ||||
| 
 | ||||
| 
 | ||||
| 	response = HttpResponse(content_type="text/csv", charset="utf-8") | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue