WIP: Kontrola starých práv #34
53 changed files with 973 additions and 227 deletions
|
@ -70,23 +70,17 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
|
|||
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
|
||||
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
|
||||
def get_queryset(self):
|
||||
nastaveni = get_object_or_404(m.Nastaveni)
|
||||
rocnik = nastaveni.aktualni_rocnik
|
||||
# Od tohoto místa dál jsem zkoušel spoustu variací podle https://django-polymorphic.readthedocs.io/en/stable/advanced.html
|
||||
temaQ = Q(Tema___rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
|
||||
ulohaQ = Q(Uloha___cislo_zadani__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
|
||||
clanekQ = Q(Clanek___cislo__rocnik = rocnik, stav=m.Problem.STAV_ZADANY)
|
||||
qs = m.Problem.objects.filter(temaQ | ulohaQ | clanekQ)
|
||||
#print(temata, ulohy, clanky)
|
||||
#ulohy.union(temata, all=True)
|
||||
#print(ulohy)
|
||||
#ulohy.union(clanky, all=True)
|
||||
#print(ulohy)
|
||||
#qs = ulohy
|
||||
print(qs)
|
||||
qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY)
|
||||
if self.q:
|
||||
qs = qs.filter(
|
||||
Q(nazev__icontains=self.q))
|
||||
|
||||
nadproblem_id = int(self.forwarded.get("nadproblem_id", -1))
|
||||
if nadproblem_id != -1:
|
||||
# Seřadíme tak, aby ty s nadproblem==None byly dole (větší motivace tam naklikat konkrétní úlohy) a pak nějak rozumně.
|
||||
# Tohle je řazení pro odevzdávátko, kde je definován nadproblém, proto je to v tomto ifu. (Jinde si to netroufám řadit)
|
||||
qs = qs.order_by("nadproblem", "kod", "nazev")
|
||||
qs = list(filter(lambda problem: problem.hlavni_problem.id == nadproblem_id, qs))
|
||||
return qs
|
||||
|
||||
class ProblemAutocomplete(autocomplete.Select2QuerySetView):
|
||||
|
|
|
@ -437,7 +437,7 @@
|
|||
"insitetree": true,
|
||||
"parent": 21,
|
||||
"sort_order": 36,
|
||||
"title": "Poslat řešení",
|
||||
"title": "Nahrát řešení",
|
||||
"tree": 1,
|
||||
"url": "seminar_nahraj_reseni",
|
||||
"urlaspattern": true
|
||||
|
@ -476,9 +476,9 @@
|
|||
"access_perm_type": 1,
|
||||
"access_permissions": [
|
||||
[
|
||||
"change_hodnoceni",
|
||||
"seminar",
|
||||
"hodnoceni"
|
||||
"org",
|
||||
"auth",
|
||||
"user"
|
||||
]
|
||||
],
|
||||
"access_restricted": true,
|
||||
|
@ -719,7 +719,7 @@
|
|||
"insitetree": true,
|
||||
"parent": 21,
|
||||
"sort_order": 36,
|
||||
"title": "Nahrát řešení",
|
||||
"title": "Vložit řešení",
|
||||
"tree": 1,
|
||||
"url": "seminar_vloz_reseni",
|
||||
"urlaspattern": true
|
||||
|
@ -1026,6 +1026,36 @@
|
|||
"model": "sitetree.treeitem",
|
||||
"pk": 51
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"access_guest": false,
|
||||
"access_loggedin": false,
|
||||
"access_perm_type": 1,
|
||||
"access_permissions": [
|
||||
[
|
||||
"resitel",
|
||||
"auth",
|
||||
"user"
|
||||
]
|
||||
],
|
||||
"access_restricted": true,
|
||||
"alias": null,
|
||||
"description": "",
|
||||
"hidden": false,
|
||||
"hint": "",
|
||||
"inbreadcrumbs": true,
|
||||
"inmenu": true,
|
||||
"insitetree": true,
|
||||
"parent": 23,
|
||||
"sort_order": 52,
|
||||
"title": "Nahrát řešení k nadproblému {{nadproblem_id}}",
|
||||
"tree": 1,
|
||||
"url": "seminar_nahraj_reseni nadproblem_id",
|
||||
"urlaspattern": true
|
||||
},
|
||||
"model": "sitetree.treeitem",
|
||||
"pk": 52
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"access_guest": false,
|
||||
|
@ -1041,13 +1071,13 @@
|
|||
"inmenu": true,
|
||||
"insitetree": true,
|
||||
"parent": 28,
|
||||
"sort_order": 52,
|
||||
"sort_order": 53,
|
||||
"title": "Přidat PDF",
|
||||
"tree": 1,
|
||||
"url": "/admin/korektury/korekturovanepdf/add/",
|
||||
"urlaspattern": false
|
||||
},
|
||||
"model": "sitetree.treeitem",
|
||||
"pk": 52
|
||||
"pk": 53
|
||||
}
|
||||
]
|
|
@ -64,6 +64,36 @@
|
|||
"ct_app_label": "galerie",
|
||||
"ct_model": "obrazek"
|
||||
},
|
||||
{
|
||||
"codename": "add_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "change_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "view_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "add_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "change_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "view_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "add_komentar",
|
||||
"ct_app_label": "korektury",
|
||||
|
@ -224,6 +254,21 @@
|
|||
"ct_app_label": "seminar",
|
||||
"ct_model": "clanek"
|
||||
},
|
||||
{
|
||||
"codename": "add_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
},
|
||||
{
|
||||
"codename": "change_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
},
|
||||
{
|
||||
"codename": "view_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
},
|
||||
{
|
||||
"codename": "add_konfera",
|
||||
"ct_app_label": "seminar",
|
||||
|
@ -304,41 +349,21 @@
|
|||
"ct_app_label": "seminar",
|
||||
"ct_model": "novinky"
|
||||
},
|
||||
{
|
||||
"codename": "add_organizator",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "organizator"
|
||||
},
|
||||
{
|
||||
"codename": "change_organizator",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "organizator"
|
||||
},
|
||||
{
|
||||
"codename": "delete_organizator",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "organizator"
|
||||
},
|
||||
{
|
||||
"codename": "view_organizator",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "organizator"
|
||||
},
|
||||
{
|
||||
"codename": "add_osoba",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "osoba"
|
||||
},
|
||||
{
|
||||
"codename": "change_osoba",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "osoba"
|
||||
},
|
||||
{
|
||||
"codename": "delete_osoba",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "osoba"
|
||||
},
|
||||
{
|
||||
"codename": "view_osoba",
|
||||
"ct_app_label": "seminar",
|
||||
|
@ -404,21 +429,11 @@
|
|||
"ct_app_label": "seminar",
|
||||
"ct_model": "problem"
|
||||
},
|
||||
{
|
||||
"codename": "add_resitel",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "resitel"
|
||||
},
|
||||
{
|
||||
"codename": "change_resitel",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "resitel"
|
||||
},
|
||||
{
|
||||
"codename": "delete_resitel",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "resitel"
|
||||
},
|
||||
{
|
||||
"codename": "view_resitel",
|
||||
"ct_app_label": "seminar",
|
||||
|
@ -603,50 +618,5 @@
|
|||
"codename": "view_taggeditem",
|
||||
"ct_app_label": "taggit",
|
||||
"ct_model": "taggeditem"
|
||||
},
|
||||
{
|
||||
"codename": "add_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "change_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "view_fotkaheader",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaheader"
|
||||
},
|
||||
{
|
||||
"codename": "add_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "change_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "view_fotkaurlvazba",
|
||||
"ct_app_label": "header_fotky",
|
||||
"ct_model": "fotkaurlvazba"
|
||||
},
|
||||
{
|
||||
"codename": "add_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
},
|
||||
{
|
||||
"codename": "change_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
},
|
||||
{
|
||||
"codename": "view_deadline",
|
||||
"ct_app_label": "seminar",
|
||||
"ct_model": "deadline"
|
||||
}
|
||||
]
|
|
@ -28,7 +28,7 @@ Generuje se za pomocí::
|
|||
|
||||
nebo (v případě meníčka)::
|
||||
|
||||
./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json
|
||||
./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json
|
||||
./fix_json.py data/sitetree_new.json data/sitetree.json
|
||||
|
||||
deploy_v2
|
||||
|
|
|
@ -27,6 +27,7 @@ Dokumentace (jak v ``docs/``, tak přímo v kódu) je psaná ve
|
|||
:titlesonly:
|
||||
|
||||
vyvoj
|
||||
zavislosti
|
||||
sphinx
|
||||
skripty
|
||||
modules/modules
|
||||
|
|
|
@ -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."
|
||||
|
|
@ -37,7 +37,7 @@ Kromě toho je potřeba mít účet na `Gitee <https://gitea.ks.matfyz.cz>`_, kd
|
|||
bydlí gitový repozitář s kódem.
|
||||
|
||||
.. tip:: Potřebné balíčky v různých distribucích jsou sepsané v :ref:`tabulce
|
||||
prerekvizit <Tabulka prerekvizit v různých distribucích>`.
|
||||
prerekvizit <Alternativní jména balíčků>`.
|
||||
|
||||
Doporučené
|
||||
^^^^^^^^^^
|
||||
|
|
97
docs/zavislosti.rst
Normal file
97
docs/zavislosti.rst
Normal file
|
@ -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."
|
||||
|
18
korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py
Normal file
18
korektury/migrations/0020_lepsi_popis_nazvu_PDF_v_adminu.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.28 on 2023-06-19 19:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('korektury', '0019_auto_20221205_2014'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='korekturovanepdf',
|
||||
name='nazev',
|
||||
field=models.CharField(help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF', max_length=50, verbose_name='název PDF'),
|
||||
),
|
||||
]
|
|
@ -55,7 +55,7 @@ class KorekturovanePDF(models.Model):
|
|||
|
||||
cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF')
|
||||
|
||||
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. 22.1 verze 4) korekturovaného PDF')
|
||||
nazev = models.CharField(u'název PDF',blank = False,max_length=50, help_text='Název (např. `22.1 | analyza v4` nebo `propagace | letacek v0`) korekturovaného PDF')
|
||||
|
||||
komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)')
|
||||
|
||||
|
|
|
@ -54,6 +54,9 @@ LOGIN_REDIRECT_URL = 'profil'
|
|||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
DOBA_ODHLASENI_PRI_ZASKRTNUTI_NEODHLASOVAT = 365 * 24 * 3600 # rok
|
||||
|
||||
# View pro chybu s CSRF tokenem (např. se sušenkami)
|
||||
CSRF_FAILURE_VIEW = 'various.views.csrf_error'
|
||||
|
||||
# Modules configuration
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
|
@ -151,6 +154,7 @@ INSTALLED_APPS = (
|
|||
'soustredeni',
|
||||
'treenode',
|
||||
'vyroci',
|
||||
'sifrovacka',
|
||||
|
||||
# Admin upravy:
|
||||
|
||||
|
|
|
@ -27,8 +27,9 @@ DEBUG = False
|
|||
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz',
|
||||
'mamweb.bezva.org','gimli.ms.mff.cuni.cz']
|
||||
ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa
|
||||
'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny…
|
||||
]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
|
|
|
@ -32,7 +32,10 @@ DEBUG = True
|
|||
|
||||
TEMPLATES[0]['OPTIONS']['debug'] = True
|
||||
|
||||
ALLOWED_HOSTS = ['*.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz', 'mam.mff.cuni.cz', 'mam-test.kam.mff.cuni.cz', 'gimli.ms.mff.cuni.cz', 'mam-test.ks.matfyz.cz']
|
||||
ALLOWED_HOSTS = [
|
||||
'mam-test.ks.matfyz.cz',
|
||||
'*.mam.mff.cuni.cz', # Asi se nikdy nepoužije…
|
||||
]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@charset "utf-8"; /* vynuť utf-8 */
|
||||
@import url("rozliseni.css");
|
||||
|
||||
@font-face {
|
||||
|
@ -53,6 +54,17 @@ a.login-ref-admin {
|
|||
color: #fffbf6;
|
||||
}
|
||||
|
||||
.napis-webarum {
|
||||
display: inline;
|
||||
color: #fffbf6;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.napis-webarum a {
|
||||
color: #f9d59e;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* odkazy a nadpisy */
|
||||
|
||||
a {
|
||||
|
@ -1233,6 +1245,7 @@ div.gdpr {
|
|||
.dosla_reseni tr th, .dosla_reseni tr td {
|
||||
padding: 1px 10px 1px 10px;
|
||||
border-collapse: collapse;
|
||||
min-width: 8em; /*Nastřeleno, aby se řádky s řešeními nezalamovaly. Teoreticky není potřeba pro th, ale whatever.*/
|
||||
}
|
||||
|
||||
.dosla_reseni tr:nth-child(even) {
|
||||
|
@ -1260,3 +1273,31 @@ label[for=id_skola] {
|
|||
.bodovani>input {
|
||||
width: 4em;
|
||||
}
|
||||
|
||||
.bodovani>input::placeholder {
|
||||
color: lightgray;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bodovani>input::-webkit-input-placeholder { /* Edge */
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
|
||||
/* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */
|
||||
/* Podle https://stackoverflow.com/a/48290544 */
|
||||
/* U autocomplete.ModelSelect2Multiple vyžaduje 'data-dropdown-css-class': 's2m-se-zaskrtavatky' */
|
||||
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=true]:before {
|
||||
content: '☑ ';
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
|
||||
.s2m-se-zaskrtavatky .select2-results__option[aria-selected=false]:before {
|
||||
content: '◻ ';
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
|
||||
/* Oranžové zvýraznění v Select2 */
|
||||
.select2-results__option--highlighted {
|
||||
background-color: #e84e10 !important;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
{% load static %}
|
||||
|
||||
{% block errorheading %}
|
||||
<br> {# Meníčko nedostaneme, protože dostáváme prázdný kontext. Tak alespoň ať se O-JO-JO-JO-JOJ neschovává pod ním #}
|
||||
{% block nadpis1a %}
|
||||
O-jo-jo-jo-joj
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block errortext %}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='cs'>
|
||||
<head>
|
||||
<meta charset="utf-8"> {# vynuť UTF-8. #}
|
||||
<title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&M{% endblock title %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon">
|
||||
|
@ -49,6 +50,8 @@
|
|||
<a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a>
|
||||
{% endif %}
|
||||
<a class="login-ref-admin" href='/admin'>[admin mainpage]</a>
|
||||
|
||||
<span class="napis-webarum">Něco ti nejde/nefunguje/mate tě? <a class="login-ref-admin" href='mailto:web@mam.mff.cuni.cz'>Napiš webařům!</a></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš
|
|||
Slovníček:
|
||||
Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám.
|
||||
Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný.
|
||||
Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
|
||||
Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli.
|
||||
Nahrát řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.)
|
||||
Vlož řešení = Vložit řešení bez vztahu k aktuálnímu uživateli.
|
||||
|
||||
TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
|
||||
"""
|
|
@ -29,6 +29,8 @@ class PosliReseniForm(forms.Form):
|
|||
attrs={
|
||||
'data-placeholder--id': '-1',
|
||||
'data-placeholder--text': '---',
|
||||
'data-close-on-select': 'false',
|
||||
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
|
||||
'data-allow-clear': 'true'
|
||||
},
|
||||
),
|
||||
|
@ -43,6 +45,8 @@ class PosliReseniForm(forms.Form):
|
|||
url='autocomplete_resitel',
|
||||
attrs = {'data-placeholder--id': '-1',
|
||||
'data-placeholder--text' : '---',
|
||||
'data-close-on-select': 'false',
|
||||
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
|
||||
'data-allow-clear': 'true'})
|
||||
)
|
||||
|
||||
|
@ -62,12 +66,6 @@ class PosliReseniForm(forms.Form):
|
|||
#poznamka = models.TextField('neveřejná poznámka', blank=True,
|
||||
# help_text='Neveřejná poznámka k řešení (plain text)')
|
||||
|
||||
#TODO body do cisla
|
||||
#TODO prilohy
|
||||
|
||||
##def __init__(self, *args, **kwargs):
|
||||
## super().__init__(*args, **kwargs)
|
||||
## #self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()])
|
||||
|
||||
class NahrajReseniForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -80,23 +78,40 @@ class NahrajReseniForm(forms.ModelForm):
|
|||
url='autocomplete_problem_odevzdatelny',
|
||||
attrs = {'data-placeholder--id': '-1',
|
||||
'data-placeholder--text' : '---',
|
||||
'data-close-on-select': 'false',
|
||||
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
|
||||
'data-allow-clear': 'true'},
|
||||
forward=["nadproblem_id"],
|
||||
),
|
||||
'resitele':
|
||||
autocomplete.ModelSelect2Multiple(
|
||||
url='autocomplete_resitel_public',
|
||||
attrs = {'data-placeholder--id': '-1',
|
||||
'data-placeholder--text' : '---',
|
||||
'data-close-on-select': 'false',
|
||||
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
|
||||
'data-allow-clear': 'true'},
|
||||
)
|
||||
}
|
||||
|
||||
nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
|
||||
if 'resitele' in self.fields:
|
||||
# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to.
|
||||
self.fields['resitele'].required = False
|
||||
self.fields['resitele'].label = "Další autoři"
|
||||
if 'problem' in self.fields:
|
||||
self.fields['problem'].label = "Všechny řešené problémy"
|
||||
|
||||
def clean_problem(self):
|
||||
problem = self.cleaned_data.get('problem')
|
||||
for p in problem:
|
||||
if p.stav != m.Problem.STAV_ZADANY:
|
||||
raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!")
|
||||
return problem
|
||||
|
||||
ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
|
||||
form = NahrajReseniForm,
|
||||
|
@ -223,6 +238,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
|
|||
'reseni_od': terminy[-2] if rocnik is None else terminy[0],
|
||||
'reseni_do': terminy[-1],
|
||||
'neobodovane': False,
|
||||
'barvicky': True,
|
||||
}
|
||||
return initial
|
||||
|
||||
|
@ -247,3 +263,4 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form):
|
|||
reseni_od = forms.DateField(input_formats=[DATE_FORMAT])
|
||||
reseni_do = forms.DateField(input_formats=[DATE_FORMAT])
|
||||
neobodovane = forms.BooleanField(required=False)
|
||||
barvicky = forms.BooleanField(required=False)
|
||||
|
|
|
@ -7,19 +7,20 @@
|
|||
{% block content %}
|
||||
<h1>
|
||||
{% block nadpis1a %}
|
||||
Vložit řešení
|
||||
Nahrát řešení
|
||||
{% endblock %}
|
||||
</h1>
|
||||
|
||||
<p style="text-align: justify">Když řešení různých témátek vložíš každé zvlášť, lépe se v nich vyznáme a třeba ti je i rychleji opravíme.</p>
|
||||
|
||||
<p>Pokud řešíte ve více lidech, je <strong>nutné</strong> přidat tyto lidi jako „Autory řešení“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobní údaje“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
|
||||
|
||||
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' %}" method="post" onsubmit="return zkontroluj_prilohy();">
|
||||
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();">
|
||||
{% csrf_token %}
|
||||
<table class='form' id="reseni">
|
||||
<table class='form'>
|
||||
<tr>
|
||||
<td><label class="field-label field-required" for="tema">Téma:</label></td>
|
||||
<td><input id="tema" disabled="" type="text" value="{{ nadproblem }}"></td>
|
||||
</tr>
|
||||
|
||||
{% with field=form.problem %}
|
||||
<tr>
|
||||
{% for field in form %}
|
||||
<td>
|
||||
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
|
||||
{{ field.label }}:
|
||||
|
@ -28,15 +29,54 @@
|
|||
<td>
|
||||
{{ field }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
{% if field.errors %}
|
||||
<tr>
|
||||
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
</table>
|
||||
|
||||
{% for field in form.hidden_fields %}
|
||||
{{ field }}
|
||||
{% endfor %}
|
||||
|
||||
<hr>
|
||||
<h4>Spolupráce s dalšími řešiteli</h4>
|
||||
|
||||
<p>Pokud řešíte ve více lidech, je <strong>potřeba</strong> přidat tyto lidi jako „Další autory“. V tomto poli se vyhledává podle přezdívek, které si lze nastavit v „Osobních údajích“. Sebe vyplňovat nemusíte a za skupinu odevzdávejte pouze <strong>jednou</strong> (ne každý sám).</p>
|
||||
|
||||
<table class='form'>
|
||||
{% with field=form.resitele %}
|
||||
<tr>
|
||||
<td>
|
||||
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
|
||||
{{ field.label }}:
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
{{ field }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% if field.errors %}
|
||||
<tr>
|
||||
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
{% include "odevzdavatko/prilohy.html" %}
|
||||
|
||||
{{form.non_field_errors}}
|
||||
|
||||
<hr>
|
||||
<h4>Odevzdat řešení</h4>
|
||||
<input type="submit" value="Odevzdat">
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% block nadpis1a %}
|
||||
Nahrát řešení
|
||||
{% endblock %}
|
||||
</h1>
|
||||
|
||||
<h4>Seznam témat k odevzdání</h4>
|
||||
|
||||
<ul>
|
||||
{% for problem in object_list %}
|
||||
<li><a href="{% url 'seminar_nahraj_reseni' problem.id %}">{{ problem }}</a></li>
|
||||
{% empty %}
|
||||
<li>Nelze nic odevzdávat.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
<h4>Soubory s řešením</h4>
|
||||
|
||||
<p style="text-align: justify">Maximální součet velikostí příloh je cca 49 MB. Pokud je to možné a dává to smysl, pošli nám prosím své řešení ve formátu PDF, ostatní formáty nemusíme umět otevřít.</p>
|
||||
<p style="text-align: justify">Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p>
|
||||
<p style="text-align: justify">Pokud je to možné a dává to smysl (tj. není to třeba kód nebo doprovodný obrázek), pošli nám prosím své řešení ve formátu <strong>PDF</strong>, ostatní formáty nemusíme umět otevřít.</p>
|
||||
<p style="text-align: justify">Pokud svůj soubor <strong>rozumně pojmenuješ</strong>, urychlíš opravování a předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z <code>img_250921_101205.pdf</code> nepoznáme, kterou úlohu jsi odevzdal, zato <code>uloha_3.pdf</code> nebo <code>tema_1.pdf</code>, to už je něco jiného. Případně můžeš využít i poznámku řešitele.</p>
|
||||
<p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49 MB</strong>.</p>
|
||||
|
||||
<div id="form_set">
|
||||
{% for form in prilohy.forms %}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #}
|
||||
{% load barvy_reseni %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -11,6 +12,7 @@
|
|||
Od data (vyjma): {{ filtr.reseni_od }}
|
||||
Do data (včetně): {{ filtr.reseni_do }}
|
||||
<span title="Jen neobodovaná řešení">🔨?</span> {{ filtr.neobodovane }}
|
||||
<span title="Obarvit shodná řešení shodně">🎨?</span> {{ filtr.barvicky }}
|
||||
<input type=submit value="→">
|
||||
</form>
|
||||
|
||||
|
@ -36,12 +38,15 @@ Do data (včetně): {{ filtr.reseni_do }}
|
|||
{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #}
|
||||
{{ resitel }}
|
||||
</td>
|
||||
{% for hodn in hodnoty %}
|
||||
{% for soucet,bunka in hodnoty %}
|
||||
<td>
|
||||
{% if hodn %}
|
||||
<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}">
|
||||
{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}}
|
||||
</a>
|
||||
{% for reseni,hodnoceni in bunka %}
|
||||
<a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}">
|
||||
{{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b)
|
||||
</a><br>
|
||||
{% endfor %}
|
||||
{% if bunka|length > 1 %}
|
||||
<b>Σ: {{soucet}} b</b>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
|
|
15
odevzdavatko/templatetags/barvy_reseni.py
Normal file
15
odevzdavatko/templatetags/barvy_reseni.py
Normal file
|
@ -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}'
|
|
@ -19,8 +19,9 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
|
|||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'),
|
||||
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
|
||||
path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'),
|
||||
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'),
|
||||
path('resitel/nahraj_reseni/<int:nadproblem_id>/', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
|
||||
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'),
|
||||
|
||||
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
|
||||
|
|
|
@ -13,6 +13,7 @@ from django.db.models import Q
|
|||
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
import logging
|
||||
|
||||
|
@ -37,14 +38,6 @@ logger = logging.getLogger(__name__)
|
|||
# Taky se může hodit:
|
||||
# - Tabulka všech řešitelů x všech problémů?
|
||||
|
||||
@dataclass
|
||||
class SouhrnReseni:
|
||||
"""Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce."""
|
||||
pocet_reseni : int
|
||||
posledni_odevzdani : datetime.datetime
|
||||
body : float
|
||||
|
||||
|
||||
class TabulkaOdevzdanychReseniView(ListView):
|
||||
template_name = 'odevzdavatko/tabulka.html'
|
||||
model = m.Hodnoceni
|
||||
|
@ -70,6 +63,7 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
reseni_od = fcd["reseni_od"]
|
||||
reseni_do = fcd["reseni_do"]
|
||||
jen_neobodovane = fcd["neobodovane"]
|
||||
self.barvicky = fcd["barvicky"]
|
||||
else:
|
||||
initial = FiltrForm.gen_initial(self.aktualni_rocnik)
|
||||
resitele = initial['resitele']
|
||||
|
@ -77,6 +71,7 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
reseni_od = initial['reseni_od'][0]
|
||||
reseni_do = initial['reseni_do'][0]
|
||||
jen_neobodovane = initial["neobodovane"]
|
||||
self.barvicky = initial["barvicky"]
|
||||
|
||||
|
||||
# Chceme jen letošní problémy
|
||||
|
@ -120,42 +115,45 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
return qs
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
# TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl…
|
||||
# self.resitele, self.reseni a self.problemy jsou již nastavené
|
||||
|
||||
ctx = super().get_context_data(*args, **kwargs)
|
||||
ctx['problemy'] = self.problemy
|
||||
ctx['resitele'] = self.resitele
|
||||
tabulka = dict()
|
||||
tabulka: dict[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict()
|
||||
soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict()
|
||||
|
||||
def pridej_reseni(problem, resitel, body, cas):
|
||||
def pridej_reseni(resitel, hodnoceni):
|
||||
problem = hodnoceni.problem
|
||||
body = hodnoceni.body
|
||||
cas = hodnoceni.reseni.cas_doruceni
|
||||
reseni = hodnoceni.reseni
|
||||
if problem not in tabulka:
|
||||
tabulka[problem] = dict()
|
||||
soucty[problem] = dict()
|
||||
if resitel not in tabulka[problem]:
|
||||
tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body)
|
||||
tabulka[problem][resitel] = [(reseni, hodnoceni)]
|
||||
soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme
|
||||
else:
|
||||
tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas)
|
||||
# Zvětšení počtu bodů o aktuální počet, pokud se tam někde nevyskytuje None – pak je součet taky None ("Pozor, nezadané body")
|
||||
tabulka[problem][resitel].body = tabulka[problem][resitel].body + body if body is not None and tabulka[problem][resitel].body is not None else None
|
||||
tabulka[problem][resitel].pocet_reseni += 1
|
||||
# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele
|
||||
tabulka[problem][resitel].problem_id = problem.id
|
||||
tabulka[problem][resitel].resitel_id = resitel.id
|
||||
tabulka[problem][resitel].append((reseni, hodnoceni))
|
||||
soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme
|
||||
|
||||
for hodnoceni in self.get_queryset():
|
||||
for resitel in hodnoceni.reseni.resitele.all():
|
||||
pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni)
|
||||
pridej_reseni(resitel, hodnoceni)
|
||||
|
||||
hodnoty = []
|
||||
resitele_do_tabulky = []
|
||||
hodnoty: list[list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
|
||||
resitele_do_tabulky: list[m.Resitel] = []
|
||||
for resitel in self.resitele:
|
||||
dostal_body = False
|
||||
resiteluv_radek = []
|
||||
resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy
|
||||
for problem in self.problemy:
|
||||
if problem in tabulka and resitel in tabulka[problem]:
|
||||
resiteluv_radek.append(tabulka[problem][resitel])
|
||||
resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel]))
|
||||
dostal_body = True
|
||||
else:
|
||||
resiteluv_radek.append(None)
|
||||
resiteluv_radek.append((Decimal(0),[]))
|
||||
if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body:
|
||||
hodnoty.append(resiteluv_radek)
|
||||
resitele_do_tabulky.append(resitel)
|
||||
|
@ -165,6 +163,7 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
ctx['form'] = ctx['filtr']
|
||||
# Pro maximum v přesměrovátku ročníků
|
||||
ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik
|
||||
ctx['barvicky'] = self.barvicky
|
||||
if 'rocnik' in self.kwargs:
|
||||
ctx['rocnik'] = self.kwargs['rocnik']
|
||||
else:
|
||||
|
@ -174,6 +173,11 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
|
||||
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
|
||||
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
|
||||
"""Rozskok mezi více řešeními téhož problému od téhož řešitele.
|
||||
|
||||
Asi už bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-)
|
||||
|
||||
V případě, že takové řešení existuje jen jedno, tak na něj přesměruje."""
|
||||
model = m.Reseni
|
||||
template_name = 'odevzdavatko/seznam.html'
|
||||
|
||||
|
@ -316,7 +320,8 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
|
|||
if len(zmeny_bodu) == 1:
|
||||
hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]])
|
||||
# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno
|
||||
if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4:
|
||||
if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4 and len(zmeny_bodu) != 2:
|
||||
# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
|
||||
logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.")
|
||||
hodnoceni.body = -0.1
|
||||
hodnoceni.save()
|
||||
|
@ -367,8 +372,8 @@ class SeznamAktualnichReseniView(SeznamReseniView):
|
|||
return qs
|
||||
|
||||
|
||||
class PosliReseniView(LoginRequiredMixin, FormView):
|
||||
template_name = 'odevzdavatko/posli_reseni.html'
|
||||
class VlozReseniView(LoginRequiredMixin, FormView):
|
||||
template_name = 'odevzdavatko/vloz_reseni.html'
|
||||
form_class = f.PosliReseniForm
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -399,12 +404,31 @@ class PosliReseniView(LoginRequiredMixin, FormView):
|
|||
return data
|
||||
|
||||
|
||||
class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
|
||||
model = m.Problem
|
||||
template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(stav=m.Problem.STAV_ZADANY, nadproblem__isnull=True)
|
||||
|
||||
|
||||
class NahrajReseniView(LoginRequiredMixin, CreateView):
|
||||
model = m.Reseni
|
||||
template_name = 'odevzdavatko/nahraj_reseni.html'
|
||||
form_class = f.NahrajReseniForm
|
||||
nadproblem: m.Problem
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
nadproblem_id = self.kwargs["nadproblem_id"]
|
||||
self.nadproblem = get_object_or_404(m.Problem, id=nadproblem_id)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Zaříznutí nezadaných problémů
|
||||
if self.nadproblem.stav != m.Problem.STAV_ZADANY:
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
# Zaříznutí starých řešitelů:
|
||||
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
|
||||
osoba = m.Osoba.objects.get(user=self.request.user)
|
||||
|
@ -417,12 +441,23 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
})
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
nadproblem_id = self.nadproblem.id
|
||||
return {
|
||||
"nadproblem_id": nadproblem_id,
|
||||
"problem": [] if self.nadproblem.podproblem.filter(stav=m.Problem.STAV_ZADANY).exists() else nadproblem_id
|
||||
|
||||
}
|
||||
|
||||
def get_context_data(self,**kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
if self.request.POST:
|
||||
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
|
||||
else:
|
||||
data['prilohy'] = f.ReseniSPrilohamiFormSet()
|
||||
|
||||
data["nadproblem_id"] = self.nadproblem.id
|
||||
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
|
||||
return data
|
||||
|
||||
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
|
||||
|
@ -474,4 +509,8 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
|
|||
to=list(prijemci),
|
||||
).send()
|
||||
|
||||
return formularOKView(self.request, text='Řešení úspěšně odevzdáno')
|
||||
return formularOKView(
|
||||
self.request,
|
||||
text='Řešení úspěšně odevzdáno',
|
||||
dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<a href="{% url 'logout' %}">Odhlásit se</a><br>
|
||||
<a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br>
|
||||
<a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br>
|
||||
<a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br>
|
||||
<a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br>
|
||||
|
||||
|
||||
|
|
63
personalni/tests.py
Normal file
63
personalni/tests.py
Normal file
|
@ -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.
|
||||
|
||||
|
|
@ -173,7 +173,11 @@ def resitelEditView(request):
|
|||
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
|
||||
resitel_edit.save()
|
||||
osoba_edit.save()
|
||||
return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>')
|
||||
return formularOKView(
|
||||
request,
|
||||
text='Údaje byly úspěšně uloženy.',
|
||||
dalsi_odkazy=[("Vrátit se zpět na profil", reverse("profil"))],
|
||||
)
|
||||
|
||||
return render(request, 'personalni/udaje/edit.html', {'form': form})
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ psycopg2
|
|||
html5lib
|
||||
ipython
|
||||
Pillow
|
||||
pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0
|
||||
pytz
|
||||
six
|
||||
pexpect
|
||||
|
@ -24,7 +25,7 @@ django-ckeditor
|
|||
django-cleanup # Uklízí media/ od smazaných „databázových“ souborů
|
||||
django-flat-theme
|
||||
django-taggit
|
||||
django-autocomplete-light>=3.9.0rc1
|
||||
django-autocomplete-light>=3.9.0
|
||||
django-crispy-forms
|
||||
django-imagekit
|
||||
django-polymorphic
|
||||
|
@ -52,9 +53,6 @@ Werkzeug
|
|||
requests
|
||||
# requests-oauthlib
|
||||
|
||||
# uWSGI
|
||||
uWSGI
|
||||
|
||||
# Potřeba pro test data
|
||||
|
||||
lorem
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -491,7 +491,7 @@ class Problem(SeminarModelBase,PolymorphicModel):
|
|||
return self.nadproblem.kod_v_rocniku+".{}".format(self.kod)
|
||||
return str(self.kod)
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
return f'<Není zadaný: {self.kod}>'
|
||||
|
||||
# def verejne(self):
|
||||
# # aktuálně podle stavu problému
|
||||
|
@ -571,9 +571,9 @@ class Tema(Problem):
|
|||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+".t{}".format(self.kod)
|
||||
return "t{}".format(self.kod)
|
||||
return 't'+self.kod
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
return f'<Není zadaný: {self.kod}>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
@ -607,9 +607,9 @@ class Clanek(Problem):
|
|||
# Nemělo by být potřeba
|
||||
# if self.nadproblem:
|
||||
# return self.nadproblem.kod_v_rocniku+".c{}".format(self.kod)
|
||||
return "c{}".format(self.kod)
|
||||
return "c" + self.kod
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
return f'<Není zadaný: {self.kod}>'
|
||||
|
||||
def node(self):
|
||||
return None
|
||||
|
@ -642,12 +642,9 @@ class Uloha(Problem):
|
|||
@cached_property
|
||||
def kod_v_rocniku(self):
|
||||
if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
|
||||
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod)
|
||||
if self.nadproblem:
|
||||
return self.nadproblem.kod_v_rocniku+name
|
||||
return name
|
||||
return f"{self.cislo_zadani.poradi}.{self.kod}"
|
||||
logger.warning(f"K problému {self} byl vyžadován kód v ročníku, i když není zadaný ani vyřešený.")
|
||||
return '<Není zadaný>'
|
||||
return f'<Není zadaný: {self.kod}>'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
|
|
@ -35,6 +35,7 @@ from django.conf import settings
|
|||
import unicodedata
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
|
||||
from seminar.utils import aktivniResitele
|
||||
|
||||
|
@ -534,7 +535,9 @@ class PosledniCisloVysledkovkaView(generic.DetailView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super(PosledniCisloVysledkovkaView, self).get_context_data()
|
||||
rocnik = context['rocnik']
|
||||
cislo = rocnik.cisla.order_by("poradi").last()
|
||||
cislo = rocnik.cisla.order_by("poradi").filter(deadline_v_cisle__isnull=False).last()
|
||||
if cislo is None:
|
||||
raise Http404(f"Ročník {rocnik.rocnik} nemá číslo s deadlinem.")
|
||||
cislopred = cislo.predchozi()
|
||||
context['vysledkovka'] = VysledkovkaDoTeXu(
|
||||
cislo,
|
||||
|
@ -677,9 +680,9 @@ def StavDatabazeView(request):
|
|||
|
||||
|
||||
# Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí)
|
||||
def formularOKView(request, text=''):
|
||||
def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()):
|
||||
template_name = 'seminar/formular_ok.html'
|
||||
odkazy = [
|
||||
odkazy = list(dalsi_odkazy) + [
|
||||
# (Text, odkaz)
|
||||
('Vrátit se na titulní stránku', reverse('titulni_strana')),
|
||||
('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')),
|
||||
|
|
0
sifrovacka/__init__.py
Normal file
0
sifrovacka/__init__.py
Normal file
8
sifrovacka/admin.py
Normal file
8
sifrovacka/admin.py
Normal file
|
@ -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
Normal file
5
sifrovacka/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SifrovackaConfig(AppConfig):
|
||||
name = 'sifrovacka'
|
18
sifrovacka/forms.py
Normal file
18
sifrovacka/forms.py
Normal file
|
@ -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
Normal file
34
sifrovacka/migrations/0001_initial.py
Normal file
|
@ -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
Normal file
28
sifrovacka/migrations/0002_auto_20231015_1944.py
Normal file
|
@ -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
Normal file
18
sifrovacka/migrations/0003_odpoveducastnika_uspech.py
Normal file
|
@ -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
Normal file
0
sifrovacka/migrations/__init__.py
Normal file
27
sifrovacka/models.py
Normal file
27
sifrovacka/models.py
Normal file
|
@ -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
Normal file
25
sifrovacka/templates/sifrovacka/odpovedi_list.html
Normal file
|
@ -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
Normal file
46
sifrovacka/templates/sifrovacka/sifrovacka.html
Normal file
|
@ -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
Normal file
17
sifrovacka/urls.py
Normal file
|
@ -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
Normal file
33
sifrovacka/views.py
Normal file
|
@ -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
|
87
various/static/various/img/zere_kostku.svg
Normal file
87
various/static/various/img/zere_kostku.svg
Normal file
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
19
various/templates/various/403_csrf.html
Normal file
19
various/templates/various/403_csrf.html
Normal file
|
@ -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 %}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
<th class='border-r'>#</th>
|
||||
<th class='border-r'>Jméno</th>
|
||||
{% for p in vysledkovka.temata_a_spol%}
|
||||
<th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </a> #}</th>
|
||||
<th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}<span title="{{ p }}">{{ p.kod_v_rocniku }}</span>{# </a> #}</th>
|
||||
|
||||
{# TODELETE #}
|
||||
{% for podproblemy in vysledkovka.podproblemy_iter.next %}
|
||||
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
|
||||
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th>
|
||||
{% endfor %}
|
||||
{# TODELETE #}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
{# TODELETE #}
|
||||
{% for podproblemy in vysledkovka.podproblemy_iter.next %}
|
||||
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
|
||||
<th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th>
|
||||
{% endfor %}
|
||||
{# TODELETE #}
|
||||
|
||||
|
|
|
@ -257,7 +257,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
|
||||
hlavni_problemy = set()
|
||||
for p in self.problemy:
|
||||
hlavni_problemy.add(p.hlavni_problem)
|
||||
hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy…
|
||||
|
||||
# zunikátnění
|
||||
hlavni_problemy = list(hlavni_problemy)
|
||||
|
@ -313,7 +313,7 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
|
||||
# Sečteme hodnocení
|
||||
for hodnoceni in self.hodnoceni_do_cisla:
|
||||
prob = hodnoceni.problem
|
||||
prob = hodnoceni.problem.get_real_instance()
|
||||
nadproblem = prob.hlavni_problem.id
|
||||
|
||||
# Když nadproblém není "téma", pak je "Ostatní"
|
||||
|
@ -366,18 +366,12 @@ class VysledkovkaCisla(Vysledkovka):
|
|||
for problem in self.problemy:
|
||||
h_problem = problem.hlavni_problem
|
||||
if h_problem in temata_a_spol:
|
||||
podproblemy[h_problem.id].append(problem)
|
||||
podproblemy[h_problem.id].append(problem.get_real_instance())
|
||||
else:
|
||||
podproblemy[-1].append(problem)
|
||||
podproblemy[-1].append(problem.get_real_instance())
|
||||
|
||||
for podproblem in podproblemy.keys():
|
||||
def int_or_zero(p):
|
||||
try:
|
||||
return int(p.kod)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero)
|
||||
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)
|
||||
return podproblemy
|
||||
|
||||
@cached_property
|
||||
|
|
Loading…
Reference in a new issue