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)