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/docs/conf.py b/docs/conf.py index 75bca8d3..8c8c2df0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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. diff --git a/make/lib.sh b/make/lib.sh index dd56ef73..2e92d6e7 100644 --- a/make/lib.sh +++ b/make/lib.sh @@ -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 } diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 3242bd62..b90d4aa2 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/layout.css b/mamweb/static/css/layout.css index bd139780..bb3f98e9 100644 --- a/mamweb/static/css/layout.css +++ b/mamweb/static/css/layout.css @@ -435,6 +435,7 @@ body.localweb, body.testweb, body.suprodweb { height: 100%; top: 0; z-index: -1000; + opacity: 0.7; } &:before { left: 0; } 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/mamweb/static/css/rozliseni.css b/mamweb/static/css/rozliseni.css new file mode 100644 index 00000000..93c11668 --- /dev/null +++ b/mamweb/static/css/rozliseni.css @@ -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; } } +/****************************************************************/ diff --git a/mamweb/vsechno.py b/mamweb/vsechno.py index efb5e48c..9130ebc2 100644 --- a/mamweb/vsechno.py +++ b/mamweb/vsechno.py @@ -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 * diff --git a/odevzdavatko/models.py b/odevzdavatko/models.py index 3b510fe7..6666fcb1 100644 --- a/odevzdavatko/models.py +++ b/odevzdavatko/models.py @@ -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 diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 616ec8e0..4d2b780a 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -191,7 +191,7 @@ Sloupce:
  • 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í).
  • -
  • 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 zde. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.
  • +
  • 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 zde. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.
  • Další poznámky diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 2a213a2c..1b626b3d 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -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.") - hodnoceni.body = -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) diff --git a/personalni/forms.py b/personalni/forms.py index ae08a8c9..39e1b6ab 100644 --- a/personalni/forms.py +++ b/personalni/forms.py @@ -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 == '': diff --git a/personalni/migrations/0018_resitel_upozorneni.py b/personalni/migrations/0018_resitel_upozorneni.py new file mode 100644 index 00000000..1b5b7280 --- /dev/null +++ b/personalni/migrations/0018_resitel_upozorneni.py @@ -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), + ] diff --git a/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py b/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py new file mode 100644 index 00000000..e5a4caaf --- /dev/null +++ b/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py @@ -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', + ), + ] diff --git a/personalni/models.py b/personalni/models.py index 636b132e..e04aca0b 100644 --- a/personalni/models.py +++ b/personalni/models.py @@ -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" diff --git a/personalni/static/personalni/jak_se_dozvedeli.css b/personalni/static/personalni/jak_se_dozvedeli.css new file mode 100644 index 00000000..15a47b80 --- /dev/null +++ b/personalni/static/personalni/jak_se_dozvedeli.css @@ -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; + } +} diff --git a/personalni/templates/personalni/jak_se_dozvedeli.html b/personalni/templates/personalni/jak_se_dozvedeli.html new file mode 100644 index 00000000..6695f94c --- /dev/null +++ b/personalni/templates/personalni/jak_se_dozvedeli.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} + +{% block custom_css %} +{% load static %} + +{% endblock %} + + + +{% block content %} +
    +
    +
    Jméno
    +
    Jak se dozvěděli
    +
    Datum registrace
    +
    + + {% for osoba in object_list %} +
    +
    {{ osoba.jmeno }} {{ osoba.prijmeni }}
    +
    {% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}
    +
    {{ osoba.datum_registrace }}
    +
    + {% endfor %} +
    +{% endblock%} + + diff --git a/personalni/templates/personalni/udaje/udaje.html b/personalni/templates/personalni/udaje/udaje.html index 894ddaf9..83bce161 100644 --- a/personalni/templates/personalni/udaje/udaje.html +++ b/personalni/templates/personalni/udaje/udaje.html @@ -51,6 +51,7 @@ {% 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 %} diff --git a/personalni/urls.py b/personalni/urls.py index c4820b90..1805bbfe 100644 --- a/personalni/urls.py +++ b/personalni/urls.py @@ -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' + ) + ] diff --git a/personalni/views.py b/personalni/views.py index 7c95325c..49442c2d 100644 --- a/personalni/views.py +++ b/personalni/views.py @@ -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: 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/migrations/0022_preklep_u_odpovedi_hlasovanioznalostech.py b/prednasky/migrations/0022_preklep_u_odpovedi_hlasovanioznalostech.py new file mode 100644 index 00000000..32c82d62 --- /dev/null +++ b/prednasky/migrations/0022_preklep_u_odpovedi_hlasovanioznalostech.py @@ -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ěď'), + ), + ] diff --git a/prednasky/models.py b/prednasky/models.py index 74b37403..f508ca7e 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 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ěď ` 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 a915a04a..326370f1 100644 --- a/prednasky/templates/prednasky/base.html +++ b/prednasky/templates/prednasky/base.html @@ -5,34 +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%} - - - {% 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 | linebreaksbr}}

    + {{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 | linebreaksbr}}

    + {{ f }} +
    + {% endfor %} + {% endblock %} diff --git a/prednasky/templates/prednasky/hotovo.html b/prednasky/templates/prednasky/hotovo.html deleted file mode 100644 index f77ea6e1..00000000 --- a/prednasky/templates/prednasky/hotovo.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html' %} - -{% load humanize %} -{% load static %} - - -{% block content %} - -

    Děkujeme.

    - -{% 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 f0694dc5..43ac983b 100644 --- a/prednasky/views.py +++ b/prednasky/views.py @@ -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í ` + a :py:class:`HlasováníOZnalostech `) + o :py:class:`Přednáškách ` + a :py:class:`Znalostech ` + """ # 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í ` """ + 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): @@ -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í ` + 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 diff --git a/requirements.txt b/requirements.txt index 96aa4d7d..0788c744 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tvorba/migrations/0008_alter_problem_opravovatele_and_more.py b/tvorba/migrations/0008_alter_problem_opravovatele_and_more.py new file mode 100644 index 00000000..b0a587ba --- /dev/null +++ b/tvorba/migrations/0008_alter_problem_opravovatele_and_more.py @@ -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', + ), + ] + ), + ] diff --git a/tvorba/models.py b/tvorba/models.py index 36f34312..d9540ba1 100644 --- a/tvorba/models.py +++ b/tvorba/models.py @@ -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') diff --git a/tvorba/views/docasne.py b/tvorba/views/docasne.py index 453909ca..9b2a435f 100644 --- a/tvorba/views/docasne.py +++ b/tvorba/views/docasne.py @@ -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, ) diff --git a/various/migrations/0007_nastaveni_aktualni_sous.py b/various/migrations/0007_nastaveni_aktualni_sous.py new file mode 100644 index 00000000..b0013fe0 --- /dev/null +++ b/various/migrations/0007_nastaveni_aktualni_sous.py @@ -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'), + ), + ] diff --git a/various/models.py b/various/models.py index 363c3137..9f46d5c6 100644 --- a/various/models.py +++ b/various/models.py @@ -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