From 51255252386b4389571812adbd169d2f92abb9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Wed, 29 Jan 2025 01:05:08 +0100 Subject: [PATCH] Dokumentace aplikace `prednasky` --- prednasky/__init__.py | 3 +++ prednasky/admin.py | 11 +++++++++ prednasky/forms.py | 16 +++++++++++++ prednasky/models.py | 40 ++++++++++++++++++++++++++----- prednasky/views.py | 56 ++++++++++++++++++++++++++++++++----------- 5 files changed, 106 insertions(+), 20 deletions(-) diff --git a/prednasky/__init__.py b/prednasky/__init__.py index e69de29b..b34d6384 100644 --- a/prednasky/__init__.py +++ b/prednasky/__init__.py @@ -0,0 +1,3 @@ +""" +Aplikace umožňující orgům vypisovat si přednášky a účastníkům o nich hlasovat. +""" diff --git a/prednasky/admin.py b/prednasky/admin.py index a6204d35..7ab77d24 100644 --- a/prednasky/admin.py +++ b/prednasky/admin.py @@ -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 ` + v adminu :py:class:`Seznamu `. + """ model = Prednaska.seznamy.through extra = 0 @@ -55,6 +59,7 @@ class Seznam_PrednaskaInline(admin.TabularInline): class SeznamAdmin(VersionAdmin): + """ Admin pro :py:class:`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 """ 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 + 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 = () diff --git a/prednasky/forms.py b/prednasky/forms.py index cee90f7f..7b0e9739 100644 --- a/prednasky/forms.py +++ b/prednasky/forms.py @@ -3,13 +3,29 @@ from django import forms from .models import Hlasovani, HlasovaniOZnalostech class HlasovaniPrednaskaForm(forms.Form): + """ :py:class:`Formulář ` pro pro :py:class:`Hlasování ` o jedné :py:class:`Přednášce ` + (neobsahuje téměř nic, většina se musí doplnit jiným způsobem) + """ + + #: ID :py:class:`Přednášky `, o které se hlasuje prednaska_id = forms.IntegerField(widget=forms.HiddenInput) + #: :py:class:`Hodnocení (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 ` :py:class:`HlasovaniPrednaskaFormů `) +#: pro :py:class:`Hlasování ` o množině :py:class:`Přednášek ` HlasovaniPrednaskaFormSet = forms.formset_factory(HlasovaniPrednaskaForm, extra=0) class HlasovaniZnalostiForm(forms.Form): + """ :py:class:`Formulář ` pro pro :py:class:`HlasováníOZnalostech ` o jedné :py:class:`Znalosti ` + (neobsahuje téměř nic, většina se musí doplnit jiným způsobem) + """ + + #: ID :py:class:`Znalosti `, o které hlasujeme znalost_id = forms.IntegerField(widget=forms.HiddenInput) + #: :py:class:`Odpověď ` na tuto znalost odpoved = forms.ChoiceField(label=False, widget=forms.RadioSelect, choices=HlasovaniOZnalostech.Odpoved.choices) +#: Množina formulářů (:py:class:`formset ` :py:class:`HlasovaniZnalostiFormů `) +#: pro :py:class:`HlasováníOZnalostech ` o množině :py:class:`Znalostí ` HlasovaniZnalostiFormSet = forms.formset_factory(HlasovaniZnalostiForm, extra=0) diff --git a/prednasky/models.py b/prednasky/models.py index fbeb6b21..f6168fdd 100644 --- a/prednasky/models.py +++ b/prednasky/models.py @@ -5,6 +5,12 @@ from personalni.models import Organizator, Osoba class Seznam(models.Model): + """ + Spojuje :py:class:`Přednášky ` + se :py:class:`Soustředěními `, + 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 `). """ 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 ` 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í `.) + """ 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 ` 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 ` + v jednom :py:class:`Seznamu ` (úč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 ` 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 `.) + """ 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 ` + v jednom :py:class:`Seznamu ` (úč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ěď ` 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) diff --git a/prednasky/views.py b/prednasky/views.py index 4f6f28ae..68d563cc 100644 --- a/prednasky/views.py +++ b/prednasky/views.py @@ -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í ` + a :py:class:`HlasováníOZnalostech `) + o :py:class:`Přednáškách ` + a :py:class:`Znalostech ` + """ # 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í ` """ 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ů ` 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 ` :py:class:`hlasů `. + (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í ` + i :py:class:`HlasováníOZnalostech ` + v daném :py:class:`Seznamu ` + jako csv soubor (řádky = účastníci, sloupce = přednášky&znalosti). + + :param seznam: ID daného :py:class:`Seznamu ` + """ 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")