move: Přesunutí models.py do správných aplikací

This commit is contained in:
Jonas Havelka 2023-06-11 16:51:28 +02:00
parent 03f0a6fd7a
commit 6c044b3632
73 changed files with 2399 additions and 1910 deletions

View file

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

View file

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

View file

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

View file

@ -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):

View file

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

View file

@ -0,0 +1 @@
from .base import *

View file

@ -150,6 +150,7 @@ INSTALLED_APPS = (
'personalni',
'soustredeni',
'treenode',
'tvorba',
# Admin upravy:

View file

@ -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):

View file

@ -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"):

View file

@ -0,0 +1,4 @@
from .hodnoceni import Hodnoceni
from .priloha_reseni import PrilohaReseni
from .reseni import Reseni
from .reseni_resitele import Reseni_Resitele

View file

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

View file

@ -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('/')

View file

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

View file

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

View file

@ -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(' ', '_')

View file

@ -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 })")

View file

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

View file

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

View file

@ -0,0 +1,5 @@
from .osoba import Osoba
from .skola import Skola
from .prijemce import Prijemce
from .resitel import Resitel
from .organizator import Organizator

View file

@ -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']

126
personalni/models/osoba.py Normal file
View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import seminar.models as m
import personalni.models as m
from various.utils import bez_diakritiky_translate
import re

View file

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

View file

@ -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):

View file

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

View file

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

View file

@ -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('<b>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 <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
# 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(
'<b>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 <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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__)

View file

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

View file

@ -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 '<Není zadaný>'
# 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 '<Není zadaný>'
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 '<Není zadaný>'
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 '<Není zadaný>'
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

View file

@ -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'<span class="{classes[deadline.typ]}" title="{deadline}">{text}</span>')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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):

View file

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

0
tvorba/__init__.py Normal file
View file

156
tvorba/admin.py Normal file
View file

@ -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('<b>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 <a href="/admin/seminar/cislo">seznamu čísel</a></b>')))
# 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(
'<b>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 <a href="/admin/seminar/cislo">seznamu čísel</a></b>'
)))
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)

5
tvorba/apps.py Normal file
View file

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

View file

View file

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

281
tvorba/models/cislo.py Normal file
View file

@ -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()

33
tvorba/models/clanek.py Normal file
View file

@ -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 '<Není zadaný>'
def node(self):
return None

83
tvorba/models/deadline.py Normal file
View file

@ -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}
)
)

View file

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

53
tvorba/models/pohadka.py Normal file
View file

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

161
tvorba/models/problem.py Normal file
View file

@ -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 '<Není zadaný>'
# 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 ""

95
tvorba/models/rocnik.py Normal file
View file

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

62
tvorba/models/tema.py Normal file
View file

@ -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 '<Není zadaný>'
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

73
tvorba/models/uloha.py Normal file
View file

@ -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 '<Není zadaný>'
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)

View file

@ -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í:

5
vysledkovky/admin.py Normal file
View file

@ -0,0 +1,5 @@
from django.contrib import admin
import vysledkovky.models as m
admin.site.register(m.ZmrazenaVysledkovka)

View file

@ -0,0 +1 @@
from .zmrazena_vysledkovka import ZmrazenaVysledkovka

View file

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

View file

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