diff --git a/api/views/autocomplete.py b/api/views/autocomplete.py index 84a915bf..edc81ff7 100644 --- a/api/views/autocomplete.py +++ b/api/views/autocomplete.py @@ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView): """ View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """ def get_queryset(self): - nastaveni = get_object_or_404(m.Nastaveni) - rocnik = nastaveni.aktualni_rocnik - # Od tohoto místa dál jsem zkoušel spoustu variací podle https://django-polymorphic.readthedocs.io/en/stable/advanced.html - temaQ = Q(Tema___rocnik = rocnik, stav=m.Problem.STAV_ZADANY) - ulohaQ = Q(Uloha___cislo_zadani__rocnik = rocnik, stav=m.Problem.STAV_ZADANY) - clanekQ = Q(Clanek___cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY) - qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ) - #print(temata, ulohy, clanky) - #ulohy.union(temata, all=True) - #print(ulohy) - #ulohy.union(clanky, all=True) - #print(ulohy) - #qs = ulohy - print(qs) + qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY) if self.q: qs = qs.filter( Q(nazev__icontains=self.q)) + + nadproblem_id = int(self.forwarded.get("nadproblem_id", -1)) + if nadproblem_id != -1: + # Seřadíme tak, aby ty s nadproblem==None byly dole (větší motivace tam naklikat konkrétní úlohy) a pak nějak rozumně. + # Tohle je řazení pro odevzdávátko, kde je definován nadproblém, proto je to v tomto ifu. (Jinde si to netroufám řadit) + qs = qs.order_by("nadproblem", "kod", "nazev") + qs = list(filter(lambda problem: problem.hlavni_problem.id == nadproblem_id, qs)) return qs class ProblemAutocomplete(autocomplete.Select2QuerySetView): diff --git a/data/sitetree.json b/data/sitetree.json index 7cfa87b0..29403e5a 100644 --- a/data/sitetree.json +++ b/data/sitetree.json @@ -437,7 +437,7 @@ "insitetree": true, "parent": 21, "sort_order": 36, - "title": "Poslat řešení", + "title": "Nahrát řešení", "tree": 1, "url": "seminar_nahraj_reseni", "urlaspattern": true @@ -476,9 +476,9 @@ "access_perm_type": 1, "access_permissions": [ [ - "change_hodnoceni", - "seminar", - "hodnoceni" + "org", + "auth", + "user" ] ], "access_restricted": true, @@ -719,7 +719,7 @@ "insitetree": true, "parent": 21, "sort_order": 36, - "title": "Nahrát řešení", + "title": "Vložit řešení", "tree": 1, "url": "seminar_vloz_reseni", "urlaspattern": true @@ -1026,6 +1026,36 @@ "model": "sitetree.treeitem", "pk": 51 }, + { + "fields": { + "access_guest": false, + "access_loggedin": false, + "access_perm_type": 1, + "access_permissions": [ + [ + "resitel", + "auth", + "user" + ] + ], + "access_restricted": true, + "alias": null, + "description": "", + "hidden": false, + "hint": "", + "inbreadcrumbs": true, + "inmenu": true, + "insitetree": true, + "parent": 23, + "sort_order": 52, + "title": "Nahrát řešení k nadproblému {{nadproblem_id}}", + "tree": 1, + "url": "seminar_nahraj_reseni nadproblem_id", + "urlaspattern": true + }, + "model": "sitetree.treeitem", + "pk": 52 + }, { "fields": { "access_guest": false, @@ -1041,13 +1071,13 @@ "inmenu": true, "insitetree": true, "parent": 28, - "sort_order": 52, + "sort_order": 53, "title": "Přidat PDF", "tree": 1, "url": "/admin/korektury/korekturovanepdf/add/", "urlaspattern": false }, "model": "sitetree.treeitem", - "pk": 52 + "pk": 53 } -] \ No newline at end of file +] diff --git a/deploy_v2/admin_org_prava.json b/deploy_v2/admin_org_prava.json index 2d07cf83..3ef169a5 100644 --- a/deploy_v2/admin_org_prava.json +++ b/deploy_v2/admin_org_prava.json @@ -64,6 +64,36 @@ "ct_app_label": "galerie", "ct_model": "obrazek" }, + { + "codename": "add_fotkaheader", + "ct_app_label": "header_fotky", + "ct_model": "fotkaheader" + }, + { + "codename": "change_fotkaheader", + "ct_app_label": "header_fotky", + "ct_model": "fotkaheader" + }, + { + "codename": "view_fotkaheader", + "ct_app_label": "header_fotky", + "ct_model": "fotkaheader" + }, + { + "codename": "add_fotkaurlvazba", + "ct_app_label": "header_fotky", + "ct_model": "fotkaurlvazba" + }, + { + "codename": "change_fotkaurlvazba", + "ct_app_label": "header_fotky", + "ct_model": "fotkaurlvazba" + }, + { + "codename": "view_fotkaurlvazba", + "ct_app_label": "header_fotky", + "ct_model": "fotkaurlvazba" + }, { "codename": "add_komentar", "ct_app_label": "korektury", @@ -224,6 +254,21 @@ "ct_app_label": "seminar", "ct_model": "clanek" }, + { + "codename": "add_deadline", + "ct_app_label": "seminar", + "ct_model": "deadline" + }, + { + "codename": "change_deadline", + "ct_app_label": "seminar", + "ct_model": "deadline" + }, + { + "codename": "view_deadline", + "ct_app_label": "seminar", + "ct_model": "deadline" + }, { "codename": "add_konfera", "ct_app_label": "seminar", @@ -304,41 +349,21 @@ "ct_app_label": "seminar", "ct_model": "novinky" }, - { - "codename": "add_organizator", - "ct_app_label": "seminar", - "ct_model": "organizator" - }, { "codename": "change_organizator", "ct_app_label": "seminar", "ct_model": "organizator" }, - { - "codename": "delete_organizator", - "ct_app_label": "seminar", - "ct_model": "organizator" - }, { "codename": "view_organizator", "ct_app_label": "seminar", "ct_model": "organizator" }, - { - "codename": "add_osoba", - "ct_app_label": "seminar", - "ct_model": "osoba" - }, { "codename": "change_osoba", "ct_app_label": "seminar", "ct_model": "osoba" }, - { - "codename": "delete_osoba", - "ct_app_label": "seminar", - "ct_model": "osoba" - }, { "codename": "view_osoba", "ct_app_label": "seminar", @@ -404,21 +429,11 @@ "ct_app_label": "seminar", "ct_model": "problem" }, - { - "codename": "add_resitel", - "ct_app_label": "seminar", - "ct_model": "resitel" - }, { "codename": "change_resitel", "ct_app_label": "seminar", "ct_model": "resitel" }, - { - "codename": "delete_resitel", - "ct_app_label": "seminar", - "ct_model": "resitel" - }, { "codename": "view_resitel", "ct_app_label": "seminar", @@ -603,50 +618,5 @@ "codename": "view_taggeditem", "ct_app_label": "taggit", "ct_model": "taggeditem" - }, - { - "codename": "add_fotkaheader", - "ct_app_label": "header_fotky", - "ct_model": "fotkaheader" - }, - { - "codename": "change_fotkaheader", - "ct_app_label": "header_fotky", - "ct_model": "fotkaheader" - }, - { - "codename": "view_fotkaheader", - "ct_app_label": "header_fotky", - "ct_model": "fotkaheader" - }, - { - "codename": "add_fotkaurlvazba", - "ct_app_label": "header_fotky", - "ct_model": "fotkaurlvazba" - }, - { - "codename": "change_fotkaurlvazba", - "ct_app_label": "header_fotky", - "ct_model": "fotkaurlvazba" - }, - { - "codename": "view_fotkaurlvazba", - "ct_app_label": "header_fotky", - "ct_model": "fotkaurlvazba" - }, - { - "codename": "add_deadline", - "ct_app_label": "seminar", - "ct_model": "deadline" - }, - { - "codename": "change_deadline", - "ct_app_label": "seminar", - "ct_model": "deadline" - }, - { - "codename": "view_deadline", - "ct_app_label": "seminar", - "ct_model": "deadline" } -] +] \ No newline at end of file diff --git a/docs/dalsi_soubory.rst b/docs/dalsi_soubory.rst index 1a59ee15..627a59d7 100644 --- a/docs/dalsi_soubory.rst +++ b/docs/dalsi_soubory.rst @@ -28,7 +28,7 @@ Generuje se za pomocí:: nebo (v případě meníčka):: - ./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json + ./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json ./fix_json.py data/sitetree_new.json data/sitetree.json deploy_v2 diff --git a/docs/index.rst b/docs/index.rst index 5481bb88..d06c7e4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,7 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve :titlesonly: vyvoj + zavislosti sphinx skripty modules/modules diff --git a/docs/tabulka_prerekvizit.rst b/docs/tabulka_prerekvizit.rst deleted file mode 100644 index 9dcce4c5..00000000 --- a/docs/tabulka_prerekvizit.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. Není odkázaná z menu, je to záměr - -Tabulka prerekvizit v různých distribucích -========= - -.. admonition:: Metodika - - Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme - ``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme - přiinstalovali. Protože větev ``makefiles`` aktuálně není mergenutá do - masteru, nefunguje synchronizace flatpages (a stejně nemáme SSH klíč), takže - tam ``make/init_local`` sestřelíme a vyzkoušíme, že ``make/test`` spustí - testy. - -.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV… - -.. csv-table:: Prerekvizity v jednotlivých distribucích - :header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky - - Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" - Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" - Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)" - openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." - Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." - diff --git a/docs/vyvoj.rst b/docs/vyvoj.rst index 0d23972a..2df0ae64 100644 --- a/docs/vyvoj.rst +++ b/docs/vyvoj.rst @@ -37,7 +37,7 @@ Kromě toho je potřeba mít účet na `Gitee `_, kd bydlí gitový repozitář s kódem. .. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce - prerekvizit `. + prerekvizit `. Doporučené ^^^^^^^^^^ diff --git a/docs/zavislosti.rst b/docs/zavislosti.rst new file mode 100644 index 00000000..c2f684bd --- /dev/null +++ b/docs/zavislosti.rst @@ -0,0 +1,97 @@ +Závislosti webu +@@@@@@@@@@@@@@@ + +Web ke svému běhu potřebuje různé další programy. Tahle stránka se snaží je pokrýt. + +Stránka je koncipována jako odrážkový seznam balíčků pro Ubuntu s případnými +komentáři, na konci stránky jsou uvedena :ref:`jména balíčků ` v různých dalších distribucích. (Seznam mj. cílí na lokální +rozchození, proto popisuji Ubuntu a ne Debian. I tak se ale snažíme popsat web +v úplnosti.) + +.. I use Arch, btw. + + +Základ webu +=========== + +- ``python3`` – Ideálně Python 3.9, jenž je na Gimlim +- ``python3-pip`` pro instalaci dalších Pythoních balíčků podle ``requirements.txt`` +- ``python3-venv`` +- ``gcc`` – kompilace Pythoních knihoven ze zdrojových distribucí (sdist), možná (neotestováno) jde jako alternativu použít ``python3-wheel`` a stahovat bdists +- ``python3-dev`` – taktéž +- ``libpq-dev`` do třetice… +- ``ghostscript`` TODO konverze PDF v korekturovátku +- ``pdflatex`` FIXME! generování obálek a stvrzenek +- ``git`` – používán :ref:`Make skripty` +- ``locales`` pro české formáty + +Nasazení na produkci / testweb +============================== + +(nejsou nutně potřeba k provozu lokální instance) + +- ``rsync`` +- ``pg_utils`` FIXME +- ``htpasswd`` FIXME – aby testweb nepoužívali náhodní kolemjdoucí +- ``postgresql-server`` TODO +- ``acl`` pro nastavování práv přes ``setfacl`` + +Pro testweb je potřeba i všechno pro :ref:`dokumentaci `, vizte níž. + +Předpokládá se nasazení v uWSGI pod Nginxem a služba běžící pod systemd, nicméně to už je spíš záležitost infrastruktury a ne specifikum mamwebu. + +Dokumentace +=========== + +- ``make`` pro zbuildění +- Pythoní balíčky podle příslušné části ``requirements.txt`` + +Vývojové nástroje +================= + +(Nejsou nezbytně nutné, ale předpokládáme jejich užitečnost. Mohou se hodit i na produkci.) + +- ``psql`` TODO pro manuální dotazy do PostgreSQL +- ``sqlite3`` TODO totéž pro SQLite3 +- ``ssh`` +- ``graphviz`` pro vygenerování schématu +- ``rsync`` +- ``ipython3`` – hezčí interaktivní shell (stačí z ``requirements.txt``) + +Potenciální usnadnění života +============================ + +(Úplně zbytečné, ale sdílíme pozitivní zkušenosti :-)) + +- ``tea`` – CLI klient pro Giteu, aby člověk nepotřeboval otevírat web pro založení PR + + +Alternativní jména balíčků +========================== + +Různé distribuce balí SW různě, takže to, co je v jedné distribuci jeden +balíček může být v jiné rozděleno do víc. Pro usnadnění nasazení je tady +přehled známých alternativních jmen. + +TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte. + +.. admonition:: Jak se pozná, že web funguje, pro účely tabulky? + + Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme + ``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme + přiinstalovali. Protože nefunguje synchronizace flatpages (nemáme SSH klíč), + ``make/init_local`` sestřelíme při pokusu o synchronizaci a vyzkoušíme, že + ``make/test`` spustí testy. + +.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV… + +.. csv-table:: Prerekvizity v jednotlivých distribucích + :header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky + + Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" + Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" + Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)" + openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." + Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." + diff --git a/korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py b/korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py new file mode 100644 index 00000000..6ea07604 --- /dev/null +++ b/korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.28 on 2023-06-19 19:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('korektury', '0019_auto_20221205_2014'), + ] + + operations = [ + migrations.AlterField( + model_name='korekturovanepdf', + name='nazev', + field=models.CharField(help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF', max_length=50, verbose_name='název PDF'), + ), + ] diff --git a/korektury/models.py b/korektury/models.py index ac82c14e..8906c00c 100644 --- a/korektury/models.py +++ b/korektury/models.py @@ -55,7 +55,7 @@ class KorekturovanePDF(models.Model): cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF') - nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. 22.1 verze 4) korekturovaného PDF') + nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF') komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)') diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 03724d3d..71bae132 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -54,6 +54,9 @@ LOGIN_REDIRECT_URL = 'profil' SESSION_EXPIRE_AT_BROWSER_CLOSE = True DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok +# View pro chybu s CSRF tokenem (např. se sušenkami) +CSRF_FAILURE_VIEW = 'various.views.csrf_error' + # Modules configuration AUTHENTICATION_BACKENDS = ( @@ -151,6 +154,7 @@ INSTALLED_APPS = ( 'soustredeni', 'treenode', 'vyroci', + 'sifrovacka', # Admin upravy: diff --git a/mamweb/settings_prod.py b/mamweb/settings_prod.py index 3a81c8c4..ebe827e4 100644 --- a/mamweb/settings_prod.py +++ b/mamweb/settings_prod.py @@ -27,8 +27,9 @@ DEBUG = False TEMPLATE_DEBUG = False -ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', - 'mamweb.bezva.org','gimli.ms.mff.cuni.cz'] +ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa + 'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny… + ] # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases diff --git a/mamweb/settings_test.py b/mamweb/settings_test.py index eac5a7b4..dc5beee8 100644 --- a/mamweb/settings_test.py +++ b/mamweb/settings_test.py @@ -32,7 +32,10 @@ DEBUG = True TEMPLATES[0]['OPTIONS']['debug'] = True -ALLOWED_HOSTS = ['*.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', 'mam.mff.cuni.cz', 'mam-test.kam.mff.cuni.cz', 'gimli.ms.mff.cuni.cz', 'mam-test.ks.matfyz.cz'] +ALLOWED_HOSTS = [ + 'mam-test.ks.matfyz.cz', + '*.mam.mff.cuni.cz', # Asi se nikdy nepoužije… + ] # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 3833ff92..84e4c79b 100644 --- a/mamweb/static/css/mamweb.css +++ b/mamweb/static/css/mamweb.css @@ -1,3 +1,4 @@ +@charset "utf-8"; /* vynuť utf-8 */ @import url("rozliseni.css"); @font-face { @@ -53,6 +54,17 @@ a.login-ref-admin { color: #fffbf6; } +.napis-webarum { + display: inline; + color: #fffbf6; + float: right; +} + +.napis-webarum a { + color: #f9d59e; + text-decoration: underline; +} + /* odkazy a nadpisy */ a { @@ -1233,6 +1245,7 @@ div.gdpr { .dosla_reseni tr th, .dosla_reseni tr td { padding: 1px 10px 1px 10px; border-collapse: collapse; + min-width: 8em; /*Nastřeleno, aby se řádky s řešeními nezalamovaly. Teoreticky není potřeba pro th, ale whatever.*/ } .dosla_reseni tr:nth-child(even) { @@ -1260,3 +1273,31 @@ label[for=id_skola] { .bodovani>input { width: 4em; } + +.bodovani>input::placeholder { + color: lightgray; + opacity: 1; +} + +.bodovani>input::-webkit-input-placeholder { /* Edge */ + color: lightgray; +} + + +/* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */ +/* Podle https://stackoverflow.com/a/48290544 */ +/* U autocomplete.ModelSelect2Multiple vyžaduje 'data-dropdown-css-class': 's2m-se-zaskrtavatky' */ +.s2m-se-zaskrtavatky .select2-results__option[aria-selected=true]:before { + content: '☑ '; + padding: 0 0 0 8px; +} + +.s2m-se-zaskrtavatky .select2-results__option[aria-selected=false]:before { + content: '◻ '; + padding: 0 0 0 8px; +} + +/* Oranžové zvýraznění v Select2 */ +.select2-results__option--highlighted { + background-color: #e84e10 !important; +} diff --git a/mamweb/templates/500.html b/mamweb/templates/500.html index 71d8e651..67085a8f 100644 --- a/mamweb/templates/500.html +++ b/mamweb/templates/500.html @@ -3,7 +3,10 @@ {% load static %} {% block errorheading %} +
{# Meníčko nedostaneme, protože dostáváme prázdný kontext. Tak alespoň ať se O-JO-JO-JO-JOJ neschovává pod ním #} + {% block nadpis1a %} O-jo-jo-jo-joj + {% endblock %} {% endblock %} {% block errortext %} diff --git a/mamweb/templates/base.html b/mamweb/templates/base.html index b10103e5..4281c6df 100644 --- a/mamweb/templates/base.html +++ b/mamweb/templates/base.html @@ -3,6 +3,7 @@ + {# vynuť UTF-8. #} {% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&M{% endblock title %} @@ -49,6 +50,8 @@ {% endif %} + + Něco ti nejde/nefunguje/mate tě? {% endif %} diff --git a/mamweb/urls.py b/mamweb/urls.py index 0855b6b6..9ef2750a 100644 --- a/mamweb/urls.py +++ b/mamweb/urls.py @@ -71,6 +71,8 @@ urlpatterns = [ # Výroční sraz path('sraz/30-let/', include('vyroci.urls')), + # Miniapka na šifrovačku + path('sifrovacka/', include('sifrovacka.urls')), ] # This is only needed when using runserver. diff --git a/odevzdavatko/__init__.py b/odevzdavatko/__init__.py index a4ee2679..ee78a49b 100644 --- a/odevzdavatko/__init__.py +++ b/odevzdavatko/__init__.py @@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš Slovníček: Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. - Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) - Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. + Nahrát řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) + Vlož řešení = Vložit řešení bez vztahu k aktuálnímu uživateli. TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? -""" \ No newline at end of file +""" diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 0b93d555..583523e3 100644 --- a/odevzdavatko/forms.py +++ b/odevzdavatko/forms.py @@ -29,6 +29,8 @@ class PosliReseniForm(forms.Form): attrs={ 'data-placeholder--id': '-1', 'data-placeholder--text': '---', + 'data-close-on-select': 'false', + 'data-dropdown-css-class': 's2m-se-zaskrtavatky', 'data-allow-clear': 'true' }, ), @@ -43,6 +45,8 @@ class PosliReseniForm(forms.Form): url='autocomplete_resitel', attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', + 'data-close-on-select': 'false', + 'data-dropdown-css-class': 's2m-se-zaskrtavatky', 'data-allow-clear': 'true'}) ) @@ -62,12 +66,6 @@ class PosliReseniForm(forms.Form): #poznamka = models.TextField('neveřejná poznámka', blank=True, # help_text='Neveřejná poznámka k řešení (plain text)') - #TODO body do cisla - #TODO prilohy - - ##def __init__(self, *args, **kwargs): - ## super().__init__(*args, **kwargs) - ## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) class NahrajReseniForm(forms.ModelForm): class Meta: @@ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm): url='autocomplete_problem_odevzdatelny', attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', + 'data-close-on-select': 'false', + 'data-dropdown-css-class': 's2m-se-zaskrtavatky', 'data-allow-clear': 'true'}, + forward=["nadproblem_id"], ), 'resitele': autocomplete.ModelSelect2Multiple( url='autocomplete_resitel_public', attrs = {'data-placeholder--id': '-1', 'data-placeholder--text' : '---', + 'data-close-on-select': 'false', + 'data-dropdown-css-class': 's2m-se-zaskrtavatky', 'data-allow-clear': 'true'}, ) } + nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput()) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele if 'resitele' in self.fields: # FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to. self.fields['resitele'].required = False + self.fields['resitele'].label = "Další autoři" + if 'problem' in self.fields: + self.fields['problem'].label = "Všechny řešené problémy" + + def clean_problem(self): + problem = self.cleaned_data.get('problem') + for p in problem: + if p.stav != m.Problem.STAV_ZADANY: + raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!") + return problem ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni, form = NahrajReseniForm, @@ -223,6 +238,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): 'reseni_od': terminy[-2] if rocnik is None else terminy[0], 'reseni_do': terminy[-1], 'neobodovane': False, + 'barvicky': True, } return initial @@ -247,3 +263,4 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) neobodovane = forms.BooleanField(required=False) + barvicky = forms.BooleanField(required=False) diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html index 739340c3..ca326d67 100644 --- a/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni.html @@ -7,19 +7,20 @@ {% block content %}

{% block nadpis1a %} - Vložit řešení + Nahrát řešení {% endblock %}

-

Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a třeba ti je i rychleji opravíme.

- -

Pokud řešíte ve více lidech, je nutné přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze jednou (ne každý sám).

- -
+ {% csrf_token %} - +
+ + + + + + {% with field=form.problem %} - {% for field in form %} - {% endfor %} + + {% if field.errors %} + + + + {% endif %} + + {% endwith %}
{{ field }}
{{ field.errors }}
+ {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + +
+

Spolupráce s dalšími řešiteli

+ +

Pokud řešíte ve více lidech, je potřeba přidat tyto lidi jako „Další autory“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze jednou (ne každý sám).

+ + + {% with field=form.resitele %} + + + + + + {% if field.errors %} + + + + {% endif %} + + {% endwith %} +
+ + + {{ field }} +
{{ field.errors }}

{% include "odevzdavatko/prilohy.html" %} +{{form.non_field_errors}} +

Odevzdat řešení

diff --git a/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html b/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html new file mode 100644 index 00000000..ccf505fa --- /dev/null +++ b/odevzdavatko/templates/odevzdavatko/nahraj_reseni_nadproblem.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +

+ {% block nadpis1a %} + Nahrát řešení + {% endblock %} +

+ +

Seznam témat k odevzdání

+ +
    + {% for problem in object_list %} +
  • {{ problem }}
  • + {% empty %} +
  • Nelze nic odevzdávat.
  • + {% endfor %} +
+ +{% endblock %} diff --git a/odevzdavatko/templates/odevzdavatko/prilohy.html b/odevzdavatko/templates/odevzdavatko/prilohy.html index 4946546b..1e33376f 100644 --- a/odevzdavatko/templates/odevzdavatko/prilohy.html +++ b/odevzdavatko/templates/odevzdavatko/prilohy.html @@ -2,8 +2,9 @@

Soubory s řešením

-

Maximální součet velikostí příloh je cca 49 MB. Pokud je to možné a dává to smysl, pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.

-

Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z img_250921_101205.pdf nepoznáme, kterou úlohu jsi odevzdal, zato uloha_3.pdf nebo tema_1.pdf, to už je něco jiného. Případně můžeš využít i poznámku řešitele.

+

Pokud je to možné a dává to smysl (tj. není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.

+

Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z img_250921_101205.pdf nepoznáme, kterou úlohu jsi odevzdal, zato uloha_3.pdf nebo tema_1.pdf, to už je něco jiného. Případně můžeš využít i poznámku řešitele.

+

Maximální součet velikostí příloh je cca 49 MB.

{% for form in prilohy.forms %} diff --git a/odevzdavatko/templates/odevzdavatko/tabulka.html b/odevzdavatko/templates/odevzdavatko/tabulka.html index 6d1232d2..7cd317e5 100644 --- a/odevzdavatko/templates/odevzdavatko/tabulka.html +++ b/odevzdavatko/templates/odevzdavatko/tabulka.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} +{% load barvy_reseni %} {% block content %} @@ -11,6 +12,7 @@ Od data (vyjma): {{ filtr.reseni_od }} Do data (včetně): {{ filtr.reseni_do }} 🔨? {{ filtr.neobodovane }} +🎨? {{ filtr.barvicky }} @@ -36,12 +38,15 @@ Do data (včetně): {{ filtr.reseni_do }} {# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #} {{ resitel }} - {% for hodn in hodnoty %} + {% for soucet,bunka in hodnoty %} - {% if hodn %} - - {{ hodn.pocet_reseni }} řeš.
{{ hodn.body }} b
{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} -
+ {% for reseni,hodnoceni in bunka %} + + {{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b) +
+ {% endfor %} + {% if bunka|length > 1 %} + Σ: {{soucet}} b {% endif %} {% endfor %} diff --git a/odevzdavatko/templates/odevzdavatko/posli_reseni.html b/odevzdavatko/templates/odevzdavatko/vloz_reseni.html similarity index 100% rename from odevzdavatko/templates/odevzdavatko/posli_reseni.html rename to odevzdavatko/templates/odevzdavatko/vloz_reseni.html diff --git a/odevzdavatko/templatetags/barvy_reseni.py b/odevzdavatko/templatetags/barvy_reseni.py new file mode 100644 index 00000000..5a3791fd --- /dev/null +++ b/odevzdavatko/templatetags/barvy_reseni.py @@ -0,0 +1,15 @@ +from django import template +register = template.Library() + +from functools import cache +import seminar.models as m + +@register.filter +@cache +def barva_reseni(r: m.Reseni): + """Vrátí nějakou barvu pro daný problém, ve tvaru '#RRGGBB' + + Efektivně hešujeme do barev.""" + + #TODO: ne všechny barvy jsou dobře rozlišitelné a vidět… + return f'#{hash(str(r.id)) & 0xffffff:06x}' diff --git a/odevzdavatko/urls.py b/odevzdavatko/urls.py index 8c53de6b..6b021f2e 100644 --- a/odevzdavatko/urls.py +++ b/odevzdavatko/urls.py @@ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \ from . import views urlpatterns = [ - path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), - path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), + path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'), + path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'), + path('resitel/nahraj_reseni//', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index e87e19ea..41af1dcb 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -13,6 +13,7 @@ from django.db.models import Q from dataclasses import dataclass import datetime +from decimal import Decimal from itertools import groupby import logging @@ -37,14 +38,6 @@ logger = logging.getLogger(__name__) # Taky se může hodit: # - Tabulka všech řešitelů x všech problémů? -@dataclass -class SouhrnReseni: - """Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce.""" - pocet_reseni : int - posledni_odevzdani : datetime.datetime - body : float - - class TabulkaOdevzdanychReseniView(ListView): template_name = 'odevzdavatko/tabulka.html' model = m.Hodnoceni @@ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView): reseni_od = fcd["reseni_od"] reseni_do = fcd["reseni_do"] jen_neobodovane = fcd["neobodovane"] + self.barvicky = fcd["barvicky"] else: initial = FiltrForm.gen_initial(self.aktualni_rocnik) resitele = initial['resitele'] @@ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView): reseni_od = initial['reseni_od'][0] reseni_do = initial['reseni_do'][0] jen_neobodovane = initial["neobodovane"] + self.barvicky = initial["barvicky"] # Chceme jen letošní problémy @@ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView): return qs def get_context_data(self, *args, **kwargs): + # TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl… # self.resitele, self.reseni a self.problemy jsou již nastavené ctx = super().get_context_data(*args, **kwargs) ctx['problemy'] = self.problemy ctx['resitele'] = self.resitele - tabulka = dict() - - def pridej_reseni(problem, resitel, body, cas): + tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict() + soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict() + + def pridej_reseni(resitel, hodnoceni): + problem = hodnoceni.problem + body = hodnoceni.body + cas = hodnoceni.reseni.cas_doruceni + reseni = hodnoceni.reseni if problem not in tabulka: tabulka[problem] = dict() + soucty[problem] = dict() if resitel not in tabulka[problem]: - tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body) + tabulka[problem][resitel] = [(reseni, hodnoceni)] + soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme else: - tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) - # Zvětšení počtu bodů o aktuální počet, pokud se tam někde nevyskytuje None – pak je součet taky None ("Pozor, nezadané body") - tabulka[problem][resitel].body = tabulka[problem][resitel].body + body if body is not None and tabulka[problem][resitel].body is not None else None - tabulka[problem][resitel].pocet_reseni += 1 - # Pro jednoduchost template si ještě poznamenáme ID problému a řešitele - tabulka[problem][resitel].problem_id = problem.id - tabulka[problem][resitel].resitel_id = resitel.id + tabulka[problem][resitel].append((reseni, hodnoceni)) + soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme for hodnoceni in self.get_queryset(): for resitel in hodnoceni.reseni.resitele.all(): - pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) + pridej_reseni(resitel, hodnoceni) - hodnoty = [] - resitele_do_tabulky = [] + hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému. + resitele_do_tabulky: list[m.Resitel] = [] for resitel in self.resitele: dostal_body = False - resiteluv_radek = [] + resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy for problem in self.problemy: if problem in tabulka and resitel in tabulka[problem]: - resiteluv_radek.append(tabulka[problem][resitel]) + resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel])) dostal_body = True else: - resiteluv_radek.append(None) + resiteluv_radek.append((Decimal(0),[])) if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body: hodnoty.append(resiteluv_radek) resitele_do_tabulky.append(resitel) @@ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView): ctx['form'] = ctx['filtr'] # Pro maximum v přesměrovátku ročníků ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik + ctx['barvicky'] = self.barvicky if 'rocnik' in self.kwargs: ctx['rocnik'] = self.kwargs['rocnik'] else: @@ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView): # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): + """Rozskok mezi více řešeními téhož problému od téhož řešitele. + + Asi už bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-) + + V případě, že takové řešení existuje jen jedno, tak na něj přesměruje.""" model = m.Reseni template_name = 'odevzdavatko/seznam.html' @@ -316,7 +320,8 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): 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: + 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 hodnoceni.save() @@ -367,8 +372,8 @@ class SeznamAktualnichReseniView(SeznamReseniView): return qs -class PosliReseniView(LoginRequiredMixin, FormView): - template_name = 'odevzdavatko/posli_reseni.html' +class VlozReseniView(LoginRequiredMixin, FormView): + template_name = 'odevzdavatko/vloz_reseni.html' form_class = f.PosliReseniForm def form_valid(self, form): @@ -399,12 +404,31 @@ class PosliReseniView(LoginRequiredMixin, FormView): return data +class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView): + model = m.Problem + template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html' + + def get_queryset(self): + return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True) + + class NahrajReseniView(LoginRequiredMixin, CreateView): model = m.Reseni template_name = 'odevzdavatko/nahraj_reseni.html' form_class = f.NahrajReseniForm + nadproblem: m.Problem + + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + nadproblem_id = self.kwargs["nadproblem_id"] + self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id) def get(self, request, *args, **kwargs): + # Zaříznutí nezadaných problémů + if self.nadproblem.stav != m.Problem.STAV_ZADANY: + raise PermissionDenied() + + # Zaříznutí starých řešitelů: # FIXME: Je to tady dost naprasené, mělo by to asi být jinde… osoba = m.Osoba.objects.get(user=self.request.user) @@ -417,12 +441,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): }) return super().get(request, *args, **kwargs) + def get_initial(self): + nadproblem_id = self.nadproblem.id + return { + "nadproblem_id": nadproblem_id, + "problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id + + } + def get_context_data(self,**kwargs): data = super().get_context_data(**kwargs) if self.request.POST: data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) else: data['prilohy'] = f.ReseniSPrilohamiFormSet() + + data["nadproblem_id"] = self.nadproblem.id + data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id) return data # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni @@ -474,4 +509,8 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): to=list(prijemci), ).send() - return formularOKView(self.request, text='Řešení úspěšně odevzdáno') + return formularOKView( + self.request, + text='Řešení úspěšně odevzdáno', + dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))], + ) diff --git a/personalni/admin.py b/personalni/admin.py index fc3cadd4..14af2c2c 100644 --- a/personalni/admin.py +++ b/personalni/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin from django.contrib.auth.models import Group from django_reverse_admin import ReverseModelAdmin +from django.contrib.messages import WARNING, ERROR, SUCCESS import seminar.models as m +from datetime import datetime @admin.register(m.Osoba) @@ -20,16 +22,24 @@ class OsobaAdmin(admin.ModelAdmin): def udelej_orgem(self,request,queryset): org_group = Group.objects.get(name='org') - print(queryset) + uspesne_vytvoreni_orgove = 0 for o in queryset: + if m.Organizator.objects.filter(osoba=o).exists(): + # Ref: https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.message_user + self.message_user(request, f"Osoba {o} už je org, přeskakuji.", level=WARNING) + continue user = o.user - print(user) + if user is None: + self.message_user(request, f"Osoba {o} nemá uživatele! Přeskakuji.", level=ERROR) + continue user.groups.add(org_group) user.is_staff = True user.save() - org = m.Organizator.objects.create(osoba=o) + org = m.Organizator.objects.create(osoba=o, organizuje_od=datetime.now()) org.save() - udelej_orgem.short_description = "Udělej vybraných osob organizátory" + uspesne_vytvoreni_orgove += 1 + self.message_user(request, f'Úspěšně vytvořeno {uspesne_vytvoreni_orgove} orgů.', level=SUCCESS) + udelej_orgem.short_description = "Udělej z vybraných osob organizátory" class OsobaInline(admin.TabularInline): model = m.Osoba diff --git a/personalni/templates/personalni/profil/resitel.html b/personalni/templates/personalni/profil/resitel.html index 9c933f0a..0bd92d63 100644 --- a/personalni/templates/personalni/profil/resitel.html +++ b/personalni/templates/personalni/profil/resitel.html @@ -11,7 +11,7 @@ Odhlásit se
Upravit údaje
-Poslat řešení
+Nahrát řešení
Již odevzdaná řešení
diff --git a/personalni/tests.py b/personalni/tests.py new file mode 100644 index 00000000..31aac8e8 --- /dev/null +++ b/personalni/tests.py @@ -0,0 +1,63 @@ +from django.test import TestCase, RequestFactory + +from django.contrib.auth.models import User, Group +from django.contrib.admin.sites import AdminSite +from personalni.admin import OsobaAdmin +# Tohle bude peklo, až jednou ty modely fakt rozstřelíme… Možná vyrobit various.all_models, které půjdou importovat jako m? :-) +import seminar.models as m + +import logging +logger = logging.getLogger(__name__) + +class DelaniOrguTest(TestCase): + def setUp(self): + # Admin musí mít instanci + # Ref: https://www.argpar.se/posts/programming/testing-django-admin/ + adm_site = AdminSite() + self.admin = OsobaAdmin(m.Osoba, adm_site) + + from django.contrib.messages.storage.cookie import CookieStorage + self.request = RequestFactory().get('/admin') + self.request._messages = CookieStorage(self.request) + + self.org_group = Group.objects.get(name='org') + + novy_user = User.objects.create(username='osoba') + self.nova_osoba = m.Osoba.objects.create( + jmeno='Milada', + prijmeni='Von Kolej', + user = novy_user, + # Snad nic dalšího nepotřebujeme, kdyžtak se doplní… + ) + stary_user = User.objects.create(username='stary_user') + stara_osoba = m.Osoba.objects.create(user=stary_user) + self.stary_org = m.Organizator.objects.create(osoba=stara_osoba) + + def test_pridani_orga(self): + # Nejdřív to není org… + self.assertFalse(m.Organizator.objects.filter(osoba=self.nova_osoba).exists()) + self.assertNotIn(self.org_group, self.nova_osoba.user.groups.all()) + self.assertFalse(self.nova_osoba.user.has_perm('auth.org')) + self.assertFalse(self.nova_osoba.user.is_staff) + + # Pak orga uděláme… + qs = m.Osoba.objects.filter(id=self.nova_osoba.id) + self.admin.udelej_orgem(self.request, qs) + + # A pak už to org má být. + self.nova_osoba.refresh_from_db() + self.assertTrue(self.nova_osoba.user.is_staff) + # FIXME: V db nejsou práva. Nový org je sice ve skupině "org", ale ta nemá právo "auth.org" + # Očekávané řešení: dodat fixture, která to přidá. + #self.assertTrue(self.nova_osoba.user.has_perm('auth.org')) + self.assertIn(self.org_group, self.nova_osoba.user.groups.all()) + self.assertTrue(m.Organizator.objects.filter(osoba=self.nova_osoba).exists()) + novy_org = m.Organizator.objects.get(osoba=self.nova_osoba) + self.assertIsNotNone(novy_org.organizuje_od) + + def test_pridani_stareho_orga(self): + self.admin.udelej_orgem(self.request, m.Osoba.objects.filter(id=self.stary_org.osoba.id)) # Ugly + # Když to spadne, tak jsem se to dozvěděl, takže už nepotřebuju nic kontrolovat. + # Jestli to funguje správně má řešit jiný test. + + diff --git a/personalni/views.py b/personalni/views.py index a45aee52..876cc7ec 100644 --- a/personalni/views.py +++ b/personalni/views.py @@ -173,7 +173,11 @@ def resitelEditView(request): msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa']) resitel_edit.save() osoba_edit.save() - return formularOKView(request, text=f'Údaje byly úspěšně uloženy. Vrátit se zpět na profil.') + return formularOKView( + request, + text='Údaje byly úspěšně uloženy.', + dalsi_odkazy=[("Vrátit se zpět na profil", reverse("profil"))], + ) return render(request, 'personalni/udaje/edit.html', {'form': form}) diff --git a/requirements.txt b/requirements.txt index 8a6a46e9..d51645de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ psycopg2 html5lib ipython Pillow +pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0 pytz six pexpect @@ -24,7 +25,7 @@ django-ckeditor django-cleanup # Uklízí media/ od smazaných „databázových“ souborů django-flat-theme django-taggit -django-autocomplete-light>=3.9.0rc1 +django-autocomplete-light>=3.9.0 django-crispy-forms django-imagekit django-polymorphic @@ -52,9 +53,6 @@ Werkzeug requests # requests-oauthlib -# uWSGI -uWSGI - # Potřeba pro test data lorem diff --git a/seminar/admin.py b/seminar/admin.py index e88af140..8f589a03 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -12,15 +12,25 @@ from django.utils.safestring import mark_safe import seminar.models as m admin.site.register(m.Rocnik) - -admin.site.register(m.Deadline) admin.site.register(m.ZmrazenaVysledkovka) - +@admin.register(m.Deadline) +class DeadlineAdmin(admin.ModelAdmin): + actions = ['pregeneruj_vysledkovku'] + + # Nikomu nezobrazovat, ale superuživatelům se může hodit :-) + @admin.action(permissions=['bazmek'], description= 'Přegeneruj výsledkovky vybraných deadlinů') + def pregeneruj_vysledkovku(self, req, qs): + for deadline in qs: + deadline.vygeneruj_vysledkovku() + + def has_bazmek_permission(self, request): + # Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu… + return request.user.is_superuser + class DeadlineAdminInline(admin.TabularInline): - model = m.Deadline - extra = 0 - + model = m.Deadline + extra = 0 class CisloForm(ModelForm): class Meta: @@ -71,7 +81,7 @@ class CisloForm(ModelForm): @admin.register(m.Cislo) class CisloAdmin(admin.ModelAdmin): form = CisloForm - actions = ['force_publish'] + actions = ['force_publish', 'pregeneruj_vysledkovky'] inlines = (DeadlineAdminInline,) def force_publish(self,request,queryset): @@ -111,6 +121,17 @@ class CisloAdmin(admin.ModelAdmin): force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' + # Jen pro superuživatele + @admin.action(permissions=['bazmek'], description='Přegenerovat výsledkovky všech deadlinů vybraných čísel') + def pregeneruj_vysledkovky(self, req, qs): + for cislo in qs: + for deadline in cislo.deadline_v_cisle.all(): + deadline.vygeneruj_vysledkovku() + + def has_bazmek_permission(self, request): + # Boilerplate: potřebujeme nějakou permission, protože nějaká haluz v Djangu… + return request.user.is_superuser + @admin.register(m.Problem) class ProblemAdmin(PolymorphicParentModelAdmin): diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index 54e769c8..1c1a3285 100644 --- a/seminar/models/tvorba.py +++ b/seminar/models/tvorba.py @@ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel): return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) return str(self.kod) logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' # def verejne(self): # # aktuálně podle stavu problému @@ -571,9 +571,9 @@ class Tema(Problem): if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: if self.nadproblem: return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) - return "t{}".format(self.kod) + return 't'+self.kod logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -607,9 +607,9 @@ class Clanek(Problem): # Nemělo by být potřeba # if self.nadproblem: # return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) - return "c{}".format(self.kod) + return "c" + self.kod logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def node(self): return None @@ -642,12 +642,9 @@ class Uloha(Problem): @cached_property def kod_v_rocniku(self): if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) - if self.nadproblem: - return self.nadproblem.kod_v_rocniku+name - return name + return f"{self.cislo_zadani.poradi}.{self.kod}" logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 4627989e..8e71fed3 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -35,6 +35,7 @@ from django.conf import settings import unicodedata import logging import time +from collections.abc import Sequence from seminar.utils import aktivniResitele @@ -534,7 +535,9 @@ class PosledniCisloVysledkovkaView(generic.DetailView): def get_context_data(self, **kwargs): context = super(PosledniCisloVysledkovkaView, self).get_context_data() rocnik = context['rocnik'] - cislo = rocnik.cisla.order_by("poradi").last() + cislo = rocnik.cisla.order_by("poradi").filter(deadline_v_cisle__isnull=False).last() + if cislo is None: + raise Http404(f"Ročník {rocnik.rocnik} nemá číslo s deadlinem.") cislopred = cislo.predchozi() context['vysledkovka'] = VysledkovkaDoTeXu( cislo, @@ -677,9 +680,9 @@ def StavDatabazeView(request): # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) -def formularOKView(request, text=''): +def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()): template_name = 'seminar/formular_ok.html' - odkazy = [ + odkazy = list(dalsi_odkazy) + [ # (Text, odkaz) ('Vrátit se na titulní stránku', reverse('titulni_strana')), ('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')), diff --git a/sifrovacka/__init__.py b/sifrovacka/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sifrovacka/admin.py b/sifrovacka/admin.py new file mode 100644 index 00000000..71d191d4 --- /dev/null +++ b/sifrovacka/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import OdpovedUcastnika, SpravnaOdpoved + +# Register your models here. + +admin.site.register(OdpovedUcastnika) +admin.site.register(SpravnaOdpoved) diff --git a/sifrovacka/apps.py b/sifrovacka/apps.py new file mode 100644 index 00000000..e9f34de6 --- /dev/null +++ b/sifrovacka/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SifrovackaConfig(AppConfig): + name = 'sifrovacka' diff --git a/sifrovacka/forms.py b/sifrovacka/forms.py new file mode 100644 index 00000000..e3eba7c7 --- /dev/null +++ b/sifrovacka/forms.py @@ -0,0 +1,18 @@ +from django.core.exceptions import ValidationError +from django.forms import ModelForm, Textarea +from .models import OdpovedUcastnika, SpravnaOdpoved + + +class SifrovackaForm(ModelForm): + class Meta: + model = OdpovedUcastnika + fields = ["sifra", "odpoved", ] + widgets = { + "odpoved": Textarea(attrs={'rows': 1, 'cols': 30}), + } + + def clean_sifra(self): + sifra = self.cleaned_data.get('sifra') + if SpravnaOdpoved.objects.filter(sifra=sifra).count() == 0: + raise ValidationError("Tohle číslo šifry v databázi nemáme. Zkontrolujte si ho prosím.") + return sifra diff --git a/sifrovacka/migrations/0001_initial.py b/sifrovacka/migrations/0001_initial.py new file mode 100644 index 00000000..742461ef --- /dev/null +++ b/sifrovacka/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.22 on 2023-10-14 09:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('seminar', '0113_resitel_zasilat_cislo_papirove'), + ] + + operations = [ + migrations.CreateModel( + name='SpravnaOdpoved', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('odpoved', models.TextField()), + ('sifra', models.IntegerField()), + ('skryty_text', models.TextField()), + ], + ), + migrations.CreateModel( + name='OdpovedUcastnika', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('odpoved', models.TextField(verbose_name='Tajenka')), + ('sifra', models.IntegerField(verbose_name='Číslo šifry')), + ('resitel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.resitel')), + ], + ), + ] diff --git a/sifrovacka/migrations/0002_auto_20231015_1944.py b/sifrovacka/migrations/0002_auto_20231015_1944.py new file mode 100644 index 00000000..dea42891 --- /dev/null +++ b/sifrovacka/migrations/0002_auto_20231015_1944.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.22 on 2023-10-15 17:44 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('sifrovacka', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='odpoveducastnika', + options={'ordering': ['-timestamp']}, + ), + migrations.AddField( + model_name='odpoveducastnika', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp'), + ), + migrations.AlterField( + model_name='odpoveducastnika', + name='odpoved', + field=models.TextField(verbose_name='Tajenka bez diakritiky'), + ), + ] diff --git a/sifrovacka/migrations/0003_odpoveducastnika_uspech.py b/sifrovacka/migrations/0003_odpoveducastnika_uspech.py new file mode 100644 index 00000000..1d61dd8c --- /dev/null +++ b/sifrovacka/migrations/0003_odpoveducastnika_uspech.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.22 on 2023-10-16 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sifrovacka', '0002_auto_20231015_1944'), + ] + + operations = [ + migrations.AddField( + model_name='odpoveducastnika', + name='uspech', + field=models.BooleanField(default=False, verbose_name='Úspěch'), + ), + ] diff --git a/sifrovacka/migrations/__init__.py b/sifrovacka/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sifrovacka/models.py b/sifrovacka/models.py new file mode 100644 index 00000000..6517c2e0 --- /dev/null +++ b/sifrovacka/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.utils import timezone + +from seminar.models.personalni import Resitel + + +# Create your models here. + + +class OdpovedUcastnika(models.Model): + class Meta: + ordering = ["-timestamp"] + + resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE) + odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,) + sifra = models.IntegerField("Číslo šifry", blank=False, null=False,) + timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now) + uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False) + + +class SpravnaOdpoved(models.Model): + odpoved = models.TextField(blank=False, null=False,) + sifra = models.IntegerField(blank=False, null=False,) + skryty_text = models.TextField(blank=False, null=False,) + + def __str__(self): + return f"{self.sifra}: {self.odpoved}" diff --git a/sifrovacka/templates/sifrovacka/odpovedi_list.html b/sifrovacka/templates/sifrovacka/odpovedi_list.html new file mode 100644 index 00000000..0024a7c1 --- /dev/null +++ b/sifrovacka/templates/sifrovacka/odpovedi_list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} + +

{% block nadpis1a %}Šifrovačka odpovědi{% endblock nadpis1a %}

+ + + + + + + + + + {% for u in object_list %} + + + + + + + {% endfor %} +
TimestampŘešitelŠifraOdpověď
{{ u.timestamp }}{{ u.resitel }}{{ u.sifra }}{{ u.odpoved }}
+ +{% endblock content %} diff --git a/sifrovacka/templates/sifrovacka/sifrovacka.html b/sifrovacka/templates/sifrovacka/sifrovacka.html new file mode 100644 index 00000000..4e0cc15a --- /dev/null +++ b/sifrovacka/templates/sifrovacka/sifrovacka.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block content %} + +
+ +

{% block nadpis1a %}M&Mí šifrovačka{% endblock nadpis1a %}

+ +
+ +

Zadat tajenku šifry:

+ +
+ + {{form.non_field_errors}} + {% for field in form %} + + + + + + + + + {% if field.errors %} + + + + {% endif %} + {% endfor %} +
+ + + + {{ field }} + {{ field.help_text|safe }} +
{{ field.errors }}
+ + {% csrf_token %} + + +
+ +{% endblock content %} diff --git a/sifrovacka/urls.py b/sifrovacka/urls.py new file mode 100644 index 00000000..a7af5e54 --- /dev/null +++ b/sifrovacka/urls.py @@ -0,0 +1,17 @@ +from django.urls import path + +from seminar.utils import org_required, resitel_or_org_required +from .views import SifrovackaView, SifrovackaListView + +urlpatterns = [ + path( + '', + resitel_or_org_required(SifrovackaView.as_view()), + name='sifrovacka' + ), + path( + 'odpovedi/', + org_required(SifrovackaListView.as_view()), + name='sifrovacka_odpovedi' + ), +] diff --git a/sifrovacka/views.py b/sifrovacka/views.py new file mode 100644 index 00000000..9c4af3ed --- /dev/null +++ b/sifrovacka/views.py @@ -0,0 +1,33 @@ +from django.urls import reverse +from django.views.generic import FormView, ListView + +from seminar.views import formularOKView +from .forms import SifrovackaForm +from .models import OdpovedUcastnika, SpravnaOdpoved +from seminar.models.personalni import Resitel + + +# Create your views here. + +class SifrovackaView(FormView): + template_name = 'sifrovacka/sifrovacka.html' + form_class = SifrovackaForm + + def form_valid(self, form): + instance = form.save(commit=False) + resitel = Resitel.objects.get(osoba__user=self.request.user) + instance.resitel = resitel + instance.save() + sifra = SpravnaOdpoved.objects.filter(sifra=instance.sifra, odpoved__iexact=instance.odpoved.strip()).first() + if sifra is None: + return formularOKView(self.request, f'

Bohužel vám hvězdy nebyly nakloněny. Rozumějte máte to blbě.

Zkusit znovu.




') + + instance.uspech = True + instance.save() + + return formularOKView(self.request, f'

{sifra.skryty_text}

Odevzdat další.




') + + +class SifrovackaListView(ListView): + template_name = 'sifrovacka/odpovedi_list.html' + model = OdpovedUcastnika diff --git a/various/static/various/img/zere_kostku.svg b/various/static/various/img/zere_kostku.svg new file mode 100644 index 00000000..bac31662 --- /dev/null +++ b/various/static/various/img/zere_kostku.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/various/templates/various/403_csrf.html b/various/templates/various/403_csrf.html new file mode 100644 index 00000000..d0082550 --- /dev/null +++ b/various/templates/various/403_csrf.html @@ -0,0 +1,19 @@ +{#{% extends "error_base.html" %} Z toho nedědíme, protože se nemá přecházet na titulní stránku. #} +{% extends "base.html" %} + +{% load static %} + +{% block content %} + +

{% block nadpis1a %}O-jo-jo-jo-joj{% endblock nadpis1a %}

+ +

+ Problém se sušenkami či něčím podobným. Zkuste {% if url %}to prosím znovu: {{ url }}. Případně můžete {% endif %}přejít na titulní stránku. +

+ +

Pokud problém přetrvává obraťte se na nás přes e-mail: mailto:mam@matfyz.cz a pošlete nám následující popis chyby: {{ reason }}

+ + + + +{% endblock %} diff --git a/various/views.py b/various/views.py index 91ea44a2..96d9a29d 100644 --- a/various/views.py +++ b/various/views.py @@ -1,3 +1,13 @@ +from django.http import HttpResponseForbidden from django.shortcuts import render # Create your views here. + + +def csrf_error(request, reason=""): + """ Jednoduchý „template view“ (třída to být nemůže) pro CSRF chyby """ + return render( + request, 'various/403_csrf.html', + {"url": request.META.get("HTTP_REFERER", None), "reason": reason}, + status=HttpResponseForbidden.status_code, + ) diff --git a/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html index ac53c811..4aa62953 100644 --- a/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html +++ b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html @@ -4,11 +4,11 @@ # Jméno {% for p in vysledkovka.temata_a_spol%} - {# #}{{ p.kod_v_rocniku }}{# #} + {# #}{{ p.kod_v_rocniku }}{# #} {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} {% endfor %} {# TODELETE #} @@ -17,7 +17,7 @@ {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} {% endfor %} {# TODELETE #} diff --git a/vysledkovky/utils.py b/vysledkovky/utils.py index 3ff59fb1..2036b9d3 100644 --- a/vysledkovky/utils.py +++ b/vysledkovky/utils.py @@ -257,7 +257,7 @@ class VysledkovkaCisla(Vysledkovka): # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) hlavni_problemy = set() for p in self.problemy: - hlavni_problemy.add(p.hlavni_problem) + hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy… # zunikátnění hlavni_problemy = list(hlavni_problemy) @@ -313,7 +313,7 @@ class VysledkovkaCisla(Vysledkovka): # Sečteme hodnocení for hodnoceni in self.hodnoceni_do_cisla: - prob = hodnoceni.problem + prob = hodnoceni.problem.get_real_instance() nadproblem = prob.hlavni_problem.id # Když nadproblém není "téma", pak je "Ostatní" @@ -366,18 +366,12 @@ class VysledkovkaCisla(Vysledkovka): for problem in self.problemy: h_problem = problem.hlavni_problem if h_problem in temata_a_spol: - podproblemy[h_problem.id].append(problem) + podproblemy[h_problem.id].append(problem.get_real_instance()) else: - podproblemy[-1].append(problem) + podproblemy[-1].append(problem.get_real_instance()) for podproblem in podproblemy.keys(): - def int_or_zero(p): - try: - return int(p.kod) - except ValueError: - return 0 - - podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero) + podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku) return podproblemy @cached_property