You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

663 lines
20 KiB

# -*- coding: utf-8 -*-
import os
import subprocess
import pathlib
import tempfile
3 years ago
import logging
from django.contrib.sites.shortcuts import get_current_site
from django.db import models
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 seminar.utils import hlavni_problem
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):
6 years ago
class Meta:
db_table = 'seminar_rocniky'
verbose_name = 'Ročník'
verbose_name_plural = 'Ročníky'
6 years ago
ordering = ['-rocnik']
6 years ago
# 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
6 years ago
def __str__(self):
return '{} ({}/{})'.format(self.rocnik, self.prvni_rok, self.prvni_rok+1)
# Ročník v římských číslech
6 years ago
def roman(self):
return roman(int(self.rocnik))
6 years ago
def verejne(self):
return len(self.verejna_cisla()) > 0
verejne.boolean = True
verejne.short_description = 'Veřejný (jen dle čísel)'
3 years ago
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
6 years ago
def verejna_cisla(self):
vc = [c for c in self.cisla.all() if c.verejne()]
vc.sort(key=lambda c: c.poradi)
6 years ago
return vc
6 years ago
def posledni_verejne_cislo(self):
vc = self.verejna_cisla()
return vc[-1] if vc else None
6 years ago
def verejne_vysledkovky_cisla(self):
vc = list(self.cisla.filter(verejna_vysledkovka=True))
vc.sort(key=lambda c: c.poradi)
6 years ago
return vc
6 years ago
def posledni_zverejnena_vysledkovka_cislo(self):
vc = self.verejne_vysledkovky_cisla()
return vc[-1] if vc else None
6 years ago
def druhy_rok(self):
return self.prvni_rok + 1
6 years ago
def verejne_url(self):
return reverse('seminar_rocnik', kwargs={'rocnik': self.rocnik})
6 years ago
@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):
6 years ago
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):
6 years ago
class Meta:
db_table = 'seminar_cisla'
verbose_name = 'Číslo'
verbose_name_plural = 'Čísla'
ordering = ['-rocnik__rocnik', '-poradi']
6 years ago
# 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)
6 years ago
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!')
6 years ago
datum_vydani = models.DateField('datum vydání', blank=True, null=True,
help_text='Datum vydání finální verze')
6 years ago
datum_deadline_soustredeni = models.DateField(
'datum deadline soustředění',
6 years ago
blank=True, null=True,
help_text='Datum pro příjem řešení pro účast na soustředění')
datum_preddeadline = models.DateField('datum předdeadline', blank=True, null=True,
help_text='Datum pro příjem řešení, která se otisknou v dalším čísle')
datum_deadline = models.DateField('datum deadline', blank=True, null=True,
help_text='Datum pro příjem řešení úloh zadaných v tomto čísle')
6 years ago
verejne_db = models.BooleanField('číslo zveřejněno',
db_column='verejne', default=False)
6 years ago
verejna_vysledkovka = models.BooleanField(
'zveřejněna výsledkovka',
6 years ago
default=False,
help_text='Je-li false u veřejného čísla, '
'není výsledkovka zatím veřejná.')
6 years ago
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k číslu (plain text)')
6 years ago
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')
6 years ago
# má OneToOneField s:
# CisloNode
6 years ago
def kod(self):
return '%s.%s' % (self.rocnik.rocnik, self.poradi)
kod.short_description = 'Kód čísla'
6 years ago
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)
6 years ago
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})
6 years ago
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
6 years ago
def nasledujici(self):
"Vrací None, pokud je toto poslední"
6 years ago
return self.relativni_v_rocniku(1)
def predchozi(self):
"Vrací None, pokud je toto první"
6 years ago
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."
6 years ago
cs = self.rocnik.cisla.order_by('cislo').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",
3 years ago
"-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()
6 years ago
@classmethod
def get(cls, rocnik, cislo):
try:
r = Rocnik.objects.get(rocnik=rocnik)
c = r.cisla.get(poradi=cislo)
6 years ago
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())
text_mailu = 'Ahoj,\n' \
'na adrese {} najdete nejnovější číslo.\n' \
'Vaše M&M\n'.format(odkaz)
# Prijemci e-mailu
emaily = map(lambda r: r.osoba.email, filter(lambda r: r.zasilat_cislo_emailem, aktivniResitele(self)))
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
return
email = EmailMessage(
subject=predmet,
body=text_mailu,
from_email=poslat_z_mailu,
bcc=list(emaily)
#bcc = příjemci skryté kopie
)
email.send()
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 clean(self):
# Finální deadline má být až poslední a je povinný, pokud nějaký deadline existuje.
# Existence:
if self.datum_deadline is None and (self.datum_preddeadline is not None or self.datum_deadline_soustredeni is not None):
raise ValidationError({'datum_deadline': "Číslo musí mít finální deadline, pokud má nějaké deadliny"})
if self.datum_deadline is not None:
if self.datum_preddeadline is not None and self.datum_preddeadline > self.datum_deadline:
raise ValidationError({'datum_preddeadline': "Předdeadline musí předcházet finálnímu deadlinu"})
if self.datum_deadline_soustredeni is not None and self.datum_deadline_soustredeni > self.datum_deadline:
raise ValidationError({'datum_deadline_soustredeni': "Soustřeďkový deadline musí předcházet finálnímu deadlinu"})
@reversion.register(ignore_duplicates=True)
# Pozor na následující řádek. *Nekrmit, asi kouše!*
class Problem(SeminarModelBase,PolymorphicModel):
6 years ago
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'
6 years ago
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
6 years ago
# 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)
6 years ago
STAV_NAVRH = 'navrh'
STAV_ZADANY = 'zadany'
STAV_VYRESENY = 'vyreseny'
6 years ago
STAV_SMAZANY = 'smazany'
STAV_CHOICES = [
(STAV_NAVRH, 'Návrh'),
(STAV_ZADANY, 'Zadaný'),
(STAV_VYRESENY, 'Vyřešený'),
(STAV_SMAZANY, 'Smazaný'),
6 years ago
]
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)
6 years ago
zamereni = TaggableManager(verbose_name='zaměření',
help_text='Zaměření M/F/I/O problému, příp. další tagy', blank=True)
6 years ago
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 ...')
6 years ago
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)
6 years ago
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)
6 years ago
opravovatele = models.ManyToManyField(pm.Organizator, verbose_name='opravovatelé',
blank=True, related_name='opravovatele_%(class)s')
6 years ago
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')
6 years ago
vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False)
6 years ago
def __str__(self):
return self.nazev
6 years ago
# Implicitini implementace, jednotlivé dědící třídy si přepíšou
@cached_property
6 years ago
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)
5 years ago
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ý>'
6 years ago
# 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
6 years ago
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, ))
6 years ago
def hlavni_problem(self):
""" Pro daný problém vrátí jeho nejvyšší nadproblém."""
return hlavni_problem(self)
# FIXME - k úloze
6 years ago
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.
5 years ago
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):
5 years ago
"""Kus pohádky před/za úlohou v čísle"""
6 years ago
class Meta:
db_table = 'seminar_pohadky'
verbose_name = 'Pohádka'
verbose_name_plural = 'Pohádky'
ordering = ['vytvoreno']
6 years ago
# Interní ID
id = models.AutoField(primary_key=True)
6 years ago
autor = models.ForeignKey(
pm.Organizator,
6 years ago
verbose_name="Autor pohádky",
6 years ago
# 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
6 years ago
)
vytvoreno = models.DateTimeField(
'Vytvořeno',
6 years ago
default=timezone.now,
blank=True,
editable=False
)
# má OneToOneField s:
# PohadkaNode
6 years ago
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):
6 years ago
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)
@property
def aktualni_rocnik(self):
return self.aktualni_cislo.rocnik
6 years ago
def __str__(self):
return 'Nastavení semináře'
6 years ago
def admin_url(self):
return reverse('admin:seminar_nastaveni_change', args=(self.id, ))
def verejne(self):
return False