Browse Source

Merge branch 'master' into dockerizace

dockerizace
Jonas Havelka 11 months ago
parent
commit
59f7f30b99
  1. 4
      aesop/ovvpfile.py
  2. 4
      aesop/utils.py
  3. 2
      aesop/views.py
  4. 1
      docs/index.rst
  5. 25
      docs/tabulka_prerekvizit.rst
  6. 2
      docs/vyvoj.rst
  7. 97
      docs/zavislosti.rst
  8. 1
      galerie/models.py
  9. 1
      korektury/models.py
  10. 1
      korektury/views.py
  11. 4
      mamweb/admin.py
  12. 5
      mamweb/settings_common.py
  13. 4
      mamweb/settings_prod.py
  14. 1
      mamweb/static/css/mamweb.css
  15. 2
      mamweb/templates/500.html
  16. 2
      mamweb/urls.py
  17. 2
      odevzdavatko/forms.py
  18. 15
      odevzdavatko/templates/odevzdavatko/tabulka.html
  19. 15
      odevzdavatko/templatetags/barvy_reseni.py
  20. 54
      odevzdavatko/views.py
  21. 18
      personalni/admin.py
  22. 63
      personalni/tests.py
  23. 1
      prednasky/models.py
  24. 8
      requirements.txt
  25. 29
      seminar/admin.py
  26. 3
      seminar/migrations/0006_problem_add_timestamp.py
  27. 40
      seminar/migrations/0114_related_name_se_zmenilo_a_django_chce_migraci_tak_dostane_migraci.py
  28. 17
      seminar/models/tvorba.py
  29. 4
      seminar/templates/seminar/archiv/cisla.html
  30. 6
      seminar/templates/seminar/archiv/cislo.html
  31. 5
      seminar/templates/seminar/archiv/rocnik.html
  32. 2
      seminar/views/views_all.py
  33. 0
      sifrovacka/__init__.py
  34. 8
      sifrovacka/admin.py
  35. 5
      sifrovacka/apps.py
  36. 18
      sifrovacka/forms.py
  37. 34
      sifrovacka/migrations/0001_initial.py
  38. 28
      sifrovacka/migrations/0002_auto_20231015_1944.py
  39. 18
      sifrovacka/migrations/0003_odpoveducastnika_uspech.py
  40. 0
      sifrovacka/migrations/__init__.py
  41. 27
      sifrovacka/models.py
  42. 25
      sifrovacka/templates/sifrovacka/odpovedi_list.html
  43. 46
      sifrovacka/templates/sifrovacka/sifrovacka.html
  44. 17
      sifrovacka/urls.py
  45. 33
      sifrovacka/views.py
  46. 2
      soustredeni/admin.py
  47. 87
      various/static/various/img/zere_kostku.svg
  48. 19
      various/templates/various/403_csrf.html
  49. 10
      various/views.py
  50. 6
      vysledkovky/templates/vysledkovky/vysledkovka_cisla.html
  51. 8
      vysledkovky/utils.py

4
aesop/ovvpfile.py

@ -1,5 +1,5 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.encoding import force_text from django.utils.encoding import force_str
class OvvpFile: class OvvpFile:
@ -20,7 +20,7 @@ class OvvpFile:
yield '\t'.join(self.columns) + '\n' yield '\t'.join(self.columns) + '\n'
# rows # rows
for r in self.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): def to_string(self):
return ''.join(self.to_lines()) return ''.join(self.to_lines())

4
aesop/utils.py

@ -1,6 +1,6 @@
import datetime import datetime
from django.utils.encoding import force_text from django.utils.encoding import force_str
from aesop.ovvpfile import OvvpFile from aesop.ovvpfile import OvvpFile
@ -9,7 +9,7 @@ def default_ovvpfile(event, rocnik):
of = OvvpFile() of = OvvpFile()
of.headers['version'] = '1' of.headers['version'] = '1'
of.headers['event'] = event 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['date'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
of.headers['id-scope'] = 'mam' of.headers['id-scope'] = 'mam'
of.headers['id-generation'] = '1' of.headers['id-generation'] = '1'

2
aesop/views.py

@ -8,7 +8,7 @@ from django.shortcuts import get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.views import generic 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 .utils import default_ovvpfile
from seminar.models import Rocnik, Soustredeni from seminar.models import Rocnik, Soustredeni

1
docs/index.rst

@ -27,6 +27,7 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve
:titlesonly: :titlesonly:
vyvoj vyvoj
zavislosti
sphinx sphinx
skripty skripty
modules/modules modules/modules

25
docs/tabulka_prerekvizit.rst

@ -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."

2
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. bydlí gitový repozitář s kódem.
.. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce .. 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é Doporučené
^^^^^^^^^^ ^^^^^^^^^^

97
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."

1
galerie/models.py

@ -2,7 +2,6 @@
from django.db import models from django.db import models
#from django.db.models import Q #from django.db.models import Q
from django.utils.encoding import force_text
from imagekit.models import ImageSpecField from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFit, Transpose from imagekit.processors import ResizeToFit, Transpose

1
korektury/models.py

@ -16,7 +16,6 @@ import os
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.utils.encoding import force_text
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename

1
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.shortcuts import get_object_or_404, render
from django.views import generic from django.views import generic
from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.core.mail import EmailMessage from django.core.mail import EmailMessage

4
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 # 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 # 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 Return a sorted list of all the installed apps that have been
registered in this site. 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. # 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())) app_list = sorted(app_dict.values(), key=lambda x: locale.strxfrm('!') if (x['name'] == "Seminar") else locale.strxfrm(x['name'].lower()))

5
mamweb/settings_common.py

@ -28,7 +28,6 @@ APPEND_SLASH = True
LANGUAGE_CODE = 'cs' LANGUAGE_CODE = 'cs'
TIME_ZONE = 'Europe/Prague' TIME_ZONE = 'Europe/Prague'
USE_I18N = True USE_I18N = True
USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@ -54,6 +53,9 @@ LOGIN_REDIRECT_URL = 'profil'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True
DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok 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 # Modules configuration
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
@ -151,6 +153,7 @@ INSTALLED_APPS = (
'soustredeni', 'soustredeni',
'treenode', 'treenode',
'vyroci', 'vyroci',
'sifrovacka',
# Admin upravy: # Admin upravy:

4
mamweb/settings_prod.py

@ -20,7 +20,9 @@ INSTALLED_APPS += (
) )
# SECURITY WARNING: keep the secret key used in production secret! # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False DEBUG = False

1
mamweb/static/css/mamweb.css

@ -1245,6 +1245,7 @@ div.gdpr {
.dosla_reseni tr th, .dosla_reseni tr td { .dosla_reseni tr th, .dosla_reseni tr td {
padding: 1px 10px 1px 10px; padding: 1px 10px 1px 10px;
border-collapse: collapse; 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) { .dosla_reseni tr:nth-child(even) {

2
mamweb/templates/500.html

@ -4,7 +4,9 @@
{% block errorheading %} {% 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 #} <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 O-jo-jo-jo-joj
{% endblock %}
{% endblock %} {% endblock %}
{% block errortext %} {% block errortext %}

2
mamweb/urls.py

@ -71,6 +71,8 @@ urlpatterns = [
# Výroční sraz # Výroční sraz
path('sraz/30-let/', include('vyroci.urls')), path('sraz/30-let/', include('vyroci.urls')),
# Miniapka na šifrovačku
path('sifrovacka/', include('sifrovacka.urls')),
] ]
# This is only needed when using runserver. # This is only needed when using runserver.

2
odevzdavatko/forms.py

@ -238,6 +238,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
'reseni_od': terminy[-2] if rocnik is None else terminy[0], 'reseni_od': terminy[-2] if rocnik is None else terminy[0],
'reseni_do': terminy[-1], 'reseni_do': terminy[-1],
'neobodovane': False, 'neobodovane': False,
'barvicky': True,
} }
return initial return initial
@ -262,3 +263,4 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) reseni_od = forms.DateField(input_formats=[DATE_FORMAT])
reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) reseni_do = forms.DateField(input_formats=[DATE_FORMAT])
neobodovane = forms.BooleanField(required=False) neobodovane = forms.BooleanField(required=False)
barvicky = forms.BooleanField(required=False)

15
odevzdavatko/templates/odevzdavatko/tabulka.html

@ -1,6 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #}
{% load barvy_reseni %}
{% block content %} {% block content %}
@ -11,6 +12,7 @@
Od data (vyjma): {{ filtr.reseni_od }} Od data (vyjma): {{ filtr.reseni_od }}
Do data (včetně): {{ filtr.reseni_do }} Do data (včetně): {{ filtr.reseni_do }}
<span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }} <span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }}
<span title="Obarvit shodná řešení shodně">🎨?</span> {{ filtr.barvicky }}
<input type=submit value="→"> <input type=submit value="→">
</form> </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? #} {# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #}
{{ resitel }} {{ resitel }}
</td> </td>
{% for hodn in hodnoty %} {% for soucet,bunka in hodnoty %}
<td> <td>
{% if hodn %} {% for reseni,hodnoceni in bunka %}
<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}"> <a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}">
{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} {{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b)
</a> </a><br>
{% endfor %}
{% if bunka|length > 1 %}
<b>Σ: {{soucet}} b</b>
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}

15
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}'

54
odevzdavatko/views.py

@ -13,6 +13,7 @@ from django.db.models import Q
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
from decimal import Decimal
from itertools import groupby from itertools import groupby
import logging import logging
@ -37,14 +38,6 @@ logger = logging.getLogger(__name__)
# Taky se může hodit: # Taky se může hodit:
# - Tabulka všech řešitelů x všech problémů? # - 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): class TabulkaOdevzdanychReseniView(ListView):
template_name = 'odevzdavatko/tabulka.html' template_name = 'odevzdavatko/tabulka.html'
model = m.Hodnoceni model = m.Hodnoceni
@ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView):
reseni_od = fcd["reseni_od"] reseni_od = fcd["reseni_od"]
reseni_do = fcd["reseni_do"] reseni_do = fcd["reseni_do"]
jen_neobodovane = fcd["neobodovane"] jen_neobodovane = fcd["neobodovane"]
self.barvicky = fcd["barvicky"]
else: else:
initial = FiltrForm.gen_initial(self.aktualni_rocnik) initial = FiltrForm.gen_initial(self.aktualni_rocnik)
resitele = initial['resitele'] resitele = initial['resitele']
@ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView):
reseni_od = initial['reseni_od'][0] reseni_od = initial['reseni_od'][0]
reseni_do = initial['reseni_do'][0] reseni_do = initial['reseni_do'][0]
jen_neobodovane = initial["neobodovane"] jen_neobodovane = initial["neobodovane"]
self.barvicky = initial["barvicky"]
# Chceme jen letošní problémy # Chceme jen letošní problémy
@ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView):
return qs return qs
def get_context_data(self, *args, **kwargs): 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é # self.resitele, self.reseni a self.problemy jsou již nastavené
ctx = super().get_context_data(*args, **kwargs) ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.problemy ctx['problemy'] = self.problemy
ctx['resitele'] = self.resitele 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: if problem not in tabulka:
tabulka[problem] = dict() tabulka[problem] = dict()
soucty[problem] = dict()
if resitel not in tabulka[problem]: 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: else:
tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) tabulka[problem][resitel].append((reseni, hodnoceni))
# 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") soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme
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
for hodnoceni in self.get_queryset(): for hodnoceni in self.get_queryset():
for resitel in hodnoceni.reseni.resitele.all(): for resitel in hodnoceni.reseni.resitele.all():
pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) pridej_reseni(resitel, hodnoceni)
hodnoty = [] 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 = [] resitele_do_tabulky: list[m.Resitel] = []
for resitel in self.resitele: for resitel in self.resitele:
dostal_body = False 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: for problem in self.problemy:
if problem in tabulka and resitel in tabulka[problem]: 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 dostal_body = True
else: else:
resiteluv_radek.append(None) resiteluv_radek.append((Decimal(0),[]))
if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body: if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body:
hodnoty.append(resiteluv_radek) hodnoty.append(resiteluv_radek)
resitele_do_tabulky.append(resitel) resitele_do_tabulky.append(resitel)
@ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView):
ctx['form'] = ctx['filtr'] ctx['form'] = ctx['filtr']
# Pro maximum v přesměrovátku ročníků # Pro maximum v přesměrovátku ročníků
ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik
ctx['barvicky'] = self.barvicky
if 'rocnik' in self.kwargs: if 'rocnik' in self.kwargs:
ctx['rocnik'] = self.kwargs['rocnik'] ctx['rocnik'] = self.kwargs['rocnik']
else: else:
@ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView):
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
"""Rozskok mezi více řešeními téhož problému od téhož řešitele.
Asi 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 model = m.Reseni
template_name = 'odevzdavatko/seznam.html' template_name = 'odevzdavatko/seznam.html'

18
personalni/admin.py

@ -1,7 +1,9 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django_reverse_admin import ReverseModelAdmin from django_reverse_admin import ReverseModelAdmin
from django.contrib.messages import WARNING, ERROR, SUCCESS
import seminar.models as m import seminar.models as m
from datetime import datetime
@admin.register(m.Osoba) @admin.register(m.Osoba)
@ -20,16 +22,24 @@ class OsobaAdmin(admin.ModelAdmin):
def udelej_orgem(self,request,queryset): def udelej_orgem(self,request,queryset):
org_group = Group.objects.get(name='org') org_group = Group.objects.get(name='org')
print(queryset) uspesne_vytvoreni_orgove = 0
for o in queryset: 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 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.groups.add(org_group)
user.is_staff = True user.is_staff = True
user.save() user.save()
org = m.Organizator.objects.create(osoba=o) org = m.Organizator.objects.create(osoba=o, organizuje_od=datetime.now())
org.save() 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): class OsobaInline(admin.TabularInline):
model = m.Osoba model = m.Osoba

63
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.

1
prednasky/models.py

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.utils.encoding import force_text
from seminar.models import Organizator, Soustredeni from seminar.models import Organizator, Soustredeni

8
requirements.txt

@ -5,6 +5,7 @@ psycopg2
html5lib html5lib
ipython ipython
Pillow Pillow
pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0
pytz pytz
six six
pexpect pexpect
@ -13,7 +14,7 @@ Unidecode
# Django and modules # Django and modules
Django<3.3 Django<5.0
#django-bootstrap-sass #django-bootstrap-sass
django-mptt django-mptt
django-reversion django-reversion
@ -24,7 +25,7 @@ django-ckeditor
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
django-flat-theme django-flat-theme
django-taggit django-taggit
django-autocomplete-light>=3.9.0rc1 django-autocomplete-light>=3.9.0
django-crispy-forms django-crispy-forms
django-imagekit django-imagekit
django-polymorphic django-polymorphic
@ -52,9 +53,6 @@ Werkzeug
requests requests
# requests-oauthlib # requests-oauthlib
# uWSGI
uWSGI
# Potřeba pro test data # Potřeba pro test data
lorem lorem

29
seminar/admin.py

@ -12,16 +12,26 @@ from django.utils.safestring import mark_safe
import seminar.models as m import seminar.models as m
admin.site.register(m.Rocnik) admin.site.register(m.Rocnik)
admin.site.register(m.Deadline)
admin.site.register(m.ZmrazenaVysledkovka) 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): class DeadlineAdminInline(admin.TabularInline):
model = m.Deadline model = m.Deadline
extra = 0 extra = 0
class CisloForm(ModelForm): class CisloForm(ModelForm):
class Meta: class Meta:
model = m.Cislo model = m.Cislo
@ -71,7 +81,7 @@ class CisloForm(ModelForm):
@admin.register(m.Cislo) @admin.register(m.Cislo)
class CisloAdmin(admin.ModelAdmin): class CisloAdmin(admin.ModelAdmin):
form = CisloForm form = CisloForm
actions = ['force_publish'] actions = ['force_publish', 'pregeneruj_vysledkovky']
inlines = (DeadlineAdminInline,) inlines = (DeadlineAdminInline,)
def force_publish(self,request,queryset): 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' 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) @admin.register(m.Problem)
class ProblemAdmin(PolymorphicParentModelAdmin): class ProblemAdmin(PolymorphicParentModelAdmin):

3
seminar/migrations/0006_problem_add_timestamp.py

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations from django.db import models, migrations
import datetime import datetime
from django.utils.timezone import utc
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -16,7 +15,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='problem', model_name='problem',
name='timestamp', 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, preserve_default=False,
), ),
migrations.AlterField( migrations.AlterField(

40
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'),
),
]

17
seminar/models/tvorba.py

@ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel):
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
return str(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ý.") 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): # def verejne(self):
# # aktuálně podle stavu problému # # 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.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
if self.nadproblem: if self.nadproblem:
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod) 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ý.") 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): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
@ -607,9 +607,9 @@ class Clanek(Problem):
# Nemělo by být potřeba # Nemělo by být potřeba
# if self.nadproblem: # if self.nadproblem:
# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod) # 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ý.") 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): def node(self):
return None return None
@ -642,12 +642,9 @@ class Uloha(Problem):
@cached_property @cached_property
def kod_v_rocniku(self): def kod_v_rocniku(self):
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod) return f"{self.cislo_zadani.poradi}.{self.kod}"
if self.nadproblem:
return self.nadproblem.kod_v_rocniku+name
return name
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ý.") 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): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)

4
seminar/templates/seminar/archiv/cisla.html

@ -40,7 +40,9 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
<a href='{{ rocnik.verejne_url }}'>Výsledková listina</a> <!-- FIXME: url výsledkovky--> {% if rocnik.verejne_vysledkovky_cisla %} {# Tohle jsem asi neměl tady použít, ale šlo to… #}
<a href='{{ rocnik.verejne_url }}#vysledky'>Výsledková listina</a> <!-- FIXME: url výsledkovky-->
{% endif %}
</div> </div>
</div> </div>

6
seminar/templates/seminar/archiv/cislo.html

@ -38,9 +38,11 @@
<h2> Orgovské odkazy </h2> <h2> Orgovské odkazy </h2>
<ul> <ul>
<li><a href="obalky.pdf">Obálky (PDF)</a></li> <li><a href="obalky.pdf">Obálky (PDF)</a></li>
<li><a href="tituly.tex" download>Tituly (TeX)</a></li> <li><a href="tituly.tex" download>Tituly (TeX, 2. deadline předchozího čísla a 1.deadline tohoto)</a></li>
<li><a href="vysledkovka.tex" download>Výsledkovka (TeX)</a></li> <li><a href="vysledkovka.tex" download>Výsledkovka (TeX, 2. deadline předchozího čísla a 1.deadline tohoto)</a></li>
<li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li> <li><a href="odmeny/{{prevcislo.rocnik.rocnik}}.{{prevcislo.poradi}}/">Odměny</a></li>
<li><a href="{% url "seminar_rocnik_titul" rocnik=cislo.rocnik.rocnik %}" download="posledni_tituly.tex">Tituly do závěrečného čísla (TeX, 2. deadline předchozího čísla a oba tohoto)</a></li>
<li><a href="{% url "seminar_rocnik_posledni_vysledkovka" rocnik=cislo.rocnik.rocnik %}" download>Výsledkovka závěrečného čísla ročníku (TeX, 2. deadline předchozího čísla a oba tohoto)</a></li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}

5
seminar/templates/seminar/archiv/rocnik.html

@ -113,15 +113,14 @@
{% if vysledkovka.radky_vysledkovky %} {% if vysledkovka.radky_vysledkovky %}
<h2>Výsledková listina</h2> <h2 id=vysledky>Výsledková listina</h2>
{% include "vysledkovky/vysledkovka_rocnik.html" %} {% include "vysledkovky/vysledkovka_rocnik.html" %}
{% endif %} {% endif %}
{% if user.je_org %} {% if user.je_org %}
<div class='mam-org-only'> <div class='mam-org-only'>
<p><a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a></p> <p><a href='vysledkovka.tex' download>Výsledkovka ročníku (LaTeX, včetně neveřejných)</a></p>
<p><a href="tituly.tex" download>Tituly (TeX, do konce ročníku = pro poslední číslo)</a></p> <p><a href="tituly.tex" download>Tituly (TeX, včetně neveřejných, všechny, nevhodné do mamtexu)</a></p>
<p><a href="posledni_vysledkovka.tex" download>Výsledkovka posledního čísla</a></p>
{# FIXME: Sice to sem asi nepatří sémanticky, ale bylo to nejjednodušší… #} {# FIXME: Sice to sem asi nepatří sémanticky, ale bylo to nejjednodušší… #}
<p><a href='{% url 'seminar_rocnik_resitele_csv' rocnik=rocnik.rocnik %}' download>CSV export řešitelů</a></p> <p><a href='{% url 'seminar_rocnik_resitele_csv' rocnik=rocnik.rocnik %}' download>CSV export řešitelů</a></p>
<h2>Výsledková listina včetně neveřejných bodů</h2> <h2>Výsledková listina včetně neveřejných bodů</h2>

2
seminar/views/views_all.py

@ -3,7 +3,7 @@ from django.http import HttpResponse
from django.urls import reverse from django.urls import reverse
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.views import generic 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.http import Http404
from django.db.models import Q, Sum, Count from django.db.models import Q, Sum, Count
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView

0
sifrovacka/__init__.py

8
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)

5
sifrovacka/apps.py

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SifrovackaConfig(AppConfig):
name = 'sifrovacka'

18
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

34
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')),
],
),
]

28
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'),
),
]

18
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'),
),
]

0
sifrovacka/migrations/__init__.py

27
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}"

25
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 %}

46
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 %}

17
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'
),
]

33
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

2
soustredeni/admin.py

@ -25,7 +25,7 @@ class SoustredeniOrganizatoriInline(admin.TabularInline):
extra = 1 extra = 1
fields = ['organizator','poznamka'] fields = ['organizator','poznamka']
autocomplete_fields = ['organizator'] autocomplete_fields = ['organizator']
ordering = ['organizator__osoba__jmeno','organizator__prijmeni'] ordering = ['organizator__osoba__jmeno','organizator__osoba__prijmeni']
formfield_overrides = { formfield_overrides = {
models.TextField: {'widget': widgets.TextInput} models.TextField: {'widget': widgets.TextInput}
} }

87
various/static/various/img/zere_kostku.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

19
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 %}

10
various/views.py

@ -1,3 +1,13 @@
from django.http import HttpResponseForbidden
from django.shortcuts import render from django.shortcuts import render
# Create your views here. # 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,
)

6
vysledkovky/templates/vysledkovky/vysledkovka_cisla.html

@ -4,11 +4,11 @@
<th class='border-r'>#</th> <th class='border-r'>#</th>
<th class='border-r'>Jméno</th> <th class='border-r'>Jméno</th>
{% for p in vysledkovka.temata_a_spol%} {% 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 #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% 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 %} {% endfor %}
{# TODELETE #} {# TODELETE #}
@ -17,7 +17,7 @@
{# TODELETE #} {# TODELETE #}
{% for podproblemy in vysledkovka.podproblemy_iter.next %} {% 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 %} {% endfor %}
{# TODELETE #} {# TODELETE #}

8
vysledkovky/utils.py

@ -257,7 +257,7 @@ class VysledkovkaCisla(Vysledkovka):
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
hlavni_problemy = set() hlavni_problemy = set()
for p in self.problemy: 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í # zunikátnění
hlavni_problemy = list(hlavni_problemy) hlavni_problemy = list(hlavni_problemy)
@ -313,7 +313,7 @@ class VysledkovkaCisla(Vysledkovka):
# Sečteme hodnocení # Sečteme hodnocení
for hodnoceni in self.hodnoceni_do_cisla: for hodnoceni in self.hodnoceni_do_cisla:
prob = hodnoceni.problem prob = hodnoceni.problem.get_real_instance()
nadproblem = prob.hlavni_problem.id nadproblem = prob.hlavni_problem.id
# Když nadproblém není "téma", pak je "Ostatní" # Když nadproblém není "téma", pak je "Ostatní"
@ -366,9 +366,9 @@ class VysledkovkaCisla(Vysledkovka):
for problem in self.problemy: for problem in self.problemy:
h_problem = problem.hlavni_problem h_problem = problem.hlavni_problem
if h_problem in temata_a_spol: if h_problem in temata_a_spol:
podproblemy[h_problem.id].append(problem) podproblemy[h_problem.id].append(problem.get_real_instance())
else: else:
podproblemy[-1].append(problem) podproblemy[-1].append(problem.get_real_instance())
for podproblem in podproblemy.keys(): for podproblem in podproblemy.keys():
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku) podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)

Loading…
Cancel
Save