From 6c044b3632dac5a29500dab6be49f05473dc9bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=C3=A1=C5=A1=20Havelka?= Date: Sun, 11 Jun 2023 16:51:28 +0200 Subject: [PATCH] =?UTF-8?q?move:=20P=C5=99esunut=C3=AD=20models.py=20do=20?= =?UTF-8?q?spr=C3=A1vn=C3=BDch=20aplikac=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aesop/views.py | 3 +- galerie/forms.py | 2 +- galerie/models.py | 2 +- galerie/views.py | 2 +- korektury/models.py | 2 +- mamweb/models/__init__.py | 1 + {seminar => mamweb}/models/base.py | 0 mamweb/settings_common.py | 1 + odevzdavatko/admin.py | 2 +- odevzdavatko/forms.py | 13 +- odevzdavatko/models/__init__.py | 4 + odevzdavatko/models/hodnoceni.py | 52 ++ odevzdavatko/models/priloha_reseni.py | 59 ++ odevzdavatko/models/reseni.py | 104 +++ odevzdavatko/models/reseni_resitele.py | 34 + odevzdavatko/templatetags/jmena.py | 4 +- odevzdavatko/views.py | 44 +- personalni/admin.py | 2 +- personalni/forms.py | 2 +- personalni/models/__init__.py | 5 + personalni/models/organizator.py | 76 ++ personalni/models/osoba.py | 126 +++ personalni/models/prijemce.py | 37 + personalni/models/resitel.py | 221 +++++ personalni/models/skola.py | 82 ++ personalni/utils.py | 2 +- personalni/views.py | 24 +- prednasky/admin.py | 2 +- prednasky/models.py | 3 +- prednasky/views.py | 3 +- seminar/admin.py | 155 +--- seminar/migrations/0100_auto_20211129_2354.py | 4 +- seminar/models/__init__.py | 10 +- seminar/models/novinky.py | 4 +- seminar/models/odevzdavatko.py | 200 ----- seminar/models/personalni.py | 447 ----------- seminar/models/pomocne.py | 2 +- seminar/models/soustredeni.py | 216 ----- seminar/models/tvorba.py | 758 ------------------ seminar/templatetags/deadliny.py | 22 +- seminar/views/views_all.py | 32 +- soustredeni/admin.py | 10 +- soustredeni/models/__init__.py | 5 + soustredeni/models/konfera.py | 83 ++ soustredeni/models/konfery_ucastnici.py | 35 + soustredeni/models/soustredeni.py | 92 +++ .../models/soustredeni_organizatori.py | 40 + soustredeni/models/soustredeni_ucastnici.py | 39 + soustredeni/views.py | 4 +- treenode/admin.py | 2 +- .../models/treenode.py => treenode/models.py | 26 +- treenode/serializers.py | 18 +- treenode/views.py | 7 +- treenode/viewsets.py | 2 +- tvorba/__init__.py | 0 tvorba/admin.py | 156 ++++ tvorba/apps.py | 5 + tvorba/migrations/__init__.py | 0 tvorba/models/__init__.py | 9 + tvorba/models/cislo.py | 281 +++++++ tvorba/models/clanek.py | 33 + tvorba/models/deadline.py | 83 ++ tvorba/models/nastaveni.py | 42 + tvorba/models/pohadka.py | 53 ++ tvorba/models/problem.py | 161 ++++ tvorba/models/rocnik.py | 95 +++ tvorba/models/tema.py | 62 ++ tvorba/models/uloha.py | 73 ++ various/utils.py | 20 + vysledkovky/admin.py | 5 + vysledkovky/models/__init__.py | 1 + vysledkovky/models/zmrazena_vysledkovka.py | 21 + vysledkovky/utils.py | 82 +- 73 files changed, 2399 insertions(+), 1910 deletions(-) create mode 100644 mamweb/models/__init__.py rename {seminar => mamweb}/models/base.py (100%) create mode 100644 odevzdavatko/models/__init__.py create mode 100644 odevzdavatko/models/hodnoceni.py create mode 100644 odevzdavatko/models/priloha_reseni.py create mode 100644 odevzdavatko/models/reseni.py create mode 100644 odevzdavatko/models/reseni_resitele.py create mode 100644 personalni/models/__init__.py create mode 100644 personalni/models/organizator.py create mode 100644 personalni/models/osoba.py create mode 100644 personalni/models/prijemce.py create mode 100644 personalni/models/resitel.py create mode 100644 personalni/models/skola.py delete mode 100644 seminar/models/odevzdavatko.py delete mode 100644 seminar/models/personalni.py delete mode 100644 seminar/models/soustredeni.py delete mode 100644 seminar/models/tvorba.py create mode 100644 soustredeni/models/__init__.py create mode 100644 soustredeni/models/konfera.py create mode 100644 soustredeni/models/konfery_ucastnici.py create mode 100644 soustredeni/models/soustredeni.py create mode 100644 soustredeni/models/soustredeni_organizatori.py create mode 100644 soustredeni/models/soustredeni_ucastnici.py rename seminar/models/treenode.py => treenode/models.py (93%) create mode 100644 tvorba/__init__.py create mode 100644 tvorba/admin.py create mode 100644 tvorba/apps.py create mode 100644 tvorba/migrations/__init__.py create mode 100644 tvorba/models/__init__.py create mode 100644 tvorba/models/cislo.py create mode 100644 tvorba/models/clanek.py create mode 100644 tvorba/models/deadline.py create mode 100644 tvorba/models/nastaveni.py create mode 100644 tvorba/models/pohadka.py create mode 100644 tvorba/models/problem.py create mode 100644 tvorba/models/rocnik.py create mode 100644 tvorba/models/tema.py create mode 100644 tvorba/models/uloha.py create mode 100644 vysledkovky/admin.py create mode 100644 vysledkovky/models/__init__.py create mode 100644 vysledkovky/models/zmrazena_vysledkovka.py diff --git a/aesop/views.py b/aesop/views.py index 4d1748b7..029b14da 100644 --- a/aesop/views.py +++ b/aesop/views.py @@ -11,7 +11,8 @@ from django.views import generic from django.utils.encoding import force_text from .utils import default_ovvpfile -from seminar.models import Rocnik, Soustredeni +from tvorba.models.rocnik import Rocnik +from soustredeni.models.soustredeni import Soustredeni from vysledkovky import utils from seminar.utils import aktivniResitele diff --git a/galerie/forms.py b/galerie/forms.py index e6666884..120aceda 100644 --- a/galerie/forms.py +++ b/galerie/forms.py @@ -1,7 +1,7 @@ #coding: utf-8 from django import forms -from seminar.models import Soustredeni +from soustredeni.models.soustredeni import Soustredeni class KomentarForm(forms.Form): komentar = forms.CharField(label = "Komentář:", max_length = 300, required=False) diff --git a/galerie/models.py b/galerie/models.py index 8e6efdc7..a992d9e9 100644 --- a/galerie/models.py +++ b/galerie/models.py @@ -8,7 +8,7 @@ from imagekit.processors import ResizeToFit, Transpose import os -from seminar.models import Soustredeni +from soustredeni.models.soustredeni import Soustredeni VZDY=0 ORG=1 diff --git a/galerie/views.py b/galerie/views.py index f0d9b53b..5c6c7b7f 100644 --- a/galerie/views.py +++ b/galerie/views.py @@ -8,7 +8,7 @@ from django.template import RequestContext from datetime import datetime from galerie.models import Obrazek, Galerie -from seminar.models import Soustredeni +from soustredeni.models.soustredeni import Soustredeni from galerie.forms import KomentarForm, NewGalerieForm def zobrazit(galerie, request): diff --git a/korektury/models.py b/korektury/models.py index ac82c14e..cf0c1df1 100644 --- a/korektury/models.py +++ b/korektury/models.py @@ -21,7 +21,7 @@ from django.core.exceptions import ObjectDoesNotExist from django.utils.functional import cached_property from django.utils.text import get_valid_filename -from seminar.models import Organizator +from personalni.models.organizator import Organizator import subprocess from reversion import revisions as reversion diff --git a/mamweb/models/__init__.py b/mamweb/models/__init__.py new file mode 100644 index 00000000..9b5ed21c --- /dev/null +++ b/mamweb/models/__init__.py @@ -0,0 +1 @@ +from .base import * diff --git a/seminar/models/base.py b/mamweb/models/base.py similarity index 100% rename from seminar/models/base.py rename to mamweb/models/base.py diff --git a/mamweb/settings_common.py b/mamweb/settings_common.py index 139190fa..a9b271d8 100644 --- a/mamweb/settings_common.py +++ b/mamweb/settings_common.py @@ -150,6 +150,7 @@ INSTALLED_APPS = ( 'personalni', 'soustredeni', 'treenode', + 'tvorba', # Admin upravy: diff --git a/odevzdavatko/admin.py b/odevzdavatko/admin.py index 168beab1..8b8b8826 100644 --- a/odevzdavatko/admin.py +++ b/odevzdavatko/admin.py @@ -6,7 +6,7 @@ s dekorátorem :func:`django.contrib.admin.register`. """ from django.contrib import admin from django_reverse_admin import ReverseModelAdmin -import seminar.models as m +import odevzdavatko.models as m class PrilohaReseniInline(admin.TabularInline): diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 0bc99927..e692d42e 100644 --- a/odevzdavatko/forms.py +++ b/odevzdavatko/forms.py @@ -4,8 +4,11 @@ from django.forms import formset_factory from django.forms.models import inlineformset_factory from django.utils import timezone -from seminar.models import Resitel -import seminar.models as m +from personalni.models.resitel import Resitel +from tvorba.models.nastaveni import Nastaveni +from tvorba.models.problem import Problem +from tvorba.models.deadline import Deadline +import odevzdavatko.models as m import logging @@ -22,7 +25,7 @@ class DateInput(forms.DateInput): class PosliReseniForm(forms.Form): problem = forms.ModelMultipleChoiceField( - queryset=m.Problem.objects.all(), + queryset=Problem.objects.all(), label="Problémy", widget=autocomplete.ModelSelect2Multiple( url='autocomplete_problem', @@ -165,7 +168,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): from django.db.utils import OperationalError try: - aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik + aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik except OperationalError: # django.db.utils.OperationalError: no such table: seminar_nastaveni # Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál @@ -181,7 +184,7 @@ class OdevzdavatkoTabulkaFiltrForm(forms.Form): result.append(("0001-01-01", f"Odjakživa")) - for deadline in m.Deadline.objects.filter( + for deadline in Deadline.objects.filter( deadline__lte=timezone.now(), cislo__rocnik=aktualni_rocnik ).order_by("deadline"): diff --git a/odevzdavatko/models/__init__.py b/odevzdavatko/models/__init__.py new file mode 100644 index 00000000..9be89640 --- /dev/null +++ b/odevzdavatko/models/__init__.py @@ -0,0 +1,4 @@ +from .hodnoceni import Hodnoceni +from .priloha_reseni import PrilohaReseni +from .reseni import Reseni +from .reseni_resitele import Reseni_Resitele diff --git a/odevzdavatko/models/hodnoceni.py b/odevzdavatko/models/hodnoceni.py new file mode 100644 index 00000000..bf54cc34 --- /dev/null +++ b/odevzdavatko/models/hodnoceni.py @@ -0,0 +1,52 @@ +from django.db import models + +from mamweb.models.base import SeminarModelBase +from .reseni import Reseni +from tvorba.models.cislo import Cislo +from tvorba.models.deadline import Deadline +from tvorba.models.problem import Problem + + +class Hodnoceni(SeminarModelBase): + class Meta: + db_table = 'seminar_hodnoceni' + verbose_name = 'Hodnocení' + verbose_name_plural = 'Hodnocení' + + # Interní ID + id = models.AutoField(primary_key=True) + + body = models.DecimalField( + max_digits=8, decimal_places=1, verbose_name='body', + blank=True, null=True, + ) + + cislo_body = models.ForeignKey( + Cislo, verbose_name='číslo pro body', + related_name='hodnoceni', blank=True, null=True, + on_delete=models.PROTECT, + ) + + # V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body + deadline_body = models.ForeignKey( + Deadline, verbose_name='deadline pro body', + related_name='hodnoceni', blank=True, null=True, + on_delete=models.PROTECT, + ) + + reseni = models.ForeignKey( + Reseni, verbose_name='řešení', on_delete=models.CASCADE, + ) + + problem = models.ForeignKey( + Problem, verbose_name='problém', + related_name='hodnoceni', on_delete=models.PROTECT, + ) + + feedback = models.TextField( + 'zpětná vazba', blank=True, default='', + help_text='Zpětná vazba řešiteli (plain text)', + ) + + def __str__(self): + return "{}, {}, {}".format(self.problem, self.reseni, self.body) diff --git a/odevzdavatko/models/priloha_reseni.py b/odevzdavatko/models/priloha_reseni.py new file mode 100644 index 00000000..f46a75d7 --- /dev/null +++ b/odevzdavatko/models/priloha_reseni.py @@ -0,0 +1,59 @@ +import os +import reversion + +from django.conf import settings +from django.db import models +from django.utils import timezone + +from various.utils import aux_generate_filename +from mamweb.models.base import SeminarModelBase +from .reseni import Reseni + + +def generate_filename(self, filename): + return os.path.join( + settings.SEMINAR_RESENI_DIR, + aux_generate_filename(self, filename) + ) + + +@reversion.register(ignore_duplicates=True) +class PrilohaReseni(SeminarModelBase): + + class Meta: + db_table = 'seminar_priloha_reseni' + verbose_name = 'Příloha řešení' + verbose_name_plural = 'Přílohy řešení' + ordering = ['reseni', 'vytvoreno'] + + # Interní ID + id = models.AutoField(primary_key=True) + + reseni = models.ForeignKey( + Reseni, verbose_name='řešení', related_name='prilohy', + on_delete=models.CASCADE, + ) + + vytvoreno = models.DateTimeField( + 'vytvořeno', default=timezone.now, blank=True, editable=False, + ) + + soubor = models.FileField('soubor', upload_to=generate_filename) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu', + ) + + res_poznamka = models.TextField( + 'poznámka řešitele', blank=True, + help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje', + ) + + def __str__(self): + return str(self.soubor) + + def split(self): + """Vrátí cestu rozsekanou po složkách. To se hodí v templatech""" + # Věřím, že tohle funguje, případně použít os.path nebo pathlib. + return self.soubor.url.split('/') diff --git a/odevzdavatko/models/reseni.py b/odevzdavatko/models/reseni.py new file mode 100644 index 00000000..1eb47ca4 --- /dev/null +++ b/odevzdavatko/models/reseni.py @@ -0,0 +1,104 @@ +import reversion + +from django.db import models +from django.utils import timezone +from django.contrib.sites.shortcuts import get_current_site +from django.urls import reverse_lazy +from django.db.models import Sum + +from mamweb.models.base import SeminarModelBase +from personalni.models.resitel import Resitel +from tvorba.models.problem import Problem +from tvorba.models.deadline import Deadline + + +@reversion.register(ignore_duplicates=True) +class Reseni(SeminarModelBase): + + class Meta: + db_table = 'seminar_reseni' + verbose_name = 'Řešení' + verbose_name_plural = 'Řešení' + # ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. + ordering = ['-cas_doruceni'] + + # Interní ID + id = models.AutoField(primary_key=True) + + # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. + problem = models.ManyToManyField( + Problem, verbose_name='problém', help_text='Problém', + through='Hodnoceni', + ) + + resitele = models.ManyToManyField( + Resitel, verbose_name='autoři řešení', + help_text='Seznam autorů řešení', through='Reseni_Resitele', + ) + + cas_doruceni = models.DateTimeField( + 'čas_doručení', default=timezone.now, blank=True, + ) + + FORMA_PAPIR = 'papir' + FORMA_EMAIL = 'email' + FORMA_UPLOAD = 'upload' + FORMA_CHOICES = [ + (FORMA_PAPIR, 'Papírové řešení'), + (FORMA_EMAIL, 'Emailem'), + (FORMA_UPLOAD, 'Upload přes web'), + ] + forma = models.CharField( + 'forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, + default=FORMA_EMAIL, + ) + + text_cely = models.OneToOneField( + 'ReseniNode', verbose_name='Plná verze textu řešení', + blank=True, null=True, related_name="reseni_cely_set", + on_delete=models.PROTECT, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k řešení (plain text)', + ) + + zverejneno = models.BooleanField( + 'řešení zveřejněno', default=False, + help_text='Udává, zda je řešení zveřejněno', + ) + + def verejne_url(self): + return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) + + def absolute_url(self): + return "https://" + str(get_current_site(None)) + self.verejne_url() + + # má OneToOneField s: + # Konfera + + # má ForeignKey s: + # Hodnoceni + + def sum_body(self): + return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] + + def __str__(self): + return "{}({}): {}({})".format( + self.resitele.first(), + len(self.resitele.all()), + self.problem.first(), + len(self.problem.all()) + ) + # NOTE: Potenciální DB HOG (bez select_related) + + def deadline_reseni(self): + return Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first() + +## Pravdepodobne uz nebude potreba: +# def save(self, *args, **kwargs): +# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and +# (self.problem.typ == Problem.TYP_ULOHA)): +# self.cislo_body = self.problem.cislo_reseni +# super(Reseni, self).save(*args, **kwargs) diff --git a/odevzdavatko/models/reseni_resitele.py b/odevzdavatko/models/reseni_resitele.py new file mode 100644 index 00000000..ef10f5bf --- /dev/null +++ b/odevzdavatko/models/reseni_resitele.py @@ -0,0 +1,34 @@ +import reversion + +from django.db import models + +from .reseni import Reseni +from personalni.models.resitel import Resitel + + +# Vazebna tabulka. Mozna se generuje automaticky. +@reversion.register(ignore_duplicates=True) +class Reseni_Resitele(models.Model): + class Meta: + db_table = 'seminar_reseni_resitele' + verbose_name = 'Řešení řešitelů' + verbose_name_plural = 'Řešení řešitelů' + ordering = ['reseni', 'resitele'] + + # Interní ID + id = models.AutoField(primary_key=True) + + resitele = models.ForeignKey( + Resitel, verbose_name='řešitel', on_delete=models.PROTECT, + ) + + reseni = models.ForeignKey( + Reseni, verbose_name='řešení', on_delete=models.CASCADE, + ) + + # podil - jakou merou se ktery resitel podilel na danem reseni + # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni + + def __str__(self): + return '{} od {}'.format(self.reseni, self.resitel) +# NOTE: Poteciální DB HOG bez select_related diff --git a/odevzdavatko/templatetags/jmena.py b/odevzdavatko/templatetags/jmena.py index 9fe91ff5..d7e4747a 100644 --- a/odevzdavatko/templatetags/jmena.py +++ b/odevzdavatko/templatetags/jmena.py @@ -2,8 +2,8 @@ from django import template register = template.Library() from personalni.utils import normalizuj_jmeno -import seminar.models as m # jen kvůli typové anotaci… +from personalni.models.osoba import Osoba @register.filter -def jmeno_jako_prefix(o: m.Osoba): +def jmeno_jako_prefix(o: Osoba): return normalizuj_jmeno(o).replace(' ', '_') diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 5660af71..bb50407e 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -16,7 +16,15 @@ import datetime from itertools import groupby import logging -import seminar.models as m +from personalni.models.resitel import Resitel +from personalni.models.organizator import Organizator +from personalni.models.osoba import Osoba +from tvorba.models.problem import Problem +from tvorba.models.nastaveni import Nastaveni +from tvorba.models.rocnik import Rocnik +from tvorba.models.deadline import Deadline +import odevzdavatko.models as m + from . import forms as f from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm from seminar.utils import resi_v_rocniku @@ -54,13 +62,13 @@ class TabulkaOdevzdanychReseniView(ListView): # FIXME: jméno metody není vypovídající... # NOTE: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistují ty objekty (?). TODO: Otestovat # TODO: Prefetches, Select related, ... - self.resitele = m.Resitel.objects.all() - self.problemy = m.Problem.objects.all() + self.resitele = Resitel.objects.all() + self.problemy = Problem.objects.all() self.reseni = m.Reseni.objects.all() - self.aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci + self.aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci if 'rocnik' in self.kwargs: - self.aktualni_rocnik = get_object_or_404(m.Rocnik, rocnik=self.kwargs['rocnik']) + self.aktualni_rocnik = get_object_or_404(Rocnik, rocnik=self.kwargs['rocnik']) form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik) if form.is_valid(): @@ -91,14 +99,14 @@ class TabulkaOdevzdanychReseniView(ListView): self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok) if problemy == FiltrForm.PROBLEMY_MOJE: - org = m.Organizator.objects.get(osoba__user=self.request.user) + org = Organizator.objects.get(osoba__user=self.request.user) self.problemy = self.problemy.filter( Q(autor=org)|Q(garant=org)|Q(opravovatele=org), - Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY), + Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY), ) elif problemy == FiltrForm.PROBLEMY_LETOSNI: self.problemy = self.problemy.filter( - Q(stav=m.Problem.STAV_ZADANY)|Q(stav=m.Problem.STAV_VYRESENY), + Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY), ) #self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník.... # NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy. @@ -164,7 +172,7 @@ class TabulkaOdevzdanychReseniView(ListView): # Pro použití hacku na automatické {{form.media}} v template: ctx['form'] = ctx['filtr'] # Pro maximum v přesměrovátku ročníků - ctx['aktualni_rocnik'] = m.Nastaveni.get_solo().aktualni_rocnik + ctx['aktualni_rocnik'] = Nastaveni.get_solo().aktualni_rocnik if 'rocnik' in self.kwargs: ctx['rocnik'] = self.kwargs['rocnik'] else: @@ -186,8 +194,8 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi if problem_id is None: raise ValueError("Nemám problém! (To je problém!)") - resitel = m.Resitel.objects.get(id=resitel_id) - problem = m.Problem.objects.get(id=problem_id) + resitel = Resitel.objects.get(id=resitel_id) + problem = Problem.objects.get(id=problem_id) qs = qs.filter( problem__in=[problem], resitele__in=[resitel], @@ -316,7 +324,7 @@ class PrehledOdevzdanychReseni(ListView): if not self.request.user.is_authenticated: raise RuntimeError("Uživatel měl být přihlášený!") # get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu - resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first() + resitel = Resitel.objects.filter(osoba__user=self.request.user).first() qs = super().get_queryset() qs = qs.filter(reseni__resitele__in=[resitel]) # Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení @@ -343,7 +351,7 @@ class SeznamReseniView(ListView): class SeznamAktualnichReseniView(SeznamReseniView): def get_queryset(self): qs = super().get_queryset() - akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi... + akt_rocnik = Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi... resitele = resi_v_rocniku(akt_rocnik) qs = qs.filter(resitele__in=resitele) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel return qs @@ -389,9 +397,9 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): def get(self, request, *args, **kwargs): # 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) + osoba = Osoba.objects.get(user=self.request.user) resitel = osoba.resitel - if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok: + if resitel.rok_maturity <= Nastaveni.get_solo().aktualni_rocnik.prvni_rok: return render(request, 'universal.html', { 'title': 'Nelze odevzdat', 'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.', @@ -416,7 +424,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): return super().form_invalid(form) with transaction.atomic(): self.object = form.save() - self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user)) + self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user)) self.object.resitele.add(*form.cleaned_data["resitele"]) self.object.cas_doruceni = timezone.now() self.object.forma = m.Reseni.FORMA_UPLOAD @@ -426,7 +434,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): prilohy.save() for hodnoceni in self.object.hodnoceni_set.all(): - hodnoceni.deadline_body = m.Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first() + hodnoceni.deadline_body = Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first() hodnoceni.save() # Pošleme mail opravovatelům a garantovi @@ -444,7 +452,7 @@ class NahrajReseniView(LoginRequiredMixin, CreateView): # FIXME: Víc informativní obsah mailů, možná vč. příloh? prijemci = map(lambda it: it.osoba.email, prijemci) - resitel = m.Osoba.objects.get(user = self.request.user) + resitel = Osoba.objects.get(user = self.request.user) seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy)) seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })") diff --git a/personalni/admin.py b/personalni/admin.py index fc3cadd4..bdaf05d5 100644 --- a/personalni/admin.py +++ b/personalni/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.models import Group from django_reverse_admin import ReverseModelAdmin -import seminar.models as m +import personalni.models as m @admin.register(m.Osoba) diff --git a/personalni/forms.py b/personalni/forms.py index 3199a8a2..57b3a27a 100644 --- a/personalni/forms.py +++ b/personalni/forms.py @@ -4,7 +4,7 @@ from django.contrib.auth.forms import PasswordResetForm from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.models import User -from seminar.models import Skola, Resitel, Osoba +from personalni.models import Skola, Resitel, Osoba from datetime import date import logging diff --git a/personalni/models/__init__.py b/personalni/models/__init__.py new file mode 100644 index 00000000..3e149ec0 --- /dev/null +++ b/personalni/models/__init__.py @@ -0,0 +1,5 @@ +from .osoba import Osoba +from .skola import Skola +from .prijemce import Prijemce +from .resitel import Resitel +from .organizator import Organizator diff --git a/personalni/models/organizator.py b/personalni/models/organizator.py new file mode 100644 index 00000000..d90c5194 --- /dev/null +++ b/personalni/models/organizator.py @@ -0,0 +1,76 @@ +import reversion + +from django.db import models +from django.utils import timezone +from django.core.exceptions import ValidationError + +from mamweb.models.base import SeminarModelBase +from .osoba import Osoba + + +@reversion.register(ignore_duplicates=True) +class Organizator(SeminarModelBase): + osoba = models.OneToOneField( + Osoba, verbose_name='osoba', related_name='org', + help_text='osobní údaje organizátora', null=False, blank=False, + on_delete=models.PROTECT, + ) + + vytvoreno = models.DateTimeField( + 'Vytvořeno', + default=timezone.now, + blank=True, + editable=False, + ) + + # Ne, date to nebude: + # SQLite: invalid literal for int() with base 10: b'17 23:00:00' + organizuje_od = models.DateTimeField('Organizuje od', blank=True, null=True) + + organizuje_do = models.DateTimeField('Organizuje do', blank=True, null=True) + + studuje = models.CharField( + 'Studium aj.', max_length=256, + null=True, blank=True, + help_text="Např. 'Studuje Obecnou fyziku (Bc.), 3. ročník', 'Vystudovala Diskrétní modely a algoritmy (Mgr.)' nebo 'Přednáší na MFF'", + ) + + strucny_popis_organizatora = models.TextField( + 'Stručný popis organizátora', + null=True, blank=True, + ) + + skola = models.CharField( + 'Škola, kterou studuje', max_length=256, null=True, blank=True, + help_text="Škola, např. MFF, VŠCHT, VUT, ... prostě aby se nemuselo psát do studuje školu, ale jen obor, možnost zobrazit zvlášť", + ) + + def clean(self): + if ( + self.organizuje_od and + self.organizuje_do and + (self.organizuje_od > self.organizuje_do) + ): + raise ValidationError( + "Organizátor nemůže skončit s organizováním dříve než začal!" + ) + super().clean() + + def __str__(self): + if self.osoba.prezdivka: + return "{} '{}' {}".format( + self.osoba.jmeno, + self.osoba.prezdivka, + self.osoba.prijmeni, + ) + else: + return "{} {}".format(self.osoba.jmeno, self.osoba.prijmeni) + + class Meta: + verbose_name = 'Organizátor' + verbose_name_plural = 'Organizátoři' + # Řadí aktivní orgy na začátek, pod tím v pořadí od nejstarších neaktivní orgy. + # TODO: Chtěl bych spíš mít nejstarší orgy dole. + # TODO: Zohledňovat přezdívky? + # TODO: Sjednotit s tím, jak se řadí organizátoři v seznau orgů na webu + ordering = ['-organizuje_do', 'osoba__jmeno', 'osoba__prijmeni'] diff --git a/personalni/models/osoba.py b/personalni/models/osoba.py new file mode 100644 index 00000000..a2d6f3ab --- /dev/null +++ b/personalni/models/osoba.py @@ -0,0 +1,126 @@ +import reversion +from imagekit.models import ImageSpecField, ProcessedImageField +from imagekit.processors import ResizeToFit, Transpose + +from django.db import models +from django.utils import timezone +from django.conf import settings +from django_countries.fields import CountryField + +from mamweb.models.base import SeminarModelBase + + +@reversion.register(ignore_duplicates=True) +class Osoba(SeminarModelBase): + + class Meta: + db_table = 'seminar_osoby' + verbose_name = 'Osoba' + verbose_name_plural = 'Osoby' + ordering = ['prijmeni', 'jmeno'] + + id = models.AutoField(primary_key=True) + + jmeno = models.CharField('jméno', max_length=256) + + prijmeni = models.CharField('příjmení', max_length=256) + + prezdivka = models.CharField( + 'přezdívka', blank=True, null=True, max_length=256 + ) + + # User, pokud má na webu účet + user = models.OneToOneField( + settings.AUTH_USER_MODEL, blank=True, null=True, + verbose_name='uživatel', on_delete=models.DO_NOTHING, + ) + + # Pohlaví. Že ho neznáme se snad nestane (a ušetří to práci při programování) + pohlavi_muz = models.BooleanField('pohlaví (muž)', default=False) + + email = models.EmailField('e-mail', max_length=256, blank=True, default='') + + telefon = models.CharField('telefon', max_length=256, blank=True, default='') + + datum_narozeni = models.DateField('datum narození', blank=True, null=True) + + # NULL dokud nedali souhlas + datum_souhlasu_udaje = models.DateField( + 'datum souhlasu (údaje)', blank=True, null=True, + help_text='Datum souhlasu se zpracováním osobních údajů', + ) + + # NULL dokud nedali souhlas + datum_souhlasu_zasilani = models.DateField( + 'datum souhlasu (spam)', blank=True, null=True, + help_text='Datum souhlasu se zasíláním MFF materiálů', + ) + + # Alespoň odhad (rok či i měsíc) + datum_registrace = models.DateField( + 'datum registrace do semináře', default=timezone.now, + ) + + # Ulice může být i jen číslo + ulice = models.CharField('ulice', max_length=256, blank=True, default='') + + mesto = models.CharField('město', max_length=256, blank=True, default='') + + psc = models.CharField('PSČ', max_length=32, blank=True, default='') + + # ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK) + # Ekvivalentní s CharField(max_length=2, default='CZ', ...) + stat = CountryField( + 'stát', default='CZ', + help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)', + ) + + jak_se_dozvedeli = models.TextField('Jak se dozvěděli', blank=True) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k osobě (plain text)', + ) + + foto = ProcessedImageField( + verbose_name='Fotografie osoby', + upload_to='image_osoby/velke/%Y/', null=True, blank=True, + help_text='Vlož fotografii osoby o libovolné velikosti', + processors=[ + Transpose(Transpose.AUTO), + ResizeToFit(500, 500, upscale=False), + ], + options={'quality': 95} + ) + + foto_male = ImageSpecField( + source='foto', + processors=[ResizeToFit(200, 200, upscale=False)], + options={'quality': 95}, + ) + + # má OneToOneField nejvýše s: + # Resitel + # Prijemce + # Organizator + + def plne_jmeno(self): + return '{} {}'.format(self.jmeno, self.prijmeni) + + def inicial_krestni(self): + jmena = self.jmeno.split() + return " ".join(['{}.'.format(jmeno[0]) for jmeno in jmena]) + + def __str__(self): + return self.plne_jmeno() + + # Overridujeme save Osoby, aby když si změní e-mail, aby se projevil i v + # Userovi (a tak se dal poslat mail s resetem hesla) + def save(self, *args, **kwargs): + if self.user is not None: + u = self.user + # U svatého tučňáka, prosím ať tohle funguje. + # (Takhle se kódit asi nemá...) + u.email = self.email + u.save() + super().save() diff --git a/personalni/models/prijemce.py b/personalni/models/prijemce.py new file mode 100644 index 00000000..58fc9691 --- /dev/null +++ b/personalni/models/prijemce.py @@ -0,0 +1,37 @@ +from django.db import models + +from mamweb.models.base import SeminarModelBase +from .osoba import Osoba + + +class Prijemce(SeminarModelBase): + class Meta: + db_table = 'seminar_prijemce' + verbose_name = 'příjemce' + verbose_name_plural = 'příjemce' + + # Interní ID + id = models.AutoField(primary_key=True) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k příemci čísel (plain text)', + ) + + osoba = models.OneToOneField( + Osoba, verbose_name='komu', blank=False, null=False, + help_text='Které osobě či na jakou adresu se mají zasílat čísla', + on_delete=models.CASCADE, + ) + + zasilat_cislo_emailem = models.BooleanField( + 'zasílat číslo emailem', + help_text='True pokud chce příjemce dostávat číslo emailem', + default=False, + ) + + # FIXME: možná chceme něco jako vazbu na osobu XOR školu a počet kusů k zaslání + # FIXME: a možná taky posílání na mail a možná taky přes něj chceme posílat i řešitelům + + def __str__(self): + return self.osoba.plne_jmeno() diff --git a/personalni/models/resitel.py b/personalni/models/resitel.py new file mode 100644 index 00000000..00809ba1 --- /dev/null +++ b/personalni/models/resitel.py @@ -0,0 +1,221 @@ +import reversion + +from django.db import models + +from mamweb.models.base import SeminarModelBase +from .osoba import Osoba +from .skola import Skola + + +@reversion.register(ignore_duplicates=True) +class Resitel(SeminarModelBase): + + class Meta: + db_table = 'seminar_resitele' + verbose_name = 'Řešitel' + verbose_name_plural = 'Řešitelé' + ordering = ['osoba'] + + # Interní ID + id = models.AutoField(primary_key=True) + + prezdivka_resitele = models.CharField( + 'přezdívka řešitele', blank=True, null=True, max_length=256, unique=True, + ) + + osoba = models.OneToOneField( + Osoba, blank=False, null=False, verbose_name='osoba', + on_delete=models.PROTECT, + ) + + skola = models.ForeignKey( + Skola, blank=True, null=True, verbose_name='škola', + on_delete=models.SET_NULL, + ) + + # Očekávaný rok maturity a vyřazení z aktivních řešitelů + rok_maturity = models.IntegerField('rok maturity', blank=True, null=True) + + ZASILAT_DOMU = 'domu' + ZASILAT_DO_SKOLY = 'do_skoly' + ZASILAT_NIKAM = 'nikam' + ZASILAT_CHOICES = [ + (ZASILAT_DOMU, 'Domů'), + (ZASILAT_DO_SKOLY, 'Do školy'), + (ZASILAT_NIKAM, 'Nezasílat papírově'), + ] + + zasilat = models.CharField( + 'kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, + default=ZASILAT_DOMU, + ) + + zasilat_cislo_emailem = models.BooleanField( + 'zasílat číslo emailem', + help_text='True pokud chce řešitel dostávat číslo emailem', + default=False, + ) + + zasilat_cislo_papirove = models.BooleanField( + 'zasílat číslo papírově', + help_text='True pokud chce řešitel dostávat číslo papírově', + default=True, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k řešiteli (plain text)', + ) + + def export_row(self): + """Slovnik pro pouziti v AESOP exportu""" + return { + 'id': self.id, + 'name': self.osoba.jmeno, + 'surname': self.osoba.prijmeni, + 'gender': 'M' if self.osoba.pohlavi_muz else 'F', + 'born': self.osoba.datum_narozeni.isoformat() if self.osoba.datum_narozeni else '', + 'email': self.osoba.email, + 'end-year': self.rok_maturity or '', + + 'street': self.osoba.ulice, + 'town': self.osoba.mesto, + 'postcode': self.osoba.psc, + 'country': self.osoba.stat, + + 'spam-flag': 'Y' if self.osoba.datum_souhlasu_zasilani else '', + 'spam-date': self.osoba.datum_souhlasu_zasilani.isoformat() if self.osoba.datum_souhlasu_zasilani else '', + + 'school': self.skola.aesop_id if self.skola else '', + 'school-name': str(self.skola) if self.skola else 'Skola neni znama', + } + + def rocnik(self, rocnik): + """Vrati skolni rocnik resitele pro zadany Rocnik. + Vraci '' pro neznamy rok maturity resitele, Z* pro ekvivalent ZŠ.""" + if self.rok_maturity is None: + return '' + rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok) + if rozdil >= 1: + return str(rozdil) + else: + return 'Z' + str(rozdil + 9) + + def vsechny_body(self): + """Spočítá body odjakživa.""" + vsechna_reseni = self.reseni_set.all() + from odevzdavatko.models import Hodnoceni + vsechna_hodnoceni = Hodnoceni.objects.filter( + reseni__in=vsechna_reseni) + return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) + + def get_titul(self, body=None): + """Vrati titul jako řetězec.""" + + # Nejprve si zadefinujeme titul + from enum import Enum + from functools import total_ordering + + @total_ordering + class Titul(Enum): + """Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace).""" + nic = (0, '') + bc = (20, 'Bc.') + mgr = (50, 'Mgr.') + dr = (100, 'Dr.') + doc = (200, 'Doc.') + prof = (500, 'Prof.') + akad = (1000, 'Akad.') + + def __lt__(self, other): + return True if self.value[0] < other.value[0] else False + + def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně. + return True if self.value[0] == other.value[0] else False + + def __str__(self): + return self.value[1] + + @classmethod + def z_bodu(cls, body): + aktualni = cls.nic + # TODO: ověřit, že to funguje + for titul in cls: # Kdyžtak použít __members__.items() + if titul.value[0] <= body: + aktualni = titul + else: + break + return aktualni + + # Hledáme body v databázi + # V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů: + # - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími + # - proto se započítávají dvojnásobně a byly posunuté hranice titulů + # - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. + from odevzdavatko.models import Hodnoceni + hodnoceni_do_25_rocniku = Hodnoceni.objects.filter( + deadline_body__cislo__rocnik__rocnik__lte=25, + reseni__in=self.reseni_set.all() + ) + novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) + + def body_z_hodnoceni(hh: list): + return sum(h.body for h in hh if h.body is not None) + + stare_body = body_z_hodnoceni(hodnoceni_do_25_rocniku) + if body is None: + nove_body = body_z_hodnoceni(novejsi_hodnoceni) + else: + # Zjistíme, kolik bodů jsou staré, tedy hodnotnější + nove_body = max(0, body - stare_body) # Všechny body nad počet původních hodnotnějších + stare_body = min(stare_body, body) # Skutečný počet hodnotnějších bodů + logicke_body = 2*stare_body + nove_body + + # Titul se určí následovně: + # - Pokud se řeší body, které jsou starší, než do 26 ročníku (včetně), dáváme tituly postaru. + # - Jinak dáváme tituly po novu... + # - ... ale titul se nesmí odebrat, pokud se zmenšil. + def titul_do_26_rocniku(body): + """Původní hranice bodů za tituly""" + if body < 10: + return Titul.nic + elif body < 20: + return Titul.bc + elif body < 50: + return Titul.mgr + elif body < 100: + return Titul.dr + elif body < 200: + return Titul.doc + elif body < 500: + return Titul.prof + else: + return Titul.akad + + from odevzdavatko.models import Hodnoceni + hodnoceni_do_26_rocniku = Hodnoceni.objects.filter( + deadline_body__cislo__rocnik__rocnik__lte=26, + reseni__in=self.reseni_set.all() + ) + novejsi_body = body_z_hodnoceni( + Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) + .difference(hodnoceni_do_26_rocniku) + ) + starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku) + if body is not None: + # Ještě z toho vybereme ty správně staré body + novejsi_body = max(0, body - starsi_body) + starsi_body = min(starsi_body, body) + + # Titul pro 26. ročník + stary_titul = titul_do_26_rocniku(starsi_body) + # Titul podle aktuálních pravidel + novy_titul = Titul.z_bodu(logicke_body) + + if novejsi_body == 0: + # Žádné nové body -- titul podle starých pravidel + return str(stary_titul) + return str(max(novy_titul, stary_titul)) + + def __str__(self): + return self.osoba.plne_jmeno() diff --git a/personalni/models/skola.py b/personalni/models/skola.py new file mode 100644 index 00000000..9c59e4b1 --- /dev/null +++ b/personalni/models/skola.py @@ -0,0 +1,82 @@ +from reversion import revisions as reversion + +from django.db import models +from django_countries.fields import CountryField + +from mamweb.models.base import SeminarModelBase +from .osoba import Osoba + + +# +# Mělo by být částečně vytaženo z Aesopa +# viz https://ovvp.mff.cuni.cz/wiki/aesop/export-skol. +# + +@reversion.register(ignore_duplicates=True) +class Skola(SeminarModelBase): + + class Meta: + db_table = 'seminar_skoly' + verbose_name = 'Škola' + verbose_name_plural = 'Školy' + ordering = ['mesto', 'nazev'] + + # Interní ID + id = models.AutoField(primary_key=True) + + # Aesopi ID "izo:..." nebo "aesop:..." + # NULL znamená v exportu do aesopa "ufo" + aesop_id = models.CharField( + 'Aesop ID', max_length=32, blank=True, default='', + help_text='Aesopi ID typu "izo:..." nebo "aesop:..."', + ) + + # IZO školy (jen české školy) + izo = models.CharField( + 'IZO', max_length=32, blank=True, + help_text='IZO školy (jen české školy)', + ) + + # Celý název školy + nazev = models.CharField( + 'název', max_length=256, + help_text='Celý název školy', + ) + + # Zkraceny nazev pro zobrazení ve výsledkovce, volitelné. + # Není v Aesopovi, musíme vytvářet sami. + kratky_nazev = models.CharField( + 'zkrácený název', max_length=256, blank=True, + help_text="Zkrácený název pro zobrazení ve výsledkovce", + ) + + # Ulice může být jen číslo + ulice = models.CharField('ulice', max_length=256) + + mesto = models.CharField('město', max_length=256) + + psc = models.CharField('PSČ', max_length=32) + + # ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK) + # Ekvivalentní s CharField(max_length=2, default='CZ', ...) + stat = CountryField( + 'stát', default='CZ', + help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)', + ) + + # Jaké vzdělání škpla poskytuje? + je_zs = models.BooleanField('základní stupeň', default=True) + je_ss = models.BooleanField('střední stupeň', default=True) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka ke škole (plain text)', + ) + + kontaktni_osoba = models.ForeignKey( + Osoba, verbose_name='Kontaktní osoba', + blank=True, null=True, on_delete=models.SET_NULL, + ) + + def __str__(self): + return '{}, {}, {}'.format(self.nazev, self.ulice, self.mesto) diff --git a/personalni/utils.py b/personalni/utils.py index 0701d66a..c9037988 100644 --- a/personalni/utils.py +++ b/personalni/utils.py @@ -1,4 +1,4 @@ -import seminar.models as m +import personalni.models as m from various.utils import bez_diakritiky_translate import re diff --git a/personalni/views.py b/personalni/views.py index a45aee52..55f8a68f 100644 --- a/personalni/views.py +++ b/personalni/views.py @@ -9,8 +9,14 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.http import HttpResponse -import seminar.models as s -import seminar.models as m +from tvorba.models.tema import Tema +from tvorba.models.clanek import Clanek +from tvorba.models.uloha import Uloha +from tvorba.models.nastaveni import Nastaveni +from soustredeni.models.soustredeni import Soustredeni +from odevzdavatko.models.hodnoceni import Hodnoceni +import personalni.models as s +import personalni.models as m from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm from datetime import date @@ -31,16 +37,16 @@ class OrgoRozcestnikView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['posledni_soustredeni'] = s.Soustredeni.objects.order_by('-datum_konce').first() - nastaveni = s.Nastaveni.objects.first() + context['posledni_soustredeni'] = Soustredeni.objects.order_by('-datum_konce').first() + nastaveni = Nastaveni.objects.first() aktualni_rocnik = nastaveni.aktualni_rocnik context['posledni_cislo_url'] = nastaveni.aktualni_cislo.verejne_url() # TODO možná chceme odkazovat na právě rozpracované číslo, a ne to poslední vydané # pokud nechceme haluzit kód (= poradi) dalšího čísla, bude asi potřeba jít # přes treenody (a dát si přitom pozor na MezicisloNode) - neobodovana_reseni = s.Hodnoceni.objects.filter(body__isnull=True) - reseni_mimo_cislo = s.Hodnoceni.objects.filter(deadline_body__isnull=True) + neobodovana_reseni = Hodnoceni.objects.filter(body__isnull=True) + reseni_mimo_cislo = Hodnoceni.objects.filter(deadline_body__isnull=True) context['pocet_neobodovanych_reseni'] = neobodovana_reseni.count() context['pocet_reseni_mimo_cislo'] = reseni_mimo_cislo.count() @@ -56,11 +62,11 @@ class OrgoRozcestnikView(TemplateView): context["pocty_neopravenych_reseni"] = [(it['problem__nazev'], it['cas'].date) for it in pocty_neopravenych_reseni.all()] #FIXME: přidat stav='STAV_ZADANY' - temata = s.Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), + temata = Tema.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), rocnik=aktualni_rocnik).distinct() - ulohy = s.Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), + ulohy = Uloha.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), cislo_zadani__rocnik=aktualni_rocnik).distinct() - clanky = s.Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), + clanky = Clanek.objects.filter(Q(garant=organizator) | Q(autor=organizator) | Q(opravovatele__in=[organizator]), cislo__rocnik=aktualni_rocnik).distinct() context['temata'] = temata diff --git a/prednasky/admin.py b/prednasky/admin.py index c9807b27..e433bae6 100644 --- a/prednasky/admin.py +++ b/prednasky/admin.py @@ -6,7 +6,7 @@ from django.utils.safestring import mark_safe from django.utils.html import escape from .models import Prednaska, Seznam, STAV_NAVRH -from seminar.models import Soustredeni +from soustredeni.models.soustredeni import Soustredeni class Seznam_PrednaskaInline(admin.TabularInline): diff --git a/prednasky/models.py b/prednasky/models.py index 50c71984..bfce9c41 100644 --- a/prednasky/models.py +++ b/prednasky/models.py @@ -3,7 +3,8 @@ from django.db import models from django.utils.encoding import force_text -from seminar.models import Organizator, Soustredeni +from personalni.models.organizator import Organizator +from soustredeni.models.soustredeni import Soustredeni STAV_NAVRH = 1 STAV_BUDE = 2 diff --git a/prednasky/views.py b/prednasky/views.py index 2c370b7a..69240842 100644 --- a/prednasky/views.py +++ b/prednasky/views.py @@ -7,7 +7,8 @@ from django.db.models import Sum from django.forms import Form from prednasky.models import Prednaska, Hlasovani, Seznam, STAV_NAVRH -from seminar.models import Soustredeni, Osoba +from soustredeni.models.soustredeni import Soustredeni +from personalni.models.osoba import Osoba def newPrednaska(request): # hlasovani se vztahuje k nejnovejsimu soustredeni diff --git a/seminar/admin.py b/seminar/admin.py index e88af140..df33db7a 100644 --- a/seminar/admin.py +++ b/seminar/admin.py @@ -1,168 +1,19 @@ from django.contrib import admin from django.db import models -from django.forms import widgets, ModelForm -from django.core.exceptions import ValidationError - -from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter -from solo.admin import SingletonModelAdmin -from django.utils.safestring import mark_safe - -# Todo: reversion +from django.forms import widgets import seminar.models as m -admin.site.register(m.Rocnik) - -admin.site.register(m.Deadline) -admin.site.register(m.ZmrazenaVysledkovka) - - -class DeadlineAdminInline(admin.TabularInline): - model = m.Deadline - extra = 0 - - -class CisloForm(ModelForm): - class Meta: - model = m.Cislo - fields = '__all__' - - def clean(self): - if self.cleaned_data.get('verejne_db') == False: - return self.cleaned_data - # cn = m.CisloNode.objects.get(cislo=self.instance) - # errors = [] - # for ch in tl.all_children(cn): - # if isinstance(ch, m.TemaVCisleNode): - # if ch.tema.stav not in \ - # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # errors.append(ValidationError('Téma %(tema)s není zadané ani vyřešené', params={'tema':ch.tema})) - # - # if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode): - # if ch.uloha.stav not in \ - # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # errors.append(ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha':ch.uloha})) - # if isinstance(ch, m.ReseniNode): - # for problem in ch.reseni.problem_set: - # if problem not in \ - # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # errors.append(ValidationError('Problém %s není zadaný ani vyřešený', code=problem)) - # if errors: - # errors.append(ValidationError(mark_safe('Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) - # raise ValidationError(errors) - - errors = [] - for ch in m.Uloha.objects.filter(cislo_zadani=self.instance): - if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - errors.append( - ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha': ch})) - if errors: - errors.append(ValidationError(mark_safe( - 'Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) - if self.cleaned_data.get('datum_vydani') == None: - self.add_error('datum_vydani','Číslo určené ke zveřejnění nemá nastavené datum vydání') - - if errors: - raise ValidationError(errors) - - return self.cleaned_data - - -@admin.register(m.Cislo) -class CisloAdmin(admin.ModelAdmin): - form = CisloForm - actions = ['force_publish'] - inlines = (DeadlineAdminInline,) - - def force_publish(self,request,queryset): - for cislo in queryset: - # cn = m.CisloNode.objects.get(cislo=cislo) - # for ch in tl.all_children(cn): - # if isinstance(ch, m.TemaVCisleNode): - # if ch.tema.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # ch.tema.stav = m.Problem.STAV_ZADANY - # ch.tema.save() - # - # if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode): - # if ch.uloha.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # ch.uloha.stav = m.Problem.STAV_ZADANY - # ch.uloha.save() - # if isinstance(ch, m.ReseniNode): - # for problem in ch.reseni.problem_set: - # if problem not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - # problem.stav = m.Problem.STAV_ZADANY - # problem.save() - - for ch in m.Uloha.objects.filter(cislo_zadani=cislo): - if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - ch.stav = m.Problem.STAV_ZADANY - ch.save() - - hp = ch.hlavni_problem - if hp.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): - hp.stav = m.Problem.STAV_ZADANY - hp.save() - - # TODO Řešení, vzoráky? - # TODO Konfera/Článek? - - cislo.verejne_db = True - cislo.save() - - force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' - - -@admin.register(m.Problem) -class ProblemAdmin(PolymorphicParentModelAdmin): - base_model = m.Problem - child_models = [ - m.Tema, - m.Clanek, - m.Uloha, - m.Konfera, - ] - # Pokud chceme orezavat na aktualni rocnik, musime do modelu pridat odkaz na rocnik. Zatim bere vse. - search_fields = ['nazev'] - -# V ProblemAdmin to nejde, protoze se to nepropise do deti -class ProblemAdminMixin(object): - show_in_index = True - autocomplete_fields = ['nadproblem','autor','garant'] - filter_horizontal = ['opravovatele'] - - -@admin.register(m.Tema) -class TemaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin): - base_model = m.Tema - -@admin.register(m.Clanek) -class ClanekAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin): - base_model = m.Clanek - -@admin.register(m.Uloha) -class UlohaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin): - base_model = m.Uloha - -@admin.register(m.Konfera) -class KonferaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin): - base_model = m.Konfera - class TextAdminInline(admin.TabularInline): model = m.Text formfield_overrides = { models.TextField: {'widget': widgets.TextInput} } - exclude = ['text_zkraceny_set','text_zkraceny'] + exclude = ['text_zkraceny_set', 'text_zkraceny'] + admin.site.register(m.Text) -class ResitelInline(admin.TabularInline): - model = m.Resitel - extra = 1 - - -# admin.site.register(m.Pohadka) admin.site.register(m.Obrazek) -admin.site.register(m.Nastaveni, SingletonModelAdmin) admin.site.register(m.Novinky) diff --git a/seminar/migrations/0100_auto_20211129_2354.py b/seminar/migrations/0100_auto_20211129_2354.py index 80906d4e..4b9e1678 100644 --- a/seminar/migrations/0100_auto_20211129_2354.py +++ b/seminar/migrations/0100_auto_20211129_2354.py @@ -1,7 +1,7 @@ # Generated by Django 2.2.24 on 2021-11-29 22:54 from django.db import migrations, models -import seminar.models.tvorba +import tvorba.models class Migration(migrations.Migration): @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='cislo', name='pdf', - field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=seminar.models.tvorba.OverwriteStorage(), upload_to=seminar.models.tvorba.cislo_pdf_filename, verbose_name='pdf'), + field=models.FileField(blank=True, help_text='PDF čísla, které si mohou řešitelé stáhnout', null=True, storage=tvorba.tvorba.OverwriteStorage(), upload_to=tvorba.tvorba.cislo_pdf_filename, verbose_name='pdf'), ), ] diff --git a/seminar/models/__init__.py b/seminar/models/__init__.py index 34712ee4..20de6b50 100644 --- a/seminar/models/__init__.py +++ b/seminar/models/__init__.py @@ -1,8 +1,2 @@ -from .tvorba import * -from .odevzdavatko import * -from .base import * -from .personalni import * -from .soustredeni import * -from .pomocne import * -from .treenode import * -from .novinky import * +from .pomocne import Text, Obrazek +from .novinky import Novinky diff --git a/seminar/models/novinky.py b/seminar/models/novinky.py index cee674a8..f462a442 100644 --- a/seminar/models/novinky.py +++ b/seminar/models/novinky.py @@ -4,7 +4,7 @@ from imagekit.processors import ResizeToFit from reversion import revisions as reversion -from . import personalni as pm +from personalni.models.organizator import Organizator @reversion.register(ignore_duplicates=True) class Novinky(models.Model): @@ -26,7 +26,7 @@ class Novinky(models.Model): ], options={'quality': 95}) - autor = models.ForeignKey(pm.Organizator, verbose_name='Autor novinky', null=True, + autor = models.ForeignKey(Organizator, verbose_name='Autor novinky', null=True, on_delete=models.SET_NULL) zverejneno = models.BooleanField('Zveřejněno', default=False) diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py deleted file mode 100644 index c286558c..00000000 --- a/seminar/models/odevzdavatko.py +++ /dev/null @@ -1,200 +0,0 @@ -import os - -import reversion - -from django.contrib.sites.shortcuts import get_current_site -from django.db import models -from django.db.models import Sum -from django.urls import reverse_lazy -from django.utils import timezone -from django.conf import settings - -from seminar.models import tvorba as am -from seminar.models import personalni as pm -from seminar.models import treenode as tm -from seminar.models import base as bm - - -@reversion.register(ignore_duplicates=True) -class Reseni(bm.SeminarModelBase): - - class Meta: - db_table = 'seminar_reseni' - verbose_name = 'Řešení' - verbose_name_plural = 'Řešení' - #ordering = ['-problem', 'resitele'] # FIXME: Takhle to chceme, ale nefunguje to. - ordering = ['-cas_doruceni'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. - problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', - through='Hodnoceni') - - resitele = models.ManyToManyField(pm.Resitel, verbose_name='autoři řešení', - help_text='Seznam autorů řešení', through='Reseni_Resitele') - - - cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) - - FORMA_PAPIR = 'papir' - FORMA_EMAIL = 'email' - FORMA_UPLOAD = 'upload' - FORMA_CHOICES = [ - (FORMA_PAPIR, 'Papírové řešení'), - (FORMA_EMAIL, 'Emailem'), - (FORMA_UPLOAD, 'Upload přes web'), - ] - forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, - default=FORMA_EMAIL) - - text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', - blank=True, null=True, related_name="reseni_cely_set", - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k řešení (plain text)') - - zverejneno = models.BooleanField('řešení zveřejněno', default=False, - help_text='Udává, zda je řešení zveřejněno') - - def verejne_url(self): - return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) - - def absolute_url(self): - return "https://" + str(get_current_site(None)) + self.verejne_url() - - # má OneToOneField s: - # Konfera - - # má ForeignKey s: - # Hodnoceni - - def sum_body(self): - return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] - - def __str__(self): - return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) - # NOTE: Potenciální DB HOG (bez select_related) - - def deadline_reseni(self): - return am.Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first() - -## Pravdepodobne uz nebude potreba: -# def save(self, *args, **kwargs): -# if ((self.cislo_body is None) and (self.problem.cislo_reseni) and -# (self.problem.typ == Problem.TYP_ULOHA)): -# self.cislo_body = self.problem.cislo_reseni -# super(Reseni, self).save(*args, **kwargs) - -class Hodnoceni(bm.SeminarModelBase): - class Meta: - db_table = 'seminar_hodnoceni' - verbose_name = 'Hodnocení' - verbose_name_plural = 'Hodnocení' - - # Interní ID - id = models.AutoField(primary_key = True) - - - body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', - blank=True, null=True) - - cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', - related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) - - # V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body - deadline_body = models.ForeignKey(am.Deadline, verbose_name='deadline pro body', - related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - problem = models.ForeignKey(am.Problem, verbose_name='problém', - related_name='hodnoceni', on_delete=models.PROTECT) - - feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)') - - def __str__(self): - return "{}, {}, {}".format(self.problem, self.reseni, self.body) - -def generate_filename(self, filename): - return os.path.join( - settings.SEMINAR_RESENI_DIR, - am.aux_generate_filename(self, filename) - ) - - -@reversion.register(ignore_duplicates=True) -class PrilohaReseni(bm.SeminarModelBase): - - class Meta: - db_table = 'seminar_priloha_reseni' - verbose_name = 'Příloha řešení' - verbose_name_plural = 'Přílohy řešení' - ordering = ['reseni', 'vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key = True) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', - on_delete=models.CASCADE) - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - soubor = models.FileField('soubor', upload_to = generate_filename) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') - - res_poznamka = models.TextField('poznámka řešitele', blank=True, - help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') - - def __str__(self): - return str(self.soubor) - - def split(self): - "Vrátí cestu rozsekanou po složkách. To se hodí v templatech" - # Věřím, že tohle funguje, případně použít os.path nebo pathlib. - return self.soubor.url.split('/') - - -# Vazebna tabulka. Mozna se generuje automaticky. -@reversion.register(ignore_duplicates=True) -class Reseni_Resitele(models.Model): - - class Meta: - db_table = 'seminar_reseni_resitele' - verbose_name = 'Řešení řešitelů' - verbose_name_plural = 'Řešení řešitelů' - ordering = ['reseni', 'resitele'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitele = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) - - # podil - jakou merou se ktery resitel podilel na danem reseni - # - pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni - - def __str__(self): - return '{} od {}'.format(self.reseni, self.resitel) - # NOTE: Poteciální DB HOG bez select_related - -class ReseniNode(tm.TreeNode): - class Meta: - db_table = 'seminar_nodes_otistene_reseni' - verbose_name = 'Otištěné řešení (Node)' - verbose_name_plural = 'Otištěná řešení (Node)' - reseni = models.ForeignKey(Reseni, - on_delete=models.PROTECT, - verbose_name = 'reseni') - - def aktualizuj_nazev(self): - self.nazev = "ReseniNode: "+str(self.reseni) - - def getOdkazStr(self): - return str(self.reseni) - diff --git a/seminar/models/personalni.py b/seminar/models/personalni.py deleted file mode 100644 index 61313e87..00000000 --- a/seminar/models/personalni.py +++ /dev/null @@ -1,447 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -from django.db import models -from django.utils import timezone -from django.conf import settings -from django.core.exceptions import ValidationError -from imagekit.models import ImageSpecField, ProcessedImageField -from imagekit.processors import ResizeToFit, Transpose - -from django_countries.fields import CountryField - -from reversion import revisions as reversion - -from .base import SeminarModelBase - -logger = logging.getLogger(__name__) - - -@reversion.register(ignore_duplicates=True) -class Osoba(SeminarModelBase): - - class Meta: - db_table = 'seminar_osoby' - verbose_name = 'Osoba' - verbose_name_plural = 'Osoby' - ordering = ['prijmeni','jmeno'] - - id = models.AutoField(primary_key = True) - - jmeno = models.CharField('jméno', max_length=256) - - prijmeni = models.CharField('příjmení', max_length=256) - - prezdivka = models.CharField('přezdívka', blank=True, null=True, max_length=256) - - # User, pokud má na webu účet - user = models.OneToOneField(settings.AUTH_USER_MODEL, blank=True, null=True, - verbose_name='uživatel', on_delete=models.DO_NOTHING) - - # Pohlaví. Že ho neznáme se snad nestane (a ušetří to práci při programování) - pohlavi_muz = models.BooleanField('pohlaví (muž)', default=False) - - email = models.EmailField('e-mail', max_length=256, blank=True, default='') - - telefon = models.CharField('telefon', max_length=256, blank=True, default='') - - datum_narozeni = models.DateField('datum narození', blank=True, null=True) - - # NULL dokud nedali souhlas - datum_souhlasu_udaje = models.DateField('datum souhlasu (údaje)', blank=True, null=True, - help_text='Datum souhlasu se zpracováním osobních údajů') - - # NULL dokud nedali souhlas - datum_souhlasu_zasilani = models.DateField('datum souhlasu (spam)', blank=True, null=True, - help_text='Datum souhlasu se zasíláním MFF materiálů') - - # Alespoň odhad (rok či i měsíc) - datum_registrace = models.DateField('datum registrace do semináře', default=timezone.now) - - # Ulice může být i jen číslo - ulice = models.CharField('ulice', max_length=256, blank=True, default='') - - mesto = models.CharField('město', max_length=256, blank=True, default='') - - psc = models.CharField('PSČ', max_length=32, blank=True, default='') - - # ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK) - # Ekvivalentní s CharField(max_length=2, default='CZ', ...) - stat = CountryField('stát', default='CZ', - help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)') - - jak_se_dozvedeli = models.TextField('Jak se dozvěděli', blank=True) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k osobě (plain text)') - - foto = ProcessedImageField(verbose_name='Fotografie osoby', - upload_to='image_osoby/velke/%Y/', null = True, blank = True, - help_text = 'Vlož fotografii osoby o libovolné velikosti', - processors=[ - Transpose(Transpose.AUTO), - ResizeToFit(500, 500, upscale=False) - ], - options={'quality': 95}) - foto_male = ImageSpecField(source='foto', - processors=[ - ResizeToFit(200, 200, upscale=False) - ], - options={'quality': 95}) - - # má OneToOneField nejvýše s: - # Resitel - # Prijemce - # Organizator - - def plne_jmeno(self): - return '{} {}'.format(self.jmeno, self.prijmeni) - - def inicial_krestni(self): - jmena = self.jmeno.split() - return " ".join(['{}.'.format(jmeno[0]) for jmeno in jmena]) - - def __str__(self): - return self.plne_jmeno() - - # Overridujeme save Osoby, aby když si změní e-mail, aby se projevil i v - # Userovi (a tak se dal poslat mail s resetem hesla) - def save(self, *args, **kwargs): - if self.user is not None: - u = self.user - # U svatého tučňáka, prosím ať tohle funguje. - # (Takhle se kódit asi nemá...) - u.email = self.email - u.save() - super().save() - -# -# Mělo by být částečně vytaženo z Aesopa -# viz https://ovvp.mff.cuni.cz/wiki/aesop/export-skol. -# - -@reversion.register(ignore_duplicates=True) -class Skola(SeminarModelBase): - - class Meta: - db_table = 'seminar_skoly' - verbose_name = 'Škola' - verbose_name_plural = 'Školy' - ordering = ['mesto', 'nazev'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Aesopi ID "izo:..." nebo "aesop:..." - # NULL znamená v exportu do aesopa "ufo" - aesop_id = models.CharField('Aesop ID', max_length=32, blank=True, default='', - help_text='Aesopi ID typu "izo:..." nebo "aesop:..."') - - # IZO školy (jen české školy) - izo = models.CharField('IZO', max_length=32, blank=True, - help_text='IZO školy (jen české školy)') - - # Celý název školy - nazev = models.CharField('název', max_length=256, - help_text='Celý název školy') - - # Zkraceny nazev pro zobrazení ve výsledkovce, volitelné. - # Není v Aesopovi, musíme vytvářet sami. - kratky_nazev = models.CharField('zkrácený název', max_length=256, blank=True, - help_text="Zkrácený název pro zobrazení ve výsledkovce") - - # Ulice může být jen číslo - ulice = models.CharField('ulice', max_length=256) - - mesto = models.CharField('město', max_length=256) - - psc = models.CharField('PSČ', max_length=32) - - # ISO 3166-1 dvojznakovy kod zeme velkym pismem (CZ, SK) - # Ekvivalentní s CharField(max_length=2, default='CZ', ...) - stat = CountryField('stát', default='CZ', - help_text='ISO 3166-1 kód země velkými písmeny (CZ, SK, ...)') - - # Jaké vzdělání škpla poskytuje? - je_zs = models.BooleanField('základní stupeň', default=True) - je_ss = models.BooleanField('střední stupeň', default=True) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka ke škole (plain text)') - - kontaktni_osoba = models.ForeignKey(Osoba, verbose_name='Kontaktní osoba', - blank=True, null=True, on_delete=models.SET_NULL) - - def __str__(self): - return '{}, {}, {}'.format(self.nazev, self.ulice, self.mesto) - -class Prijemce(SeminarModelBase): - class Meta: - db_table = 'seminar_prijemce' - verbose_name = 'příjemce' - verbose_name_plural = 'příjemce' - - - # Interní ID - id = models.AutoField(primary_key = True) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k příemci čísel (plain text)') - - osoba = models.OneToOneField(Osoba, verbose_name='komu', blank=False, null=False, - help_text='Které osobě či na jakou adresu se mají zasílat čísla', - on_delete=models.CASCADE) - - zasilat_cislo_emailem = models.BooleanField('zasílat číslo emailem', help_text='True pokud chce příjemce dostávat číslo emailem', default=False) - - # FIXME: možná chceme něco jako vazbu na osobu XOR školu a počet kusů k zaslání - # FIXME: a možná taky posílání na mail a možná taky přes něj chceme posílat i řešitelům - - def __str__(self): - return self.osoba.plne_jmeno() - - -@reversion.register(ignore_duplicates=True) -class Resitel(SeminarModelBase): - - class Meta: - db_table = 'seminar_resitele' - verbose_name = 'Řešitel' - verbose_name_plural = 'Řešitelé' - ordering = ['osoba'] - - # Interní ID - id = models.AutoField(primary_key = True) - - prezdivka_resitele = models.CharField('přezdívka řešitele', blank=True, null=True, max_length=256, unique=True) - - osoba = models.OneToOneField(Osoba, blank=False, null=False, verbose_name='osoba', - on_delete=models.PROTECT) - - - skola = models.ForeignKey(Skola, blank=True, null=True, verbose_name='škola', - on_delete=models.SET_NULL) - - # Očekávaný rok maturity a vyřazení z aktivních řešitelů - rok_maturity = models.IntegerField('rok maturity', blank=True, null=True) - - ZASILAT_DOMU = 'domu' - ZASILAT_DO_SKOLY = 'do_skoly' - ZASILAT_NIKAM = 'nikam' - ZASILAT_CHOICES = [ - (ZASILAT_DOMU, 'Domů'), - (ZASILAT_DO_SKOLY, 'Do školy'), - (ZASILAT_NIKAM, 'Nezasílat papírově'), - ] - - zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU) - - zasilat_cislo_emailem = models.BooleanField('zasílat číslo emailem', help_text='True pokud chce řešitel dostávat číslo emailem', default=False) - - zasilat_cislo_papirove = models.BooleanField('zasílat číslo papírově', help_text='True pokud chce řešitel dostávat číslo papírově', default=True) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k řešiteli (plain text)') - - - def export_row(self): - "Slovnik pro pouziti v AESOP exportu" - return { - 'id': self.id, - 'name': self.osoba.jmeno, - 'surname': self.osoba.prijmeni, - 'gender': 'M' if self.osoba.pohlavi_muz else 'F', - 'born': self.osoba.datum_narozeni.isoformat() if self.osoba.datum_narozeni else '', - 'email': self.osoba.email, - 'end-year': self.rok_maturity or '', - - 'street': self.osoba.ulice, - 'town': self.osoba.mesto, - 'postcode': self.osoba.psc, - 'country': self.osoba.stat, - - 'spam-flag': 'Y' if self.osoba.datum_souhlasu_zasilani else '', - 'spam-date': self.osoba.datum_souhlasu_zasilani.isoformat() if self.osoba.datum_souhlasu_zasilani else '', - - 'school': self.skola.aesop_id if self.skola else '', - 'school-name': str(self.skola) if self.skola else 'Skola neni znama', - } - - def rocnik(self, rocnik): - """Vrati skolni rocnik resitele pro zadany Rocnik. - Vraci '' pro neznamy rok maturity resitele, Z* pro ekvivalent ZŠ.""" - if self.rok_maturity is None: - return '' - rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok) - if rozdil >= 1: - return str(rozdil) - else: - return 'Z' + str(rozdil + 9) - - def vsechny_body(self): - "Spočítá body odjakživa." - vsechna_reseni = self.reseni_set.all() - from .odevzdavatko import Hodnoceni - vsechna_hodnoceni = Hodnoceni.objects.filter( - reseni__in=vsechna_reseni) - return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) - - - def get_titul(self, body=None): - "Vrati titul jako řetězec." - - # Nejprve si zadefinujeme titul - from enum import Enum - from functools import total_ordering - @total_ordering - class Titul(Enum): - """ Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace). """ - nic = (0, '') - bc = (20, 'Bc.') - mgr = (50, 'Mgr.') - dr = (100, 'Dr.') - doc = (200, 'Doc.') - prof = (500, 'Prof.') - akad = (1000, 'Akad.') - - def __lt__(self, other): - return True if self.value[0] < other.value[0] else False - def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně. - return True if self.value[0] == other.value[0] else False - - def __str__(self): - return self.value[1] - - @classmethod - def z_bodu(cls, body): - aktualni = cls.nic - # TODO: ověřit, že to funguje - for titul in cls: # Kdyžtak použít __members__.items() - if titul.value[0] <= body: - aktualni = titul - else: - break - return aktualni - - # Hledáme body v databázi - # V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů: - # - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími - # - proto se započítávají dvojnásobně a byly posunuté hranice titulů - # - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. - from .odevzdavatko import Hodnoceni - hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all()) - novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) - - def body_z_hodnoceni(hh : list): - return sum(h.body for h in hh if h.body is not None) - - stare_body = body_z_hodnoceni(hodnoceni_do_25_rocniku) - if body is None: - nove_body = body_z_hodnoceni(novejsi_hodnoceni) - else: - # Zjistíme, kolik bodů jsou staré, tedy hodnotnější - nove_body = max(0, body - stare_body) # Všechny body nad počet původních hodnotnějších - stare_body = min(stare_body, body) # Skutečný počet hodnotnějších bodů - logicke_body = 2*stare_body + nove_body - - - # Titul se určí následovně: - # - Pokud se řeší body, které jsou starší, než do 26 ročníku (včetně), dáváme tituly postaru. - # - Jinak dáváme tituly po novu... - # - ... ale titul se nesmí odebrat, pokud se zmenšil. - def titul_do_26_rocniku(body): - """ Původní hranice bodů za tituly """ - if body < 10: - return Titul.nic - elif body < 20: - return Titul.bc - elif body < 50: - return Titul.mgr - elif body < 100: - return Titul.dr - elif body < 200: - return Titul.doc - elif body < 500: - return Titul.prof - else: - return Titul.akad - - from .odevzdavatko import Hodnoceni - hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all()) - novejsi_body = body_z_hodnoceni( - Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) - .difference(hodnoceni_do_26_rocniku) - ) - starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku) - if body is not None: - # Ještě z toho vybereme ty správně staré body - novejsi_body = max(0, body - starsi_body) - starsi_body = min(starsi_body, body) - - # Titul pro 26. ročník - stary_titul = titul_do_26_rocniku(starsi_body) - # Titul podle aktuálních pravidel - novy_titul = Titul.z_bodu(logicke_body) - - if novejsi_body == 0: - # Žádné nové body -- titul podle starých pravidel - return str(stary_titul) - return str(max(novy_titul, stary_titul)) - - - def __str__(self): - return self.osoba.plne_jmeno() - - -@reversion.register(ignore_duplicates=True) -class Organizator(SeminarModelBase): - osoba = models.OneToOneField(Osoba, verbose_name='osoba', related_name='org', - help_text='osobní údaje organizátora', null=False, blank=False, - on_delete=models.PROTECT) - - vytvoreno = models.DateTimeField( - 'Vytvořeno', - default=timezone.now, - blank=True, - editable=False - ) - - # Ne, date to nebude. SQLite: invalid literal for int() with base 10: b'17 23:00:00' - organizuje_od = models.DateTimeField('Organizuje od', blank=True, null=True) - - organizuje_do = models.DateTimeField('Organizuje do', blank=True, null=True) - - studuje = models.CharField('Studium aj.', max_length = 256, - null = True, blank = True, - help_text="Např. 'Studuje Obecnou fyziku (Bc.), 3. ročník', " - "'Vystudovala Diskrétní modely a algoritmy (Mgr.)' nebo " - "'Přednáší na MFF'") - - strucny_popis_organizatora = models.TextField('Stručný popis organizátora', - null = True, blank = True) - - skola = models.CharField('Škola, kterou studuje', max_length = 256, null=True, blank=True, - help_text="Škola, např. MFF, VŠCHT, VUT, ... prostě aby se nemuselo psát do studuje" - "školu, ale jen obor, možnost zobrazit zvlášť") - - def clean(self): - if self.organizuje_od and self.organizuje_do and (self.organizuje_od > self.organizuje_do): - raise ValidationError("Organizátor nemůže skončit s organizováním dříve než začal!") - super().clean() - - def __str__(self): - if self.osoba.prezdivka: - return "{} '{}' {}".format(self.osoba.jmeno, - self.osoba.prezdivka, - self.osoba.prijmeni) - else: - return "{} {}".format(self.osoba.jmeno, self.osoba.prijmeni) - - class Meta: - verbose_name = 'Organizátor' - verbose_name_plural = 'Organizátoři' - # Řadí aktivní orgy na začátek, pod tím v pořadí od nejstarších neaktivní orgy. - # TODO: Chtěl bych spíš mít nejstarší orgy dole. - # TODO: Zohledňovat přezdívky? - # TODO: Sjednotit s tím, jak se řadí organizátoři v seznau orgů na webu - ordering = ['-organizuje_do', 'osoba__jmeno', 'osoba__prijmeni'] diff --git a/seminar/models/pomocne.py b/seminar/models/pomocne.py index cb552a67..2cde3be9 100644 --- a/seminar/models/pomocne.py +++ b/seminar/models/pomocne.py @@ -3,7 +3,7 @@ import logging import os from django.db import models -from .base import SeminarModelBase +from mamweb.models.base import SeminarModelBase logger = logging.getLogger(__name__) diff --git a/seminar/models/soustredeni.py b/seminar/models/soustredeni.py deleted file mode 100644 index 03ff5909..00000000 --- a/seminar/models/soustredeni.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -import os - -from django.db import models -from django.urls import reverse -from reversion import revisions as reversion - -from django.conf import settings - -from . import personalni as pm - -from .base import SeminarModelBase -from seminar.models import tvorba as am - -logger = logging.getLogger(__name__) - - -@reversion.register(ignore_duplicates=True) -class Soustredeni(SeminarModelBase): - - class Meta: - db_table = 'seminar_soustredeni' - verbose_name = 'Soustředění' - verbose_name_plural = 'Soustředění' - ordering = ['-rocnik__rocnik', '-datum_zacatku'] - - # Interní ID - id = models.AutoField(primary_key = True) - - rocnik = models.ForeignKey(am.Rocnik, verbose_name='ročník', related_name='soustredeni', - on_delete=models.PROTECT) - - datum_zacatku = models.DateField('datum začátku', blank=True, null=True, - help_text='První den soustředění') - - datum_konce = models.DateField('datum konce', blank=True, null=True, - help_text='Poslední den soustředění') - - verejne_db = models.BooleanField('soustředění zveřejněno', db_column='verejne', default=False) - - misto = models.CharField('místo soustředění', max_length=256, blank=True, default='', - help_text='Místo (název obce, volitelně též objektu') - - ucastnici = models.ManyToManyField(pm.Resitel, verbose_name='účastníci soustředění', - help_text='Seznam účastníků soustředění', through='Soustredeni_Ucastnici') - - organizatori = models.ManyToManyField(pm.Organizator, - verbose_name='Organizátoři soustředění', - help_text='Seznam organizátorů soustředění', - through='Soustredeni_Organizatori') - - text = models.TextField('text k soustředění (HTML)', blank=True, default='') - - TYP_JARNI = 'jarni' - TYP_PODZIMNI = 'podzimni' - TYP_VIKEND = 'vikend' - TYP_VYLET = 'vylet' - TYP_CHOICES = [ - (TYP_JARNI, 'Jarní soustředění'), - (TYP_PODZIMNI, 'Podzimní soustředění'), - (TYP_VIKEND, 'Víkendový sraz'), - (TYP_VYLET, 'Výlet'), - ] - typ = models.CharField('typ akce', max_length=16, choices=TYP_CHOICES, blank=False, default=TYP_PODZIMNI) - - exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False, - help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti)') - - def __str__(self): - return '{} ({})'.format(self.misto, self.datum_zacatku) - - def verejne(self): - return self.verejne_db - verejne.boolean = True - - def verejne_url(self): - #return reverse('seminar_soustredeni', kwargs={'pk': self.id}) - return reverse('seminar_seznam_soustredeni') - - -@reversion.register(ignore_duplicates=True) -class Soustredeni_Ucastnici(SeminarModelBase): -# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu - - class Meta: - db_table = 'seminar_soustredeni_ucastnici' - verbose_name = 'Účast na soustředění' - verbose_name_plural = 'Účasti na soustředění' - ordering = ['soustredeni', 'resitel'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitel = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti (plain text)') - - def __str__(self): - return '{} na {}'.format(self.resitel, self.soustredeni) - # NOTE: Poteciální DB HOG bez select_related - - -@reversion.register(ignore_duplicates=True) -class Soustredeni_Organizatori(SeminarModelBase): -# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu - - class Meta: - db_table = 'seminar_soustredeni_organizatori' - verbose_name = 'Účast organizátorů na soustředění' - verbose_name_plural = 'Účasti organizátorů na soustředění' - ordering = ['soustredeni', 'organizator'] - - # Interní ID - id = models.AutoField(primary_key = True) - - organizator = models.ForeignKey(pm.Organizator, verbose_name='organizátor', - on_delete=models.PROTECT) - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - on_delete=models.PROTECT) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti organizátora (plain text)') - - def __str__(self): - return '{} na {}'.format(self.organizator, self.soustredeni) - # NOTE: Poteciální DB HOG bez select_related - - -# FIXME cycle import - - -# Django neumí jednoduše serializovat partial nebo třídu s __call__ -# (https://docs.djangoproject.com/en/1.8/topics/migrations/), -# neprojdou pak migrace. Takže rozlišení funkcí generujících názvy souboru -# podle adresáře řešíme takto. - -## -def generate_filename_konfera(self, filename): - return os.path.join( - settings.SEMINAR_KONFERY_DIR, - am.aux_generate_filename(self, filename) - ) - -## - -@reversion.register(ignore_duplicates=True) -class Konfera(am.Problem): - class Meta: - db_table = 'seminar_konfera' - verbose_name = 'Konfera' - verbose_name_plural = 'Konfery' - - anotace = models.TextField('anotace', blank=True, - help_text='Popis, o čem bude konfera.') - - abstrakt = models.TextField('abstrakt', blank=True, - help_text='Abstrakt konfery tak, jak byl uveden ve sborníku') - - # FIXME: Umíme omezit jen na účastníky daného soustřeďka? - ucastnici = models.ManyToManyField(pm.Resitel, verbose_name='účastníci konfery', - help_text='Seznam účastníků konfery', through='Konfery_Ucastnici') - - soustredeni = models.ForeignKey(Soustredeni, verbose_name='soustředění', - related_name='konfery', on_delete = models.SET_NULL, null=True) - - TYP_VELETRH = 'veletrh' - TYP_PREZENTACE = 'prezentace' - TYP_CHOICES = [ - (TYP_VELETRH, 'Veletrh (postery)'), - (TYP_PREZENTACE, 'Prezentace (přednáška)'), - ] - typ_prezentace = models.CharField('typ prezentace', max_length=16, choices=TYP_CHOICES, - blank=False, default=TYP_VELETRH) - - prezentace = models.FileField('prezentace',help_text = 'Prezentace nebo fotka posteru', - upload_to = generate_filename_konfera, blank=True) - - materialy = models.FileField('materialy', - help_text = 'Další materiály ke konfeře zabalené do jednoho souboru', - upload_to = generate_filename_konfera, blank=True) - - def __str__(self): - return "{}: ({})".format(self.nazev, self.soustredeni) - - def cislo_node(self): - return None - - -@reversion.register(ignore_duplicates=True) -class Konfery_Ucastnici(models.Model): - - class Meta: - db_table = 'seminar_konfery_ucastnici' - verbose_name = 'Účast na konfeře' - verbose_name_plural = 'Účasti na konfeře' - ordering = ['konfera', 'resitel'] - - # Interní ID - id = models.AutoField(primary_key = True) - - resitel = models.ForeignKey(pm.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) - - konfera = models.ForeignKey(Konfera, verbose_name='konfera', on_delete=models.CASCADE) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k účasti (plain text)') - - def __str__(self): - return '{} na {}'.format(self.resitel, self.konfera) - # NOTE: Poteciální DB HOG bez select_related diff --git a/seminar/models/tvorba.py b/seminar/models/tvorba.py deleted file mode 100644 index 54e769c8..00000000 --- a/seminar/models/tvorba.py +++ /dev/null @@ -1,758 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -import os -import subprocess -import pathlib -import tempfile -import logging - -from django.contrib.sites.shortcuts import get_current_site -from django.db import models -from django.db.models import Q -from django.template.loader import render_to_string -from django.utils import timezone -from django.conf import settings -from django.urls import reverse -from django.core.cache import cache -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.core.files.storage import FileSystemStorage -from django.utils.text import get_valid_filename -from django.utils.functional import cached_property - -from solo.models import SingletonModel -from taggit.managers import TaggableManager - -from reversion import revisions as reversion - -from seminar.utils import roman -from treenode import treelib - -from unidecode import unidecode # Používám pro získání ID odkazu (ještě je to někde po někom zakomentované) - -from polymorphic.models import PolymorphicModel - -from django.core.mail import EmailMessage -from seminar.utils import aktivniResitele - -from . import personalni as pm - -from .base import SeminarModelBase - -logger = logging.getLogger(__name__) - -class OverwriteStorage(FileSystemStorage): - """ Varianta FileSystemStorage, která v případě, že soubor cílového - jména již existuje, ho smaže a místo něj uloží soubor nový""" - def get_available_name(self,name, max_length=None): - if self.exists(name): - os.remove(os.path.join(self.location,name)) - return super().get_available_name(name,max_length) - -@reversion.register(ignore_duplicates=True) -class Rocnik(SeminarModelBase): - - class Meta: - db_table = 'seminar_rocniky' - verbose_name = 'Ročník' - verbose_name_plural = 'Ročníky' - ordering = ['-rocnik'] - - # Interní ID - id = models.AutoField(primary_key = True) - - prvni_rok = models.IntegerField('první rok', db_index=True, unique=True) - - rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True) - - exportovat = models.BooleanField('export do AESOPa', db_column='exportovat', default=False, - help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti),' - ' a to jen čísla s veřejnou výsledkovkou') - - # má OneToOneField s: - # RocnikNode - - def __str__(self): - return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1) - - # Ročník v římských číslech - def roman(self): - return roman(int(self.rocnik)) - - def verejne(self): - return len(self.verejna_cisla()) > 0 - verejne.boolean = True - verejne.short_description = 'Veřejný (jen dle čísel)' - - def neverejna_cisla(self): - vc = [c for c in self.cisla.all() if not c.verejne()] - vc.sort(key=lambda c: c.poradi) - return vc - - def verejna_cisla(self): - vc = [c for c in self.cisla.all() if c.verejne()] - vc.sort(key=lambda c: c.poradi) - return vc - - def posledni_verejne_cislo(self): - vc = self.verejna_cisla() - return vc[-1] if vc else None - - def verejne_vysledkovky_cisla(self): - vc = list(self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct()) - vc.sort(key=lambda c: c.poradi) - return vc - - def posledni_zverejnena_vysledkovka_cislo(self): - vc = self.verejne_vysledkovky_cisla() - return vc[-1] if vc else None - - def druhy_rok(self): - return self.prvni_rok + 1 - - def verejne_url(self): - return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik}) - - @classmethod - def cached_rocnik(cls, r_id): - name = 'rocnik_%s' % (r_id, ) - c = cache.get(name) - if c is None: - c = cls.objects.get(id=r_id) - cache.set(name, c, 300) - return c - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.rocniknode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - -def cislo_pdf_filename(self, filename): - rocnik = str(self.rocnik.rocnik) - return pathlib.Path('cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi)) - -def cislo_png_filename(self, filename): - rocnik = str(self.rocnik.rocnik) - return pathlib.Path('cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi)) - -@reversion.register(ignore_duplicates=True) -class Cislo(SeminarModelBase): - - class Meta: - db_table = 'seminar_cisla' - verbose_name = 'Číslo' - verbose_name_plural = 'Čísla' - ordering = ['-rocnik__rocnik', '-poradi'] - - # Interní ID - id = models.AutoField(primary_key = True) - - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník', related_name='cisla', - db_index=True,on_delete=models.PROTECT) - - poradi = models.CharField('název čísla', max_length=32, db_index=True, - help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!') - - datum_vydani = models.DateField('datum vydání', blank=True, null=True, - help_text='Datum vydání finální verze') - - verejne_db = models.BooleanField('číslo zveřejněno', - db_column='verejne', default=False) - - poznamka = models.TextField('neveřejná poznámka', blank=True, - help_text='Neveřejná poznámka k číslu (plain text)') - - pdf = models.FileField('pdf', upload_to=cislo_pdf_filename, null=True, blank=True, - help_text='PDF čísla, které si mohou řešitelé stáhnout', storage=OverwriteStorage()) - - titulka_nahled = models.ImageField('Obrázek titulní strany', upload_to=cislo_png_filename, null=True, blank=True, - help_text='Obrázek titulní strany, generuje se automaticky') - - # má OneToOneField s: - # CisloNode - - def kod(self): - return '%s.%s' % (self.rocnik.rocnik, self.poradi) - kod.short_description = 'Kód čísla' - - def __str__(self): - # Potenciální DB HOG, pokud by se ročník necachoval - r = Rocnik.cached_rocnik(self.rocnik_id) - return '{}.{}'.format(r.rocnik, self.poradi) - - def verejne(self): - return self.verejne_db - verejne.boolean = True - - def verejne_url(self): - return reverse('seminar_cislo', kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi}) - - def absolute_url(self): - return "https://" + str(get_current_site(None)) + self.verejne_url() - - def nasledujici(self): - "Vrací None, pokud je toto poslední" - return self.relativni_v_rocniku(1) - - def predchozi(self): - "Vrací None, pokud je toto první" - return self.relativni_v_rocniku(-1) - - def relativni_v_rocniku(self, rel_index): - "Číslo o `index` dále v ročníku. None pokud neexistuje." - cs = self.rocnik.cisla.order_by('poradi').all() - i = list(cs).index(self) + rel_index - if (i < 0) or (i >= len(cs)): - return None - return cs[i] - - def vygeneruj_nahled(self): - VYSKA = 594 - sirka = int(VYSKA*210/297) - if not self.pdf: - return - - - # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej - if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path): - png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png') - - subprocess.run([ - "gs", - "-sstdout=%stderr", - "-dSAFER", - "-dNOPAUSE", - "-dBATCH", - "-dNOPROMPT", - "-sDEVICE=png16m", - "-r300x300", - "-dFirstPage=1d", - "-dLastPage=1d", - "-sOutputFile=" + str(png_filename), - "-f%s" % self.pdf.path - ], - check=True, - capture_output=True - ) - - with open(png_filename,'rb') as f: - self.titulka_nahled.save('',f,True) - - png_filename.unlink() - png_filename.parent.rmdir() - - - - @classmethod - def get(cls, rocnik, cislo): - try: - r = Rocnik.objects.get(rocnik=rocnik) - c = r.cisla.get(poradi=cislo) - except ObjectDoesNotExist: - return None - return c - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_verejne = self.verejne_db - - def posli_cislo_mailem(self): - # parametry e-mailu - odkaz = self.absolute_url() - - poslat_z_mailu = 'zadani@mam.mff.cuni.cz' - predmet = 'Vyšlo číslo {}'.format(self.kod()) - # TODO Možná nechceme všem psát „Ahoj“, např. příjemcům… - text_mailu = 'Ahoj,\n' \ - 'na adrese {} najdete nejnovější číslo.\n' \ - 'Vaše M&M\n'.format(odkaz) - - predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!' - text_mailu_prvni = 'Milý řešiteli,\n'\ - 'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'\ - 'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'\ - 'Organizátoři M&M\n'.format(self.rocnik.rocnik, odkaz) - - predmet_resitel = predmet_prvni if self.poradi == "1" else predmet - text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu - - - # Prijemci e-mailu - resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True) - - def posli(subject, text, resitele): - emaily = map(lambda resitel: resitel.osoba.email, resitele) - if not settings.POSLI_MAILOVOU_NOTIFIKACI: - print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily)) - return - - email = EmailMessage( - subject=subject, - body=text, - from_email=poslat_z_mailu, - bcc=list(emaily) - #bcc = příjemci skryté kopie - ) - - email.send() - - paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/" - - posli(predmet_resitel, text_mailu_resitel + paticka, resitele_vsichni.filter(zasilat_cislo_papirove=False)) - posli(predmet_resitel, text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka, - resitele_vsichni.filter(zasilat_cislo_papirove=True)) - - paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz." - posli(predmet, text_mailu + paticka_prijemce, pm.Prijemce.objects.filter(zasilat_cislo_emailem=True)) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - self.vygeneruj_nahled() - # Při zveřejnění pošle mail - if self.verejne_db and not self.__original_verejne: - self.posli_cislo_mailem() - # *Node.save() aktualizuje název *Nodu. - try: - self.cislonode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit - logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…') - from seminar.models.treenode import CisloNode - CisloNode.objects.create(cislo=self) - - def zlomovy_deadline_pro_papirove_cislo(self): - prvni_deadline = Deadline.objects.filter(Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS), cislo=self).first() - if prvni_deadline is None: - posledni_deadline = self.posledni_deadline - if posledni_deadline is None: - # TODO promyslet, co se má stát tady - return Deadline.objects.filter(Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) | Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)).order_by("deadline").last() - return posledni_deadline - return prvni_deadline - - @property - def posledni_deadline(self): - return self.deadline_v_cisle.all().order_by("deadline").last() - -class Deadline(SeminarModelBase): - class Meta: - db_table = 'seminar_deadliny' - verbose_name = 'Deadline' - verbose_name_plural = 'Deadliny' - ordering = ['deadline'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_verejna_vysledkovka = self.verejna_vysledkovka - - id = models.AutoField(primary_key=True) - - # V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min) - deadline = models.DateTimeField(blank=False, default=timezone.make_aware(datetime.datetime.combine(timezone.now(), datetime.time.max))) - - cislo = models.ForeignKey(Cislo, verbose_name='deadline v čísle', - related_name='deadline_v_cisle', blank=False, - on_delete=models.CASCADE) - - TYP_CISLA = 'cisla' - TYP_PRVNI_A_SOUS = 'prvniasous' - TYP_PRVNI = 'prvni' - TYP_SOUS = 'sous' - TYP_CHOICES = [ - (TYP_CISLA, 'Deadline celého čísla'), - (TYP_PRVNI, 'První deadline'), - (TYP_PRVNI_A_SOUS, 'Sousový a první deadline'), - (TYP_SOUS, 'Sousový deadline'), - ] - CHOICES_MAP = dict(TYP_CHOICES) - typ = models.CharField('typ deadlinu', max_length=32, - choices=TYP_CHOICES, blank=False) - - verejna_vysledkovka = models.BooleanField('veřejná výsledkovka', - db_column='verejna_vysledkovka', - default=False) - - def __str__(self): - return self.CHOICES_MAP[self.typ] + " " + str(self.cislo) - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka: - self.vygeneruj_vysledkovku() - if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"): - self.vysledkovka_v_deadlinu.delete() - - def vygeneruj_vysledkovku(self): - from vysledkovky.utils import VysledkovkaCisla - if hasattr(self, "vysledkovka_v_deadlinu"): - self.vysledkovka_v_deadlinu.delete() - vysledkovka = VysledkovkaCisla(self.cislo, jen_verejne=True, do_deadlinu=self) - if len(vysledkovka.radky_vysledkovky) != 0: - ZmrazenaVysledkovka.objects.create( - deadline=self, - html=render_to_string( - "vysledkovky/vysledkovka_cisla.html", - context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id} - ) - ) - - -class ZmrazenaVysledkovka(SeminarModelBase): - class Meta: - db_table = 'seminar_vysledkovky' - verbose_name = 'Zmražená výsledkovka' - verbose_name_plural = 'Zmražené výsledkovky' - - deadline = models.OneToOneField( - Deadline, - on_delete=models.CASCADE, - primary_key=True, - related_name="vysledkovka_v_deadlinu" - ) - - html = models.TextField(null=False, blank=False) - - -@reversion.register(ignore_duplicates=True) -# Pozor na následující řádek. *Nekrmit, asi kouše!* -class Problem(SeminarModelBase,PolymorphicModel): - - class Meta: - # Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys. - # TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali - # po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí - # modelu Problem? - - #abstract = True - db_table = 'seminar_problemy' - verbose_name = 'Problém' - verbose_name_plural = 'Problémy' - ordering = ['nazev'] - - # Interní ID - id = models.AutoField(primary_key = True) - - # Název - nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky - - # Problém má podproblémy - nadproblem = models.ForeignKey('self', verbose_name='nadřazený problém', - related_name='podproblem', null=True, blank=True, - on_delete=models.SET_NULL) - - STAV_NAVRH = 'navrh' - STAV_ZADANY = 'zadany' - STAV_VYRESENY = 'vyreseny' - STAV_SMAZANY = 'smazany' - STAV_CHOICES = [ - (STAV_NAVRH, 'Návrh'), - (STAV_ZADANY, 'Zadaný'), - (STAV_VYRESENY, 'Vyřešený'), - (STAV_SMAZANY, 'Smazaný'), - ] - stav = models.CharField('stav problému', max_length=32, choices=STAV_CHOICES, blank=False, default=STAV_NAVRH) - # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) - - zamereni = TaggableManager(verbose_name='zaměření', - help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True) - - poznamka = models.TextField('org poznámky (HTML)', blank=True, - help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...') - - autor = models.ForeignKey(pm.Organizator, verbose_name='autor problému', - related_name='autor_problemu_%(class)s', null=True, blank=True, - on_delete=models.SET_NULL) - - garant = models.ForeignKey(pm.Organizator, verbose_name='garant zadaného problému', - related_name='garant_problemu_%(class)s', null=True, blank=True, - on_delete=models.SET_NULL) - - opravovatele = models.ManyToManyField(pm.Organizator, verbose_name='opravovatelé', - blank=True, related_name='opravovatele_%(class)s') - - kod = models.CharField('lokální kód', max_length=32, blank=True, default='', - help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku') - - - vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) - - - def __str__(self): - return self.nazev - - # Implicitini implementace, jednotlivé dědící třídy si přepíšou - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: - if self.nadproblem: - 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 '' - -# def verejne(self): -# # aktuálně podle stavu problému -# # FIXME pro některé problémy možná chceme override -# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je. -# # Je to tak správně? Podle aktuální představy ano. -# stav_verejny = False -# if self.stav == 'zadany' or self.stav == 'vyreseny': -# stav_verejny = True -# print("stav_verejny: {}".format(stav_verejny)) -# -# cislo_verejne = False -# cislonode = self.cislo_node() -# if cislonode is None: -# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu -# print("empty node") -# return stav_verejny -# else: -# cislo_zadani = cislonode.cislo -# if (cislo_zadani and cislo_zadani.verejne()): -# print("cislo: {}".format(cislo_zadani)) -# cislo_verejne = True -# print("stav_verejny: {}".format(stav_verejny)) -# print("cislo_verejne: {}".format(cislo_verejne)) -# return (stav_verejny and cislo_verejne) -# verejne.boolean = True - - def verejne_url(self): - return reverse('seminar_problem', kwargs={'pk': self.id}) - - def admin_url(self): - return reverse('admin:seminar_problem_change', args=(self.id, )) - - @cached_property - def hlavni_problem(self): - """ Pro daný problém vrátí jeho nejvyšší nadproblém.""" - problem = self - while not (problem.nadproblem is None): - problem = problem.nadproblem - return problem - -# FIXME - k úloze - def body_v_zavorce(self): - """Vrať string s body v závorce jsou-li u problému vyplněné, jinak '' - - Je-li desetinná část nulová, nezobrazuj ji. - """ - pocet_bodu = None - if self.body: - b = self.body - pocet_bodu = int(b) if int(b) == b else b - return "({}\u2009b)".format(pocet_bodu) if self.body else "" - -class Tema(Problem): - class Meta: - db_table = 'seminar_temata' - verbose_name = 'Téma' - verbose_name_plural = 'Témata' - - TEMA_TEMA = 'tema' - TEMA_SERIAL = 'serial' - TEMA_CHOICES = [ - (TEMA_TEMA, 'Téma'), - (TEMA_SERIAL, 'Seriál'), - ] - tema_typ = models.CharField('Typ tématu', max_length=16, choices=TEMA_CHOICES, - blank=False, default=TEMA_TEMA) - - rocnik = models.ForeignKey(Rocnik, verbose_name='ročník',related_name='temata',blank=True, null=True, - on_delete=models.PROTECT) - - abstrakt = models.TextField('Abstrakt na rozcestník', blank=True) - obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True) - - @cached_property - def kod_v_rocniku(self): - 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) - 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 '' - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - for tvcn in self.temavcislenode_set.all(): - tvcn.save() - - def cislo_node(self): - tema_node_set = self.temavcislenode_set.all() - tema_cisla_vyskyt = [] - from seminar.models.treenode import CisloNode - for tn in tema_node_set: - tema_cisla_vyskyt.append( - treelib.get_upper_node_of_type(tn, CisloNode).cislo) - tema_cisla_vyskyt.sort(key=lambda x:x.datum_vydani) - prvni_zadani = tema_cisla_vyskyt[0] - return prvni_zadani.cislonode - -class Clanek(Problem): - class Meta: - db_table = 'seminar_clanky' - verbose_name = 'Článek' - verbose_name_plural = 'Články' - - cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT, - verbose_name='číslo vydání', related_name='vydane_clanky') - - @cached_property - def kod_v_rocniku(self): - if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: -# 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) - 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 '' - - def node(self): - return None - - -class Uloha(Problem): - class Meta: - db_table = 'seminar_ulohy' - verbose_name = 'Úloha' - verbose_name_plural = 'Úlohy' - - cislo_zadani = models.ForeignKey(Cislo, verbose_name='číslo zadání', blank=True, - null=True, related_name='zadane_ulohy', on_delete=models.PROTECT) - - cislo_deadline = models.ForeignKey(Cislo, verbose_name='číslo deadlinu', blank=True, - null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT) - - cislo_reseni = models.ForeignKey(Cislo, verbose_name='číslo řešení', blank=True, - null=True, related_name='resene_ulohy', - help_text='Číslo s řešením úlohy, jen pro úlohy', - on_delete=models.PROTECT) - - max_body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='maximum bodů', - blank=True, null=True) - - # má OneToOneField s: - # UlohaZadaniNode - # UlohaVzorakNode - - @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 - 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 '' - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.ulohazadaninode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - try: - self.ulohavzoraknode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - - def cislo_node(self): - zadani_node = self.ulohazadaninode - from seminar.models.treenode import CisloNode - return treelib.get_upper_node_of_type(zadani_node, CisloNode) - - -def aux_generate_filename(self, filename): - """Pomocná funkce generující ošetřený název souboru v adresáři s datem""" - clean = get_valid_filename( - unidecode(filename.replace('/', '-').replace('\0', '')) - ) - datedir = timezone.now().strftime('%Y-%m') - fname = "{}/{}".format( - timezone.now().strftime('%Y-%m-%d-%H:%M'), - clean) - return os.path.join(datedir, fname) - - -class Pohadka(SeminarModelBase): - """Kus pohádky před/za úlohou v čísle""" - - class Meta: - db_table = 'seminar_pohadky' - verbose_name = 'Pohádka' - verbose_name_plural = 'Pohádky' - ordering = ['vytvoreno'] - - # Interní ID - id = models.AutoField(primary_key=True) - - autor = models.ForeignKey( - pm.Organizator, - verbose_name="Autor pohádky", - - # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je - null=True, - blank=False, - on_delete=models.SET_NULL - ) - - vytvoreno = models.DateTimeField( - 'Vytvořeno', - default=timezone.now, - blank=True, - editable=False - ) - - # má OneToOneField s: - # PohadkaNode - - def __str__(self): - uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..." - return uryvek - - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - # *Node.save() aktualizuje název *Nodu. - try: - self.pohadkanode.save() - except ObjectDoesNotExist: - # Neexistující *Node nemá smysl aktualizovat. - pass - - -@reversion.register(ignore_duplicates=True) -class Nastaveni(SingletonModel): - - class Meta: - db_table = 'seminar_nastaveni' - verbose_name = 'Nastavení semináře' - -# aktualni_rocnik = models.ForeignKey(Rocnik, verbose_name='aktuální ročník', -# null=False, on_delete=models.PROTECT) - - aktualni_cislo = models.ForeignKey(Cislo, verbose_name='Aktuální číslo', - null=False, on_delete=models.PROTECT) - - cena_sous = models.IntegerField(null=False, - verbose_name="Účastnický poplatek za soustředění", - default=1000) - - @property - def aktualni_rocnik(self): - return self.aktualni_cislo.rocnik - - def __str__(self): - return 'Nastavení semináře' - - def admin_url(self): - return reverse('admin:seminar_nastaveni_change', args=(self.id, )) - - def verejne(self): - return False diff --git a/seminar/templatetags/deadliny.py b/seminar/templatetags/deadliny.py index 199a1eef..b0eeaf2c 100644 --- a/seminar/templatetags/deadliny.py +++ b/seminar/templatetags/deadliny.py @@ -1,30 +1,30 @@ from django import template from django.utils.safestring import mark_safe register = template.Library() -import seminar.models as m +from tvorba.models.deadline import Deadline @register.filter(name='deadline_kratseji') -def deadline_kratsi_text(deadline: m.Deadline): +def deadline_kratsi_text(deadline: Deadline): if deadline is None: return 'NONE' strings = { - m.Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯", - m.Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ", - m.Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ", - m.Deadline.TYP_CISLA: f"{deadline.cislo} ✓", + Deadline.TYP_PRVNI: f"{deadline.cislo} ⭯", + Deadline.TYP_SOUS: f"{deadline.cislo} Ⓢ", + Deadline.TYP_PRVNI_A_SOUS: f"{deadline.cislo} ⭯Ⓢ", + Deadline.TYP_CISLA: f"{deadline.cislo} ✓", } return strings[deadline.typ] @register.filter(name='deadline_html') -def deadline_html(deadline: m.Deadline): +def deadline_html(deadline: Deadline): if deadline is None: return 'Neznámý deadline' text = deadline_kratsi_text(deadline) classes = { - m.Deadline.TYP_PRVNI: 'preddeadline', - m.Deadline.TYP_SOUS: 'sous_deadline', - m.Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline', - m.Deadline.TYP_CISLA: 'final_deadline', + Deadline.TYP_PRVNI: 'preddeadline', + Deadline.TYP_SOUS: 'sous_deadline', + Deadline.TYP_PRVNI_A_SOUS: 'sous_deadline', + Deadline.TYP_CISLA: 'final_deadline', } return mark_safe(f'{text}') diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 4627989e..aebdb83f 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -9,11 +9,17 @@ from django.db.models import Q, Sum, Count from django.views.generic.base import RedirectView from django.core.exceptions import PermissionDenied -import seminar.models as s -import seminar.models as m -from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, \ - Organizator, Resitel, Novinky, Tema, Clanek, \ - Deadline # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci +from tvorba.models.problem import Problem +from tvorba.models.tema import Tema +from tvorba.models.clanek import Clanek +from tvorba.models.nastaveni import Nastaveni +from tvorba.models.rocnik import Rocnik +from tvorba.models.cislo import Cislo +from tvorba.models.deadline import Deadline +from personalni.models.organizator import Organizator +from personalni.models.resitel import Resitel +from seminar.models.novinky import Novinky + #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva from seminar import utils from treenode import treelib @@ -113,7 +119,7 @@ def ZadaniTemataView(request): nastaveni = get_object_or_404(Nastaveni) verejne = nastaveni.aktualni_cislo.verejne() akt_rocnik = nastaveni.aktualni_cislo.rocnik - temata = s.Tema.objects.filter(rocnik=akt_rocnik, stav='zadany') + temata = Tema.objects.filter(rocnik=akt_rocnik, stav='zadany') return render(request, 'seminar/tematka/rozcestnik.html', { 'tematka': temata, @@ -244,7 +250,7 @@ class TitulniStranaView(generic.ListView): context = super(TitulniStranaView, self).get_context_data(**kwargs) nastaveni = get_object_or_404(Nastaveni) - deadline = m.Deadline.objects.filter(deadline__gte=timezone.now()).order_by("deadline").first() + deadline = Deadline.objects.filter(deadline__gte=timezone.now()).order_by("deadline").first() context['nejblizsi_deadline'] = deadline # Aktuální témata @@ -349,7 +355,7 @@ def resiteleRocnikuCsvExportView(request, rocnik): assert request.method in ('GET', 'HEAD') return dataResiteluCsvResponse( utils.resi_v_rocniku( - get_object_or_404(m.Rocnik, rocnik=rocnik) + get_object_or_404(Rocnik, rocnik=rocnik) ) ) @@ -411,10 +417,10 @@ class CisloView(generic.DetailView): deadliny_s_vysledkovkami = [] nadpisy = { - m.Deadline.TYP_CISLA: "Výsledkovka", - m.Deadline.TYP_PRVNI: "Výsledkovka do prvního deadlinu", - m.Deadline.TYP_PRVNI_A_SOUS: "Výsledkovka do prvního deadlinu a deadlinu pro účast na soustředění", - m.Deadline.TYP_SOUS: "Výsledkovka do deadlinu pro účast na soustředění", + Deadline.TYP_CISLA: "Výsledkovka", + Deadline.TYP_PRVNI: "Výsledkovka do prvního deadlinu", + Deadline.TYP_PRVNI_A_SOUS: "Výsledkovka do prvního deadlinu a deadlinu pro účast na soustředění", + Deadline.TYP_SOUS: "Výsledkovka do deadlinu pro účast na soustředění", } for deadline in deadliny: @@ -703,5 +709,5 @@ class AktualniRocnikRedirectView(RedirectView): pattern_name = 'seminar_rocnik' def get_redirect_url(self, *args, **kwargs): - aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik.rocnik + aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik.rocnik return super().get_redirect_url(rocnik=aktualni_rocnik, *args, **kwargs) diff --git a/soustredeni/admin.py b/soustredeni/admin.py index 091f9c59..a0bc25ca 100644 --- a/soustredeni/admin.py +++ b/soustredeni/admin.py @@ -1,8 +1,11 @@ from django.contrib import admin from django.forms import widgets from django.db import models +from polymorphic.admin import PolymorphicChildModelAdmin -from seminar.models import soustredeni as m +from soustredeni import models as m + +from tvorba.admin import ProblemAdminMixin class SoustredeniUcastniciInline(admin.TabularInline): @@ -41,3 +44,8 @@ class SoustredeniAdmin(admin.ModelAdmin): inline_type = 'tabular' inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline] + +@admin.register(m.Konfera) +class KonferaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin): + base_model = m.Konfera + diff --git a/soustredeni/models/__init__.py b/soustredeni/models/__init__.py new file mode 100644 index 00000000..f58b7146 --- /dev/null +++ b/soustredeni/models/__init__.py @@ -0,0 +1,5 @@ +from .soustredeni import Soustredeni +from .soustredeni_ucastnici import Soustredeni_Ucastnici +from .soustredeni_organizatori import Soustredeni_Organizatori +from .konfera import Konfera +from .konfery_ucastnici import Konfery_Ucastnici diff --git a/soustredeni/models/konfera.py b/soustredeni/models/konfera.py new file mode 100644 index 00000000..df1329b9 --- /dev/null +++ b/soustredeni/models/konfera.py @@ -0,0 +1,83 @@ +import os +from reversion import revisions as reversion + +from django.db import models +from django.conf import settings + +from various.utils import aux_generate_filename + +from personalni.models.resitel import Resitel +from tvorba.models.problem import Problem +from .soustredeni import Soustredeni + + +# Django neumí jednoduše serializovat partial nebo třídu s __call__ +# (https://docs.djangoproject.com/en/1.8/topics/migrations/), +# neprojdou pak migrace. Takže rozlišení funkcí generujících názvy souboru +# podle adresáře řešíme takto. + +## +def generate_filename_konfera(self, filename): + return os.path.join( + settings.SEMINAR_KONFERY_DIR, + aux_generate_filename(self, filename) + ) + +## + + +@reversion.register(ignore_duplicates=True) +class Konfera(Problem): + class Meta: + db_table = 'seminar_konfera' + verbose_name = 'Konfera' + verbose_name_plural = 'Konfery' + + anotace = models.TextField( + 'anotace', blank=True, + help_text='Popis, o čem bude konfera.', + ) + + abstrakt = models.TextField( + 'abstrakt', blank=True, + help_text='Abstrakt konfery tak, jak byl uveden ve sborníku', + ) + + # FIXME: Umíme omezit jen na účastníky daného soustřeďka? + ucastnici = models.ManyToManyField( + Resitel, verbose_name='účastníci konfery', + help_text='Seznam účastníků konfery', through='Konfery_Ucastnici', + ) + + soustredeni = models.ForeignKey( + Soustredeni, verbose_name='soustředění', + related_name='konfery', on_delete=models.SET_NULL, null=True, + ) + + TYP_VELETRH = 'veletrh' + TYP_PREZENTACE = 'prezentace' + TYP_CHOICES = [ + (TYP_VELETRH, 'Veletrh (postery)'), + (TYP_PREZENTACE, 'Prezentace (přednáška)'), + ] + typ_prezentace = models.CharField( + 'typ prezentace', max_length=16, choices=TYP_CHOICES, + blank=False, default=TYP_VELETRH, + ) + + prezentace = models.FileField( + 'prezentace', help_text='Prezentace nebo fotka posteru', + upload_to=generate_filename_konfera, blank=True, + ) + + materialy = models.FileField( + 'materialy', + help_text='Další materiály ke konfeře zabalené do jednoho souboru', + upload_to=generate_filename_konfera, blank=True, + ) + + def __str__(self): + return "{}: ({})".format(self.nazev, self.soustredeni) + + def cislo_node(self): + return None diff --git a/soustredeni/models/konfery_ucastnici.py b/soustredeni/models/konfery_ucastnici.py new file mode 100644 index 00000000..27ac3516 --- /dev/null +++ b/soustredeni/models/konfery_ucastnici.py @@ -0,0 +1,35 @@ +from reversion import revisions as reversion + +from django.db import models + +from personalni.models.resitel import Resitel +from .konfera import Konfera + + +@reversion.register(ignore_duplicates=True) +class Konfery_Ucastnici(models.Model): + class Meta: + db_table = 'seminar_konfery_ucastnici' + verbose_name = 'Účast na konfeře' + verbose_name_plural = 'Účasti na konfeře' + ordering = ['konfera', 'resitel'] + + # Interní ID + id = models.AutoField(primary_key=True) + + resitel = models.ForeignKey( + Resitel, verbose_name='řešitel', on_delete=models.PROTECT, + ) + + konfera = models.ForeignKey( + Konfera, verbose_name='konfera', on_delete=models.CASCADE, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k účasti (plain text)', + ) + + def __str__(self): + return '{} na {}'.format(self.resitel, self.konfera) + # NOTE: Poteciální DB HOG bez select_related diff --git a/soustredeni/models/soustredeni.py b/soustredeni/models/soustredeni.py new file mode 100644 index 00000000..f7aaaa5e --- /dev/null +++ b/soustredeni/models/soustredeni.py @@ -0,0 +1,92 @@ +from reversion import revisions as reversion + +from django.db import models +from django.urls import reverse + +from mamweb.models.base import SeminarModelBase + +from personalni.models.resitel import Resitel +from personalni.models.organizator import Organizator +from tvorba.models.rocnik import Rocnik + + +@reversion.register(ignore_duplicates=True) +class Soustredeni(SeminarModelBase): + + class Meta: + db_table = 'seminar_soustredeni' + verbose_name = 'Soustředění' + verbose_name_plural = 'Soustředění' + ordering = ['-rocnik__rocnik', '-datum_zacatku'] + + # Interní ID + id = models.AutoField(primary_key=True) + + rocnik = models.ForeignKey( + Rocnik, verbose_name='ročník', related_name='soustredeni', + on_delete=models.PROTECT, + ) + + datum_zacatku = models.DateField( + 'datum začátku', blank=True, null=True, + help_text='První den soustředění', + ) + + datum_konce = models.DateField( + 'datum konce', blank=True, null=True, + help_text='Poslední den soustředění', + ) + + verejne_db = models.BooleanField( + 'soustředění zveřejněno', db_column='verejne', default=False, + ) + + misto = models.CharField( + 'místo soustředění', max_length=256, blank=True, default='', + help_text='Místo (název obce, volitelně též objektu', + ) + + ucastnici = models.ManyToManyField( + Resitel, verbose_name='účastníci soustředění', + help_text='Seznam účastníků soustředění', through='Soustredeni_Ucastnici', + ) + + organizatori = models.ManyToManyField( + Organizator, + verbose_name='Organizátoři soustředění', + help_text='Seznam organizátorů soustředění', + through='Soustredeni_Organizatori', + ) + + text = models.TextField('text k soustředění (HTML)', blank=True, default='') + + TYP_JARNI = 'jarni' + TYP_PODZIMNI = 'podzimni' + TYP_VIKEND = 'vikend' + TYP_VYLET = 'vylet' + TYP_CHOICES = [ + (TYP_JARNI, 'Jarní soustředění'), + (TYP_PODZIMNI, 'Podzimní soustředění'), + (TYP_VIKEND, 'Víkendový sraz'), + (TYP_VYLET, 'Výlet'), + ] + typ = models.CharField( + 'typ akce', max_length=16, choices=TYP_CHOICES, blank=False, + default=TYP_PODZIMNI, + ) + + exportovat = models.BooleanField( + 'export do AESOPa', db_column='exportovat', default=False, + help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti)', + ) + + def __str__(self): + return '{} ({})'.format(self.misto, self.datum_zacatku) + + def verejne(self): + return self.verejne_db + verejne.boolean = True + + def verejne_url(self): + # return reverse('seminar_soustredeni', kwargs={'pk': self.id}) + return reverse('seminar_seznam_soustredeni') diff --git a/soustredeni/models/soustredeni_organizatori.py b/soustredeni/models/soustredeni_organizatori.py new file mode 100644 index 00000000..5d0ba82d --- /dev/null +++ b/soustredeni/models/soustredeni_organizatori.py @@ -0,0 +1,40 @@ +from reversion import revisions as reversion + +from django.db import models + +from mamweb.models.base import SeminarModelBase +from personalni.models.organizator import Organizator +from .soustredeni import Soustredeni + + +@reversion.register(ignore_duplicates=True) +class Soustredeni_Organizatori(SeminarModelBase): + # zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu + + class Meta: + db_table = 'seminar_soustredeni_organizatori' + verbose_name = 'Účast organizátorů na soustředění' + verbose_name_plural = 'Účasti organizátorů na soustředění' + ordering = ['soustredeni', 'organizator'] + + # Interní ID + id = models.AutoField(primary_key=True) + + organizator = models.ForeignKey( + Organizator, verbose_name='organizátor', + on_delete=models.PROTECT, + ) + + soustredeni = models.ForeignKey( + Soustredeni, verbose_name='soustředění', + on_delete=models.PROTECT, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k účasti organizátora (plain text)', + ) + + def __str__(self): + return '{} na {}'.format(self.organizator, self.soustredeni) + # NOTE: Poteciální DB HOG bez select_related diff --git a/soustredeni/models/soustredeni_ucastnici.py b/soustredeni/models/soustredeni_ucastnici.py new file mode 100644 index 00000000..04abf49a --- /dev/null +++ b/soustredeni/models/soustredeni_ucastnici.py @@ -0,0 +1,39 @@ +from reversion import revisions as reversion + +from django.db import models + +from mamweb.models.base import SeminarModelBase +from personalni.models.resitel import Resitel +from .soustredeni import Soustredeni + + +@reversion.register(ignore_duplicates=True) +class Soustredeni_Ucastnici(SeminarModelBase): + # zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu + + class Meta: + db_table = 'seminar_soustredeni_ucastnici' + verbose_name = 'Účast na soustředění' + verbose_name_plural = 'Účasti na soustředění' + ordering = ['soustredeni', 'resitel'] + + # Interní ID + id = models.AutoField(primary_key=True) + + resitel = models.ForeignKey( + Resitel, verbose_name='řešitel', on_delete=models.PROTECT, + ) + + soustredeni = models.ForeignKey( + Soustredeni, verbose_name='soustředění', + on_delete=models.PROTECT, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k účasti (plain text)', + ) + + def __str__(self): + return '{} na {}'.format(self.resitel, self.soustredeni) + # NOTE: Poteciální DB HOG bez select_related diff --git a/soustredeni/views.py b/soustredeni/views.py index e5ae2992..46ffc4dd 100644 --- a/soustredeni/views.py +++ b/soustredeni/views.py @@ -3,7 +3,9 @@ from django.http import HttpResponse from django.views import generic from django.conf import settings from django.contrib.staticfiles.finders import find -from seminar.models import Soustredeni, Resitel, Soustredeni_Ucastnici, Nastaveni # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci +from tvorba.models.nastaveni import Nastaveni +from personalni.models.resitel import Resitel +from soustredeni.models import Soustredeni, Soustredeni_Ucastnici import csv import tempfile import shutil diff --git a/treenode/admin.py b/treenode/admin.py index 92c85cd5..3189ad10 100644 --- a/treenode/admin.py +++ b/treenode/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter -import seminar.models as m +import treenode.models as m # Polymorfismus pro stromy # TODO: Inlines podle https://django-polymorphic.readthedocs.io/en/stable/admin.html diff --git a/seminar/models/treenode.py b/treenode/models.py similarity index 93% rename from seminar/models/treenode.py rename to treenode/models.py index 50261d1a..18d61aea 100644 --- a/seminar/models/treenode.py +++ b/treenode/models.py @@ -9,13 +9,15 @@ from unidecode import unidecode # Používám pro získání ID odkazu (ještě from polymorphic.models import PolymorphicModel -from . import personalni as pm +from personalni import models as pm -from .pomocne import Text +from seminar.models.pomocne import Text +from odevzdavatko.models.reseni import Reseni logger = logging.getLogger(__name__) -from seminar.models import tvorba as am +from tvorba import models as am + class TreeNode(PolymorphicModel): class Meta: @@ -264,3 +266,21 @@ class CastNode(TreeNode): def getOdkazStr(self): return str(self.nadpis) + + +class ReseniNode(TreeNode): + class Meta: + db_table = 'seminar_nodes_otistene_reseni' + verbose_name = 'Otištěné řešení (Node)' + verbose_name_plural = 'Otištěná řešení (Node)' + reseni = models.ForeignKey( + Reseni, + on_delete=models.PROTECT, + verbose_name='reseni' + ) + + def aktualizuj_nazev(self): + self.nazev = "ReseniNode: "+str(self.reseni) + + def getOdkazStr(self): + return str(self.reseni) diff --git a/treenode/serializers.py b/treenode/serializers.py index eedb03b1..f6b50887 100644 --- a/treenode/serializers.py +++ b/treenode/serializers.py @@ -1,9 +1,13 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer -import seminar.models as m from treenode import treelib +from tvorba.models.problem import Problem +from tvorba.models.uloha import Uloha +from odevzdavatko.models.reseni import Reseni +import treenode.models as m + DEFAULT_NODE_DEPTH = 2 @@ -14,17 +18,17 @@ class TextSerializer(serializers.ModelSerializer): class ProblemSerializer(serializers.ModelSerializer): class Meta: - model = m.Problem + model = Problem fields = '__all__' class UlohaSerializer(serializers.ModelSerializer): class Meta: - model = m.Uloha + model = Uloha fields = '__all__' class ReseniSerializer(serializers.ModelSerializer): class Meta: - model = m.Reseni + model = Reseni fields = '__all__' class RocnikNodeSerializer(serializers.ModelSerializer): @@ -184,7 +188,7 @@ class UlohaZadaniNodeCreateSerializer(serializers.ModelSerializer): cislo = travelnode.cislo travelnode = treelib.get_parent(travelnode) # Vyrobime ulohu - uloha = m.Uloha.objects.create(cislo_zadani=cislo, nadproblem = tema, **temp_uloha) + uloha = Uloha.objects.create(cislo_zadani=cislo, nadproblem = tema, **temp_uloha) # A vyrobime UlohaZadaniNode if where == 'syn': @@ -211,7 +215,7 @@ class UlohaVzorakNodeSerializer(serializers.ModelSerializer): depth = DEFAULT_NODE_DEPTH class UlohaVzorakNodeWriteSerializer(serializers.ModelSerializer): - uloha = serializers.PrimaryKeyRelatedField(queryset=m.Uloha.objects.all(), many=False, read_only=False) + uloha = serializers.PrimaryKeyRelatedField(queryset=Uloha.objects.all(), many=False, read_only=False) class Meta: model = m.UlohaVzorakNode @@ -226,7 +230,7 @@ class UlohaVzorakNodeCreateSerializer(serializers.ModelSerializer): def create(self, validated_data): uloha_id = validated_data.pop('uloha_id') - uloha = m.Uloha.objects.get(pk=uloha_id) + uloha = Uloha.objects.get(pk=uloha_id) where = validated_data.pop('where') refnode_id = validated_data.pop('refnode') refnode = m.TreeNode.objects.get(pk=refnode_id) diff --git a/treenode/views.py b/treenode/views.py index 2c300263..00e07072 100644 --- a/treenode/views.py +++ b/treenode/views.py @@ -6,8 +6,9 @@ from django.views.generic.edit import CreateView from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import PermissionDenied -import seminar.models as s -import seminar.models as m +from seminar.models.pomocne import Obrazek +import treenode.models as s +import treenode.models as m from treenode import treelib import treenode.forms as f import treenode.templatetags as tnltt @@ -300,7 +301,7 @@ class VueTestView(generic.TemplateView): class NahrajObrazekKTreeNoduView(LoginRequiredMixin, CreateView): - model = s.Obrazek + model = Obrazek form_class = f.NahrajObrazekKTreeNoduForm def get_initial(self): diff --git a/treenode/viewsets.py b/treenode/viewsets.py index 16dce6d6..80246997 100644 --- a/treenode/viewsets.py +++ b/treenode/viewsets.py @@ -3,7 +3,7 @@ from rest_framework import status from rest_framework.response import Response from django.core.exceptions import PermissionDenied from rest_framework.permissions import BasePermission, AllowAny -from seminar import models as m +from treenode import models as m import treenode.serializers as views from treenode.permissions import AllowWrite diff --git a/tvorba/__init__.py b/tvorba/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tvorba/admin.py b/tvorba/admin.py new file mode 100644 index 00000000..eac771d2 --- /dev/null +++ b/tvorba/admin.py @@ -0,0 +1,156 @@ +from django.contrib import admin +from django.forms import ModelForm +from django.core.exceptions import ValidationError +from django.utils.safestring import mark_safe +from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin +from solo.admin import SingletonModelAdmin + +from soustredeni.models.konfera import Konfera +import tvorba.models as m + + +admin.site.register(m.Rocnik) +admin.site.register(m.Deadline) + +# Todo: reversion + + +class DeadlineAdminInline(admin.TabularInline): + model = m.Deadline + extra = 0 + + +class CisloForm(ModelForm): + class Meta: + model = m.Cislo + fields = '__all__' + + def clean(self): + if not self.cleaned_data.get('verejne_db'): + return self.cleaned_data + # cn = m.CisloNode.objects.get(cislo=self.instance) + # errors = [] + # for ch in tl.all_children(cn): + # if isinstance(ch, m.TemaVCisleNode): + # if ch.tema.stav not in \ + # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # errors.append(ValidationError('Téma %(tema)s není zadané ani vyřešené', params={'tema':ch.tema})) + # + # if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode): + # if ch.uloha.stav not in \ + # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # errors.append(ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha':ch.uloha})) + # if isinstance(ch, m.ReseniNode): + # for problem in ch.reseni.problem_set: + # if problem not in \ + # (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # errors.append(ValidationError('Problém %s není zadaný ani vyřešený', code=problem)) + # if errors: + # errors.append(ValidationError(mark_safe('Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel'))) + # raise ValidationError(errors) + + errors = [] + for ch in m.Uloha.objects.filter(cislo_zadani=self.instance): + if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + errors.append( + ValidationError( + 'Úloha %(uloha)s není zadaná ani vyřešená', + params={'uloha': ch} + ) + ) + if errors: + errors.append(ValidationError(mark_safe( + 'Pokud chceš učinit všechny problémy, co nejsou zadané ani vyřešené, zadanými a číslo zveřejnit, můžeš to udělat pomocí akce v seznamu čísel' + ))) + if self.cleaned_data.get('datum_vydani') is None: + self.add_error( + 'datum_vydani', 'Číslo určené ke zveřejnění nemá nastavené datum vydání' + ) + + if errors: + raise ValidationError(errors) + + return self.cleaned_data + + +@admin.register(m.Cislo) +class CisloAdmin(admin.ModelAdmin): + form = CisloForm + actions = ['force_publish'] + inlines = (DeadlineAdminInline,) + + def force_publish(self, _request, queryset): + for cislo in queryset: + # cn = m.CisloNode.objects.get(cislo=cislo) + # for ch in tl.all_children(cn): + # if isinstance(ch, m.TemaVCisleNode): + # if ch.tema.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # ch.tema.stav = m.Problem.STAV_ZADANY + # ch.tema.save() + # + # if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode): + # if ch.uloha.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # ch.uloha.stav = m.Problem.STAV_ZADANY + # ch.uloha.save() + # if isinstance(ch, m.ReseniNode): + # for problem in ch.reseni.problem_set: + # if problem not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + # problem.stav = m.Problem.STAV_ZADANY + # problem.save() + + for ch in m.Uloha.objects.filter(cislo_zadani=cislo): + if ch.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + ch.stav = m.Problem.STAV_ZADANY + ch.save() + + hp = ch.hlavni_problem + if hp.stav not in (m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY): + hp.stav = m.Problem.STAV_ZADANY + hp.save() + + # TODO Řešení, vzoráky? + # TODO Konfera/Článek? + + cislo.verejne_db = True + cislo.save() + + force_publish.short_description = 'Zveřejnit vybraná čísla a všechny návrhy úloh v nich učinit zadanými' + + +@admin.register(m.Problem) +class ProblemAdmin(PolymorphicParentModelAdmin): + base_model = m.Problem + child_models = [ + m.Tema, + m.Clanek, + m.Uloha, + Konfera, + ] + # Pokud chceme orezavat na aktualni rocnik, musime do modelu pridat odkaz na rocnik. Zatim bere vse. + search_fields = ['nazev'] + + +# V ProblemAdmin to nejde, protoze se to nepropise do deti +class ProblemAdminMixin(object): + show_in_index = True + autocomplete_fields = ['nadproblem', 'autor', 'garant'] + filter_horizontal = ['opravovatele'] + + +@admin.register(m.Tema) +class TemaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin): + base_model = m.Tema + + +@admin.register(m.Clanek) +class ClanekAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin): + base_model = m.Clanek + + +@admin.register(m.Uloha) +class UlohaAdmin(ProblemAdminMixin, PolymorphicChildModelAdmin): + base_model = m.Uloha + + +# admin.site.register(m.Pohadka) +admin.site.register(m.Nastaveni, SingletonModelAdmin) diff --git a/tvorba/apps.py b/tvorba/apps.py new file mode 100644 index 00000000..e8666d97 --- /dev/null +++ b/tvorba/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TvorbaConfig(AppConfig): + name = 'tvorba' diff --git a/tvorba/migrations/__init__.py b/tvorba/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tvorba/models/__init__.py b/tvorba/models/__init__.py new file mode 100644 index 00000000..2d044303 --- /dev/null +++ b/tvorba/models/__init__.py @@ -0,0 +1,9 @@ +from .cislo import Cislo +from .clanek import Clanek +from .deadline import Deadline +from .nastaveni import Nastaveni +from .pohadka import Pohadka +from .problem import Problem +from .rocnik import Rocnik +from .tema import Tema +from .uloha import Uloha diff --git a/tvorba/models/cislo.py b/tvorba/models/cislo.py new file mode 100644 index 00000000..ffef1b75 --- /dev/null +++ b/tvorba/models/cislo.py @@ -0,0 +1,281 @@ +import os +import subprocess +import pathlib +import tempfile +import logging +from reversion import revisions as reversion + +from django.contrib.sites.shortcuts import get_current_site +from django.db import models +from django.db.models import Q +from django.conf import settings +from django.urls import reverse +from django.core.exceptions import ObjectDoesNotExist +from django.core.files.storage import FileSystemStorage +from django.core.mail import EmailMessage + +from seminar.utils import aktivniResitele + +from mamweb.models.base import SeminarModelBase +from personalni.models.prijemce import Prijemce +from .rocnik import Rocnik + + +class OverwriteStorage(FileSystemStorage): + """Varianta FileSystemStorage, která v případě, že soubor cílového + jména již existuje, ho smaže a místo něj uloží soubor nový""" + def get_available_name(self, name, max_length=None): + if self.exists(name): + os.remove(os.path.join(self.location, name)) + return super().get_available_name(name, max_length) + + +def cislo_pdf_filename(self, _filename): + rocnik = str(self.rocnik.rocnik) + return pathlib.Path( + 'cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi), + ) + + +def cislo_png_filename(self, _filename): + rocnik = str(self.rocnik.rocnik) + return pathlib.Path( + 'cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi), + ) + + +@reversion.register(ignore_duplicates=True) +class Cislo(SeminarModelBase): + + class Meta: + db_table = 'seminar_cisla' + verbose_name = 'Číslo' + verbose_name_plural = 'Čísla' + ordering = ['-rocnik__rocnik', '-poradi'] + + # Interní ID + id = models.AutoField(primary_key=True) + + rocnik = models.ForeignKey( + Rocnik, verbose_name='ročník', related_name='cisla', + db_index=True, on_delete=models.PROTECT, + ) + + poradi = models.CharField( + 'název čísla', max_length=32, db_index=True, + help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!', + ) + + datum_vydani = models.DateField( + 'datum vydání', blank=True, null=True, + help_text='Datum vydání finální verze', + ) + + verejne_db = models.BooleanField( + 'číslo zveřejněno', db_column='verejne', default=False, + ) + + poznamka = models.TextField( + 'neveřejná poznámka', blank=True, + help_text='Neveřejná poznámka k číslu (plain text)', + ) + + pdf = models.FileField( + 'pdf', upload_to=cislo_pdf_filename, null=True, blank=True, + help_text='PDF čísla, které si mohou řešitelé stáhnout', + storage=OverwriteStorage(), + ) + + titulka_nahled = models.ImageField( + 'Obrázek titulní strany', upload_to=cislo_png_filename, + null=True, blank=True, + help_text='Obrázek titulní strany, generuje se automaticky', + ) + + # má OneToOneField s: + # CisloNode + + def kod(self): + return '%s.%s' % (self.rocnik.rocnik, self.poradi) + kod.short_description = 'Kód čísla' + + def __str__(self): + # Potenciální DB HOG, pokud by se ročník necachoval + r = Rocnik.cached_rocnik(self.rocnik_id) + return '{}.{}'.format(r.rocnik, self.poradi) + + def verejne(self): + return self.verejne_db + verejne.boolean = True + + def verejne_url(self): + return reverse( + 'seminar_cislo', + kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi}, + ) + + def absolute_url(self): + return "https://" + str(get_current_site(None)) + self.verejne_url() + + def nasledujici(self): + """Vrací None, pokud je toto poslední""" + return self.relativni_v_rocniku(1) + + def predchozi(self): + """Vrací None, pokud je toto první""" + return self.relativni_v_rocniku(-1) + + def relativni_v_rocniku(self, rel_index): + """Číslo o `index` dále v ročníku. None pokud neexistuje.""" + cs = self.rocnik.cisla.order_by('poradi').all() + i = list(cs).index(self) + rel_index + if (i < 0) or (i >= len(cs)): + return None + return cs[i] + + def vygeneruj_nahled(self): + VYSKA = 594 + sirka = int(VYSKA*210/297) + if not self.pdf: + return + + # Pokud obrázek neexistuje nebo není aktuální, vytvoř jej + if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path): + png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png') + + subprocess.run([ + "gs", + "-sstdout=%stderr", + "-dSAFER", + "-dNOPAUSE", + "-dBATCH", + "-dNOPROMPT", + "-sDEVICE=png16m", + "-r300x300", + "-dFirstPage=1d", + "-dLastPage=1d", + "-sOutputFile=" + str(png_filename), + "-f%s" % self.pdf.path + ], + check=True, + capture_output=True + ) + + with open(png_filename, 'rb') as f: + self.titulka_nahled.save('', f, True) + + png_filename.unlink() + png_filename.parent.rmdir() + + @classmethod + def get(cls, rocnik, cislo): + try: + r = Rocnik.objects.get(rocnik=rocnik) + c = r.cisla.get(poradi=cislo) + except ObjectDoesNotExist: + return None + return c + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_verejne = self.verejne_db + + def posli_cislo_mailem(self): + # parametry e-mailu + odkaz = self.absolute_url() + + poslat_z_mailu = 'zadani@mam.mff.cuni.cz' + predmet = 'Vyšlo číslo {}'.format(self.kod()) + # TODO Možná nechceme všem psát „Ahoj“, např. příjemcům… + text_mailu = ( + 'Ahoj,\n' + 'na adrese {} najdete nejnovější číslo.\n' + 'Vaše M&M\n' + ).format(odkaz) + + predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!' + text_mailu_prvni = ( + 'Milý řešiteli,\n' + 'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n' + 'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n' + 'Organizátoři M&M\n' + ).format(self.rocnik.rocnik, odkaz) + + predmet_resitel = predmet_prvni if self.poradi == "1" else predmet + text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu + + # Prijemci e-mailu + resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True) + + def posli(subject, text, resitele): + emaily = map(lambda resitel: resitel.osoba.email, resitele) + if not settings.POSLI_MAILOVOU_NOTIFIKACI: + print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily)) + return + + email = EmailMessage( + subject=subject, + body=text, + from_email=poslat_z_mailu, + # bcc = příjemci skryté kopie + bcc=list(emaily) + ) + + email.send() + + paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/" + + posli( + predmet_resitel, + text_mailu_resitel + paticka, + resitele_vsichni.filter(zasilat_cislo_papirove=False), + ) + posli( + predmet_resitel, + text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka, + resitele_vsichni.filter(zasilat_cislo_papirove=True), + ) + + paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz." + posli( + predmet, + text_mailu + paticka_prijemce, + Prijemce.objects.filter(zasilat_cislo_emailem=True) + ) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.vygeneruj_nahled() + # Při zveřejnění pošle mail + if self.verejne_db and not self.__original_verejne: + self.posli_cislo_mailem() + # *Node.save() aktualizuje název *Nodu. + try: + self.cislonode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit + logger = logging.getLogger(__name__) + logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…') + from treenode.models import CisloNode + CisloNode.objects.create(cislo=self) + + def zlomovy_deadline_pro_papirove_cislo(self): + from .deadline import Deadline + prvni_deadline = Deadline.objects.filter( + Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS), + cislo=self + ).first() + if prvni_deadline is None: + posledni_deadline = self.posledni_deadline + if posledni_deadline is None: + # TODO promyslet, co se má stát tady + return Deadline.objects.filter( + Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) | + Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik) + ).order_by("deadline").last() + return posledni_deadline + return prvni_deadline + + @property + def posledni_deadline(self): + return self.deadline_v_cisle.all().order_by("deadline").last() diff --git a/tvorba/models/clanek.py b/tvorba/models/clanek.py new file mode 100644 index 00000000..098c1060 --- /dev/null +++ b/tvorba/models/clanek.py @@ -0,0 +1,33 @@ +import logging + +from django.db import models +from django.utils.functional import cached_property + +from .problem import Problem +from .cislo import Cislo + + +class Clanek(Problem): + class Meta: + db_table = 'seminar_clanky' + verbose_name = 'Článek' + verbose_name_plural = 'Články' + + cislo = models.ForeignKey( + Cislo, blank=True, null=True, on_delete=models.PROTECT, + verbose_name='číslo vydání', related_name='vydane_clanky', + ) + + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: + # 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) + logger = logging.getLogger(__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ý.") + return '' + + def node(self): + return None diff --git a/tvorba/models/deadline.py b/tvorba/models/deadline.py new file mode 100644 index 00000000..e98d8539 --- /dev/null +++ b/tvorba/models/deadline.py @@ -0,0 +1,83 @@ +import datetime + +from django.utils import timezone +from django.db import models +from django.template.loader import render_to_string + +from mamweb.models.base import SeminarModelBase +from .cislo import Cislo + + +class Deadline(SeminarModelBase): + class Meta: + db_table = 'seminar_deadliny' + verbose_name = 'Deadline' + verbose_name_plural = 'Deadliny' + ordering = ['deadline'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_verejna_vysledkovka = self.verejna_vysledkovka + + id = models.AutoField(primary_key=True) + + # V ročníku < 26 nastaveno na datetime.datetime.combine(datetime.date(1994 + cislo.rocnik.rocnik, 6, int(cislo.poradi[0])), datetime.time.min) + deadline = models.DateTimeField( + blank=False, + default=timezone.make_aware( + datetime.datetime.combine(timezone.now(), datetime.time.max), + ), + ) + + cislo = models.ForeignKey( + Cislo, verbose_name='deadline v čísle', + related_name='deadline_v_cisle', blank=False, + on_delete=models.CASCADE, + ) + + TYP_CISLA = 'cisla' + TYP_PRVNI_A_SOUS = 'prvniasous' + TYP_PRVNI = 'prvni' + TYP_SOUS = 'sous' + TYP_CHOICES = [ + (TYP_CISLA, 'Deadline celého čísla'), + (TYP_PRVNI, 'První deadline'), + (TYP_PRVNI_A_SOUS, 'Sousový a první deadline'), + (TYP_SOUS, 'Sousový deadline'), + ] + CHOICES_MAP = dict(TYP_CHOICES) + typ = models.CharField( + 'typ deadlinu', max_length=32, + choices=TYP_CHOICES, blank=False, + ) + + verejna_vysledkovka = models.BooleanField( + 'veřejná výsledkovka', db_column='verejna_vysledkovka', default=False, + ) + + def __str__(self): + return self.CHOICES_MAP[self.typ] + " " + str(self.cislo) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if self.verejna_vysledkovka and not self.__original_verejna_vysledkovka: + self.vygeneruj_vysledkovku() + if not self.verejna_vysledkovka and hasattr(self, "vysledkovka_v_deadlinu"): + self.vysledkovka_v_deadlinu.delete() + + def vygeneruj_vysledkovku(self): + from vysledkovky.utils import VysledkovkaCisla + if hasattr(self, "vysledkovka_v_deadlinu"): + self.vysledkovka_v_deadlinu.delete() + vysledkovka = VysledkovkaCisla( + self.cislo, jen_verejne=True, do_deadlinu=self, + ) + if len(vysledkovka.radky_vysledkovky) != 0: + from vysledkovky.models.zmrazena_vysledkovka import ZmrazenaVysledkovka + ZmrazenaVysledkovka.objects.create( + deadline=self, + html=render_to_string( + "vysledkovky/vysledkovka_cisla.html", + context={"vysledkovka": vysledkovka, "oznaceni_vysledkovky": self.id} + ) + ) diff --git a/tvorba/models/nastaveni.py b/tvorba/models/nastaveni.py new file mode 100644 index 00000000..0363c551 --- /dev/null +++ b/tvorba/models/nastaveni.py @@ -0,0 +1,42 @@ +from reversion import revisions as reversion + +from solo.models import SingletonModel +from django.db import models +from django.urls import reverse + +from .cislo import Cislo + + +@reversion.register(ignore_duplicates=True) +class Nastaveni(SingletonModel): + + class Meta: + db_table = 'seminar_nastaveni' + verbose_name = 'Nastavení semináře' + + # aktualni_rocnik = models.ForeignKey(Rocnik, verbose_name='aktuální ročník', + # null=False, on_delete=models.PROTECT) + + aktualni_cislo = models.ForeignKey( + Cislo, verbose_name='Aktuální číslo', + null=False, on_delete=models.PROTECT, + ) + + cena_sous = models.IntegerField( + null=False, + verbose_name="Účastnický poplatek za soustředění", + default=1000, + ) + + @property + def aktualni_rocnik(self): + return self.aktualni_cislo.rocnik + + def __str__(self): + return 'Nastavení semináře' + + def admin_url(self): + return reverse('admin:seminar_nastaveni_change', args=(self.id, )) + + def verejne(self): + return False diff --git a/tvorba/models/pohadka.py b/tvorba/models/pohadka.py new file mode 100644 index 00000000..bad994ca --- /dev/null +++ b/tvorba/models/pohadka.py @@ -0,0 +1,53 @@ +from django.db import models +from django.utils import timezone +from django.core.exceptions import ObjectDoesNotExist + +from mamweb.models.base import SeminarModelBase +from personalni.models.organizator import Organizator + + +class Pohadka(SeminarModelBase): + """Kus pohádky před/za úlohou v čísle""" + + class Meta: + db_table = 'seminar_pohadky' + verbose_name = 'Pohádka' + verbose_name_plural = 'Pohádky' + ordering = ['vytvoreno'] + + # Interní ID + id = models.AutoField(primary_key=True) + + autor = models.ForeignKey( + Organizator, + verbose_name="Autor pohádky", + + # Při nahrávání z TeXu není vyplnění vyžadováno, v adminu je + null=True, + blank=False, + on_delete=models.SET_NULL + ) + + vytvoreno = models.DateTimeField( + 'Vytvořeno', + default=timezone.now, + blank=True, + editable=False + ) + + # má OneToOneField s: + # PohadkaNode + + def __str__(self): + # FIXME pohádka text nemá! + uryvek = self.text if len(self.text) < 50 else self.text[:(50-3)]+"..." + return uryvek + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.pohadkanode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass diff --git a/tvorba/models/problem.py b/tvorba/models/problem.py new file mode 100644 index 00000000..4a170ad5 --- /dev/null +++ b/tvorba/models/problem.py @@ -0,0 +1,161 @@ +from reversion import revisions as reversion +import logging + +from taggit.managers import TaggableManager + +from django.db import models +from django.utils import timezone +from django.utils.functional import cached_property +from django.urls import reverse +from polymorphic.models import PolymorphicModel + +from mamweb.models.base import SeminarModelBase +from personalni.models.organizator import Organizator + + +@reversion.register(ignore_duplicates=True) +# Pozor na následující řádek. *Nekrmit, asi kouše!* +class Problem(SeminarModelBase, PolymorphicModel): + + class Meta: + # Není abstraktní, protože se na něj jinak nedají dělat ForeignKeys. + # TODO: Udělat to polymorfní (pomocí django-polymorphic), abychom dostali + # po těch vazbách přímo tu úlohu/témátko vč. fieldů, které nejsou součástí + # modelu Problem? + + # abstract = True + db_table = 'seminar_problemy' + verbose_name = 'Problém' + verbose_name_plural = 'Problémy' + ordering = ['nazev'] + + # Interní ID + id = models.AutoField(primary_key=True) + + # Název + nazev = models.CharField('název', max_length=256) # Zveřejnitelný na stránky + + # Problém má podproblémy + nadproblem = models.ForeignKey( + 'self', verbose_name='nadřazený problém', + related_name='podproblem', null=True, blank=True, + on_delete=models.SET_NULL, + ) + + STAV_NAVRH = 'navrh' + STAV_ZADANY = 'zadany' + STAV_VYRESENY = 'vyreseny' + STAV_SMAZANY = 'smazany' + STAV_CHOICES = [ + (STAV_NAVRH, 'Návrh'), + (STAV_ZADANY, 'Zadaný'), + (STAV_VYRESENY, 'Vyřešený'), + (STAV_SMAZANY, 'Smazaný'), + ] + stav = models.CharField( + 'stav problému', max_length=32, choices=STAV_CHOICES, blank=False, + default=STAV_NAVRH, + ) + # Téma je taky Problém, takže má stavy, "zadané" témátko je aktuálně otevřené a dá se k němu něco poslat (řešení nebo článek) + + zamereni = TaggableManager( + verbose_name='zaměření', + help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True, + ) + + poznamka = models.TextField( + 'org poznámky (HTML)', blank=True, + help_text='Neveřejný návrh úlohy, návrh řešení, text zadání, poznámky ...', + ) + + autor = models.ForeignKey( + Organizator, verbose_name='autor problému', + related_name='autor_problemu_%(class)s', null=True, blank=True, + on_delete=models.SET_NULL, + ) + + garant = models.ForeignKey( + Organizator, verbose_name='garant zadaného problému', + related_name='garant_problemu_%(class)s', null=True, blank=True, + on_delete=models.SET_NULL, + ) + + opravovatele = models.ManyToManyField( + Organizator, verbose_name='opravovatelé', + blank=True, related_name='opravovatele_%(class)s', + ) + + kod = models.CharField( + 'lokální kód', max_length=32, blank=True, default='', + help_text='Číslo/kód úlohy v čísle nebo kód tématu/článku/seriálu v ročníku', + ) + + vytvoreno = models.DateTimeField( + 'vytvořeno', default=timezone.now, blank=True, editable=False, + ) + + def __str__(self): + return self.nazev + + # Implicitini implementace, jednotlivé dědící třídy si přepíšou + @cached_property + def kod_v_rocniku(self): + if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY: + if self.nadproblem: + return self.nadproblem.kod_v_rocniku+".{}".format(self.kod) + return str(self.kod) + logger = logging.getLogger(__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ý.") + return '' + + # def verejne(self): + # # aktuálně podle stavu problému + # # FIXME pro některé problémy možná chceme override + # # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je. + # # Je to tak správně? Podle aktuální představy ano. + # stav_verejny = False + # if self.stav == 'zadany' or self.stav == 'vyreseny': + # stav_verejny = True + # print("stav_verejny: {}".format(stav_verejny)) + # + # cislo_verejne = False + # cislonode = self.cislo_node() + # if cislonode is None: + # # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu + # print("empty node") + # return stav_verejny + # else: + # cislo_zadani = cislonode.cislo + # if (cislo_zadani and cislo_zadani.verejne()): + # print("cislo: {}".format(cislo_zadani)) + # cislo_verejne = True + # print("stav_verejny: {}".format(stav_verejny)) + # print("cislo_verejne: {}".format(cislo_verejne)) + # return (stav_verejny and cislo_verejne) + # verejne.boolean = True + + def verejne_url(self): + return reverse('seminar_problem', kwargs={'pk': self.id}) + + def admin_url(self): + return reverse('admin:seminar_problem_change', args=(self.id, )) + + @cached_property + def hlavni_problem(self): + """Pro daný problém vrátí jeho nejvyšší nadproblém.""" + problem = self + while not (problem.nadproblem is None): + problem = problem.nadproblem + return problem + + # FIXME - k úloze + def body_v_zavorce(self): + """Vrať string s body v závorce jsou-li u problému vyplněné, jinak '' + + Je-li desetinná část nulová, nezobrazuj ji. + """ + pocet_bodu = None + if self.body: + b = self.body + pocet_bodu = int(b) if int(b) == b else b + return "({}\u2009b)".format(pocet_bodu) if self.body else "" diff --git a/tvorba/models/rocnik.py b/tvorba/models/rocnik.py new file mode 100644 index 00000000..e4168f34 --- /dev/null +++ b/tvorba/models/rocnik.py @@ -0,0 +1,95 @@ +from reversion import revisions as reversion + +from django.db import models +from django.urls import reverse +from django.core.cache import cache +from django.core.exceptions import ObjectDoesNotExist + +from seminar.utils import roman +from mamweb.models.base import SeminarModelBase + + +@reversion.register(ignore_duplicates=True) +class Rocnik(SeminarModelBase): + + class Meta: + db_table = 'seminar_rocniky' + verbose_name = 'Ročník' + verbose_name_plural = 'Ročníky' + ordering = ['-rocnik'] + + # Interní ID + id = models.AutoField(primary_key=True) + + prvni_rok = models.IntegerField('první rok', db_index=True, unique=True) + + rocnik = models.IntegerField('číslo ročníku', db_index=True, unique=True) + + exportovat = models.BooleanField( + 'export do AESOPa', db_column='exportovat', default=False, + help_text='Exportuje se jen podle tohoto flagu (ne veřejnosti), a to jen čísla s veřejnou výsledkovkou', + ) + + # má OneToOneField s: + # RocnikNode + + def __str__(self): + return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1) + + # Ročník v římských číslech + def roman(self): + return roman(int(self.rocnik)) + + def verejne(self): + return len(self.verejna_cisla()) > 0 + verejne.boolean = True + verejne.short_description = 'Veřejný (jen dle čísel)' + + def neverejna_cisla(self): + vc = [c for c in self.cisla.all() if not c.verejne()] + vc.sort(key=lambda c: c.poradi) + return vc + + def verejna_cisla(self): + vc = [c for c in self.cisla.all() if c.verejne()] + vc.sort(key=lambda c: c.poradi) + return vc + + def posledni_verejne_cislo(self): + vc = self.verejna_cisla() + return vc[-1] if vc else None + + def verejne_vysledkovky_cisla(self): + vc = list( + self.cisla.filter(deadline_v_cisle__verejna_vysledkovka=True).distinct() + ) + vc.sort(key=lambda c: c.poradi) + return vc + + def posledni_zverejnena_vysledkovka_cislo(self): + vc = self.verejne_vysledkovky_cisla() + return vc[-1] if vc else None + + def druhy_rok(self): + return self.prvni_rok + 1 + + def verejne_url(self): + return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik}) + + @classmethod + def cached_rocnik(cls, r_id): + name = 'rocnik_%s' % (r_id, ) + c = cache.get(name) + if c is None: + c = cls.objects.get(id=r_id) + cache.set(name, c, 300) + return c + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.rocniknode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass diff --git a/tvorba/models/tema.py b/tvorba/models/tema.py new file mode 100644 index 00000000..44e3d405 --- /dev/null +++ b/tvorba/models/tema.py @@ -0,0 +1,62 @@ +import logging + +from django.db import models +from django.utils.functional import cached_property + +from treenode import treelib + +from .problem import Problem +from .rocnik import Rocnik + + +class Tema(Problem): + class Meta: + db_table = 'seminar_temata' + verbose_name = 'Téma' + verbose_name_plural = 'Témata' + + TEMA_TEMA = 'tema' + TEMA_SERIAL = 'serial' + TEMA_CHOICES = [ + (TEMA_TEMA, 'Téma'), + (TEMA_SERIAL, 'Seriál'), + ] + tema_typ = models.CharField( + 'Typ tématu', max_length=16, choices=TEMA_CHOICES, + blank=False, default=TEMA_TEMA, + ) + + rocnik = models.ForeignKey( + Rocnik, verbose_name='ročník', related_name='temata', + blank=True, null=True, on_delete=models.PROTECT, + ) + + abstrakt = models.TextField('Abstrakt na rozcestník', blank=True) + obrazek = models.ImageField('Obrázek na rozcestník', null=True, blank=True) + + @cached_property + def kod_v_rocniku(self): + 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) + logger = logging.getLogger(__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ý.") + return '' + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + for tvcn in self.temavcislenode_set.all(): + tvcn.save() + + def cislo_node(self): + tema_node_set = self.temavcislenode_set.all() + tema_cisla_vyskyt = [] + from treenode.models import CisloNode + for tn in tema_node_set: + tema_cisla_vyskyt.append( + treelib.get_upper_node_of_type(tn, CisloNode).cislo) + tema_cisla_vyskyt.sort(key=lambda x: x.datum_vydani) + prvni_zadani = tema_cisla_vyskyt[0] + return prvni_zadani.cislonode diff --git a/tvorba/models/uloha.py b/tvorba/models/uloha.py new file mode 100644 index 00000000..fe8403b6 --- /dev/null +++ b/tvorba/models/uloha.py @@ -0,0 +1,73 @@ +import logging + +from django.db import models +from django.utils.functional import cached_property +from django.core.exceptions import ObjectDoesNotExist + +from treenode import treelib + +from .problem import Problem +from .cislo import Cislo + + +class Uloha(Problem): + class Meta: + db_table = 'seminar_ulohy' + verbose_name = 'Úloha' + verbose_name_plural = 'Úlohy' + + cislo_zadani = models.ForeignKey( + Cislo, verbose_name='číslo zadání', blank=True, + null=True, related_name='zadane_ulohy', on_delete=models.PROTECT, + ) + + cislo_deadline = models.ForeignKey( + Cislo, verbose_name='číslo deadlinu', blank=True, + null=True, related_name='deadlinove_ulohy', on_delete=models.PROTECT, + ) + + cislo_reseni = models.ForeignKey( + Cislo, verbose_name='číslo řešení', blank=True, + null=True, related_name='resene_ulohy', + help_text='Číslo s řešením úlohy, jen pro úlohy', + on_delete=models.PROTECT, + ) + + max_body = models.DecimalField( + max_digits=8, decimal_places=1, verbose_name='maximum bodů', + blank=True, null=True, + ) + + # má OneToOneField s: + # UlohaZadaniNode + # UlohaVzorakNode + + @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 + logger = logging.getLogger(__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ý.") + return '' + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # *Node.save() aktualizuje název *Nodu. + try: + self.ulohazadaninode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + try: + self.ulohavzoraknode.save() + except ObjectDoesNotExist: + # Neexistující *Node nemá smysl aktualizovat. + pass + + def cislo_node(self): + zadani_node = self.ulohazadaninode + from treenode.models import CisloNode + return treelib.get_upper_node_of_type(zadani_node, CisloNode) diff --git a/various/utils.py b/various/utils.py index 5905b2f6..b0f328a7 100644 --- a/various/utils.py +++ b/various/utils.py @@ -1,3 +1,23 @@ +import os +from unidecode import unidecode # Používám pro získání ID odkazu + +from django.utils.text import get_valid_filename +from django.utils import timezone + + +def aux_generate_filename(_, filename): + """Pomocná funkce generující ošetřený název souboru v adresáři s datem""" + clean = get_valid_filename( + unidecode(filename.replace('/', '-').replace('\0', '')) + ) + datedir = timezone.now().strftime('%Y-%m') + fname = "{}/{}".format( + timezone.now().strftime('%Y-%m-%d-%H:%M'), + clean) + return os.path.join(datedir, fname) + + + bez_diakritiky = ({} # FIXME: funguje jen pro český a slovenský text, jinak jsou špatně # transliterace. Potenciální řešení: diff --git a/vysledkovky/admin.py b/vysledkovky/admin.py new file mode 100644 index 00000000..f73c2157 --- /dev/null +++ b/vysledkovky/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +import vysledkovky.models as m + +admin.site.register(m.ZmrazenaVysledkovka) diff --git a/vysledkovky/models/__init__.py b/vysledkovky/models/__init__.py new file mode 100644 index 00000000..a9396ffa --- /dev/null +++ b/vysledkovky/models/__init__.py @@ -0,0 +1 @@ +from .zmrazena_vysledkovka import ZmrazenaVysledkovka diff --git a/vysledkovky/models/zmrazena_vysledkovka.py b/vysledkovky/models/zmrazena_vysledkovka.py new file mode 100644 index 00000000..4ba84de0 --- /dev/null +++ b/vysledkovky/models/zmrazena_vysledkovka.py @@ -0,0 +1,21 @@ +from django.db import models + +from mamweb.models.base import SeminarModelBase + +from tvorba.models.deadline import Deadline + + +class ZmrazenaVysledkovka(SeminarModelBase): + class Meta: + db_table = 'seminar_vysledkovky' + verbose_name = 'Zmražená výsledkovka' + verbose_name_plural = 'Zmražené výsledkovky' + + deadline = models.OneToOneField( + Deadline, + on_delete=models.CASCADE, + primary_key=True, + related_name="vysledkovka_v_deadlinu" + ) + + html = models.TextField(null=False, blank=False) diff --git a/vysledkovky/utils.py b/vysledkovky/utils.py index 3ff59fb1..f7e5f7eb 100644 --- a/vysledkovky/utils.py +++ b/vysledkovky/utils.py @@ -2,10 +2,18 @@ import abc from functools import cached_property from typing import Union, Iterable # TODO: s pythonem 3.10 přepsat na '|' -import seminar.models as m from django.db.models import Q, Sum from seminar.utils import resi_v_rocniku +from tvorba.models.problem import Problem +from tvorba.models.clanek import Clanek +from tvorba.models.rocnik import Rocnik +from tvorba.models.cislo import Cislo +from tvorba.models.deadline import Deadline +from personalni.models.resitel import Resitel +from odevzdavatko.models.hodnoceni import Hodnoceni +from soustredeni.models.konfera import Konfera + ROCNIK_ZRUSENI_TEMAT = 25 @@ -18,11 +26,11 @@ class FixedIterator: def body_resitelu( - za: Union[m.Cislo, m.Rocnik, None] = None, - do: m.Deadline = None, - od: m.Deadline = None, + za: Union[Cislo, Rocnik, None] = None, + do: Deadline = None, + od: Deadline = None, jen_verejne: bool = True, - resitele: Iterable[m.Resitel] = None, + resitele: Iterable[Resitel] = None, null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body ) -> dict[int, int]: filtr = Q() @@ -31,9 +39,9 @@ def body_resitelu( filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True) # Zjistíme, typ objektu v parametru "za" - if isinstance(za, m.Rocnik): + if isinstance(za, Rocnik): filtr &= Q(reseni__hodnoceni__deadline_body__cislo__rocnik=za) - elif isinstance(za, m.Cislo): + elif isinstance(za, Cislo): filtr &= Q(reseni__hodnoceni__deadline_body__cislo=za) if do: @@ -42,7 +50,7 @@ def body_resitelu( if od: filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline) - resiteleQuery = m.Resitel.objects.all() + resiteleQuery = Resitel.objects.all() if resitele is not None: resitele_id = [r.id for r in resitele] @@ -63,12 +71,12 @@ def body_resitelu( class Vysledkovka(abc.ABC): jen_verejne: bool - rocnik: m.Rocnik - do_deadlinu: m.Deadline + rocnik: Rocnik + do_deadlinu: Deadline @property @abc.abstractmethod - def aktivni_resitele(self) -> list[m.Resitel]: + def aktivni_resitele(self) -> list[Resitel]: ... @cached_property @@ -143,20 +151,20 @@ class Vysledkovka(abc.ABC): class VysledkovkaRocniku(Vysledkovka): - def __init__(self, rocnik: m.Rocnik, jen_verejne: bool = True): + def __init__(self, rocnik: Rocnik, jen_verejne: bool = True): self.rocnik = rocnik self.jen_verejne = jen_verejne - deadliny = m.Deadline.objects.filter(cislo__rocnik=rocnik) + deadliny = Deadline.objects.filter(cislo__rocnik=rocnik) if jen_verejne: deadliny = deadliny.filter(verejna_vysledkovka=True) self.do_deadlinu = deadliny.order_by("deadline").last() @cached_property - def aktivni_resitele(self) -> list[m.Resitel]: + def aktivni_resitele(self) -> list[Resitel]: return list(resi_v_rocniku(self.rocnik)) @cached_property - def cisla_rocniku(self) -> list[m.Cislo]: + def cisla_rocniku(self) -> list[Cislo]: """ Vrátí všechna čísla daného ročníku. """ if self.jen_verejne: return self.rocnik.verejne_vysledkovky_cisla() @@ -164,7 +172,7 @@ class VysledkovkaRocniku(Vysledkovka): return self.rocnik.cisla.all().order_by('poradi') @cached_property - def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: m.Cislo.id → ( m.Resitel.id → body ) + def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: Cislo.id → ( Resitel.id → body ) # TODO: Body jsou decimal! body_cisla_slovnik = dict() for cislo in self.cisla_rocniku: @@ -197,7 +205,7 @@ class VysledkovkaRocniku(Vysledkovka): radky_vysledkovky = [] setrizeni_resitele_dict = dict() - for r in m.Resitel.objects.filter( + for r in Resitel.objects.filter( id__in=self.setrizeni_resitele_id ).select_related('osoba'): setrizeni_resitele_dict[r.id] = r @@ -227,31 +235,31 @@ class VysledkovkaRocniku(Vysledkovka): class VysledkovkaCisla(Vysledkovka): def __init__( self, - cislo: m.Cislo, + cislo: Cislo, jen_verejne: bool = True, - do_deadlinu: m.Deadline = None + do_deadlinu: Deadline = None ): self.cislo = cislo self.rocnik = cislo.rocnik self.jen_verejne = jen_verejne if do_deadlinu is None: - do_deadlinu = m.Deadline.objects.filter(cislo=cislo).last() + do_deadlinu = Deadline.objects.filter(cislo=cislo).last() self.do_deadlinu = do_deadlinu @cached_property - def aktivni_resitele(self) -> list[m.Resitel]: + def aktivni_resitele(self) -> list[Resitel]: # TODO možná chytřeji vybírat aktivní řešitele return list(resi_v_rocniku(self.rocnik)) @cached_property - def problemy(self) -> list[m.Problem]: + def problemy(self) -> list[Problem]: """ Vrátí seznam všech problémů s body v daném čísle. """ - return m.Problem.objects.filter( - hodnoceni__in=m.Hodnoceni.objects.filter(deadline_body__cislo=self.cislo) + return Problem.objects.filter( + hodnoceni__in=Hodnoceni.objects.filter(deadline_body__cislo=self.cislo) ).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem') @cached_property - def hlavni_problemy(self) -> list[m.Problem]: + def hlavni_problemy(self) -> list[Problem]: """ Vrátí seznam všech problémů, které již nemají nadproblém. """ # hlavní problémy čísla # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) @@ -269,7 +277,7 @@ class VysledkovkaCisla(Vysledkovka): # Není cached, protože si myslím, že queryset lze použít ve for jen jednou. @property def hodnoceni_do_cisla(self): - hodnoceni = m.Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni') + hodnoceni = Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni') if self.jen_verejne: hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True) return hodnoceni.filter( @@ -347,7 +355,7 @@ class VysledkovkaCisla(Vysledkovka): return self.sectene_body[2] @cached_property - def temata_a_spol(self) -> list[m.Problem]: + def temata_a_spol(self) -> list[Problem]: if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT: return self.hlavni_problemy else: @@ -358,7 +366,7 @@ class VysledkovkaCisla(Vysledkovka): return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0 @cached_property - def podproblemy(self) -> dict[int, list[m.Problem]]: + def podproblemy(self) -> dict[int, list[Problem]]: podproblemy = {hp.id: [] for hp in self.temata_a_spol} temata_a_spol = set(self.temata_a_spol) podproblemy[-1] = [] @@ -381,7 +389,7 @@ class VysledkovkaCisla(Vysledkovka): return podproblemy @cached_property - def podproblemy_seznam(self) -> list[list[m.Problem]]: + def podproblemy_seznam(self) -> list[list[Problem]]: return [self.podproblemy[it.id] for it in self.temata_a_spol] + [self.podproblemy[-1]] @cached_property @@ -411,7 +419,7 @@ class VysledkovkaCisla(Vysledkovka): radky_vysledkovky = [] setrizeni_resitele_slovnik = {} - setrizeni_resitele = m.Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba') + setrizeni_resitele = Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba') for r in setrizeni_resitele: setrizeni_resitele_slovnik[r.id] = r @@ -462,29 +470,29 @@ class VysledkovkaCisla(Vysledkovka): @staticmethod def ne_clanek_ne_konfera(problem): inst = problem.get_real_instance() - return not (isinstance(inst, m.Clanek) or isinstance(inst, m.Konfera)) + return not (isinstance(inst, Clanek) or isinstance(inst, Konfera)) class VysledkovkaDoTeXu(VysledkovkaCisla): def __init__( self, - nejake_cislo: m.Cislo, - od_vyjma: m.Deadline, - do_vcetne: m.Deadline + nejake_cislo: Cislo, + od_vyjma: Deadline, + do_vcetne: Deadline ): super().__init__(nejake_cislo, False, do_vcetne) self.od_deadlinu = od_vyjma @cached_property - def problemy(self) -> list[m.Problem]: - return m.Problem.objects.filter(hodnoceni__in=m.Hodnoceni.objects.filter( + def problemy(self) -> list[Problem]: + return Problem.objects.filter(hodnoceni__in=Hodnoceni.objects.filter( deadline_body__deadline__gt=self.od_deadlinu.deadline, deadline_body__deadline__lte=self.do_deadlinu.deadline, )).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem') @property def hodnoceni_do_cisla(self): - hodnoceni = m.Hodnoceni.objects.prefetch_related( + hodnoceni = Hodnoceni.objects.prefetch_related( 'problem', 'reseni', 'reseni__resitele') if self.jen_verejne: hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)