diff --git a/data/groups.json b/data/groups.json index df9516a0..cf10eb83 100644 --- a/data/groups.json +++ b/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" ], diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 3bfbcfc4..f8cac53f 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -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', diff --git a/mamweb/static/css/modules.css b/mamweb/static/css/modules.css index 02a9b2bf..e698e2fb 100644 --- a/mamweb/static/css/modules.css +++ b/mamweb/static/css/modules.css @@ -503,5 +503,10 @@ label[for=id_skola] { font-weight: bold; } +/* Přednášky */ +.textznalosti, .textprednasky { + font-style: italic; +} + /*******************/ 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 19eace7f..07615f43 100644 --- a/prednasky/admin.py +++ b/prednasky/admin.py @@ -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 ` + v adminu :py:class:`Seznamu `. + """ 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 ` + v adminu :py:class:`Seznamu `. + """ + model = Znalost.seznamy.through + extra = 0 + + def znalost__nazev(self, obj): + return mark_safe( + f"{obj.znalost.nazev}" + ) + + def znalost__text(self, obj): + return mark_safe( + f"
{escape(obj.znalost.text)}
" + ) + + 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 ` """ 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 """ 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 + 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) diff --git a/prednasky/forms.py b/prednasky/forms.py index f095a64e..7b0e9739 100644 --- a/prednasky/forms.py +++ b/prednasky/forms.py @@ -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ář ` 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/migrations/0019_znalost_hlasovanioznalostech.py b/prednasky/migrations/0019_znalost_hlasovanioznalostech.py new file mode 100644 index 00000000..916302c8 --- /dev/null +++ b/prednasky/migrations/0019_znalost_hlasovanioznalostech.py @@ -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')), + ], + ), + ] diff --git a/prednasky/migrations/0020_alter_hlasovani_body.py b/prednasky/migrations/0020_alter_hlasovani_body.py new file mode 100644 index 00000000..c3c348c2 --- /dev/null +++ b/prednasky/migrations/0020_alter_hlasovani_body.py @@ -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'), + ), + ] diff --git a/prednasky/migrations/0021_alter_hlasovanioznalostech_odpoved.py b/prednasky/migrations/0021_alter_hlasovanioznalostech_odpoved.py new file mode 100644 index 00000000..ff0c63b9 --- /dev/null +++ b/prednasky/migrations/0021_alter_hlasovanioznalostech_odpoved.py @@ -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), + ] diff --git a/prednasky/models.py b/prednasky/models.py index 74b37403..a0597be2 100644 --- a/prednasky/models.py +++ b/prednasky/models.py @@ -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 ` + se :py:class:`Soustředěními `, + 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 `). """ + 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 ` 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í `.) + """ 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) - 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) + 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=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) 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 ` + 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í' - 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 ` + 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 `.) + """ + 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 ` + v jednom :py:class:`Seznamu ` (úč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 bychm, ž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ěď ` 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}" diff --git a/prednasky/templates/prednasky/base.html b/prednasky/templates/prednasky/base.html index ce201cc6..37ea178c 100644 --- a/prednasky/templates/prednasky/base.html +++ b/prednasky/templates/prednasky/base.html @@ -5,36 +5,36 @@ {% block content %} -

-{% block nadpis1a %}Hlasování o přednáškách{% endblock %} -

- -

-Jak moc by ses chtěl(a) zúčastnit následujících přednášek? -
-Obtížnost 1 je nejlehčí, 3 nejtěžší. -

+

{% block nadpis1a %}Hlasování o přednáškách{% endblock %}

{% csrf_token %} - - {% for p, h in prednasky %} - - - - - {% if p.klicova %}{% endif%} - - - {% empty %} - Nejsou žádné přednášky o kterých by šlo hlasovat. - {% endfor %} - -

{{p.anotace}}

{{p.obor}}
{{p.obtiznost}}
{{p.klicova}}
Hodnocení: - rozhodně nechci - je mi to jedno - rozhodně chci -
 
+ +

Jak moc by ses chtěl(a) zúčastnit následujících přednášek?

+

Obtížnost 1 je nejlehčí, 3 nejtěžší.

+ {{ form_set_prednasky.management_form }} + {% for f, p in formy_a_prednasky %} +

{{p.nazev}} ({{p.org}})

+

{{p.anotace}}

+ {{p.obor}}
+ {{p.obtiznost}}
+ {% if p.klicova %} {{p.klicova}}
{% endif%} +
+ {{ f }} +
+ {% empty %} + Nejsou žádné přednášky o kterých by šlo hlasovat. + {% endfor %} + + {{ form_set_znalosti.management_form }} + {% for f, z in formy_a_znalosti %} + {% if forloop.first %}

Jak moc znáš následující?

{% endif %} +

{{z.nazev}}

+

{{z.text}}

+ {{ f }} +
+ {% endfor %} +
{% endblock %} diff --git a/prednasky/templates/prednasky/metaseznam_prednasek.html b/prednasky/templates/prednasky/metaseznam_prednasek.html index 9db97b08..dfc13caf 100644 --- a/prednasky/templates/prednasky/metaseznam_prednasek.html +++ b/prednasky/templates/prednasky/metaseznam_prednasek.html @@ -14,7 +14,7 @@ {% else %} Seznam přednášek na soustředění {{seznam.soustredeni.misto}} {% endif %} - Export + Export {% endfor %} diff --git a/prednasky/urls.py b/prednasky/urls.py index eecc45ad..24d8535a 100644 --- a/prednasky/urls.py +++ b/prednasky/urls.py @@ -12,10 +12,15 @@ urlpatterns = [ 'prednasky/metaseznam_prednasek', org_required(views.MetaSeznamListView.as_view()), name='metaseznam-list'), + # path( + # 'prednasky/seznam_prednasek//export', + # org_required(views.SeznamExportView), + # name='seznam-export' + # ), path( - 'prednasky/seznam_prednasek//export', - org_required(views.SeznamExportView), - name='seznam-export' + 'prednasky/seznam_prednasek//hlasovani.csv', + org_required(views.PrednaskyExportView), + name='seznam-export-csv' ), path( 'prednasky/seznam_prednasek//', diff --git a/prednasky/views.py b/prednasky/views.py index a2a2594c..43ac983b 100644 --- a/prednasky/views.py +++ b/prednasky/views.py @@ -1,77 +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 various.views.pomocne import formularOKView +from .forms import HlasovaniPrednaskaFormSet, HlasovaniZnalostiFormSet from various.models import Nastaveni -from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH +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í ` + 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 = STAV_NAVRH).first() + 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): +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): @@ -87,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') @@ -96,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í ` + 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)) + + 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