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 <https://gitea.ks.matfyz.cz>`_, kd bydlí gitový repozitář s kódem. .. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce - prerekvizit <Tabulka prerekvizit v různých distribucích>`. + prerekvizit <Alternativní jména balíčků>`. 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ů <Alternativní 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 <Dokumentace>`, 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 %} + <br> {# 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 @@ <!DOCTYPE html> <html lang='cs'> <head> + <meta charset="utf-8"> {# vynuť UTF-8. #} <title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&M{% endblock title %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon"> @@ -49,6 +50,8 @@ <a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a> {% endif %} <a class="login-ref-admin" href='/admin'>[admin mainpage]</a> + + <span class="napis-webarum">Něco ti nejde/nefunguje/mate tě? <a class="login-ref-admin" href='mailto:web@mam.mff.cuni.cz'>Napiš webařům!</a></span> </div> {% 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 %} <h1> {% block nadpis1a %} - Vložit řešení + Nahrát řešení {% endblock %} </h1> -<p style="text-align: justify">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.</p> - - <p>Pokud řešíte ve více lidech, je <strong>nutné</strong> 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 <strong>jednou</strong> (ne každý sám).</p> - -<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();"> +<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();"> {% csrf_token %} - <table class='form' id="reseni"> + <table class='form'> + <tr> + <td><label class="field-label field-required" for="tema">Téma:</label></td> + <td><input id="tema" disabled="" type="text" value="{{ nadproblem }}"></td> + </tr> + + {% with field=form.problem %} <tr> - {% for field in form %} <td> <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> {{ field.label }}: @@ -28,15 +29,54 @@ <td> {{ field }} </td> - {% endfor %} </tr> + + {% if field.errors %} + <tr> + <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> + </tr> + {% endif %} + + {% endwith %} </table> + {% for field in form.hidden_fields %} + {{ field }} + {% endfor %} + + <hr> + <h4>Spolupráce s dalšími řešiteli</h4> + + <p>Pokud řešíte ve více lidech, je <strong>potřeba</strong> 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 <strong>jednou</strong> (ne každý sám).</p> + + <table class='form'> + {% with field=form.resitele %} + <tr> + <td> + <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> + {{ field.label }}: + </label> + </td> + <td> + {{ field }} + </td> + </tr> + + {% if field.errors %} + <tr> + <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> + </tr> + {% endif %} + + {% endwith %} + </table> <hr> {% include "odevzdavatko/prilohy.html" %} +{{form.non_field_errors}} + <hr> <h4>Odevzdat řešení</h4> <input type="submit" value="Odevzdat"> 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 %} +<h1> + {% block nadpis1a %} + Nahrát řešení + {% endblock %} +</h1> + +<h4>Seznam témat k odevzdání</h4> + +<ul> + {% for problem in object_list %} + <li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li> + {% empty %} + <li>Nelze nic odevzdávat.</li> + {% endfor %} +</ul> + +{% 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 @@ <h4>Soubory s řešením</h4> -<p style="text-align: justify">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.</p> -<p style="text-align: justify">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 <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p> +<p style="text-align: justify">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 <strong>PDF</strong>, ostatní formáty nemusíme umět otevřít.</p> +<p style="text-align: justify">Pokud svůj soubor <strong>rozumně pojmenuješ</strong>, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p> +<p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49 MB</strong>.</p> <div id="form_set"> {% 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 }} <span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }} +<span title="Obarvit shodná řešení shodně">🎨?</span> {{ filtr.barvicky }} <input type=submit value="→"> </form> @@ -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 }} </td> - {% for hodn in hodnoty %} + {% for soucet,bunka in hodnoty %} <td> - {% if hodn %} - <a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}"> - {{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} - </a> + {% for reseni,hodnoceni in bunka %} + <a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}"> + {{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b) + </a><br> + {% endfor %} + {% if bunka|length > 1 %} + <b>Σ: {{soucet}} b</b> {% endif %} </td> {% 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/<int:nadproblem_id>/', 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() + 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(problem, resitel, body, cas): + 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 @@ <a href="{% url 'logout' %}">Odhlásit se</a><br> <a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br> -<a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br> +<a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br> <a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br> 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. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>') + 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 '<Není zadaný>' + return f'<Není zadaný: {self.kod}>' # 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 '<Není zadaný>' + return f'<Není zadaný: {self.kod}>' 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 '<Není zadaný>' + return f'<Není zadaný: {self.kod}>' 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 '<Není zadaný>' + return f'<Není zadaný: {self.kod}>' 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 %} + + <h1>{% block nadpis1a %}Šifrovačka odpovědi{% endblock nadpis1a %}</h1> + + <table class="dosla_reseni"> + <tr> + <th>Timestamp</th> + <th>Řešitel</th> + <th>Šifra</th> + <th>Odpověď</th> + </tr> + + {% for u in object_list %} + <tr> + <td>{{ u.timestamp }}</td> + <td>{{ u.resitel }}</td> + <td>{{ u.sifra }}</td> + <td style="color: {% if u.uspech %}green{% else %}red{% endif %};">{{ u.odpoved }}</td> + </tr> + {% endfor %} + </table> + +{% 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 %} + + <br> + + <h1>{% block nadpis1a %}M&Mí šifrovačka{% endblock nadpis1a %}</h1> + + <br> + + <h2>Zadat tajenku šifry:</h2> + + <form action="{% url 'sifrovacka' %}" method="post"> + <table class="form"> + {{form.non_field_errors}} + {% for field in form %} + <tr> + <td> + <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> + {{ field.label }} + </label> + + </td> + + <td {% if field.help_text %} class="field-with-comment"{% endif %}> + {{ field }} + <span class="field-comment">{{ field.help_text|safe }}</span> + </td> + + </tr> + + + {% if field.errors %} + <tr> + <td colspan="2"><span class="field-error">{{ field.errors }}</span></td> + </tr> + {% endif %} + {% endfor %} + </table> + + {% csrf_token %} + + <input type="submit" value="Tak pravím!"> + </form> + +{% 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'<h1>Bohužel vám hvězdy nebyly nakloněny. Rozumějte <i>máte to blbě</i>.</h1> <p><a href="{reverse("sifrovacka")}">Zkusit znovu.</a></p><br><br><br>') + + instance.uspech = True + instance.save() + + return formularOKView(self.request, f'<h1>{sifra.skryty_text}</h1> <p><a href="{reverse("sifrovacka")}">Odevzdat další.</a></p><br><br><br>') + + +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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + version="1.1" + id="svg1" + width="624" + height="550.66669" + viewBox="0 0 624 550.66669" + sodipodi:docname="zere-kostku.svg" + inkscape:version="1.3 (0e150ed6c4, 2023-07-21)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="0.88942308" + inkscape:cx="312" + inkscape:cy="275.45946" + inkscape:window-width="1280" + inkscape:window-height="730" + inkscape:window-x="-6" + inkscape:window-y="-6" + inkscape:window-maximized="1" + inkscape:current-layer="g1"> + <inkscape:page + x="0" + y="0" + inkscape:label="1" + id="page1" + width="624" + height="550.66669" + margin="0" + bleed="0" /> + </sodipodi:namedview> + <g + id="g1" + inkscape:groupmode="layer" + inkscape:label="1"> + <path + id="path1" + d="m 1544.13,3717.76 c -27.5,-17.2 -52.75,-33.58 -55.79,-36.19 -3.09,-2.64 -11.9,-8.87 -19.84,-14.01 -7.89,-5.11 -25.15,-23.79 -38.6,-41.77 -13.45,-17.99 -27.63,-34.42 -31.73,-36.75 -4.23,-2.42 -7.4,-7.18 -7.4,-11.11 0,-3.76 -8.03,-22.21 -17.72,-40.72 -9.68,-18.51 -23.46,-48.22 -30.4,-65.57 -20.1,-50.24 -41.98,-78.55 -63.46,-82.1 -37.01,-6.11 -109.99,-66.72 -122.09,-101.4 -3.14,-8.99 -8.31,-14.24 -12.49,-12.69 -3.95,1.46 -16.13,4.18 -27.23,6.08 -11.06,1.89 -48.65,10.1 -83.56,18.24 -34.898,8.15 -77.019,17.56 -93.597,20.91 -40.504,8.2 -74.258,31.45 -121.684,83.8 -21.559,23.79 -33.57,24.77 -39.031,3.17 -3.172,-12.54 -2.004,-23.79 7.613,-73.5 17.828,-92.17 28.152,-271.81 26.281,-457.42 -1.632,-161.82 -1.324,-167.83 10.746,-210.47 11.832,-41.78 11.985,-44.38 3.176,-53.94 -5.09,-5.52 -14.015,-11.57 -19.832,-13.43 -22.008,-7.03 -128.941,-84.4 -159.054,-115.07 -43.856,-44.66 -51.438,-65.04 -54.895,-147.54 -1.574,-37.54 -4.883,-69.4 -7.355,-70.86 -2.368,-1.39 -4.497,-7.4 -4.497,-12.69 0,-15.77 -24.324,-57.01 -42.836,-72.62 -20.093,-16.95 -61.21,-43.72 -67.156,-43.72 -7.062,0 -47.511,-41.25 -64.301,-65.57 -30.664,-44.42 -56.015,-115.81 -60.843,-171.34 -1.84,-21.15 -7.016,-51.03 -11.453,-66.1 -9.059,-30.76 -55.524,-121.62 -85.18,-166.57 -10.902,-16.52 -35.895,-62.4 -55.313,-101.53 -48.543,-97.83 -63.484,-167.39 -51.796,-241.14 3.183,-20.09 9.109,-41.48 13.187,-47.59 4.055,-6.09 8.723,-31.2 10.32,-55.53 3.477,-52.88 7.207,-72.38 16.586,-86.72 13.184,-20.16 32.856,-79.32 43.961,-132.21 17.883,-85.133 80.301,-227.274 113.688,-258.891 3.199,-3.031 12.836,-15.027 21.414,-26.66 8.582,-11.637 17.031,-22.344 18.773,-23.797 1.801,-1.5 8.383,-9.519 15.196,-18.508 66.527,-87.785 171.285,-185.593 216.953,-202.566 4.593,-1.703 16.922,-8.34 26.968,-14.512 10.098,-6.203 33.051,-13.894 50.504,-16.922 17.614,-3.058 53.145,-14.234 79.055,-24.867 74.449,-30.551 97.301,-35.019 174.508,-34.121 103.114,1.195 150.284,2.922 182.444,6.676 16.2,1.894 45.47,4.355 66.09,5.562 32.65,1.911 120.05,18.707 224.75,43.196 30.97,7.242 147.01,17.55 302.48,26.867 34.9,2.094 71.38,1.027 95.19,-2.777 56.05,-8.957 70.67,-10.997 117.92,-16.446 23.34,-2.695 76.68,-8.824 118.98,-13.676 77.74,-8.914 251.97,-10.113 259.33,-1.785 4.94,5.606 87.04,15.532 129.35,15.641 58.17,0.152 112.49,10.723 169.22,32.934 46.53,18.218 67.16,27.394 171.86,76.472 34.9,16.363 59.12,24.18 79.32,25.61 39.13,2.773 215.23,12.093 293.49,15.539 89.9,3.953 138.83,7.089 212.06,13.578 78.26,6.937 101.58,2.218 127.9,-25.887 20.81,-22.211 38.14,-48.652 38.14,-58.172 0,-3.551 7.23,-17.976 16.43,-32.785 18.57,-29.895 48.62,-47.895 96.74,-57.957 50.76,-10.613 121.95,11.07 138.18,42.094 8.01,15.316 9.62,15.863 46.63,15.863 33.32,0 41.6,2.078 63.2,15.863 14.8,9.453 24.85,19.715 24.85,25.383 0,13.234 10.05,22.211 24.86,22.211 20.09,0 59.41,21.215 72.52,39.133 25.6,34.996 28.51,61.871 10.97,101.531 -7.02,15.863 -11.22,28.555 -9.46,28.555 1.73,0 -1.33,5.289 -6.73,11.633 -13.96,16.394 -23.41,34.121 -23.41,43.894 0,5.875 5.55,7.93 21.41,7.93 11.64,0 26.74,3.019 33.06,6.609 7.48,4.254 33.31,6.613 72.44,6.613 56.06,0 62.88,1.118 87.25,14.278 14.78,7.976 42.31,16.554 62.41,19.441 44.41,6.383 66.75,21.883 87.04,60.406 13.43,25.5 14.43,31.731 13.7,85.668 -0.74,53.409 -2.31,61.809 -19.27,102.589 -19.35,46.54 -34.56,66.75 -59.79,79.51 -8.46,4.27 -20.79,11.89 -27.5,17 -6.61,5.03 -24.59,13.35 -39.4,18.25 -14.81,4.88 -34.71,13.3 -43.63,18.45 -27.74,16.03 -80.38,23.42 -137.49,19.32 -5.81,-0.42 -58.16,-0.28 -116.33,0.3 l -105.77,1.05 44.95,29 c 86.73,55.96 170.35,90.86 232.68,97.11 29.61,2.98 34.37,4.92 34.37,14.03 0,9.21 -5.81,11.38 -44.95,16.82 -82.49,11.46 -167.89,6.77 -245.89,-13.51 -50.53,-13.13 -86.73,-20 -142.78,-27.12 -89.9,-11.4 -158.73,-20.73 -211.53,-28.65 -71.01,-10.65 -117.39,-10.07 -211.52,2.62 -43.89,5.93 -87.59,11.78 -97.83,13.11 -48.65,6.3 -116.69,26.25 -162.87,47.76 -15.58,7.25 -41.78,16.66 -57.64,20.7 -16.05,4.08 -56.59,16.67 -90.43,28.08 -64.63,21.77 -102.59,31.96 -171.33,46 -22.74,4.64 -41.25,10.19 -41.25,12.38 0,2.2 7.66,7.74 17.18,12.43 9.41,4.63 38.61,27.72 64.52,51.03 25.91,23.3 59.29,48.34 73.77,55.34 27.12,13.1 94.65,32.51 143.31,41.2 14.68,2.62 47.59,10.6 74.03,17.94 26.23,7.29 85.93,22.72 132.99,34.37 47.07,11.66 93.24,25.63 102.86,31.11 25.45,14.5 66.1,22.02 144.36,26.7 50.77,3.03 77.15,2.17 103.12,-3.34 19.08,-4.04 52.88,-7.88 74.56,-8.47 43.89,-1.17 70.44,8.25 105.24,37.4 l 18.51,15.5 -5.56,27.49 -5.55,27.5 17.45,7.31 c 24.33,10.2 35.55,28.94 31.09,51.92 -3.28,16.92 -2.23,19.21 12.27,26.78 19.04,9.93 32.79,27.98 32.79,43.02 0,11.67 -13.33,42.83 -42.98,100.48 -20.44,39.74 -28.94,46.35 -73.36,57.07 -14.81,3.57 -26.44,7.89 -26.44,9.82 0,1.96 -3.96,2.53 -9.25,1.32 -5.06,-1.16 -37.81,-4.25 -72.19,-6.82 -62.92,-4.71 -71.43,-7.98 -71.77,-27.55 -0.13,-7.38 10.17,-8.4 80.5,-7.93 76.68,0.5 81.94,-0.14 107.09,-12.99 30.83,-15.77 50.44,-41.48 65.92,-86.43 8.02,-23.27 9.3,-32.95 5.04,-38.08 -14.95,-18 -30.14,-3.17 -58.48,57.12 l -11.43,24.32 -40.19,3.44 c -22.21,1.9 -50.36,4.38 -62.93,5.55 -13.75,1.28 -21.48,0.13 -19.56,-2.91 1.71,-2.7 -3.18,-5.02 -10.58,-5.02 -7.4,0 -12.4,-2.2 -10.84,-4.76 1.63,-2.67 0,-5.92 -3.7,-7.4 -3.55,-1.42 -1.59,-2.89 4.23,-3.18 6.19,-0.3 14.8,-5.29 20.09,-11.63 6.18,-7.41 12.25,-9.85 18.25,-7.33 14.28,5.99 52.19,9.62 65.57,6.27 8.2,-2.05 17.09,-12.16 25.12,-28.55 6.73,-13.75 18.03,-34.35 24.65,-44.95 18.51,-29.62 20.51,-43.95 7.87,-56.58 -11.11,-11.11 -30.69,-15.74 -24.6,-5.82 1.58,2.57 -1.58,7.91 -6.86,11.63 -5.29,3.72 -10.49,9.31 -11.32,12.16 -3.62,12.42 -18.56,29.92 -31.25,36.59 -17.98,9.46 -67,6.84 -79.33,-4.24 -5.1,-4.59 -14.54,-8.55 -20.35,-8.55 -5.82,0 -13.79,-3.68 -17.19,-7.93 -5.08,-6.35 -4.63,-10.58 2.22,-21.15 10.16,-15.68 9.41,-15.62 33.74,-2.58 33.32,17.85 48.76,14.35 72.67,-16.46 11.49,-14.81 23.25,-29.6 26.09,-32.79 11.73,-13.22 23.92,-35.32 23.92,-43.36 0,-9.15 -24.85,-21.39 -65.04,-32.02 -20.75,-5.5 -33.84,-5.22 -79.32,1.68 -66.45,10.09 -60.81,10.03 -146.48,1.61 -67.16,-6.61 -127,-21.25 -163.93,-40.13 -11.76,-6 -147.01,-44.36 -195.66,-55.48 -215.23,-49.21 -231.24,-55.92 -326.28,-136.72 -48.12,-40.92 -53.2,-43.87 -71.12,-41.42 -10.62,1.46 -28.82,0.86 -40.46,-1.32 -14.41,-2.7 -32.79,-1 -57.64,5.34 -65.78,16.78 -177.15,53.62 -249.07,82.39 -39.13,15.65 -73.35,28.34 -76.41,28.34 -3.04,0 -26.18,9.84 -51.03,21.68 -24.85,11.85 -64.18,28.52 -86.73,36.76 -22.73,8.31 -54.77,22.45 -71.39,31.51 -38.96,21.26 -104.7,50.33 -151.76,67.12 -20.33,7.25 -66.1,27.16 -101.53,44.14 -35.44,16.99 -73.58,32.61 -84.62,34.64 -11.1,2.05 -22.45,5.43 -25.38,7.56 -12.32,8.98 -83.02,26.79 -134.84,33.96 -46.9,6.5 -211,20.25 -316.76,26.55 -15.86,0.95 -43.63,-0.43 -61.08,-3.03 -17.69,-2.62 -52.08,-7.17 -76.94,-10.15 -53.41,-6.42 -170,-28.61 -213.11,-40.56 -16.922,-4.7 -31.976,-7.21 -33.582,-5.6 -1.625,1.62 -10.84,-0.39 -20.886,-4.55 -21.153,-8.78 -77.684,-9.77 -93.598,-1.65 -6.465,3.3 -17.981,11.83 -25.914,19.2 -16.113,14.98 -32.801,13.7 -35.16,-2.69 -2.153,-14.93 38.863,-44.82 66.89,-48.73 23.942,-3.35 99.414,6.18 139.075,17.55 44.847,12.87 187.195,39.46 245.375,45.83 65.96,7.23 133.25,7.22 265.98,-0.03 135.38,-7.39 186.95,-17.13 320.99,-60.64 11.6,-3.76 53.41,-21.08 92.55,-38.33 39.13,-17.24 80.94,-34.57 92.54,-38.33 11.6,-3.77 58.17,-24.19 103.12,-45.22 44.94,-21.02 96.05,-43.23 113.16,-49.18 16.92,-5.88 35.31,-14.59 40.19,-19.03 4.76,-4.34 11.6,-8.2 14.54,-8.2 2.98,0 12.43,-3.51 20.36,-7.57 26.55,-13.57 170.81,-66.16 271.81,-99.08 53.94,-17.59 101.39,-33.2 105.76,-34.81 4.44,-1.63 13.75,-3.84 21.15,-5.02 7.28,-1.16 51.3,-11.28 97.83,-22.48 46.54,-11.19 98.18,-23.09 114.75,-26.44 16.55,-3.33 61.88,-16.45 100.48,-29.08 38.6,-12.63 85.12,-27.12 103.12,-32.12 35.95,-10 39.33,-18.38 7.4,-18.38 -19.49,0 -93.07,-23.08 -136.43,-42.79 -12.16,-5.53 -23.41,-10.09 -24.86,-10.09 -1.43,0 -14.28,-6.82 -28.02,-14.87 -28.03,-16.42 -130.81,-59.17 -142.25,-59.17 -8.03,0 -15.65,-22.74 -10.45,-31.2 2.01,-3.27 10.97,-5.81 20.49,-5.81 9.31,0 31.2,-6.18 48.65,-13.73 59.76,-25.87 110.93,-42.99 140.14,-46.9 16.11,-2.15 44.42,-7.19 63.46,-11.29 19.03,-4.1 44.28,-7.4 56.58,-7.4 31.73,0 31.73,-8.26 0,-20.06 -66.1,-24.58 -157.92,-42.72 -215.23,-42.51 -24.85,0.09 -48.02,-0.09 -51.82,-0.41 -9.48,-0.8 -7.93,-32.21 1.59,-32.21 8.05,0 74.56,-27.75 126.91,-52.946 17.45,-8.398 43.03,-16.816 56.85,-18.707 13.75,-1.882 25.12,-5.593 25.12,-8.199 0,-7.207 -77.74,-62.48 -119.52,-84.976 -26.44,-14.235 -47.93,-21.786 -68.21,-23.957 -16.39,-1.758 -30.91,-5.297 -32.53,-7.934 -5.7,-9.32 4.5,-25.211 18.25,-28.406 7.9,-1.836 34.37,-9.231 59.23,-16.543 39.29,-11.555 56.05,-13.321 133.26,-14.012 l 88.31,-0.793 -8.75,-13.219 c -8.58,-12.957 -49.95,-52.914 -82.73,-79.914 -8.7,-7.156 -21.35,-19.504 -29.21,-28.492 -29.12,-33.316 -64.17,-65.875 -79.2,-73.578 -8.43,-4.32 -37.02,-12.609 -63.46,-18.406 -41.24,-9.035 -78.87,-24.5 -73.42,-30.172 1.12,-1.164 19.75,1.332 41.43,5.551 21.68,4.222 40.07,6.957 40.89,6.082 2.06,-2.18 -77.64,-38.286 -109.9,-49.786 -39.5,-14.082 -116.34,-26.914 -195.66,-32.671 -36.49,-2.649 -89.79,-7.504 -118.98,-10.844 -57.12,-6.531 -210.01,-3.039 -251.19,5.734 -13.09,2.789 -42.83,7.332 -66.1,10.098 -23.17,2.75 -69.8,8.594 -103.12,12.918 -75.79,9.84 -111.05,10.418 -224.74,3.676 -116.34,-6.895 -165.02,-13.375 -271.81,-36.184 -48.12,-10.277 -105.7,-20.422 -127.44,-22.453 -21.68,-2.024 -55.25,-6.219 -74.04,-9.254 -18.97,-3.063 -63.98,-5.906 -100.47,-6.344 -36.49,-0.437 -81.026,-2.926 -99.417,-5.554 -60.41,-8.629 -135.375,11.027 -234.262,61.429 -25.382,12.938 -56.925,24.649 -71.39,26.5 -60.446,7.742 -172.785,107.731 -275.52,245.223 -20.547,27.496 -41.843,59.211 -47.054,70.066 -5.329,11.106 -11.145,19.832 -13.219,19.832 -2.02,0 -6.106,6.61 -8.992,14.539 -2.918,8.036 -14.871,39.399 -26.703,70.071 -11.762,30.492 -28.45,84.609 -37.016,120.035 -8.567,35.43 -24.539,83.11 -35.43,105.77 -16.273,33.84 -20.023,47.89 -20.89,78.26 -0.579,20.24 -6.477,54.99 -13.012,76.68 -19.141,63.49 -10.801,152.29 21.351,227.38 12.524,29.25 80.118,151.77 107.067,194.08 30.316,47.59 67.191,143.76 67.324,175.56 0.125,30.47 19.008,95.19 39.656,135.91 23.063,45.47 54.629,80.68 86.969,97 37.543,18.95 73.91,47.59 86.988,68.52 6.543,10.46 18.203,38.07 25.91,61.34 11.493,34.69 14.016,51.82 14.016,95.18 0,65.57 10.723,91.18 56.582,135.1 35.59,34.08 108.934,84.16 153.356,104.71 13.218,6.12 28.027,2.9 28.027,-6.07 0,-9.3 49.707,-52.69 75.09,-65.55 45.476,-23.04 88.129,-39.58 93.599,-36.3 3.29,1.97 5.82,8.78 5.82,15.65 0,10.54 -6.35,14.43 -47.595,29.19 -79.336,28.39 -119.863,75.52 -143.633,167 -10.75,41.37 -12.062,56.58 -10.031,116.34 3.793,111.57 3.324,276.84 -0.926,325.21 -5.734,65.33 4.305,33.32 14.098,-44.94 16.945,-135.38 24.254,-175.57 38,-208.88 7.851,-19.04 17.012,-37.91 20.527,-42.31 3.383,-4.23 7.653,-11.63 9.149,-15.86 3.98,-11.25 79.004,-103.65 109.921,-135.38 26.8,-27.5 93.68,-65.57 115.19,-65.57 8.67,0 11.63,3.44 11.63,13.48 0,11.68 -5.81,16.15 -43.35,33.32 -48.05,21.98 -66.67,38.87 -130.325,118.19 -36.922,46.01 -68.352,103.15 -79.402,144.36 -4.762,17.75 -16.356,89.9 -35.266,219.46 -3.594,24.63 -22.504,109.46 -37.484,168.16 -7.692,30.14 -6.637,47.7 2.125,35.43 2.062,-2.88 17,-15.38 32.863,-27.5 28.801,-22 62.926,-33.24 140.129,-46.17 11.7,-1.95 41.25,-8.26 66.11,-14.11 24.85,-5.85 50.26,-11.64 56.84,-12.96 7.41,-1.48 11.9,-6.37 11.9,-12.95 0,-12.69 14.21,-19.64 22.74,-11.11 3.06,3.06 10.58,5.82 15.86,5.82 12.43,0 59.76,-30.33 87.79,-56.25 56.58,-52.34 67.91,-65.03 78.69,-88.12 13.07,-28.02 19.66,-30.72 28.41,-11.63 5.31,11.58 3.71,16.66 -12.97,40.98 -10.32,15.05 -31.03,40.72 -45.22,56.06 l -26.17,28.29 10.84,18.51 c 22.23,37.97 78,57.93 176.36,63.15 35.95,1.91 65.57,2.28 65.57,0.83 0,-1.47 -8,-11.1 -17.99,-21.68 -24.22,-25.63 -39.81,-59.76 -46.39,-101.53 -7.57,-48.12 -5.81,-51.05 28.42,-47.38 35.96,3.86 105.76,-16.43 139.6,-40.58 37.55,-26.79 37.55,-26.79 -63.98,-51.43 -41.25,-10.01 -42.29,-10.64 -41.52,-25.34 0.44,-8.24 4.63,-30.4 9.26,-48.91 4.63,-18.51 7.26,-35.04 5.82,-36.49 -4.5,-4.49 -35.96,10.48 -62.93,29.95 -16.92,12.21 -29.4,26.59 -35.96,41.44 -7.94,17.98 -12.71,22.74 -22.74,22.74 -10.41,0 -13.68,-3.7 -18.23,-20.62 -5.97,-22.21 -25.89,-54.19 -35.71,-57.32 -7.62,-2.43 -20.58,24.53 -27.81,57.85 -4.36,20.09 -3.85,29.45 2.43,44.95 5.14,12.69 6.07,21.42 2.64,24.85 -8.82,8.83 -20.05,-1.06 -29.43,-25.91 -8.69,-23.05 -8.68,-25.91 0.09,-54.47 14.45,-47.06 35.62,-98.29 44.15,-106.82 10.45,-10.45 24.01,-1.06 27.49,19.04 1.44,8.32 7.18,23.79 12.56,33.84 16.97,31.73 27.27,34.27 42.8,10.58 6.78,-10.34 23.44,-22.35 45.12,-32.52 19.04,-8.94 39.1,-18.38 44.95,-21.16 5.7,-2.7 20.09,-13.45 31.2,-23.28 23.27,-20.6 32.44,-22.35 40.22,-7.65 6.67,12.6 2.62,23.21 -15.89,41.54 -15.53,15.39 -28.78,61.58 -25.54,89.07 1.44,12.22 5.44,15.91 22.89,21.08 52.88,15.68 75.83,18.49 94.66,11.61 14.8,-5.42 20.2,-5.36 26.97,0.29 32.17,26.89 -41.25,93.37 -138.02,124.98 -30.67,10.02 -60.02,18.14 -65.57,18.14 -12.98,0 -13.33,11.1 -1.51,47.59 7.01,21.62 15.52,33.32 39.84,54.73 24.33,21.42 30.6,30.06 29.36,40.45 -1.32,10.91 -5.29,13.87 -22.75,16.91 -65.57,11.43 -165.9,-2.74 -224.74,-31.73 -21.68,-10.69 -41.46,-20.39 -43.63,-21.4 -2.19,-1.02 -3.96,13.49 -3.96,32.53 0,36.32 3.84,45.47 39.93,95.18 8.83,12.16 19.95,30.58 24.58,40.72 4.63,10.12 21,46.53 36.14,80.38 29.1,65.04 64.51,114.72 103.47,145.14 25.91,20.24 74.75,47.43 78.37,43.64 1.4,-1.46 2.89,-39.92 3.33,-85.93 0.69,-71.92 2.57,-89.06 13.8,-125.59 19.5,-63.46 31.02,-86.91 56.12,-114.22 32.48,-35.36 52.89,-67.16 68.88,-107.35 16.63,-41.81 17.46,-41.22 -60.8,-43.74 -43.36,-1.4 -55.04,-3.5 -59.56,-10.73 -9.98,-15.98 3.51,-22.61 29.94,-14.72 14.94,4.46 52.89,6.65 110.53,6.4 152.3,-0.68 315.91,-35.32 374.92,-79.39 10.05,-7.5 19.24,-15.51 20.1,-17.52 0.87,-2.03 18.24,-17.13 38.34,-33.32 20.13,-16.22 52.29,-48.38 71.12,-71.12 18.83,-22.74 38.44,-43.16 43.37,-45.16 36.4,-14.75 155.46,-13.66 232.67,2.13 29.62,6.06 67.77,12.23 84.61,13.68 16.92,1.46 41.34,3.85 54.47,5.33 35.96,4.07 37.82,4.16 65.04,3.36 l 25.91,-0.76 -3.58,-21.15 c -4.39,-25.91 6.97,-52.23 30.55,-70.78 9.23,-7.25 27.5,-23.04 40.19,-34.72 12.69,-11.68 26.64,-21.42 30.68,-21.42 10.32,0 9,-7.4 -11.6,-65.57 -11.06,-31.2 -24.44,-58.55 -33.13,-67.68 -16.88,-17.76 -49.94,-28.4 -93.29,-30.02 -17.46,-0.65 -34.23,-3.09 -37.29,-5.42 -3.05,-2.32 -24.59,-5.54 -47.86,-7.14 -24.32,-1.67 -59.6,-8.75 -83.02,-16.65 -22.21,-7.5 -51.53,-13.75 -64.51,-13.75 -13.22,0 -30.03,-2.35 -37.81,-5.29 -7.67,-2.89 -29.35,-7.45 -47.86,-10.05 -20.1,-2.83 -39.54,-9.27 -47.86,-15.86 -7.93,-6.29 -17.45,-11.11 -21.94,-11.11 -7.5,0 -49.53,-30.14 -53.1,-38.07 -0.98,-2.2 -5.34,-4.23 -9.04,-4.23 -3.95,0 -10.21,-5.56 -14.01,-12.43 -3.81,-6.89 -12.3,-22.21 -19.04,-34.37 -6.74,-12.17 -18.76,-27.96 -26.97,-35.43 -8.1,-7.39 -17.81,-23.53 -21.46,-35.7 -11.71,-39.13 -36.19,-81.68 -54.16,-94.16 -9.52,-6.62 -18.65,-13.57 -20.09,-15.3 -1.48,-1.77 -19.04,-15.18 -39.66,-30.29 -44.42,-32.54 -52.86,-45.58 -47.8,-73.89 3.56,-19.96 37.75,-78.26 68.85,-117.39 8.91,-11.22 21.87,-30.14 29.32,-42.83 25.89,-44.07 56.45,-76.68 106.15,-113.31 28.03,-20.65 63.44,-47.59 79.06,-60.14 15.34,-12.33 37.13,-27.35 47.6,-32.79 18.93,-9.86 19.27,-10.58 17.71,-38.08 -0.87,-15.33 -0.44,-32.09 0.94,-37.01 4.38,-15.56 31.32,-20.8 72.04,-14.02 26.43,4.41 38.09,4.48 42.3,0.27 8.18,-8.18 -38.6,-36.75 -67.16,-41.01 -46.01,-6.86 -115.53,-4.31 -148.06,5.43 -39.67,11.87 -109.27,39.13 -115.81,45.36 -2.98,2.82 -9,5.03 -13.75,5.03 -23.8,0 -137.5,73.02 -137.5,88.31 0,3.82 -4.23,6.87 -9.51,6.87 -10.29,0 -26.04,15.34 -51.59,50.24 -22.34,30.51 -42.01,46.68 -93.84,77.14 -25.38,14.91 -47.19,28.93 -48.65,31.26 -1.46,2.35 -22.21,5.86 -46.53,7.89 -42.31,3.53 -93.51,-2.53 -114.22,-13.51 -10.05,-5.32 -69.77,-31.39 -71.92,-31.39 -7.57,0 -5.62,26.44 5.57,75.62 11.18,49.18 16.11,61.73 29.07,74.03 9.51,9.04 15.59,20.06 15.59,28.29 0,13.38 -0.26,13.46 -33.05,10.05 -20.61,-2.14 -54.73,-0.35 -90.69,4.77 -59.23,8.44 -144.47,6.93 -162.87,-2.89 -5.04,-2.68 -14.81,-4.79 -22.21,-4.79 -15.94,0 -18.06,12.17 -6.72,38.6 4.02,9.41 10.17,27.5 13.83,40.72 10.09,36.49 52.06,112.26 70.1,126.52 8.56,6.78 23.53,14.14 32.52,16 9.51,1.96 17.41,7.29 18.42,12.42 2.86,14.54 -36.67,20.91 -133.97,21.59 -71.39,0.49 -95.94,-1.28 -119.24,-8.63 -16.05,-5.06 -46.01,-12.73 -66.11,-16.92 -20.09,-4.19 -41.06,-9.23 -46.26,-11.11 -8.79,-3.17 -8.63,-1.06 2.11,27.5 6.36,16.92 15.58,35.73 20.36,41.51 4.88,5.93 12.37,16.39 17.01,23.8 8.9,14.18 20.01,28.02 52.63,65.57 26.27,30.24 45.55,74.04 50.05,113.7 3.73,32.78 3.59,33.28 -8.91,31.46 -11.21,-1.64 -13.1,-5.55 -16.21,-33.58 -4.47,-40.19 -14.62,-64.18 -39.81,-94.13 -70.31,-83.55 -77.4,-93.51 -100.4,-141.19 -10.46,-21.68 -20.82,-40.89 -22.84,-42.37 -13.948,-10.22 -7.94,-42.24 7.93,-42.24 4.04,0 15.33,6.85 24.85,15.07 11.64,10.05 26.18,16.39 43.63,19.04 14.51,2.2 45.74,9.62 70.07,16.66 36.23,10.47 54.73,12.6 106.03,12.16 34.37,-0.29 68.75,-2.07 76.94,-3.97 14.27,-3.31 14.46,-3.81 5.11,-14.01 -24.56,-26.79 -46.56,-68.22 -66,-124.27 -11.55,-33.32 -26.82,-72.53 -33.77,-86.73 -10.1,-20.62 -11.43,-27.42 -6.5,-33.31 7.78,-9.29 34.52,-6.66 67.84,6.66 26.56,10.63 114.22,13.07 195.66,5.47 55.52,-5.19 61.09,-6.76 54.26,-15.31 -2.79,-3.47 -12.97,-38.6 -22.53,-77.73 -9.57,-39.13 -21.03,-79.03 -25.38,-88.31 -6.45,-13.75 -6.7,-18.41 -1.36,-24.85 7.02,-8.46 32.03,-11.77 32.03,-4.23 0,7.4 71.91,43.36 86.72,43.36 7.72,0 23.27,-4.74 33.85,-10.31 10.76,-5.68 43.36,-21.26 72.44,-34.64 29.09,-13.38 70.85,-34.91 92.81,-47.86 23.48,-13.84 56.58,-27.67 80.38,-33.58 30.67,-7.61 46.59,-15.16 65.84,-31.2 64.79,-53.99 159.69,-103.12 262.28,-135.77 l 61.88,-19.7 58.69,6.02 c 80.38,8.25 147.54,37.76 147.54,64.84 0,9.16 3.18,9.77 35.96,6.88 48.65,-4.29 154.36,-4.92 202,-1.22 126.39,9.84 179.57,15 200.95,19.49 13.13,2.76 75.36,7.84 138.82,11.34 63.45,3.5 115.01,8.12 115.01,10.31 0,2.18 -6.08,7.9 -13.48,12.69 -12.11,7.84 -22.74,8.19 -104.18,3.44 -49.71,-2.9 -92.6,-4.09 -94.92,-2.64 -2.35,1.47 -17.72,-1.34 -34.64,-6.35 -16.86,-4.98 -56.31,-10.87 -88.57,-13.22 -32.11,-2.33 -87.79,-6.84 -124.27,-10.05 -36.49,-3.21 -82.69,-4.43 -103.12,-2.73 -33.73,2.81 -74.03,5.52 -156,10.49 -16.19,0.98 -42.57,2 -59.49,2.29 l -30.41,0.53 v 21.15 c 0,11.64 2.62,21.15 5.82,21.15 13.12,0 1.06,24.2 -16.39,32.88 -26.97,13.41 -32.82,23.49 -28.8,49.62 2.11,13.75 1.2,23.52 -2.4,25.65 -3.82,2.25 -5.91,42.04 -6.09,115.54 -0.14,61.87 -1.04,120.4 -2.01,130.62 -1.82,19.28 -7.96,153.35 -10.18,222.1 -0.71,22.21 -3.18,43.15 -5.52,46.8 -2.25,3.52 -1.15,10.31 2.38,14.54 5.08,6.1 5.19,11.11 0.43,21.68 -8.42,18.75 -4.92,20.61 19.93,10.62 19.04,-7.65 21.95,-7.71 29.08,-0.57 7.54,7.53 11.04,33.31 8.62,63.45 -2.09,26.02 26.29,36.81 111.42,42.36 l 56.06,3.65 16.92,-24.32 16.92,-24.33 -12.16,-2.77 c -14.14,-3.22 -78.79,-12 -123.74,-16.81 -32.79,-3.5 -44.42,-9.17 -44.42,-21.67 0,-12.7 13.22,-13.11 58.7,-1.8 63.98,15.9 141.36,15.59 172.92,-0.69 28.02,-14.46 41.44,-9.52 39.76,14.66 -6.63,95.18 -3.34,107.87 27.93,107.87 33.84,0 74,11.86 99.94,29.51 28.48,19.39 37.13,36.07 65.87,127.02 9.86,31.2 15.71,40.59 34.6,55.53 12.69,10.03 24.09,22.65 25.78,28.55 8.67,30.14 -0.57,112.64 -16.16,144.37 -9.44,19.21 -10.15,19.54 -38.7,18.51 -16.11,-0.59 -44.42,-6.01 -63.46,-12.17 -24.92,-8.05 -57.64,-12.41 -118.98,-15.86 -46.54,-2.62 -98.89,-8.57 -116.34,-13.22 -17.68,-4.71 -48.12,-10.69 -68.75,-13.49 -20.62,-2.79 -43.34,-6.25 -51.29,-7.8 -23.74,-4.62 -106.29,-4.67 -130.62,-0.07 -24.78,4.68 -36.59,14.49 -79.89,66.31 -34.91,41.77 -52.18,58.17 -61.3,58.17 -4.65,0 -17.14,9.51 -27.74,21.15 -23.63,25.91 -104.86,68.74 -130.37,68.74 -10.05,0 -19.24,2.1 -20.84,4.76 -2.69,4.49 -46.32,13.35 -92.86,18.86 -13.09,1.55 -38.07,5.2 -55.52,8.11 -17.45,2.91 -42.44,5.29 -55.53,5.29 -35.95,0 -46.27,4.21 -49.27,20.1 -6.89,36.48 -25.02,68.21 -80.64,141.19 -59.98,78.7 -76.19,132.2 -72.11,237.96 1.91,49.18 5.3,74.99 11.92,90.69 6.91,16.4 7.78,23.42 3.44,27.77 -9.4,9.39 -13.75,7.73 -67.16,-25.69 z m -284.24,-429.88 c -1.59,-35.43 -3.15,-65.14 -3.44,-65.58 -0.29,-0.43 -15.6,8.35 -34.11,19.57 -18.5,11.22 -36.19,21.19 -39.39,22.21 -4,1.27 -3.63,7.14 1.2,18.77 7.67,18.51 60.42,69.81 71.77,69.81 5.5,0 6.29,-12.96 3.97,-64.78 z m 1590.4,-309.62 c 0,-5.44 -38.61,-27.5 -48.13,-27.5 -9.6,0 -1.05,7.28 24.33,20.72 23.8,12.59 23.8,12.59 23.8,6.78 z m -214.17,-385.51 c -0.59,-22.73 -3.39,-41.5 -6.35,-42.49 -5.38,-1.81 -54.47,58.89 -54.47,67.35 0,4.5 32.79,14.37 50.77,15.29 10.38,0.54 11.04,-2.07 10.05,-40.15 z m -283.99,-50.76 c 1.07,-9.52 -0.86,-14.28 -5.8,-14.28 -11.1,0 -20.36,13.09 -16.45,23.27 5.21,13.55 20.42,7.4 22.25,-8.99 z m -64.82,-30.67 c 4.18,-18.51 1.42,-23.29 -10.78,-18.73 -11.64,4.34 -15.58,17.94 -7.89,27.19 9.74,11.72 14.61,9.52 18.67,-8.46 z m -13.43,-48.77 c 9.52,-4.87 12.71,-10.16 11.37,-18.92 -1.02,-6.7 2.07,-20.36 6.88,-30.41 6.23,-13.03 8.42,-29.87 7.66,-58.96 -0.58,-22.39 0.73,-64.51 2.91,-93.6 2.19,-29.13 4.56,-112.37 5.29,-185.34 0.73,-72.98 2.94,-135.09 4.92,-138.29 6.12,-9.9 -15.23,-18.04 -26.34,-10.03 -9.16,6.6 -9.07,7.91 2.12,34.89 8.78,21.15 10.21,29.71 5.85,34.9 -8.52,10.1 -15.37,9.21 -29.12,-3.8 -7.93,-7.51 -15.08,-9.92 -22.47,-7.57 -10.56,3.35 -10.5,3.96 2.68,24.06 15.94,24.32 14.69,44.23 -2.95,46.85 -6.87,1.01 -16.64,-2.34 -22.47,-7.72 -9.22,-8.5 -12.96,-8.82 -35.17,-2.96 -39.13,10.33 -51.17,21.94 -50.79,48.97 0.36,25.38 10.57,51.58 29.07,74.56 17.63,21.91 7.97,28.61 -30.1,20.88 -41.25,-8.36 -58.71,-7.69 -76.68,2.96 -29.88,17.72 -23.79,45.8 14.81,68.29 33.82,19.7 40.72,25.26 79.85,64.4 27.5,27.49 37.76,34.38 54.73,36.75 12.69,1.77 23.94,7.27 28.66,14.01 11.11,15.87 9.62,38.76 -3.01,46.57 -12.2,7.55 -13.14,14.25 -4.6,32.75 7.02,15.21 25.22,17.84 46.9,6.76 z m -68.74,-67.04 c 0,-3.02 -2.38,-5.29 -5.55,-5.29 -3.18,0 -4.2,2.27 -2.38,5.29 1.9,3.17 4.12,5.29 5.55,5.29 1.43,0 2.38,-2.12 2.38,-5.29 z m -79.32,-234.79 c 0,-7.41 -21.68,-50.77 -25.39,-50.77 -4.32,0 -30.79,30.67 -40.14,46.54 -2.86,4.84 4.45,6.34 30.89,6.34 19.03,0 34.64,-0.95 34.64,-2.11 z m 78.26,-154.68 -14.28,-14.01 -14.8,10.04 -14.81,10.05 23.79,3.17 c 13.23,1.77 26.15,3.53 29.09,3.97 2.94,0.44 -1.06,-5.44 -8.99,-13.22 z m -443.14,-12.68 c 46.53,-15.26 98.41,-45.37 127.97,-74.28 33.84,-33.1 38.6,-43.94 16.92,-38.56 -15.17,3.77 -92.54,43.03 -124.27,63.06 -11.1,7.01 -23.31,13.02 -26.44,13.02 -12.16,0 -88.62,39.15 -85.71,43.89 3.79,6.17 65.62,1.36 91.53,-7.13 z m 495.76,-38.09 c -1.29,-3.56 -5.56,-6.61 -9.26,-6.61 -3.7,0 -7.97,3.05 -9.25,6.61 -1.41,3.91 2.38,6.62 9.25,6.62 6.88,0 10.66,-2.71 9.26,-6.62 z m 1778.12,-577.19 c 0,-1.17 -8.99,-5.01 -20.09,-8.58 -38.61,-12.43 -125.62,-54.24 -139.08,-66.84 -7.4,-6.92 -15.43,-12.48 -18.24,-12.63 -2.8,-0.15 -14.28,-7.17 -25.92,-15.86 -11.63,-8.69 -22.53,-15.6 -24.59,-15.6 -8.53,0 -126.91,-73.48 -231.61,-143.77 -69.04,-46.35 -112.11,-73.1 -185.62,-115.316 -21.81,-12.523 -77.73,-50.496 -124.27,-84.379 -46.53,-33.882 -98.88,-71.242 -116.34,-83.023 -17.49,-11.805 -50.23,-34.23 -72.97,-49.973 -22.74,-15.742 -58.57,-39.171 -79.85,-52.214 -33.53,-20.547 -57.64,-38.36 -106.29,-78.5 -14.28,-11.782 -46.07,-35.184 -55,-40.477 -19.2,-11.391 7.93,19.832 50.76,58.414 25.39,22.863 53.75,49.969 62.41,59.633 8.74,9.762 27.4,29.348 41.57,43.625 27.28,27.5 30.6,43.23 9.72,46.035 -6.34,0.852 -15.24,-2.047 -19.57,-6.371 -4.37,-4.379 -21.68,-9.867 -38.6,-12.246 -41.78,-5.875 -138.08,-4.16 -160.23,2.851 -9.69,3.071 -28.82,8.493 -41.51,11.774 l -23.53,6.082 36.75,17.976 c 20.32,9.942 56.85,32.164 81.7,49.711 24.86,17.543 57.36,40.121 72.71,50.5 15.34,10.371 28.14,21.141 28.61,24.063 2.76,17.359 1.27,17.988 -43.15,18.246 -37.02,0.215 -50.08,2.687 -74.03,14.008 -16,7.561 -43.36,20.291 -60.82,28.291 l -31.72,14.54 68.74,12.02 c 138.02,24.13 245.9,65.48 245.9,94.27 0,4 -21.16,6.09 -67.69,6.68 -70.33,0.88 -180.75,18.87 -218.4,35.58 -9.83,4.36 -35.96,16.25 -57.64,26.22 l -39.66,18.25 74.03,35.44 c 148.07,70.88 185.33,82.82 245.9,78.76 21.68,-1.45 40.78,-3.68 42.84,-5 2.09,-1.34 8.98,-0.43 15.86,2.11 8.11,3 17.98,2.12 29.61,-2.65 16.79,-6.88 77.21,-19.22 165.52,-33.82 61.87,-10.23 254.93,-9.45 290.85,1.17 16.1,4.76 44.41,10.18 63.45,12.13 61.87,6.37 252.8,39.01 264.41,45.21 16.92,9.03 185.08,21.44 185.08,13.66 z m 58.17,-169.05 c 25.05,-4.45 58.17,-15.91 89.9,-31.1 27.5,-13.18 56.7,-25.71 64.51,-27.69 29.76,-7.56 66.82,-60.36 81.7,-116.42 23.16,-87.25 -6.39,-162.957 -67.95,-174.058 -23.25,-4.196 -26.48,-0.446 -31.43,36.57 -18.02,134.848 -22.85,149.528 -52.12,158.678 -10.05,3.14 -25.04,9.01 -32.79,12.84 -16.92,8.36 -30.67,4.25 -30.67,-9.17 0,-12.97 7.41,-18.55 35.43,-26.71 29.98,-8.72 36.74,-23 43.93,-92.802 3.16,-30.672 7.07,-59.406 8.64,-63.457 4.03,-10.449 -12.38,-33.813 -30.36,-43.223 -17.26,-9.039 -46.53,-9.976 -74.03,-2.379 l -19.57,5.411 0.53,59.492 c 0.44,49.707 -1.3,63.492 -10.57,83.816 -9.42,20.621 -14.81,25.772 -35.44,33.842 -13.53,5.3 -37.81,9.76 -54.73,10.05 -27.49,0.48 -30.56,-0.61 -31.99,-11.37 -1.43,-10.74 0.53,-11.901 20.09,-11.901 65.9,0 94.61,-44.945 83.43,-130.613 -5.32,-40.719 -10.57,-48.492 -36.36,-53.805 -26.44,-5.453 -41.6,-1.551 -46.75,12.028 -5.62,14.808 -60.88,71.918 -69.59,71.918 -3.46,0 -14.28,6.007 -23.8,13.218 -9.52,7.215 -20.09,13.223 -23.26,13.223 -3.15,0 -14.81,4.605 -25.38,10.02 -38.02,19.468 -206.24,44.093 -294.02,43.039 -32.26,-0.391 -75.97,-2.493 -97.04,-4.672 -21.16,-2.188 -38.34,-2.309 -38.34,-0.266 0,2.027 17.45,13.957 38.6,26.387 59.76,35.114 160.37,99.124 208.88,132.904 24.33,16.93 45.07,30.55 46.54,30.55 1.47,0 18.24,10.56 37.81,23.8 31.2,21.11 37.46,23.41 55.52,20.36 11.64,-1.97 25.35,-0.83 31.99,2.64 6.35,3.32 24.86,7.04 40.72,8.19 50.77,3.67 196.99,-2.07 237.97,-9.34 z M 3631.87,946.5 c 98.36,-10.066 144.13,-20.555 195.13,-44.723 54.99,-26.062 94.32,-59.254 111.07,-93.754 56.5,-116.339 70.31,-157.613 62.77,-187.73 -4.66,-18.66 -43.75,-44.996 -63.85,-43.016 -17.85,1.758 -17.94,2.297 -12.76,73.688 1.72,23.797 0.17,32.367 -7.39,40.719 -12.45,13.75 -67.93,42.625 -76.09,39.601 -14.14,-5.238 -6.35,-22.113 16.39,-35.48 38.71,-22.754 42.86,-33.735 35.12,-92.961 -3.59,-27.5 -8.96,-56.699 -11.85,-64.516 -6.6,-17.808 -29.09,-25.988 -59.76,-21.734 -20.09,2.785 -23.7,5.211 -26.44,17.769 -1.76,8.078 -1.58,23.004 0.4,33.578 5.55,29.614 -1.96,72.899 -15.59,89.899 -10.14,12.64 -19.18,16.59 -51.44,22.473 -21.68,3.957 -46.79,8.671 -56.32,10.578 -15.86,3.172 -17.18,2.359 -17.18,-10.578 0,-7.93 0.92,-14.012 2.11,-14.012 10.58,0 82.39,-17.192 91.49,-21.903 12.33,-6.386 24.94,-38.91 20.41,-52.66 -1.32,-4.015 -3.52,-21.679 -4.81,-38.601 -1.3,-16.922 -5.05,-32.774 -8.2,-34.637 -3.06,-1.812 -5.4,-8.199 -4.94,-13.488 1.93,-22.207 -1.6,-28.016 -22.03,-36.285 -28.34,-11.473 -59.75,-12.024 -95.18,-1.676 -35.36,10.328 -45.58,20.512 -71.54,71.277 -23.25,45.477 -66.38,88.047 -97.68,96.41 -11.45,3.059 -31.47,4.438 -43.63,3.008 -12.31,-1.449 -49.97,-4.437 -83.29,-6.613 -33.31,-2.172 -62.01,-4.258 -63.46,-4.613 -25.91,-6.348 -396.6,-21.559 -396.6,-16.274 0,5.074 123.21,98.82 142.77,108.633 13.3,6.668 36.76,21.973 53.15,34.676 16.39,12.699 40.09,29.441 53.67,37.918 30.75,19.187 81.97,54.82 154.42,107.425 49.18,35.711 59.89,41.391 72.97,38.684 8.47,-1.754 20.56,-0.566 26.97,2.641 6.32,3.16 20.1,6.715 30.14,7.773 40.89,4.324 172.93,3.43 221.05,-1.496" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path2" + d="m 2009.48,2892.98 c -7.41,-2.2 -19.73,-7.39 -26.71,-11.23 -7.11,-3.92 -47.32,-16.87 -89.1,-28.7 -79.32,-22.45 -93.35,-30.89 -81.93,-49.3 10.53,-16.96 30.1,-14.45 68.18,8.75 39.06,23.81 108.94,31.18 158.64,16.74 92.01,-26.73 123.39,-29.7 147.54,-13.98 12.34,8.04 14.26,12.29 11.55,25.51 -4.04,19.7 -14.19,27.76 -45.39,36.01 -13.22,3.49 -34.14,9.54 -47.07,13.6 -24.85,7.82 -73.71,9.14 -95.71,2.6" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path3" + d="m 2915.33,2820.67 c -1.44,-4.04 -1.2,-11.37 0.53,-16.13 2.76,-7.59 9.52,-8.26 52.35,-5.16 51.4,3.73 228.45,-4.1 288.2,-12.74 92.02,-13.3 112.11,-18 112.11,-26.25 0,-10.84 -55,-58.12 -86.73,-74.56 -66.09,-34.26 -120.91,-67.82 -126.92,-77.74 -5.25,-8.66 6.36,-18.48 16.4,-13.88 5.29,2.43 11.87,4.48 14.81,4.63 2.81,0.14 11.37,4.61 18.24,9.52 6.88,4.91 14.7,9.25 16.66,9.25 7,0 103.12,60.55 131.68,82.95 l 29.08,22.81 9.25,-11.1 c 7.33,-8.79 9.38,-21.68 9.84,-61.88 2.07,-179.79 3.99,-237.12 9.55,-283.96 3.44,-29.09 7.33,-66.45 8.64,-83.03 1.3,-16.39 4.79,-40.51 7.67,-52.88 2.9,-12.48 5.29,-34.9 5.29,-49.71 0,-20.09 2.69,-29.39 10.57,-36.49 5.82,-5.23 10.87,-7.85 11.22,-5.81 1.16,6.62 2.66,11.1 6.95,20.62 2.24,5 1.46,19.57 -1.77,32.79 -3.23,13.2 -7.11,42.04 -8.73,64.78 -1.61,22.72 -6.34,66.89 -10.61,99.15 -9.09,68.74 -14.83,348.36 -7.43,362.23 3.39,6.35 2.62,11.56 -2.32,15.64 -8.99,7.43 -14.33,26.67 -7.4,26.67 2.92,0 3.98,3.43 2.38,7.66 -1.61,4.24 0.41,10.43 4.49,13.82 19.3,16.01 -5.81,43.93 -28.55,31.74 -5.94,-3.18 -23.27,-3.31 -46.01,-0.34 -20.14,2.63 -90.95,6.06 -158.11,7.67 -67.16,1.6 -156.8,3.85 -200.16,5.02 -62.93,1.7 -79.05,0.65 -81.17,-5.29 z m 435.74,-37.01 c -1.71,-1.71 -6.35,-1.96 -10.05,-0.53 -4.34,1.67 -3.17,2.83 3.18,3.17 5.28,0.28 8.54,-0.98 6.87,-2.64 z m 31.73,-5.29 c -1.71,-1.71 -6.35,-1.96 -10.05,-0.53 -4.34,1.67 -3.17,2.84 3.17,3.17 5.29,0.28 8.55,-0.97 6.88,-2.64" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path4" + d="m 2940.18,2588.29 c -50.76,-3.64 -118.2,-8.43 -149.39,-10.61 -49.7,-3.47 -57.26,-5.29 -60.16,-14.54 -5.18,-16.48 5.43,-22.29 28.17,-15.43 29.36,8.87 122.16,16.74 242.2,20.54 71.39,2.27 108.94,5.5 115.54,9.96 9.32,6.3 9.26,6.89 -1.32,12.48 -12.96,6.85 -53.94,6.29 -175.04,-2.4" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path5" + d="m 3133.2,2534.88 c -5.57,-5.42 -7.69,-12.99 -5.58,-19.86 10.27,-33.45 13.04,-51.82 13.27,-87.78 0.13,-21.68 2.4,-49.25 5,-60.81 2.63,-11.7 6.41,-61.35 8.46,-111.05 2.05,-49.71 4.41,-91.09 5.29,-92.55 0.88,-1.45 3.13,-51.29 5.02,-111.05 1.9,-59.75 5.93,-128.1 8.99,-152.3 3.08,-24.32 4.38,-48.36 2.91,-53.93 -1.39,-5.29 0.62,-14.43 4.23,-19.3 6.09,-8.19 8.46,-8.43 20.62,-2.12 10.58,5.49 16.8,5.8 26.98,1.32 11.63,-5.12 14.35,-4.23 22.68,7.41 25.38,35.43 57.25,75.63 75.45,95.18 67.43,72.45 133.93,166.84 128.89,182.97 -4.78,15.28 -18.55,8.99 -33.59,-15.33 -25.81,-41.78 -55.42,-83.67 -72.14,-102.07 -33.66,-37.01 -73.79,-83.5 -88.57,-102.59 -8.19,-10.57 -18.04,-19.03 -22.15,-19.03 -4.04,0 -11.13,-5.29 -15.6,-11.63 -4.47,-6.35 -9.64,-10.2 -11.37,-8.47 -9.11,9.11 -18.56,92.02 -21.28,186.67 -1.67,58.17 -4.83,120.04 -7.01,137.49 -2.18,17.45 -4.44,55.53 -5.02,84.61 -0.59,29.09 -3.32,76.68 -6.09,105.77 -2.77,29.24 -5.84,78.52 -6.87,110.25 -1.33,41.25 -3.93,58.71 -9.25,62.11 -10.05,6.42 -13.22,5.89 -23.27,-3.91" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path6" + d="m 957.145,2232.46 c -41.543,-11.71 -94.657,-31.01 -147.008,-53.41 -21.68,-9.28 -48.918,-18.4 -61.078,-20.45 -17.45,-2.94 -22.317,-6.19 -23.754,-15.86 -3.18,-21.4 6.039,-22.22 44.644,-3.99 20.621,9.74 49.461,21.31 63.985,25.67 14.804,4.44 36.211,12.94 48.648,19.31 44.727,22.93 97.305,31.9 185.618,31.66 52.35,-0.13 94.38,-2.98 116.34,-7.87 19.03,-4.25 40.74,-9.14 48.64,-10.97 17.46,-4.02 19.5,4.53 4.23,17.66 -21.2,18.23 -53.41,23.76 -150.71,25.88 -80.38,1.74 -100.528,0.56 -129.555,-7.63" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + <path + id="path7" + d="m 1341.22,2138.51 c -5.99,-15.1 -3.86,-16.7 26.81,-20.18 36.49,-4.15 79.73,-23.12 111.05,-48.73 23.8,-19.46 38.6,-20.41 38.6,-2.48 0,16.78 -32.25,36.77 -95.18,58.98 -64.52,22.77 -76.45,24.59 -81.28,12.41" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + transform="matrix(0.13333333,0,0,-0.13333333,0,550.66667)" /> + </g> +</svg> 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 %} + + <h2>{% block nadpis1a %}O-jo-jo-jo-joj{% endblock nadpis1a %}</h2> + + <p> + Problém se sušenkami či něčím podobným. Zkuste {% if url %}to prosím znovu: <a href="{{ url }}">{{ url }}</a>. Případně můžete {% endif %}přejít na <a href="/">titulní stránku</a>. + </p> + + <p>Pokud problém přetrvává obraťte se na nás přes e-mail: <a href="mailto:mam@matfyz.cz">mailto:mam@matfyz.cz</a> a pošlete nám následující popis chyby: <code>{{ reason }}</code></p> + + <img src="{% static 'various/img/zere_kostku.svg' %}"> + + +{% 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 @@ <th class='border-r'>#</th> <th class='border-r'>Jméno</th> {% for p in vysledkovka.temata_a_spol%} - <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}</th> + <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}<span title="{{ p }}">{{ p.kod_v_rocniku }}</span>{# </a> #}</th> {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> + <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> {% endfor %} {# TODELETE #} @@ -17,7 +17,7 @@ {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th> + <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> {% 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