695 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			695 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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.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 tvorba.utils import roman, aktivniResitele
 | |
| 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 various.models import SeminarModelBase, OverwriteStorage
 | |
| from personalni.models import Prijemce, Organizator
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| @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')
 | |
| 
 | |
| 	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('tvorba_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')
 | |
| 
 | |
| 	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('tvorba_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)
 | |
| 
 | |
| 			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, 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 treenode.models 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_CISLA_A_SOUS = 'cislaasous' # Přidáno https://gitea.ks.matfyz.cz/mam/mamweb/pulls/74
 | |
| 	TYP_PRVNI_A_SOUS = 'prvniasous'
 | |
| 	TYP_PRVNI = 'prvni'
 | |
| 	TYP_SOUS = 'sous'
 | |
| 	TYP_CHOICES = [
 | |
| 		(TYP_CISLA, 'Deadline celého čísla'),
 | |
| 		(TYP_CISLA_A_SOUS, 'Sousový a celočíslový deadline'),
 | |
| 		(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(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', db_table='seminar_problemy_opravovatele')
 | |
| 
 | |
| 	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 f'<Není zadaný: {self.kod}>'
 | |
| 
 | |
| #	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('tvorba_problem', kwargs={'pk': self.id})
 | |
| 
 | |
| 	def admin_url(self):
 | |
| 			return reverse('admin:tvorba_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'+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 f'<Není zadaný: {self.kod}>'
 | |
| 
 | |
| 	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
 | |
| 
 | |
| 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')
 | |
| 	
 | |
| 	strana = models.PositiveIntegerField(verbose_name="první strana", blank=True, null=True)
 | |
| 
 | |
| 	@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" + 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 f'<Není zadaný: {self.kod}>'
 | |
| 	
 | |
| 	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)
 | |
| 
 | |
| 	@cached_property
 | |
| 	def kod_v_rocniku(self):
 | |
| 		if self.stav == Problem.STAV_ZADANY or self.stav == Problem.STAV_VYRESENY:
 | |
| 			return f"{self.cislo_zadani.poradi}.{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 f'<Není zadaný: {self.kod}>'
 | |
| 
 | |
| 	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)
 | |
| 
 | |
| 
 | |
| 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(
 | |
| 		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
 | |
| 	)
 | |
| 
 | |
| 	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
 | |
| 
 |