Web M&M
https://mam.matfyz.cz
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.
1682 lines
51 KiB
1682 lines
51 KiB
# -*- coding: utf-8 -*-
|
|
import os
|
|
import random
|
|
import subprocess
|
|
import pathlib
|
|
import tempfile
|
|
|
|
from django.db import models
|
|
from django.contrib import auth
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from django.utils.encoding import force_text
|
|
from django.utils.text import slugify
|
|
from django.urls import reverse
|
|
from django.core.cache import cache
|
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils.text import get_valid_filename
|
|
from imagekit.models import ImageSpecField, ProcessedImageField
|
|
from imagekit.processors import ResizeToFit, Transpose
|
|
|
|
from django_countries.fields import CountryField
|
|
from solo.models import SingletonModel
|
|
from taggit.managers import TaggableManager
|
|
|
|
from reversion import revisions as reversion
|
|
|
|
from seminar.utils import roman, FirstTagParser # Pro získání úryvku z TextNode
|
|
|
|
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
|
|
|
|
|
|
class SeminarModelBase(models.Model):
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def verejne(self):
|
|
return False
|
|
|
|
def get_absolute_url(self):
|
|
return self.verejne_url() # TODO "absolute"
|
|
|
|
def admin_url(self):
|
|
model_name = self.__class__.__name__.lower()
|
|
return reverse('admin:seminar_{}_change'.format(model_name), args=(self.id, ))
|
|
|
|
def verejne_url(self):
|
|
return None
|
|
|
|
@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, ...)')
|
|
|
|
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)
|
|
|
|
# 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)
|
|
|
|
osoba = models.OneToOneField(Osoba, blank=False, null=True, verbose_name='osoba',
|
|
on_delete=models.SET_NULL) # FIXME opravit po prvni migraci
|
|
|
|
|
|
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, 'Nikam'),
|
|
]
|
|
zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU)
|
|
|
|
|
|
poznamka = models.TextField('neveřejná poznámka', blank=True,
|
|
help_text='Neveřejná poznámka k řešiteli (plain text)')
|
|
|
|
|
|
def export_row(self):
|
|
"Slovnik pro pouziti v AESOP exportu"
|
|
return {
|
|
'id': self.id,
|
|
'name': self.osoba.jmeno,
|
|
'surname': self.osoba.prijmeni,
|
|
'gender': 'M' if self.osoba.pohlavi_muz else 'F',
|
|
'born': self.osoba.datum_narozeni.isoformat() if self.osoba.datum_narozeni else '',
|
|
'email': self.osoba.email,
|
|
'end-year': self.rok_maturity or '',
|
|
|
|
'street': self.osoba.ulice,
|
|
'town': self.osoba.mesto,
|
|
'postcode': self.osoba.psc,
|
|
'country': self.osoba.stat,
|
|
|
|
'spam-flag': 'Y' if self.osoba.datum_souhlasu_zasilani else '',
|
|
'spam-date': self.osoba.datum_souhlasu_zasilani.isoformat() if self.osoba.datum_souhlasu_zasilani else '',
|
|
|
|
'school': self.skola.aesop_id if self.skola else '',
|
|
'school-name': str(self.skola) if self.skola else 'Skola neni znama',
|
|
}
|
|
|
|
def rocnik(self, rocnik):
|
|
"""Vrati skolni rocnik resitele pro zadany Rocnik.
|
|
Vraci '' pro neznamy rok maturity resitele, Z* pro ekvivalent ZŠ."""
|
|
if self.rok_maturity is None:
|
|
return ''
|
|
rozdil = 5 - (self.rok_maturity - rocnik.prvni_rok)
|
|
if rozdil >= 1:
|
|
return str(rozdil)
|
|
else:
|
|
return 'Z' + str(rozdil + 9)
|
|
|
|
def vsechny_body(self):
|
|
"Spočítá body odjakživa."
|
|
vsechna_reseni = self.reseni_set.all()
|
|
vsechna_hodnoceni = Hodnoceni.objects.filter(
|
|
reseni__in=vsechna_reseni)
|
|
return sum(h.body for h in list(vsechna_hodnoceni))
|
|
|
|
|
|
def get_titul(self, celkove_body=None):
|
|
"Vrati titul"
|
|
if celkove_body is None:
|
|
celkove_body = self.vsechny_body()
|
|
|
|
if celkove_body < 10:
|
|
return ''
|
|
elif celkove_body < 20:
|
|
return 'Bc.'
|
|
elif celkove_body < 50:
|
|
return 'Mgr.'
|
|
elif celkove_body < 100:
|
|
return 'Dr.'
|
|
elif celkove_body < 200:
|
|
return 'Doc.'
|
|
elif celkove_body < 500:
|
|
return 'Prof.'
|
|
else:
|
|
return 'Akad.'
|
|
def __str__(self):
|
|
return self.osoba.plne_jmeno()
|
|
|
|
|
|
|
|
@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 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(verejna_vysledkovka=True))
|
|
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')
|
|
|
|
datum_deadline = models.DateField('datum deadline', blank=True, null=True,
|
|
help_text='Datum pro příjem řešení úloh zadaných v tomto čísle')
|
|
|
|
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_soustredeni = models.DateField(
|
|
'datum deadline soustředění',
|
|
blank=True, null=True,
|
|
help_text='Datum pro příjem řešení pro účast na soustředění')
|
|
|
|
verejne_db = models.BooleanField('číslo zveřejněno',
|
|
db_column='verejne', default=False)
|
|
|
|
verejna_vysledkovka = models.BooleanField(
|
|
'zveřejněna výsledkovka',
|
|
default=False,
|
|
help_text='Je-li false u veřejného čísla, '
|
|
'není výsledkovka zatím veřejná.')
|
|
|
|
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')
|
|
|
|
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 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('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.call([
|
|
"convert",
|
|
"-density", "300x300",
|
|
"-geometry", "{}x{}".format(VYSKA, sirka),
|
|
"-background", "white",
|
|
"-flatten",
|
|
"{}[0]".format(self.pdf.path), # titulní strana
|
|
png_filename
|
|
])
|
|
|
|
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 save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
self.vygeneruj_nahled()
|
|
# *Node.save() aktualizuje název *Nodu.
|
|
try:
|
|
self.cislonode.save()
|
|
except ObjectDoesNotExist:
|
|
# Neexistující *Node nemá smysl aktualizovat.
|
|
pass
|
|
|
|
@reversion.register(ignore_duplicates=True)
|
|
class Organizator(SeminarModelBase):
|
|
# zmena dedicnosti z models.Model na SeminarModelBase, potencialni vznik bugu
|
|
|
|
osoba = models.OneToOneField(Osoba, verbose_name='osoba', related_name='org',
|
|
help_text='osobní údaje organizátora', null=True, blank=False,
|
|
on_delete=models.SET_NULL) #FIXME opravit po migraci
|
|
|
|
vytvoreno = models.DateTimeField(
|
|
'Vytvořeno',
|
|
default=timezone.now,
|
|
blank=True,
|
|
editable=False
|
|
)
|
|
|
|
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']
|
|
|
|
@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_CHOICES = [
|
|
(TYP_JARNI, 'Jarní soustředění'),
|
|
(TYP_PODZIMNI, 'Podzimní soustředění'),
|
|
(TYP_VIKEND, 'Víkendový sraz'),
|
|
]
|
|
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)
|
|
# 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
|
|
def kod_v_rocniku(self):
|
|
if self.stav == 'zadany':
|
|
if self.nadproblem:
|
|
return self.nadproblem.kod_v_rocniku()+".{}".format(self.kod)
|
|
return str(self.kod)
|
|
return '<Není zadaný>'
|
|
|
|
def verejne(self):
|
|
# FIXME: Tohle se liší podle typu problému, má se udělat polymorfně.
|
|
# Zatím je tu jen dummy fail-safe default: nic není veřejné.
|
|
# Doporučené řešení: dělat tohle podle stavu problému a veřejnosti čísla, ve kterém je
|
|
return False
|
|
# FIXME: Tohle je blbost
|
|
return (self.cislo_zadani and self.cislo_zadani.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, ))
|
|
|
|
# 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',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)
|
|
|
|
def kod_v_rocniku(self):
|
|
if self.stav == 'zadany':
|
|
if self.nadproblem:
|
|
return self.nadproblem.kod_v_rocniku()+".t{}".format(self.kod)
|
|
return "t{}".format(self.kod)
|
|
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()
|
|
|
|
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')
|
|
|
|
def kod_v_rocniku(self):
|
|
if self.stav == 'zadany':
|
|
# 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)
|
|
return '<Není zadaný>'
|
|
|
|
class Text(SeminarModelBase):
|
|
class Meta:
|
|
db_table = 'seminar_texty'
|
|
verbose_name = 'text'
|
|
verbose_name_plural = 'texty'
|
|
|
|
na_web = models.TextField('text na web', blank=True,
|
|
help_text='Text ke zveřejnění na webu')
|
|
|
|
do_cisla = models.TextField('text do čísla', blank=True,
|
|
help_text='Text ke zveřejnění v čísle')
|
|
|
|
# má OneToOneField s:
|
|
# Reseni (je u něj jako reseni_cele)
|
|
|
|
# obrázky mají návaznost opačným směrem (vazba z druhé strany)
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
# *Node.save() aktualizuje název *Nodu.
|
|
for tn in self.textnode_set.all():
|
|
tn.save()
|
|
|
|
def __str__(self):
|
|
return str(self.na_web)[:20]
|
|
|
|
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
|
|
|
|
def kod_v_rocniku(self):
|
|
if self.stav == 'zadany':
|
|
name="{}.u{}".format(self.cislo_zadani.poradi,self.kod)
|
|
if self.nadproblem:
|
|
return self.nadproblem.kod_v_rocniku()+name
|
|
return name
|
|
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
|
|
|
|
|
|
@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')
|
|
|
|
# má OneToOneField s:
|
|
# Konfera
|
|
|
|
# má ForeignKey s:
|
|
# Hodnoceni
|
|
|
|
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)
|
|
|
|
## 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(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=False, null=True)
|
|
|
|
cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body',
|
|
related_name='hodnoceni', blank=False, 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)
|
|
|
|
def __str__(self):
|
|
return "{}, {}, {}".format(self.problem, self.reseni, self.body)
|
|
|
|
|
|
|
|
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)
|
|
|
|
# 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)
|
|
)
|
|
|
|
##
|
|
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)
|
|
|
|
|
|
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):
|
|
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 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
|
|
|
|
@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
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
|
# 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
|
|
|
|
@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
|
|
|
|
class Obrazek(SeminarModelBase):
|
|
class Meta:
|
|
db_table = 'seminar_obrazky'
|
|
verbose_name = 'obrázek'
|
|
verbose_name_plural = 'obrázky'
|
|
|
|
# Interní ID
|
|
id = models.AutoField(primary_key = True)
|
|
|
|
na_web = models.ImageField('obrázek na web', upload_to='obrazky/%Y/%m/%d/',
|
|
null=True, blank=True)
|
|
|
|
text = models.ForeignKey(Text, verbose_name='text',
|
|
help_text='text, ve kterém se obrázek vyskytuje',
|
|
null=False, blank=False, on_delete=models.CASCADE)
|
|
|
|
do_cisla_barevny = models.FileField('barevný obrázek do čísla',
|
|
help_text = 'Barevná verze obrázku do čísla',
|
|
upload_to = 'obrazky/%Y/%m/%d/', blank=True, null=True)
|
|
|
|
do_cisla_cernobily = models.FileField('černobílý obrázek do čísla',
|
|
help_text = 'Černobílá verze obrázku do čísla',
|
|
upload_to = 'obrazky/%Y/%m/%d/', blank=True, null=True)
|
|
|
|
# TODO placement hint - chci ho tady / pred textem / za textem
|
|
|
|
class TreeNode(PolymorphicModel):
|
|
class Meta:
|
|
db_table = "seminar_nodes_treenode"
|
|
verbose_name = "TreeNode"
|
|
verbose_name_plural = "TreeNody"
|
|
|
|
# TODO: Nechceme radši jako root vyžadovat přímo RocnikNode?
|
|
root = models.ForeignKey('TreeNode',
|
|
related_name="potomci_set",
|
|
null = True,
|
|
blank = False,
|
|
on_delete = models.SET_NULL, # Vrcholy s null kořenem jsou sirotci bez ročníku
|
|
verbose_name="kořen stromu")
|
|
first_child = models.OneToOneField('TreeNode',
|
|
related_name='father_of_first',
|
|
null = True,
|
|
blank = True,
|
|
on_delete=models.SET_NULL,
|
|
verbose_name="první potomek")
|
|
succ = models.OneToOneField('TreeNode',
|
|
related_name="prev",
|
|
null = True,
|
|
blank = True,
|
|
on_delete=models.SET_NULL,
|
|
verbose_name="další element na stejné úrovni")
|
|
nazev = models.TextField("název tohoto node",
|
|
help_text = "Tento název se zobrazuje v nabídkách pro výběr vhodného TreeNode",
|
|
blank=False,
|
|
null=True) # Nezveřejnitelný název na stránky - pouze do adminu
|
|
zajimave = models.BooleanField(default = False,
|
|
verbose_name = "Zajímavé",
|
|
help_text = "Zobrazí se daná věc na rozcestníku témátek")
|
|
srolovatelne = models.BooleanField(null = True, blank = True,
|
|
verbose_name = "Srolovatelné",
|
|
help_text = "Bude na stránce témátka možnost tuto položku skrýt")
|
|
|
|
def getOdkazStr(self): # String na rozcestník
|
|
return self.first_child.getOdkazStr()
|
|
|
|
def getOdkaz(self): # ID HTML tagu, na který se bude scrollovat #{{self.getOdkaz}}
|
|
# Jsem si vědom, že tu potenciálně vznikají kolize.
|
|
# Přijdou mi natolik nepravděpodobné, že je neřeším
|
|
# Chtěl jsem ale hezké odkazy
|
|
string = unidecode(self.getOdkazStr())
|
|
returnVal = ""
|
|
i = 0
|
|
while len(returnVal) < 16: # Max 15 znaků
|
|
if i == len(string):
|
|
break
|
|
if string[i] == " ":
|
|
returnVal += "-"
|
|
if string[i].isalnum():
|
|
returnVal += string[i].lower()
|
|
i += 1
|
|
return returnVal
|
|
|
|
def __str__(self):
|
|
if self.nazev:
|
|
return self.nazev
|
|
else:
|
|
#TODO: logování
|
|
return "Nepojmenovaný Treenode"
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.aktualizuj_nazev()
|
|
super().save(*args, **kwargs)
|
|
|
|
def aktualizuj_nazev(self):
|
|
raise NotImplementedError("Pokus o aktualizaci názvu obecného TreeNode místo konkrétní instance")
|
|
|
|
def get_admin_url(self):
|
|
content_type = ContentType.objects.get_for_model(self.__class__)
|
|
return reverse("admin:%s_%s_change" % (content_type.app_label, content_type.model), args=(self.id,))
|
|
|
|
class RocnikNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_rocnik'
|
|
verbose_name = 'Ročník (Node)'
|
|
verbose_name_plural = 'Ročníky (Node)'
|
|
rocnik = models.OneToOneField(Rocnik,
|
|
on_delete = models.PROTECT, # Pokud chci mazat ročník, musím si Node pořešit ručně
|
|
verbose_name = "ročník")
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "RocnikNode: "+str(self.rocnik)
|
|
|
|
class CisloNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_cislo'
|
|
verbose_name = 'Číslo (Node)'
|
|
verbose_name_plural = 'Čísla (Node)'
|
|
cislo = models.OneToOneField(Cislo,
|
|
on_delete = models.PROTECT, # Pokud chci mazat číslo, musím si Node pořešit ručně
|
|
verbose_name = "číslo")
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "CisloNode: "+str(self.cislo)
|
|
|
|
def getOdkazStr(self):
|
|
return "Číslo " + str(self.cislo)
|
|
|
|
class MezicisloNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_mezicislo'
|
|
verbose_name = 'Mezičíslo (Node)'
|
|
verbose_name_plural = 'Mezičísla (Node)'
|
|
|
|
# TODO: Využít TreeLib
|
|
def aktualizuj_nazev(self):
|
|
from seminar.treelib import safe_pred
|
|
if safe_pred(self) is not None:
|
|
if (self.prev.get_real_instance_class() != CisloNode and
|
|
self.prev.get_real_instance_class() != MezicisloNode):
|
|
raise ValueError("Předchůdce není číslo!")
|
|
posledni = self.prev.cislo
|
|
self.nazev = "MezicisloNode: Mezičíslo po čísle"+str(posledni)
|
|
elif self.root:
|
|
if self.root.get_real_instance_class() != RocnikNode:
|
|
raise ValueError("Kořen stromu není ročník!")
|
|
rocnik = self.root.rocnik
|
|
self.nazev = "MezicisloNode: První mezičíslo ročníku "+str(rocnik)
|
|
else:
|
|
print("!!!!! Nějaké neidentifikované mezičíslo !!!!!")
|
|
self.nazev = "MezicisloNode: Neidentifikovatelné mezičíslo!"
|
|
def getOdkazStr(self):
|
|
return "Obsah dostupný pouze na webu"
|
|
|
|
class TemaVCisleNode(TreeNode):
|
|
""" Obsahuje příspěvky k tématu v daném čísle """
|
|
class Meta:
|
|
db_table = 'seminar_nodes_temavcisle'
|
|
verbose_name = 'Téma v čísle (Node)'
|
|
verbose_name_plural = 'Témata v čísle (Node)'
|
|
tema = models.ForeignKey(Tema,
|
|
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
|
|
verbose_name = "téma v čísle")
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "TemaVCisleNode: "+str(self.tema)
|
|
|
|
def getOdkazStr(self):
|
|
return str(self.tema)
|
|
|
|
class OrgTextNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_orgtextnode'
|
|
verbose_name = 'Organizátorský článek (Node)'
|
|
verbose_name_plural = 'Organizátorské články (Node)'
|
|
|
|
organizator = models.ForeignKey(Organizator,
|
|
null=False,
|
|
blank=False,
|
|
on_delete=models.DO_NOTHING,
|
|
verbose_name="Organizátor",
|
|
)
|
|
org_verejny = models.BooleanField(default = True,
|
|
verbose_name = "Org je veřejný?",
|
|
help_text = "Pokud ano, bude org pod článkem podepsaný",
|
|
null=False,
|
|
)
|
|
|
|
def aktualizuj_nazev(self):
|
|
return f"OrgTextNode začínající následujícim: {self.first_child.nazev}"
|
|
|
|
# FIXME!!!
|
|
#def getOdkazStr(self):
|
|
# return str(self.clanek)
|
|
|
|
|
|
class UlohaZadaniNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_uloha_zadani'
|
|
verbose_name = 'Zadání úlohy (Node)'
|
|
verbose_name_plural = 'Zadání úloh (Node)'
|
|
uloha = models.OneToOneField(Uloha,
|
|
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
|
|
verbose_name = "úloha",
|
|
null=True,
|
|
blank=False)
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "UlohaZadaniNode: "+str(self.uloha)
|
|
|
|
def getOdkazStr(self):
|
|
return str(self.uloha)
|
|
|
|
|
|
class PohadkaNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_pohadka'
|
|
verbose_name = 'Pohádka (Node)'
|
|
verbose_name_plural = 'Pohádky (Node)'
|
|
pohadka = models.OneToOneField(Pohadka,
|
|
on_delete=models.PROTECT, # Pokud chci mazat pohádku, musím si Node pořešit ručně
|
|
verbose_name = "pohádka",
|
|
)
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "PohadkaNode: "+str(self.pohadka)
|
|
|
|
class UlohaVzorakNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_uloha_vzorak'
|
|
verbose_name = 'Vzorák úlohy (Node)'
|
|
verbose_name_plural = 'Vzoráky úloh (Node)'
|
|
uloha = models.OneToOneField(Uloha,
|
|
on_delete=models.PROTECT, # Pokud chci mazat téma, musím si Node pořešit ručně
|
|
verbose_name = "úloha",
|
|
null=True,
|
|
blank=False)
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "UlohaVzorakNode: "+str(self.uloha)
|
|
|
|
def getOdkazStr(self):
|
|
return str(self.uloha)
|
|
|
|
|
|
class TextNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_obsah'
|
|
verbose_name = 'Text (Node)'
|
|
verbose_name_plural = 'Text (Node)'
|
|
text = models.ForeignKey(Text,
|
|
on_delete=models.CASCADE,
|
|
verbose_name = 'text')
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "TextNode: "+str(self.text)
|
|
|
|
def getOdkazStr(self):
|
|
return str(self.text)
|
|
|
|
class CastNode(TreeNode):
|
|
class Meta:
|
|
db_table = 'seminar_nodes_cast'
|
|
verbose_name = 'Část (Node)'
|
|
verbose_name_plural = 'Části (Node)'
|
|
|
|
nadpis = models.CharField('Nadpis', max_length=100, help_text = 'Nadpis podvěšené části obsahu')
|
|
|
|
def aktualizuj_nazev(self):
|
|
self.nazev = "CastNode: "+str(self.nadpis)
|
|
|
|
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 = "OtisteneReseniNode: "+str(self.reseni)
|
|
|
|
def getOdkazStr(self):
|
|
return str(self.reseni)
|
|
|
|
## FIXME: Logiku přesunout do views.
|
|
#class VysledkyBase(SeminarModelBase):
|
|
#
|
|
# class Meta:
|
|
# verbose_name = 'Řádek výsledkovky'
|
|
# verbose_name_plural = 'Řádky výsledkovky'
|
|
# ordering = ['body']
|
|
# abstract = True
|
|
# managed = False
|
|
#
|
|
# dummy_id = models.CharField('dummy ID pro view', max_length=32, primary_key=True,
|
|
# db_column='id')
|
|
#
|
|
# cislo = models.ForeignKey(Cislo, verbose_name='číslo pro body', db_column='cislo_id',
|
|
# on_delete=models.DO_NOTHING)
|
|
#
|
|
# resitel = models.ForeignKey(Resitel, verbose_name='řešitel', db_column='resitel_id',
|
|
# on_delete=models.DO_NOTHING)
|
|
#
|
|
# body = models.DecimalField(max_digits=8, decimal_places=1, db_column='body',
|
|
# verbose_name='body za číslo')
|
|
#
|
|
# def __str__(self):
|
|
# return "%s: %sb (%s)".format(self.resitel.plne_jmeno(), self.body,
|
|
# str(self.poradi))
|
|
# # NOTE: DB zatez pri vypisu (ale nepouzivany)
|
|
|
|
|
|
## FIXME: Logiku přesunout do views.
|
|
#class VysledkyZaCislo(VysledkyBase):
|
|
#
|
|
# class Meta:
|
|
# db_table = 'seminar_body_za_cislo'
|
|
# abstract = False
|
|
# managed = False
|
|
#
|
|
#
|
|
## FIXME: Logiku přesunout do views.
|
|
#class VysledkyKCisluZaRocnik(VysledkyBase):
|
|
#
|
|
# class Meta:
|
|
# db_table = 'seminar_body_k_cislu_rocnik'
|
|
# abstract = False
|
|
# managed = False
|
|
#
|
|
## body = models.DecimalField(max_digits=8, decimal_places=1, db_column='body',
|
|
## verbose_name='body do čísla (za ročník)')
|
|
#
|
|
#
|
|
## FIXME: Logiku přesunout do views.
|
|
#class VysledkyKCisluOdjakziva(VysledkyBase):
|
|
#
|
|
# class Meta:
|
|
# db_table = 'seminar_body_k_cislu_odjakziva'
|
|
# abstract = False
|
|
# managed = False
|
|
#
|
|
## body = models.DecimalField(max_digits=8, decimal_places=1, db_column='body',
|
|
## verbose_name='body do čísla (i minulé ročníky)')
|
|
#
|
|
#
|
|
## FIXME: Logiku přesunout do views.
|
|
#class VysledkyCelkemKCislu(VysledkyBase):
|
|
#
|
|
# class Meta:
|
|
# db_table = 'seminar_body_celkem_k_cislu'
|
|
# abstract = False
|
|
# managed = False
|
|
#
|
|
# body_celkem = models.DecimalField(max_digits=8, decimal_places=1, db_column='body_celkem',
|
|
# verbose_name='body celkem do čísla včetně minulých ročníků')
|
|
#
|
|
# def __str__(self):
|
|
# # NOTE: DB HOG (ale nepouzivany)
|
|
# return "%s: %sb / %sb (do %s)" % (self.resitel.plne_jmeno(), self.body, self.body_celkem, str(self.poradi))
|
|
##mozna potreba upravit
|
|
|
|
|
|
@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='poslední vydané číslo',
|
|
null=False, on_delete=models.PROTECT)
|
|
|
|
@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
|
|
|
|
|
|
@reversion.register(ignore_duplicates=True)
|
|
class Novinky(models.Model):
|
|
|
|
class Meta:
|
|
verbose_name = 'Novinka'
|
|
verbose_name_plural = 'Novinky'
|
|
ordering = ['-datum']
|
|
|
|
datum = models.DateField(auto_now_add=True)
|
|
|
|
text = models.TextField('Text novinky', blank=True, null=True)
|
|
obrazek = models.ImageField('Obrázek', upload_to='image_novinky/%Y/%m/%d/',
|
|
null=True, blank=True)
|
|
|
|
obrazek_maly = ImageSpecField(source='obrazek',
|
|
processors=[
|
|
ResizeToFit(350, 200, upscale=False)
|
|
],
|
|
options={'quality': 95})
|
|
|
|
autor = models.ForeignKey(Organizator, verbose_name='Autor novinky', null=True,
|
|
on_delete=models.SET_NULL)
|
|
|
|
zverejneno = models.BooleanField('Zveřejněno', default=False)
|
|
|
|
def __str__(self):
|
|
if self.text:
|
|
return '[' + str(self.datum) + '] ' + self.text[0:50]
|
|
else:
|
|
return '[' + str(self.datum) + '] '
|
|
|
|
|
|
|
|
|
|
# FIXME: Tohle nepatří do aplikace 'seminar'
|
|
# Nefunkční alternativa vestavěného Usera, který má jméno a mail v přidružené Osobě
|
|
# from django.contrib.auth.models import User as Django_User
|
|
#
|
|
# class Uzivatel(Django_User):
|
|
# class Meta:
|
|
# proxy = True
|
|
#
|
|
# @property
|
|
# def first_name(self):
|
|
# osoby = Osoba.objects.filter(user=self)
|
|
# if len(osoby) == 0:
|
|
# return None
|
|
# return osoby.first().krestni_jmeno
|
|
#
|
|
# @property
|
|
# def last_name(self):
|
|
# osoby = Osoba.objects.filter(user=self)
|
|
# if len(osoby) == 0:
|
|
# return None
|
|
# return osoby.first().prijmeni
|
|
#
|
|
# @property
|
|
# def email(self):
|
|
# osoby = Osoba.objects.filter(user=self)
|
|
# if len(osoby) == 0:
|
|
# return None
|
|
# return osoby.first().email
|
|
|