diff --git a/aesop/ovvpfile.py b/aesop/ovvpfile.py index 4d58af35..ba0873f1 100644 --- a/aesop/ovvpfile.py +++ b/aesop/ovvpfile.py @@ -1,5 +1,5 @@ from django.http import HttpResponse -from django.utils.encoding import force_text +from django.utils.encoding import force_str class OvvpFile: @@ -20,7 +20,7 @@ class OvvpFile: yield '\t'.join(self.columns) + '\n' # rows for r in self.rows: - yield '\t'.join([force_text(r[c]) for c in self.columns]) + '\n' + yield '\t'.join([force_str(r[c]) for c in self.columns]) + '\n' def to_string(self): return ''.join(self.to_lines()) diff --git a/aesop/utils.py b/aesop/utils.py index 3ea62dce..58b170ec 100644 --- a/aesop/utils.py +++ b/aesop/utils.py @@ -1,6 +1,6 @@ import datetime -from django.utils.encoding import force_text +from django.utils.encoding import force_str from aesop.ovvpfile import OvvpFile @@ -9,7 +9,7 @@ def default_ovvpfile(event, rocnik): of = OvvpFile() of.headers['version'] = '1' of.headers['event'] = event - of.headers['year'] = force_text(rocnik.prvni_rok) + of.headers['year'] = force_str(rocnik.prvni_rok) of.headers['date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") of.headers['id-scope'] = 'mam' of.headers['id-generation'] = '1' diff --git a/aesop/views.py b/aesop/views.py index 4d1748b7..e4b3364b 100644 --- a/aesop/views.py +++ b/aesop/views.py @@ -8,7 +8,7 @@ from django.shortcuts import get_object_or_404 from django.http import HttpResponse from django.urls import reverse from django.views import generic -from django.utils.encoding import force_text +from django.utils.encoding import force_str from .utils import default_ovvpfile from seminar.models import Rocnik, Soustredeni diff --git a/docs/index.rst b/docs/index.rst index 5481bb88..d06c7e4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,6 +27,7 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve :titlesonly: vyvoj + zavislosti sphinx skripty modules/modules diff --git a/docs/tabulka_prerekvizit.rst b/docs/tabulka_prerekvizit.rst deleted file mode 100644 index 9dcce4c5..00000000 --- a/docs/tabulka_prerekvizit.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. Není odkázaná z menu, je to záměr - -Tabulka prerekvizit v různých distribucích -========= - -.. admonition:: Metodika - - Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme - ``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme - přiinstalovali. Protože větev ``makefiles`` aktuálně není mergenutá do - masteru, nefunguje synchronizace flatpages (a stejně nemáme SSH klíč), takže - tam ``make/init_local`` sestřelíme a vyzkoušíme, že ``make/test`` spustí - testy. - -.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV… - -.. csv-table:: Prerekvizity v jednotlivých distribucích - :header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky - - Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" - Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" - Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)" - openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." - Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." - diff --git a/docs/vyvoj.rst b/docs/vyvoj.rst index 0d23972a..2df0ae64 100644 --- a/docs/vyvoj.rst +++ b/docs/vyvoj.rst @@ -37,7 +37,7 @@ Kromě toho je potřeba mít účet na `Gitee `_, kd bydlí gitový repozitář s kódem. .. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce - prerekvizit `. + prerekvizit `. Doporučené ^^^^^^^^^^ diff --git a/docs/zavislosti.rst b/docs/zavislosti.rst new file mode 100644 index 00000000..c2f684bd --- /dev/null +++ b/docs/zavislosti.rst @@ -0,0 +1,97 @@ +Závislosti webu +@@@@@@@@@@@@@@@ + +Web ke svému běhu potřebuje různé další programy. Tahle stránka se snaží je pokrýt. + +Stránka je koncipována jako odrážkový seznam balíčků pro Ubuntu s případnými +komentáři, na konci stránky jsou uvedena :ref:`jména balíčků ` v různých dalších distribucích. (Seznam mj. cílí na lokální +rozchození, proto popisuji Ubuntu a ne Debian. I tak se ale snažíme popsat web +v úplnosti.) + +.. I use Arch, btw. + + +Základ webu +=========== + +- ``python3`` – Ideálně Python 3.9, jenž je na Gimlim +- ``python3-pip`` pro instalaci dalších Pythoních balíčků podle ``requirements.txt`` +- ``python3-venv`` +- ``gcc`` – kompilace Pythoních knihoven ze zdrojových distribucí (sdist), možná (neotestováno) jde jako alternativu použít ``python3-wheel`` a stahovat bdists +- ``python3-dev`` – taktéž +- ``libpq-dev`` do třetice… +- ``ghostscript`` TODO konverze PDF v korekturovátku +- ``pdflatex`` FIXME! generování obálek a stvrzenek +- ``git`` – používán :ref:`Make skripty` +- ``locales`` pro české formáty + +Nasazení na produkci / testweb +============================== + +(nejsou nutně potřeba k provozu lokální instance) + +- ``rsync`` +- ``pg_utils`` FIXME +- ``htpasswd`` FIXME – aby testweb nepoužívali náhodní kolemjdoucí +- ``postgresql-server`` TODO +- ``acl`` pro nastavování práv přes ``setfacl`` + +Pro testweb je potřeba i všechno pro :ref:`dokumentaci `, vizte níž. + +Předpokládá se nasazení v uWSGI pod Nginxem a služba běžící pod systemd, nicméně to už je spíš záležitost infrastruktury a ne specifikum mamwebu. + +Dokumentace +=========== + +- ``make`` pro zbuildění +- Pythoní balíčky podle příslušné části ``requirements.txt`` + +Vývojové nástroje +================= + +(Nejsou nezbytně nutné, ale předpokládáme jejich užitečnost. Mohou se hodit i na produkci.) + +- ``psql`` TODO pro manuální dotazy do PostgreSQL +- ``sqlite3`` TODO totéž pro SQLite3 +- ``ssh`` +- ``graphviz`` pro vygenerování schématu +- ``rsync`` +- ``ipython3`` – hezčí interaktivní shell (stačí z ``requirements.txt``) + +Potenciální usnadnění života +============================ + +(Úplně zbytečné, ale sdílíme pozitivní zkušenosti :-)) + +- ``tea`` – CLI klient pro Giteu, aby člověk nepotřeboval otevírat web pro založení PR + + +Alternativní jména balíčků +========================== + +Různé distribuce balí SW různě, takže to, co je v jedné distribuci jeden +balíček může být v jiné rozděleno do víc. Pro usnadnění nasazení je tady +přehled známých alternativních jmen. + +TODO: tabulka není úplná. Pokud na něco narazíte, tak ji prosím doplňte. + +.. admonition:: Jak se pozná, že web funguje, pro účely tabulky? + + Na čistém repozitáři (``git clean -fxd``) a čistém systému spouštíme + ``make/init_local``. Když to spadne, tak do tabulky zapíšeme, co jsme + přiinstalovali. Protože nefunguje synchronizace flatpages (nemáme SSH klíč), + ``make/init_local`` sestřelíme při pokusu o synchronizaci a vyzkoušíme, že + ``make/test`` spustí testy. + +.. Grafické tabulky (grid-tables, simple-tables) jsou strašný porod vyrábět, dlabu na to a cpu to do CSV… + +.. csv-table:: Prerekvizity v jednotlivých distribucích + :header: Distribuce / OS, Repozitář s Py3.9, venv, py knihovny, PostgreSQL knihovna, poznámky + + Ubuntu 22.10, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "Je potřeba zapnout zdroj ``universe`` a nainstalovat kompilátor C (``gcc``)?" + Linux Mint 21, ??, ``python3-venv``, ``python3-dev``, ``libpq-dev``, "" + Archlinux 2022.11.01, AUR, vestavěný, vestavěné, ``postgresql-libs``, "Je potřeba céčkový kompilátor (``gcc``)" + openSUSE Leap 15.4, oficiální (``python39``), předinstalovaný?, ``python39-devel``, ??FIXME!!, "Výchozí verze pythonu je 3.6 a ta je moc stará, potřeba instalovat ``gcc``. Nevím jak sehnat pg_config." + Debian 11, "oficiální, výchozí", ??, ??, ??, "Určitě to tam rozběhat jde, protože Gimli. Nejspíš bude relativně podobné Ubuntu." + diff --git a/galerie/models.py b/galerie/models.py index 8e6efdc7..78551969 100644 --- a/galerie/models.py +++ b/galerie/models.py @@ -2,7 +2,6 @@ from django.db import models #from django.db.models import Q -from django.utils.encoding import force_text from imagekit.models import ImageSpecField from imagekit.processors import ResizeToFit, Transpose diff --git a/korektury/models.py b/korektury/models.py index 8906c00c..c9d47dfa 100644 --- a/korektury/models.py +++ b/korektury/models.py @@ -16,7 +16,6 @@ import os from django.db import models from django.utils import timezone from django.conf import settings -from django.utils.encoding import force_text from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import cached_property from django.utils.text import get_valid_filename diff --git a/korektury/views.py b/korektury/views.py index 564e1331..1bdfaa92 100644 --- a/korektury/views.py +++ b/korektury/views.py @@ -5,7 +5,6 @@ třídy většinou rozšiřující nějakou třídu z :mod:`django.views.generic """ from django.shortcuts import get_object_or_404, render from django.views import generic -from django.utils.translation import ugettext as _ from django.conf import settings from django.http import HttpResponseForbidden from django.core.mail import EmailMessage diff --git a/mamweb/admin.py b/mamweb/admin.py index b6924468..5d0351df 100644 --- a/mamweb/admin.py +++ b/mamweb/admin.py @@ -35,13 +35,13 @@ locale.setlocale(locale.LC_COLLATE, 'cs_CZ.UTF-8') # https://books.agiliq.com/projects/django-admin-cookbook/en/latest/set_ordering.html # FIXME zpraseno pomocí toho, že Python umí bez problému přepisovat funkce -def get_app_list(self, request): +def get_app_list(self, request, app_label=None): """ Return a sorted list of all the installed apps that have been registered in this site. """ - app_dict = self._build_app_dict(request) + app_dict = self._build_app_dict(request, label=app_label) # Sort the apps alphabetically. app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower())) diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 03724d3d..d6ed2852 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -28,7 +28,6 @@ APPEND_SLASH = True LANGUAGE_CODE = 'cs' TIME_ZONE = 'Europe/Prague' USE_I18N = True -USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) @@ -54,6 +53,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 +153,7 @@ INSTALLED_APPS = ( 'soustredeni', 'treenode', 'vyroci', + 'sifrovacka', # Admin upravy: diff --git a/mamweb/settings_prod.py b/mamweb/settings_prod.py index ebe827e4..060ba870 100644 --- a/mamweb/settings_prod.py +++ b/mamweb/settings_prod.py @@ -20,7 +20,9 @@ INSTALLED_APPS += ( ) # SECURITY WARNING: keep the secret key used in production secret! -assert not SECRET_KEY.startswith('12345') +# `'DOCUTILSCONFIG' in os.environ` kvůli sphinxu +# FIXME zjistit, zda je bezpečné a zda se to nedá udělat lépe +assert 'DOCUTILSCONFIG' in os.environ or not SECRET_KEY.startswith('12345') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 68029ad1..84e4c79b 100644 --- a/mamweb/static/css/mamweb.css +++ b/mamweb/static/css/mamweb.css @@ -1245,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) { diff --git a/mamweb/templates/500.html b/mamweb/templates/500.html index 7fc267fe..67085a8f 100644 --- a/mamweb/templates/500.html +++ b/mamweb/templates/500.html @@ -4,7 +4,9 @@ {% block errorheading %}
{# Meníčko nedostaneme, protože dostáváme prázdný kontext. Tak alespoň ať se O-JO-JO-JO-JOJ neschovává pod ním #} + {% block nadpis1a %} O-jo-jo-jo-joj + {% endblock %} {% endblock %} {% block errortext %} diff --git a/mamweb/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/forms.py b/odevzdavatko/forms.py index 074bc6da..583523e3 100644 --- a/odevzdavatko/forms.py +++ b/odevzdavatko/forms.py @@ -238,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 @@ -262,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/tabulka.html b/odevzdavatko/templates/odevzdavatko/tabulka.html index 6d1232d2..7cd317e5 100644 --- a/odevzdavatko/templates/odevzdavatko/tabulka.html +++ b/odevzdavatko/templates/odevzdavatko/tabulka.html @@ -1,6 +1,7 @@ {% extends "base.html" %} {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} +{% load barvy_reseni %} {% block content %} @@ -11,6 +12,7 @@ Od data (vyjma): {{ filtr.reseni_od }} Do data (včetně): {{ filtr.reseni_do }} 🔨? {{ filtr.neobodovane }} +🎨? {{ filtr.barvicky }} @@ -36,12 +38,15 @@ Do data (včetně): {{ filtr.reseni_do }} {# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #} {{ resitel }} - {% for hodn in hodnoty %} + {% for soucet,bunka in hodnoty %} - {% if hodn %} - - {{ hodn.pocet_reseni }} řeš.
{{ hodn.body }} b
{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} -
+ {% for reseni,hodnoceni in bunka %} + + {{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b) +
+ {% endfor %} + {% if bunka|length > 1 %} + Σ: {{soucet}} b {% endif %} {% endfor %} diff --git a/odevzdavatko/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/views.py b/odevzdavatko/views.py index 35c99f68..41af1dcb 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -13,6 +13,7 @@ from django.db.models import Q from dataclasses import dataclass import datetime +from decimal import Decimal from itertools import groupby import logging @@ -37,14 +38,6 @@ logger = logging.getLogger(__name__) # Taky se může hodit: # - Tabulka všech řešitelů x všech problémů? -@dataclass -class SouhrnReseni: - """Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce.""" - pocet_reseni : int - posledni_odevzdani : datetime.datetime - body : float - - class TabulkaOdevzdanychReseniView(ListView): template_name = 'odevzdavatko/tabulka.html' model = m.Hodnoceni @@ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView): reseni_od = fcd["reseni_od"] reseni_do = fcd["reseni_do"] jen_neobodovane = fcd["neobodovane"] + self.barvicky = fcd["barvicky"] else: initial = FiltrForm.gen_initial(self.aktualni_rocnik) resitele = initial['resitele'] @@ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView): reseni_od = initial['reseni_od'][0] reseni_do = initial['reseni_do'][0] jen_neobodovane = initial["neobodovane"] + self.barvicky = initial["barvicky"] # Chceme jen letošní problémy @@ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView): return qs def get_context_data(self, *args, **kwargs): + # TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl… # self.resitele, self.reseni a self.problemy jsou již nastavené ctx = super().get_context_data(*args, **kwargs) ctx['problemy'] = self.problemy ctx['resitele'] = self.resitele - tabulka = dict() - - def pridej_reseni(problem, resitel, body, cas): + tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict() + soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict() + + def pridej_reseni(resitel, hodnoceni): + problem = hodnoceni.problem + body = hodnoceni.body + cas = hodnoceni.reseni.cas_doruceni + reseni = hodnoceni.reseni if problem not in tabulka: tabulka[problem] = dict() + soucty[problem] = dict() if resitel not in tabulka[problem]: - tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body) + tabulka[problem][resitel] = [(reseni, hodnoceni)] + soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme else: - tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) - # Zvětšení počtu bodů o aktuální počet, pokud se tam někde nevyskytuje None – pak je součet taky None ("Pozor, nezadané body") - tabulka[problem][resitel].body = tabulka[problem][resitel].body + body if body is not None and tabulka[problem][resitel].body is not None else None - tabulka[problem][resitel].pocet_reseni += 1 - # Pro jednoduchost template si ještě poznamenáme ID problému a řešitele - tabulka[problem][resitel].problem_id = problem.id - tabulka[problem][resitel].resitel_id = resitel.id + tabulka[problem][resitel].append((reseni, hodnoceni)) + soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme for hodnoceni in self.get_queryset(): for resitel in hodnoceni.reseni.resitele.all(): - pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) + pridej_reseni(resitel, hodnoceni) - hodnoty = [] - resitele_do_tabulky = [] + hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému. + resitele_do_tabulky: list[m.Resitel] = [] for resitel in self.resitele: dostal_body = False - resiteluv_radek = [] + resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy for problem in self.problemy: if problem in tabulka and resitel in tabulka[problem]: - resiteluv_radek.append(tabulka[problem][resitel]) + resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel])) dostal_body = True else: - resiteluv_radek.append(None) + resiteluv_radek.append((Decimal(0),[])) if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body: hodnoty.append(resiteluv_radek) resitele_do_tabulky.append(resitel) @@ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView): ctx['form'] = ctx['filtr'] # Pro maximum v přesměrovátku ročníků ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik + ctx['barvicky'] = self.barvicky if 'rocnik' in self.kwargs: ctx['rocnik'] = self.kwargs['rocnik'] else: @@ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView): # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): + """Rozskok mezi více řešeními téhož problému od téhož řešitele. + + Asi už bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-) + + V případě, že takové řešení existuje jen jedno, tak na něj přesměruje.""" model = m.Reseni template_name = 'odevzdavatko/seznam.html' 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/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/prednasky/models.py b/prednasky/models.py index 50c71984..dcf44cbc 100644 --- a/prednasky/models.py +++ b/prednasky/models.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from django.db import models -from django.utils.encoding import force_text from seminar.models import Organizator, Soustredeni diff --git a/requirements.txt b/requirements.txt index 8a6a46e9..53c528ab 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 @@ -13,7 +14,7 @@ Unidecode # Django and modules -Django<3.3 +Django<5.0 #django-bootstrap-sass django-mptt django-reversion @@ -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/migrations/0006_problem_add_timestamp.py b/seminar/migrations/0006_problem_add_timestamp.py index 3df8bfdb..fd1509de 100644 --- a/seminar/migrations/0006_problem_add_timestamp.py +++ b/seminar/migrations/0006_problem_add_timestamp.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.db import models, migrations import datetime -from django.utils.timezone import utc class Migration(migrations.Migration): @@ -16,7 +15,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='problem', name='timestamp', - field=models.DateTimeField(default=datetime.datetime(2015, 5, 15, 8, 54, 56, 319985, tzinfo=utc), verbose_name='vytvo\u0159eno', auto_now=True), + field=models.DateTimeField(default=datetime.datetime(2015, 5, 15, 8, 54, 56, 319985, tzinfo=datetime.timezone.utc), verbose_name='vytvo\u0159eno', auto_now=True), preserve_default=False, ), migrations.AlterField( diff --git a/seminar/migrations/0114_related_name_se_zmenilo_a_django_chce_migraci_tak_dostane_migraci.py b/seminar/migrations/0114_related_name_se_zmenilo_a_django_chce_migraci_tak_dostane_migraci.py new file mode 100644 index 00000000..fccc850c --- /dev/null +++ b/seminar/migrations/0114_related_name_se_zmenilo_a_django_chce_migraci_tak_dostane_migraci.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.7 on 2023-11-20 21:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('seminar', '0113_resitel_zasilat_cislo_papirove'), + ] + + operations = [ + migrations.AlterField( + model_name='problem', + name='autor', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='autor_problemu_%(class)s', to='seminar.organizator', verbose_name='autor problému'), + ), + migrations.AlterField( + model_name='problem', + name='garant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='garant_problemu_%(class)s', to='seminar.organizator', verbose_name='garant zadaného problému'), + ), + migrations.AlterField( + model_name='problem', + name='opravovatele', + field=models.ManyToManyField(blank=True, related_name='opravovatele_%(class)s', to='seminar.organizator', verbose_name='opravovatelé'), + ), + migrations.AlterField( + model_name='problem', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='treenode', + name='polymorphic_ctype', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), + ), + ] diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py index 54e769c8..1c1a3285 100644 --- a/seminar/models/tvorba.py +++ b/seminar/models/tvorba.py @@ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel): return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) return str(self.kod) logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' # def verejne(self): # # aktuálně podle stavu problému @@ -571,9 +571,9 @@ class Tema(Problem): if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: if self.nadproblem: return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) - return "t{}".format(self.kod) + return 't'+self.kod logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def save(self, *args, **kwargs): super().save(*args, **kwargs) @@ -607,9 +607,9 @@ class Clanek(Problem): # Nemělo by být potřeba # if self.nadproblem: # return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) - return "c{}".format(self.kod) + return "c" + self.kod logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def node(self): return None @@ -642,12 +642,9 @@ class Uloha(Problem): @cached_property def kod_v_rocniku(self): if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) - if self.nadproblem: - return self.nadproblem.kod_v_rocniku+name - return name + return f"{self.cislo_zadani.poradi}.{self.kod}" logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.") - return '' + return f'' def save(self, *args, **kwargs): super().save(*args, **kwargs) diff --git a/seminar/templates/seminar/archiv/cisla.html b/seminar/templates/seminar/archiv/cisla.html index ce3b3dba..830e37b4 100644 --- a/seminar/templates/seminar/archiv/cisla.html +++ b/seminar/templates/seminar/archiv/cisla.html @@ -40,7 +40,9 @@ {% endif %} {% endfor %} - Výsledková listina + {% if rocnik.verejne_vysledkovky_cisla %} {# Tohle jsem asi neměl tady použít, ale šlo to… #} + Výsledková listina + {% endif %} diff --git a/seminar/templates/seminar/archiv/cislo.html b/seminar/templates/seminar/archiv/cislo.html index b8edce90..fa34e965 100644 --- a/seminar/templates/seminar/archiv/cislo.html +++ b/seminar/templates/seminar/archiv/cislo.html @@ -38,9 +38,11 @@

Orgovské odkazy

{% endif %} diff --git a/seminar/templates/seminar/archiv/rocnik.html b/seminar/templates/seminar/archiv/rocnik.html index 410b9361..fd2a99b6 100644 --- a/seminar/templates/seminar/archiv/rocnik.html +++ b/seminar/templates/seminar/archiv/rocnik.html @@ -113,15 +113,14 @@ {% if vysledkovka.radky_vysledkovky %} -

Výsledková listina

+

Výsledková listina

{% include "vysledkovky/vysledkovka_rocnik.html" %} {% endif %} {% if user.je_org %}

Výsledkovka ročníku (LaTeX, včetně neveřejných)

-

Tituly (TeX, do konce ročníku = pro poslední číslo)

-

Výsledkovka posledního čísla

+

Tituly (TeX, včetně neveřejných, všechny, nevhodné do mamtexu)

{# FIXME: Sice to sem asi nepatří sémanticky, ale bylo to nejjednodušší… #}

CSV export řešitelů

Výsledková listina včetně neveřejných bodů

diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 8e71fed3..318eee21 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -3,7 +3,7 @@ from django.http import HttpResponse from django.urls import reverse from django.core.exceptions import ObjectDoesNotExist from django.views import generic -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.http import Http404 from django.db.models import Q, Sum, Count from django.views.generic.base import RedirectView diff --git a/sifrovacka/__init__.py b/sifrovacka/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sifrovacka/admin.py b/sifrovacka/admin.py new file mode 100644 index 00000000..71d191d4 --- /dev/null +++ b/sifrovacka/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import OdpovedUcastnika, SpravnaOdpoved + +# Register your models here. + +admin.site.register(OdpovedUcastnika) +admin.site.register(SpravnaOdpoved) diff --git a/sifrovacka/apps.py b/sifrovacka/apps.py new file mode 100644 index 00000000..e9f34de6 --- /dev/null +++ b/sifrovacka/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SifrovackaConfig(AppConfig): + name = 'sifrovacka' diff --git a/sifrovacka/forms.py b/sifrovacka/forms.py new file mode 100644 index 00000000..e3eba7c7 --- /dev/null +++ b/sifrovacka/forms.py @@ -0,0 +1,18 @@ +from django.core.exceptions import ValidationError +from django.forms import ModelForm, Textarea +from .models import OdpovedUcastnika, SpravnaOdpoved + + +class SifrovackaForm(ModelForm): + class Meta: + model = OdpovedUcastnika + fields = ["sifra", "odpoved", ] + widgets = { + "odpoved": Textarea(attrs={'rows': 1, 'cols': 30}), + } + + def clean_sifra(self): + sifra = self.cleaned_data.get('sifra') + if SpravnaOdpoved.objects.filter(sifra=sifra).count() == 0: + raise ValidationError("Tohle číslo šifry v databázi nemáme. Zkontrolujte si ho prosím.") + return sifra diff --git a/sifrovacka/migrations/0001_initial.py b/sifrovacka/migrations/0001_initial.py new file mode 100644 index 00000000..742461ef --- /dev/null +++ b/sifrovacka/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.22 on 2023-10-14 09:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('seminar', '0113_resitel_zasilat_cislo_papirove'), + ] + + operations = [ + migrations.CreateModel( + name='SpravnaOdpoved', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('odpoved', models.TextField()), + ('sifra', models.IntegerField()), + ('skryty_text', models.TextField()), + ], + ), + migrations.CreateModel( + name='OdpovedUcastnika', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('odpoved', models.TextField(verbose_name='Tajenka')), + ('sifra', models.IntegerField(verbose_name='Číslo šifry')), + ('resitel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='seminar.resitel')), + ], + ), + ] diff --git a/sifrovacka/migrations/0002_auto_20231015_1944.py b/sifrovacka/migrations/0002_auto_20231015_1944.py new file mode 100644 index 00000000..dea42891 --- /dev/null +++ b/sifrovacka/migrations/0002_auto_20231015_1944.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.22 on 2023-10-15 17:44 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('sifrovacka', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='odpoveducastnika', + options={'ordering': ['-timestamp']}, + ), + migrations.AddField( + model_name='odpoveducastnika', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp'), + ), + migrations.AlterField( + model_name='odpoveducastnika', + name='odpoved', + field=models.TextField(verbose_name='Tajenka bez diakritiky'), + ), + ] diff --git a/sifrovacka/migrations/0003_odpoveducastnika_uspech.py b/sifrovacka/migrations/0003_odpoveducastnika_uspech.py new file mode 100644 index 00000000..1d61dd8c --- /dev/null +++ b/sifrovacka/migrations/0003_odpoveducastnika_uspech.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.22 on 2023-10-16 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sifrovacka', '0002_auto_20231015_1944'), + ] + + operations = [ + migrations.AddField( + model_name='odpoveducastnika', + name='uspech', + field=models.BooleanField(default=False, verbose_name='Úspěch'), + ), + ] diff --git a/sifrovacka/migrations/__init__.py b/sifrovacka/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sifrovacka/models.py b/sifrovacka/models.py new file mode 100644 index 00000000..6517c2e0 --- /dev/null +++ b/sifrovacka/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.utils import timezone + +from seminar.models.personalni import Resitel + + +# Create your models here. + + +class OdpovedUcastnika(models.Model): + class Meta: + ordering = ["-timestamp"] + + resitel = models.ForeignKey(Resitel, blank=False, null=False, on_delete=models.CASCADE) + odpoved = models.TextField("Tajenka bez diakritiky", blank=False, null=False,) + sifra = models.IntegerField("Číslo šifry", blank=False, null=False,) + timestamp = models.DateTimeField("Timestamp", blank=False, null=False, default=timezone.now) + uspech = models.BooleanField("Úspěch", blank=False, null=False, default=False) + + +class SpravnaOdpoved(models.Model): + odpoved = models.TextField(blank=False, null=False,) + sifra = models.IntegerField(blank=False, null=False,) + skryty_text = models.TextField(blank=False, null=False,) + + def __str__(self): + return f"{self.sifra}: {self.odpoved}" diff --git a/sifrovacka/templates/sifrovacka/odpovedi_list.html b/sifrovacka/templates/sifrovacka/odpovedi_list.html new file mode 100644 index 00000000..0024a7c1 --- /dev/null +++ b/sifrovacka/templates/sifrovacka/odpovedi_list.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block content %} + +

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

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

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

+ +
+ +

Zadat tajenku šifry:

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

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

Zkusit znovu.




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

{sifra.skryty_text}

Odevzdat další.




') + + +class SifrovackaListView(ListView): + template_name = 'sifrovacka/odpovedi_list.html' + model = OdpovedUcastnika diff --git a/soustredeni/admin.py b/soustredeni/admin.py index 091f9c59..c6f048db 100644 --- a/soustredeni/admin.py +++ b/soustredeni/admin.py @@ -25,7 +25,7 @@ class SoustredeniOrganizatoriInline(admin.TabularInline): extra = 1 fields = ['organizator','poznamka'] autocomplete_fields = ['organizator'] - ordering = ['organizator__osoba__jmeno','organizator__prijmeni'] + ordering = ['organizator__osoba__jmeno','organizator__osoba__prijmeni'] formfield_overrides = { models.TextField: {'widget': widgets.TextInput} } diff --git a/various/static/various/img/zere_kostku.svg b/various/static/various/img/zere_kostku.svg new file mode 100644 index 00000000..bac31662 --- /dev/null +++ b/various/static/various/img/zere_kostku.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + diff --git a/various/templates/various/403_csrf.html b/various/templates/various/403_csrf.html new file mode 100644 index 00000000..d0082550 --- /dev/null +++ b/various/templates/various/403_csrf.html @@ -0,0 +1,19 @@ +{#{% extends "error_base.html" %} Z toho nedědíme, protože se nemá přecházet na titulní stránku. #} +{% extends "base.html" %} + +{% load static %} + +{% block content %} + +

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

+ +

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

+ +

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

+ + + + +{% endblock %} diff --git a/various/views.py b/various/views.py index 91ea44a2..96d9a29d 100644 --- a/various/views.py +++ b/various/views.py @@ -1,3 +1,13 @@ +from django.http import HttpResponseForbidden from django.shortcuts import render # Create your views here. + + +def csrf_error(request, reason=""): + """ Jednoduchý „template view“ (třída to být nemůže) pro CSRF chyby """ + return render( + request, 'various/403_csrf.html', + {"url": request.META.get("HTTP_REFERER", None), "reason": reason}, + status=HttpResponseForbidden.status_code, + ) diff --git a/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html index ac53c811..4aa62953 100644 --- a/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html +++ b/vysledkovky/templates/vysledkovky/vysledkovka_cisla.html @@ -4,11 +4,11 @@ # Jméno {% for p in vysledkovka.temata_a_spol%} - {# #}{{ p.kod_v_rocniku }}{# #} + {# #}{{ p.kod_v_rocniku }}{# #} {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} {% endfor %} {# TODELETE #} @@ -17,7 +17,7 @@ {# TODELETE #} {% for podproblemy in vysledkovka.podproblemy_iter.next %} - {# #}{{ podproblemy.kod_v_rocniku }}{# #} + {# #}{{ podproblemy.kod_v_rocniku }}{# #} {% endfor %} {# TODELETE #} diff --git a/vysledkovky/utils.py b/vysledkovky/utils.py index 56d05c31..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,9 +366,9 @@ 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(): podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)