Compare commits

..

No commits in common. "b1204c1a20a9ca82d7440795afe6770287f89afd" and "166fe0862abfbc3f018916928eab6cd9393b7274" have entirely different histories.

62 changed files with 243 additions and 985 deletions

View file

@ -1,5 +1,5 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.encoding import force_str from django.utils.encoding import force_text
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_str(r[c]) for c in self.columns]) + '\n' yield '\t'.join([force_text(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())

View file

@ -1,6 +1,6 @@
import datetime import datetime
from django.utils.encoding import force_str from django.utils.encoding import force_text
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_str(rocnik.prvni_rok) of.headers['year'] = force_text(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'

View file

@ -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_str from django.utils.encoding import force_text
from .utils import default_ovvpfile from .utils import default_ovvpfile
from seminar.models import Rocnik, Soustredeni from seminar.models import Rocnik, Soustredeni

View file

@ -70,17 +70,23 @@ class PublicResitelAutocomplete(LoginRequiredAjaxMixin, autocomplete.Select2Quer
class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView): class OdevzdatelnyProblemAutocomplete(autocomplete.Select2QuerySetView):
""" View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """ """ View k :mod:`dal.autocomplete` pro vyhledávání problémů především v odevzdávátku. """
def get_queryset(self): def get_queryset(self):
qs = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY) 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)
if self.q: if self.q:
qs = qs.filter( qs = qs.filter(
Q(nazev__icontains=self.q)) 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 return qs
class ProblemAutocomplete(autocomplete.Select2QuerySetView): class ProblemAutocomplete(autocomplete.Select2QuerySetView):

View file

@ -437,7 +437,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Nahrát řešení", "title": "Poslat řešení",
"tree": 1, "tree": 1,
"url": "seminar_nahraj_reseni", "url": "seminar_nahraj_reseni",
"urlaspattern": true "urlaspattern": true
@ -476,9 +476,9 @@
"access_perm_type": 1, "access_perm_type": 1,
"access_permissions": [ "access_permissions": [
[ [
"org", "change_hodnoceni",
"auth", "seminar",
"user" "hodnoceni"
] ]
], ],
"access_restricted": true, "access_restricted": true,
@ -719,7 +719,7 @@
"insitetree": true, "insitetree": true,
"parent": 21, "parent": 21,
"sort_order": 36, "sort_order": 36,
"title": "Vložit řešení", "title": "Nahrát řešení",
"tree": 1, "tree": 1,
"url": "seminar_vloz_reseni", "url": "seminar_vloz_reseni",
"urlaspattern": true "urlaspattern": true
@ -1026,36 +1026,6 @@
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 51 "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": { "fields": {
"access_guest": false, "access_guest": false,
@ -1071,13 +1041,13 @@
"inmenu": true, "inmenu": true,
"insitetree": true, "insitetree": true,
"parent": 28, "parent": 28,
"sort_order": 53, "sort_order": 52,
"title": "Přidat PDF", "title": "Přidat PDF",
"tree": 1, "tree": 1,
"url": "/admin/korektury/korekturovanepdf/add/", "url": "/admin/korektury/korekturovanepdf/add/",
"urlaspattern": false "urlaspattern": false
}, },
"model": "sitetree.treeitem", "model": "sitetree.treeitem",
"pk": 53 "pk": 52
} }
] ]

View file

@ -64,36 +64,6 @@
"ct_app_label": "galerie", "ct_app_label": "galerie",
"ct_model": "obrazek" "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", "codename": "add_komentar",
"ct_app_label": "korektury", "ct_app_label": "korektury",
@ -254,21 +224,6 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "clanek" "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", "codename": "add_konfera",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -349,21 +304,41 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "novinky" "ct_model": "novinky"
}, },
{
"codename": "add_organizator",
"ct_app_label": "seminar",
"ct_model": "organizator"
},
{ {
"codename": "change_organizator", "codename": "change_organizator",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "organizator" "ct_model": "organizator"
}, },
{
"codename": "delete_organizator",
"ct_app_label": "seminar",
"ct_model": "organizator"
},
{ {
"codename": "view_organizator", "codename": "view_organizator",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "organizator" "ct_model": "organizator"
}, },
{
"codename": "add_osoba",
"ct_app_label": "seminar",
"ct_model": "osoba"
},
{ {
"codename": "change_osoba", "codename": "change_osoba",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "osoba" "ct_model": "osoba"
}, },
{
"codename": "delete_osoba",
"ct_app_label": "seminar",
"ct_model": "osoba"
},
{ {
"codename": "view_osoba", "codename": "view_osoba",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -429,11 +404,21 @@
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "problem" "ct_model": "problem"
}, },
{
"codename": "add_resitel",
"ct_app_label": "seminar",
"ct_model": "resitel"
},
{ {
"codename": "change_resitel", "codename": "change_resitel",
"ct_app_label": "seminar", "ct_app_label": "seminar",
"ct_model": "resitel" "ct_model": "resitel"
}, },
{
"codename": "delete_resitel",
"ct_app_label": "seminar",
"ct_model": "resitel"
},
{ {
"codename": "view_resitel", "codename": "view_resitel",
"ct_app_label": "seminar", "ct_app_label": "seminar",
@ -618,5 +603,50 @@
"codename": "view_taggeditem", "codename": "view_taggeditem",
"ct_app_label": "taggit", "ct_app_label": "taggit",
"ct_model": "taggeditem" "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"
} }
] ]

View file

@ -28,7 +28,7 @@ Generuje se za pomocí::
nebo (v případě meníčka):: nebo (v případě meníčka)::
./manage.py dumpdata sitetree --natural-foreign > data/sitetree_new.json ./manage.py dumpdata sitetree --natrual-foreign > data/sitetree_new.json
./fix_json.py data/sitetree_new.json data/sitetree.json ./fix_json.py data/sitetree_new.json data/sitetree.json
deploy_v2 deploy_v2

View file

@ -27,7 +27,6 @@ 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

View file

@ -0,0 +1,25 @@
.. 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."

View file

@ -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 <Alternativní jména balíčků>`. prerekvizit <Tabulka prerekvizit v různých distribucích>`.
Doporučené Doporučené
^^^^^^^^^^ ^^^^^^^^^^

View file

@ -1,97 +0,0 @@
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."

View file

@ -2,6 +2,7 @@
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

View file

@ -1,18 +0,0 @@
# 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'),
),
]

View file

@ -16,6 +16,7 @@ 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
@ -54,7 +55,7 @@ class KorekturovanePDF(models.Model):
cas = models.DateTimeField(u'čas vložení PDF',default=timezone.now,help_text = 'Čas vložení PDF') 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 | analyza v4` nebo `propagace | letacek v0`) korekturovaného 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')
komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)') komentar = models.TextField(u'komentář k PDF',blank = True, help_text='Komentář ke korekturovanému PDF (např. na co se zaměřit)')

View file

@ -5,6 +5,7 @@ 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

View file

@ -28,6 +28,7 @@ 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)
@ -53,9 +54,6 @@ 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 = (
@ -153,7 +151,6 @@ INSTALLED_APPS = (
'soustredeni', 'soustredeni',
'treenode', 'treenode',
'vyroci', 'vyroci',
'sifrovacka',
# Admin upravy: # Admin upravy:

View file

@ -27,9 +27,8 @@ DEBUG = False
TEMPLATE_DEBUG = False TEMPLATE_DEBUG = False
ALLOWED_HOSTS = ['mam.mff.cuni.cz', # Hlavní a asi jediná funkční adresa ALLOWED_HOSTS = ['mam.mff.cuni.cz', 'www.mam.mff.cuni.cz', 'atrey.karlin.mff.cuni.cz',
'mam.matfyz.cz', # Ne že by se tohle použilo, ale pro potenciální případ změny… 'mamweb.bezva.org','gimli.ms.mff.cuni.cz']
]
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

View file

@ -32,10 +32,7 @@ DEBUG = True
TEMPLATES[0]['OPTIONS']['debug'] = True TEMPLATES[0]['OPTIONS']['debug'] = True
ALLOWED_HOSTS = [ 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']
'mam-test.ks.matfyz.cz',
'*.mam.mff.cuni.cz', # Asi se nikdy nepoužije…
]
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases

View file

@ -1,4 +1,3 @@
@charset "utf-8"; /* vynuť utf-8 */
@import url("rozliseni.css"); @import url("rozliseni.css");
@font-face { @font-face {
@ -54,17 +53,6 @@ a.login-ref-admin {
color: #fffbf6; color: #fffbf6;
} }
.napis-webarum {
display: inline;
color: #fffbf6;
float: right;
}
.napis-webarum a {
color: #f9d59e;
text-decoration: underline;
}
/* odkazy a nadpisy */ /* odkazy a nadpisy */
a { a {
@ -1245,7 +1233,6 @@ 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) {
@ -1273,31 +1260,3 @@ label[for=id_skola] {
.bodovani>input { .bodovani>input {
width: 4em; 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;
}

View file

@ -3,10 +3,7 @@
{% load static %} {% load static %}
{% 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 #}
{% block nadpis1a %}
O-jo-jo-jo-joj O-jo-jo-jo-joj
{% endblock %}
{% endblock %} {% endblock %}
{% block errortext %} {% block errortext %}

View file

@ -3,7 +3,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang='cs'> <html lang='cs'>
<head> <head>
<meta charset="utf-8"> {# vynuť UTF-8. #}
<title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&amp;M{% endblock title %}</title> <title>{% block title %}{% block nadpis1a %}🦊{% endblock %} | Korespondenční seminář M&amp;M{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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"> <link rel="shortcut icon" href="{% static 'images/MATFYZ_MM_barevne.svg' %}" type="image/x-icon">
@ -50,8 +49,6 @@
<a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a> <a class="login-ref-admin" href='{% url 'admin:flatpages_flatpage_change' flatpage.id %}'>[admin]</a>
{% endif %} {% endif %}
<a class="login-ref-admin" href='/admin'>[admin mainpage]</a> <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> </div>
{% endif %} {% endif %}

View file

@ -71,8 +71,6 @@ 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.

View file

@ -4,8 +4,8 @@ Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řeš
Slovníček: Slovníček:
Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. 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ý. Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný.
Nahrát řešení = Odevdat řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) Poslat řešení = Odevdat ř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. Nahrát řešení = Nahrání ř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? TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného?
""" """

View file

@ -29,8 +29,6 @@ class PosliReseniForm(forms.Form):
attrs={ attrs={
'data-placeholder--id': '-1', 'data-placeholder--id': '-1',
'data-placeholder--text': '---', 'data-placeholder--text': '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true' 'data-allow-clear': 'true'
}, },
), ),
@ -45,8 +43,6 @@ class PosliReseniForm(forms.Form):
url='autocomplete_resitel', url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}) 'data-allow-clear': 'true'})
) )
@ -66,6 +62,12 @@ class PosliReseniForm(forms.Form):
#poznamka = models.TextField('neveřejná poznámka', blank=True, #poznamka = models.TextField('neveřejná poznámka', blank=True,
# help_text='Neveřejná poznámka k řešení (plain text)') # 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 NahrajReseniForm(forms.ModelForm):
class Meta: class Meta:
@ -78,40 +80,23 @@ class NahrajReseniForm(forms.ModelForm):
url='autocomplete_problem_odevzdatelny', url='autocomplete_problem_odevzdatelny',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
forward=["nadproblem_id"],
), ),
'resitele': 'resitele':
autocomplete.ModelSelect2Multiple( autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel_public', url='autocomplete_resitel_public',
attrs = {'data-placeholder--id': '-1', attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---', 'data-placeholder--text' : '---',
'data-close-on-select': 'false',
'data-dropdown-css-class': 's2m-se-zaskrtavatky',
'data-allow-clear': 'true'}, 'data-allow-clear': 'true'},
) )
} }
nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele # FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
if 'resitele' in self.fields: if 'resitele' in self.fields:
# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to. # 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'].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, ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,
form = NahrajReseniForm, form = NahrajReseniForm,
@ -238,7 +223,6 @@ 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
@ -263,4 +247,3 @@ 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)

View file

@ -7,20 +7,19 @@
{% block content %} {% block content %}
<h1> <h1>
{% block nadpis1a %} {% block nadpis1a %}
Nahrát řešení Vložit řešení
{% endblock %} {% endblock %}
</h1> </h1>
<form enctype="multipart/form-data" action="{% url 'seminar_nahraj_reseni' nadproblem_id %}" method="post" onsubmit="return zkontroluj_prilohy();"> <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&nbsp;třeba ti je i&nbsp;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();">
{% csrf_token %} {% csrf_token %}
<table class='form'> <table class='form' id="reseni">
<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> <tr>
{% for field in form %}
<td> <td>
<label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}"> <label class="field-label{% if field.field.required %} field-required{% endif %}" for="{{ field.id_for_label }}">
{{ field.label }}: {{ field.label }}:
@ -29,54 +28,15 @@
<td> <td>
{{ field }} {{ field }}
</td> </td>
</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 %} {% endfor %}
<hr>
<h4>Spolupráce s&nbsp;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&nbsp;tomto poli se vyhledává podle přezdívek, které si lze nastavit v&nbsp;„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> </tr>
{% if field.errors %}
<tr>
<td colspan="2"><span class="field-error">{{ field.errors }}</span></td>
</tr>
{% endif %}
{% endwith %}
</table> </table>
<hr> <hr>
{% include "odevzdavatko/prilohy.html" %} {% include "odevzdavatko/prilohy.html" %}
{{form.non_field_errors}}
<hr> <hr>
<h4>Odevzdat řešení</h4> <h4>Odevzdat řešení</h4>
<input type="submit" value="Odevzdat"> <input type="submit" value="Odevzdat">

View file

@ -1,21 +0,0 @@
{% 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 %}

View file

@ -2,9 +2,8 @@
<h4>Soubory s řešením</h4> <h4>Soubory s řešením</h4>
<p style="text-align: justify">Pokud je to možné a&nbsp;dává to smysl (tj.&nbsp;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">Maximální součet velikostí příloh je cca 49&nbsp;MB. Pokud je to možné a&nbsp;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 <strong>rozumně pojmenuješ</strong>, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<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&nbsp;poznámku řešitele.</p> <p style="text-align: justify">Pokud svůj soubor rozumně pojmenuješ, urychlíš opravování a&nbsp;předejdeš tomu, že si nějakého tvého řešení nevšimneme. Například z&nbsp;<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&nbsp;poznámku řešitele.</p>
<p style="text-align: justify">Maximální součet velikostí příloh je cca <strong>49&nbsp;MB</strong>.</p>
<div id="form_set"> <div id="form_set">
{% for form in prilohy.forms %} {% for form in prilohy.forms %}

View file

@ -1,7 +1,6 @@
{% 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 %}
@ -12,7 +11,6 @@
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>
@ -38,15 +36,12 @@ 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 soucet,bunka in hodnoty %} {% for hodn in hodnoty %}
<td> <td>
{% for reseni,hodnoceni in bunka %} {% if hodn %}
<a {% if barvicky %} style="color: {{reseni|barva_reseni}};" {% endif %} href="{% url 'odevzdavatko_detail_reseni' pk=reseni.id %}"> <a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}">
{{reseni.cas_doruceni | date:"j. n."}} ({{ hodnoceni.body|default_if_none:"🔨"}} b) {{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}}
</a><br> </a>
{% endfor %}
{% if bunka|length > 1 %}
<b>Σ: {{soucet}} b</b>
{% endif %} {% endif %}
</td> </td>
{% endfor %} {% endfor %}

View file

@ -1,15 +0,0 @@
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}'

View file

@ -19,9 +19,8 @@ from seminar.utils import org_required, resitel_required, viewMethodSwitch, \
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('org/add_solution', org_required(views.VlozReseniView.as_view()), name='seminar_vloz_reseni'), path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'),
path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniRozcestnikTematekView.as_view()), name='seminar_nahraj_reseni'), path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'),
path('resitel/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('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'), path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),

View file

@ -13,7 +13,6 @@ 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
@ -38,6 +37,14 @@ 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
@ -63,7 +70,6 @@ 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']
@ -71,7 +77,6 @@ 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
@ -115,45 +120,42 @@ 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[m.Problem, dict[m.Resitel, list[tuple[m.Reseni, m.Hodnoceni]]]] = dict() tabulka = dict()
soucty: dict[m.Problem, dict[m.Resitel, Decimal]] = dict()
def pridej_reseni(resitel, hodnoceni): def pridej_reseni(problem, resitel, body, cas):
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] = [(reseni, hodnoceni)] tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body)
soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme
else: else:
tabulka[problem][resitel].append((reseni, hodnoceni)) tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas)
soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme # 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
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(resitel, hodnoceni) pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni)
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. hodnoty = []
resitele_do_tabulky: list[m.Resitel] = [] resitele_do_tabulky = []
for resitel in self.resitele: for resitel in self.resitele:
dostal_body = False dostal_body = False
resiteluv_radek: list[tuple[Decimal,list[tuple[m.Reseni, m.Hodnoceni]]]] = [] # podle pořadí v self.problemy resiteluv_radek = []
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((soucty[problem][resitel], tabulka[problem][resitel])) resiteluv_radek.append(tabulka[problem][resitel])
dostal_body = True dostal_body = True
else: else:
resiteluv_radek.append((Decimal(0),[])) resiteluv_radek.append(None)
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)
@ -163,7 +165,6 @@ 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:
@ -173,11 +174,6 @@ 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'
@ -320,8 +316,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
if len(zmeny_bodu) == 1: if len(zmeny_bodu) == 1:
hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]]) 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 # > 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 and len(zmeny_bodu) != 2: if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4:
# 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.") 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.body = -0.1
hodnoceni.save() hodnoceni.save()
@ -372,8 +367,8 @@ class SeznamAktualnichReseniView(SeznamReseniView):
return qs return qs
class VlozReseniView(LoginRequiredMixin, FormView): class PosliReseniView(LoginRequiredMixin, FormView):
template_name = 'odevzdavatko/vloz_reseni.html' template_name = 'odevzdavatko/posli_reseni.html'
form_class = f.PosliReseniForm form_class = f.PosliReseniForm
def form_valid(self, form): def form_valid(self, form):
@ -404,31 +399,12 @@ class VlozReseniView(LoginRequiredMixin, FormView):
return data 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): class NahrajReseniView(LoginRequiredMixin, CreateView):
model = m.Reseni model = m.Reseni
template_name = 'odevzdavatko/nahraj_reseni.html' template_name = 'odevzdavatko/nahraj_reseni.html'
form_class = f.NahrajReseniForm 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): 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ů: # Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… # FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
osoba = m.Osoba.objects.get(user=self.request.user) osoba = m.Osoba.objects.get(user=self.request.user)
@ -441,23 +417,12 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
}) })
return super().get(request, *args, **kwargs) 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): def get_context_data(self,**kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
if self.request.POST: if self.request.POST:
data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
else: else:
data['prilohy'] = f.ReseniSPrilohamiFormSet() data['prilohy'] = f.ReseniSPrilohamiFormSet()
data["nadproblem_id"] = self.nadproblem.id
data["nadproblem"] = get_object_or_404(m.Problem, id=self.nadproblem.id)
return data return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni # FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
@ -509,8 +474,4 @@ class NahrajReseniView(LoginRequiredMixin, CreateView):
to=list(prijemci), to=list(prijemci),
).send() ).send()
return formularOKView( return formularOKView(self.request, text='Řešení úspěšně odevzdáno')
self.request,
text='Řešení úspěšně odevzdáno',
dalsi_odkazy=[("Odevzdat další řešení", reverse("seminar_nahraj_reseni"))],
)

View file

@ -1,9 +1,7 @@
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)
@ -22,24 +20,16 @@ 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')
uspesne_vytvoreni_orgove = 0 print(queryset)
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
if user is None: print(user)
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, organizuje_od=datetime.now()) org = m.Organizator.objects.create(osoba=o)
org.save() org.save()
uspesne_vytvoreni_orgove += 1 udelej_orgem.short_description = "Udělej vybraných osob organizátory"
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

View file

@ -11,7 +11,7 @@
<a href="{% url 'logout' %}">Odhlásit se</a><br> <a href="{% url 'logout' %}">Odhlásit se</a><br>
<a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br> <a href="{% url 'seminar_resitel_edit' %}">Upravit údaje</a><br>
<a href="{% url 'seminar_nahraj_reseni' %}">Nahrát řešení</a><br> <a href="{% url 'seminar_nahraj_reseni' %}">Poslat řešení</a><br>
<a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br> <a href="{% url 'seminar_resitel_odevzdana_reseni' %}">Již odevzdaná řešení</a><br>

View file

@ -1,63 +0,0 @@
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.

View file

@ -173,11 +173,7 @@ def resitelEditView(request):
msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa']) msg = "Unknown school {}, {}".format(fcd['skola_nazev'],fcd['skola_adresa'])
resitel_edit.save() resitel_edit.save()
osoba_edit.save() osoba_edit.save()
return formularOKView( return formularOKView(request, text=f'Údaje byly úspěšně uloženy. <a href="{reverse("profil")}">Vrátit se zpět na profil.</a>')
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}) return render(request, 'personalni/udaje/edit.html', {'form': form})

View file

@ -1,6 +1,7 @@
# -*- 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

View file

@ -5,7 +5,6 @@ psycopg2
html5lib html5lib
ipython ipython
Pillow Pillow
pilkit>=3.0 # Kvůli kompatibilitě s Pillow>=10.0.0
pytz pytz
six six
pexpect pexpect
@ -14,7 +13,7 @@ Unidecode
# Django and modules # Django and modules
Django<5.0 Django<3.3
#django-bootstrap-sass #django-bootstrap-sass
django-mptt django-mptt
django-reversion django-reversion
@ -25,7 +24,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.0 django-autocomplete-light>=3.9.0rc1
django-crispy-forms django-crispy-forms
django-imagekit django-imagekit
django-polymorphic django-polymorphic
@ -53,6 +52,9 @@ Werkzeug
requests requests
# requests-oauthlib # requests-oauthlib
# uWSGI
uWSGI
# Potřeba pro test data # Potřeba pro test data
lorem lorem

View file

@ -12,26 +12,16 @@ 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
@ -81,7 +71,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', 'pregeneruj_vysledkovky'] actions = ['force_publish']
inlines = (DeadlineAdminInline,) inlines = (DeadlineAdminInline,)
def force_publish(self,request,queryset): def force_publish(self,request,queryset):
@ -121,17 +111,6 @@ 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):

View file

@ -3,6 +3,7 @@ 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):
@ -15,7 +16,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=datetime.timezone.utc), verbose_name='vytvo\u0159eno', auto_now=True), field=models.DateTimeField(default=datetime.datetime(2015, 5, 15, 8, 54, 56, 319985, tzinfo=utc), verbose_name='vytvo\u0159eno', auto_now=True),
preserve_default=False, preserve_default=False,
), ),
migrations.AlterField( migrations.AlterField(

View file

@ -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 f'<Není zadaný: {self.kod}>' return '<Není zadaný>'
# 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'+self.kod return "t{}".format(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 f'<Není zadaný: {self.kod}>' return '<Není zadaný>'
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" + self.kod return "c{}".format(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 f'<Není zadaný: {self.kod}>' return '<Není zadaný>'
def node(self): def node(self):
return None return None
@ -642,9 +642,12 @@ 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:
return f"{self.cislo_zadani.poradi}.{self.kod}" name="{}.u{}".format(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 f'<Není zadaný: {self.kod}>' return '<Není zadaný>'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)

View file

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

View file

@ -113,7 +113,7 @@
{% if vysledkovka.radky_vysledkovky %} {% if vysledkovka.radky_vysledkovky %}
<h2 id=vysledky>Výsledková listina</h2> <h2>Výsledková listina</h2>
{% include "vysledkovky/vysledkovka_rocnik.html" %} {% include "vysledkovky/vysledkovka_rocnik.html" %}
{% endif %} {% endif %}

View file

@ -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 gettext as _ from django.utils.translation import ugettext 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
@ -35,7 +35,6 @@ from django.conf import settings
import unicodedata import unicodedata
import logging import logging
import time import time
from collections.abc import Sequence
from seminar.utils import aktivniResitele from seminar.utils import aktivniResitele
@ -535,9 +534,7 @@ class PosledniCisloVysledkovkaView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(PosledniCisloVysledkovkaView, self).get_context_data() context = super(PosledniCisloVysledkovkaView, self).get_context_data()
rocnik = context['rocnik'] rocnik = context['rocnik']
cislo = rocnik.cisla.order_by("poradi").filter(deadline_v_cisle__isnull=False).last() cislo = rocnik.cisla.order_by("poradi").last()
if cislo is None:
raise Http404(f"Ročník {rocnik.rocnik} nemá číslo s deadlinem.")
cislopred = cislo.predchozi() cislopred = cislo.predchozi()
context['vysledkovka'] = VysledkovkaDoTeXu( context['vysledkovka'] = VysledkovkaDoTeXu(
cislo, cislo,
@ -680,9 +677,9 @@ def StavDatabazeView(request):
# Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí) # Interní, nemá se nikdy objevit v urls (jinak to účastníci vytrolí)
def formularOKView(request, text='', dalsi_odkazy: Sequence[tuple[str, str]] = ()): def formularOKView(request, text=''):
template_name = 'seminar/formular_ok.html' template_name = 'seminar/formular_ok.html'
odkazy = list(dalsi_odkazy) + [ odkazy = [
# (Text, odkaz) # (Text, odkaz)
('Vrátit se na titulní stránku', reverse('titulni_strana')), ('Vrátit se na titulní stránku', reverse('titulni_strana')),
('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')), ('Zobrazit aktuální zadání', reverse('seminar_aktualni_zadani')),

View file

View file

@ -1,8 +0,0 @@
from django.contrib import admin
from .models import OdpovedUcastnika, SpravnaOdpoved
# Register your models here.
admin.site.register(OdpovedUcastnika)
admin.site.register(SpravnaOdpoved)

View file

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

View file

@ -1,18 +0,0 @@
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

View file

@ -1,34 +0,0 @@
# 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')),
],
),
]

View file

@ -1,28 +0,0 @@
# 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'),
),
]

View file

@ -1,18 +0,0 @@
# 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'),
),
]

View file

@ -1,27 +0,0 @@
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}"

View file

@ -1,25 +0,0 @@
{% 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 %}

View file

@ -1,46 +0,0 @@
{% 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 %}

View file

@ -1,17 +0,0 @@
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'
),
]

View file

@ -1,33 +0,0 @@
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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

View file

@ -1,19 +0,0 @@
{#{% 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 %}

View file

@ -1,13 +1,3 @@
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,
)

View file

@ -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 }}"> #}<span title="{{ p }}">{{ p.kod_v_rocniku }}</span>{# </a> #}</th> <th class='border-r' id="problem{{ oznaceni_vysledkovky }}_{{ forloop.counter0 }}">{# <a href="{{ p.verejne_url }}"> #}{{ p.kod_v_rocniku }}{# </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 }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ forloop.parentloop.counter0 }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </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 }}"> #}<span title="{{ podproblemy }}">{{ podproblemy.kod_v_rocniku }}</span>{# </a> #}</th> <th class='border-r podproblem{{ oznaceni_vysledkovky }}_{{ vysledkovka.temata_a_spol| length }} podproblem'>{# <a href="{{ podproblemy.verejne_url }}"> #}{{ podproblemy.kod_v_rocniku }}{# </a> #}</th>
{% endfor %} {% endfor %}
{# TODELETE #} {# TODELETE #}

View file

@ -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) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy… hlavni_problemy.add(p.hlavni_problem)
# 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.get_real_instance() prob = hodnoceni.problem
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,12 +366,18 @@ 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.get_real_instance()) podproblemy[h_problem.id].append(problem)
else: else:
podproblemy[-1].append(problem.get_real_instance()) podproblemy[-1].append(problem)
for podproblem in podproblemy.keys(): for podproblem in podproblemy.keys():
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku) def int_or_zero(p):
try:
return int(p.kod)
except ValueError:
return 0
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=int_or_zero)
return podproblemy return podproblemy
@cached_property @cached_property