281 lines
8.6 KiB
Python
281 lines
8.6 KiB
Python
import os
|
|
import subprocess
|
|
import pathlib
|
|
import tempfile
|
|
import logging
|
|
from reversion import revisions as reversion
|
|
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.conf import settings
|
|
from django.urls import reverse
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core.files.storage import FileSystemStorage
|
|
from django.core.mail import EmailMessage
|
|
|
|
from seminar.utils import aktivniResitele
|
|
|
|
from mamweb.models.base import SeminarModelBase
|
|
from personalni.models.prijemce import Prijemce
|
|
from .rocnik import Rocnik
|
|
|
|
|
|
class OverwriteStorage(FileSystemStorage):
|
|
"""Varianta FileSystemStorage, která v případě, že soubor cílového
|
|
jména již existuje, ho smaže a místo něj uloží soubor nový"""
|
|
def get_available_name(self, name, max_length=None):
|
|
if self.exists(name):
|
|
os.remove(os.path.join(self.location, name))
|
|
return super().get_available_name(name, max_length)
|
|
|
|
|
|
def cislo_pdf_filename(self, _filename):
|
|
rocnik = str(self.rocnik.rocnik)
|
|
return pathlib.Path(
|
|
'cislo', 'pdf', rocnik, '{}-{}.pdf'.format(rocnik, self.poradi),
|
|
)
|
|
|
|
|
|
def cislo_png_filename(self, _filename):
|
|
rocnik = str(self.rocnik.rocnik)
|
|
return pathlib.Path(
|
|
'cislo', 'png', rocnik, '{}-{}.png'.format(rocnik, self.poradi),
|
|
)
|
|
|
|
|
|
@reversion.register(ignore_duplicates=True)
|
|
class Cislo(SeminarModelBase):
|
|
|
|
class Meta:
|
|
db_table = 'seminar_cisla'
|
|
verbose_name = 'Číslo'
|
|
verbose_name_plural = 'Čísla'
|
|
ordering = ['-rocnik__rocnik', '-poradi']
|
|
|
|
# Interní ID
|
|
id = models.AutoField(primary_key=True)
|
|
|
|
rocnik = models.ForeignKey(
|
|
Rocnik, verbose_name='ročník', related_name='cisla',
|
|
db_index=True, on_delete=models.PROTECT,
|
|
)
|
|
|
|
poradi = models.CharField(
|
|
'název čísla', max_length=32, db_index=True,
|
|
help_text='Většinou jen "1", vyjímečně "7-8", lexikograficky určuje pořadí v ročníku!',
|
|
)
|
|
|
|
datum_vydani = models.DateField(
|
|
'datum vydání', blank=True, null=True,
|
|
help_text='Datum vydání finální verze',
|
|
)
|
|
|
|
verejne_db = models.BooleanField(
|
|
'číslo zveřejněno', db_column='verejne', default=False,
|
|
)
|
|
|
|
poznamka = models.TextField(
|
|
'neveřejná poznámka', blank=True,
|
|
help_text='Neveřejná poznámka k číslu (plain text)',
|
|
)
|
|
|
|
pdf = models.FileField(
|
|
'pdf', upload_to=cislo_pdf_filename, null=True, blank=True,
|
|
help_text='PDF čísla, které si mohou řešitelé stáhnout',
|
|
storage=OverwriteStorage(),
|
|
)
|
|
|
|
titulka_nahled = models.ImageField(
|
|
'Obrázek titulní strany', upload_to=cislo_png_filename,
|
|
null=True, blank=True,
|
|
help_text='Obrázek titulní strany, generuje se automaticky',
|
|
)
|
|
|
|
# má OneToOneField s:
|
|
# CisloNode
|
|
|
|
def kod(self):
|
|
return '%s.%s' % (self.rocnik.rocnik, self.poradi)
|
|
kod.short_description = 'Kód čísla'
|
|
|
|
def __str__(self):
|
|
# Potenciální DB HOG, pokud by se ročník necachoval
|
|
r = Rocnik.cached_rocnik(self.rocnik_id)
|
|
return '{}.{}'.format(r.rocnik, self.poradi)
|
|
|
|
def verejne(self):
|
|
return self.verejne_db
|
|
verejne.boolean = True
|
|
|
|
def verejne_url(self):
|
|
return reverse(
|
|
'seminar_cislo',
|
|
kwargs={'rocnik': self.rocnik.rocnik, 'cislo': self.poradi},
|
|
)
|
|
|
|
def absolute_url(self):
|
|
return "https://" + str(get_current_site(None)) + self.verejne_url()
|
|
|
|
def nasledujici(self):
|
|
"""Vrací None, pokud je toto poslední"""
|
|
return self.relativni_v_rocniku(1)
|
|
|
|
def predchozi(self):
|
|
"""Vrací None, pokud je toto první"""
|
|
return self.relativni_v_rocniku(-1)
|
|
|
|
def relativni_v_rocniku(self, rel_index):
|
|
"""Číslo o `index` dále v ročníku. None pokud neexistuje."""
|
|
cs = self.rocnik.cisla.order_by('poradi').all()
|
|
i = list(cs).index(self) + rel_index
|
|
if (i < 0) or (i >= len(cs)):
|
|
return None
|
|
return cs[i]
|
|
|
|
def vygeneruj_nahled(self):
|
|
VYSKA = 594
|
|
sirka = int(VYSKA*210/297)
|
|
if not self.pdf:
|
|
return
|
|
|
|
# Pokud obrázek neexistuje nebo není aktuální, vytvoř jej
|
|
if not self.titulka_nahled or os.path.getmtime(self.titulka_nahled.path) < os.path.getmtime(self.pdf.path):
|
|
png_filename = pathlib.Path(tempfile.mkdtemp(), 'nahled.png')
|
|
|
|
subprocess.run([
|
|
"gs",
|
|
"-sstdout=%stderr",
|
|
"-dSAFER",
|
|
"-dNOPAUSE",
|
|
"-dBATCH",
|
|
"-dNOPROMPT",
|
|
"-sDEVICE=png16m",
|
|
"-r300x300",
|
|
"-dFirstPage=1d",
|
|
"-dLastPage=1d",
|
|
"-sOutputFile=" + str(png_filename),
|
|
"-f%s" % self.pdf.path
|
|
],
|
|
check=True,
|
|
capture_output=True
|
|
)
|
|
|
|
with open(png_filename, 'rb') as f:
|
|
self.titulka_nahled.save('', f, True)
|
|
|
|
png_filename.unlink()
|
|
png_filename.parent.rmdir()
|
|
|
|
@classmethod
|
|
def get(cls, rocnik, cislo):
|
|
try:
|
|
r = Rocnik.objects.get(rocnik=rocnik)
|
|
c = r.cisla.get(poradi=cislo)
|
|
except ObjectDoesNotExist:
|
|
return None
|
|
return c
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.__original_verejne = self.verejne_db
|
|
|
|
def posli_cislo_mailem(self):
|
|
# parametry e-mailu
|
|
odkaz = self.absolute_url()
|
|
|
|
poslat_z_mailu = 'zadani@mam.mff.cuni.cz'
|
|
predmet = 'Vyšlo číslo {}'.format(self.kod())
|
|
# TODO Možná nechceme všem psát „Ahoj“, např. příjemcům…
|
|
text_mailu = (
|
|
'Ahoj,\n'
|
|
'na adrese {} najdete nejnovější číslo.\n'
|
|
'Vaše M&M\n'
|
|
).format(odkaz)
|
|
|
|
predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!'
|
|
text_mailu_prvni = (
|
|
'Milý řešiteli,\n'
|
|
'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'
|
|
'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'
|
|
'Organizátoři M&M\n'
|
|
).format(self.rocnik.rocnik, odkaz)
|
|
|
|
predmet_resitel = predmet_prvni if self.poradi == "1" else predmet
|
|
text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu
|
|
|
|
# Prijemci e-mailu
|
|
resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True)
|
|
|
|
def posli(subject, text, resitele):
|
|
emaily = map(lambda resitel: resitel.osoba.email, resitele)
|
|
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
|
|
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
|
|
return
|
|
|
|
email = EmailMessage(
|
|
subject=subject,
|
|
body=text,
|
|
from_email=poslat_z_mailu,
|
|
# bcc = příjemci skryté kopie
|
|
bcc=list(emaily)
|
|
)
|
|
|
|
email.send()
|
|
|
|
paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/"
|
|
|
|
posli(
|
|
predmet_resitel,
|
|
text_mailu_resitel + paticka,
|
|
resitele_vsichni.filter(zasilat_cislo_papirove=False),
|
|
)
|
|
posli(
|
|
predmet_resitel,
|
|
text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka,
|
|
resitele_vsichni.filter(zasilat_cislo_papirove=True),
|
|
)
|
|
|
|
paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz."
|
|
posli(
|
|
predmet,
|
|
text_mailu + paticka_prijemce,
|
|
Prijemce.objects.filter(zasilat_cislo_emailem=True)
|
|
)
|
|
|
|
def save(self, *args, **kwargs):
|
|
super().save(*args, **kwargs)
|
|
self.vygeneruj_nahled()
|
|
# Při zveřejnění pošle mail
|
|
if self.verejne_db and not self.__original_verejne:
|
|
self.posli_cislo_mailem()
|
|
# *Node.save() aktualizuje název *Nodu.
|
|
try:
|
|
self.cislonode.save()
|
|
except ObjectDoesNotExist:
|
|
# Neexistující *Node nemá smysl aktualizovat, ale je potřeba ho naopak vyrobit
|
|
logger = logging.getLogger(__name__)
|
|
logger.warning(f'Číslo {self} nemělo ČísloNode, vyrábím…')
|
|
from treenode.models import CisloNode
|
|
CisloNode.objects.create(cislo=self)
|
|
|
|
def zlomovy_deadline_pro_papirove_cislo(self):
|
|
from .deadline import Deadline
|
|
prvni_deadline = Deadline.objects.filter(
|
|
Q(typ=Deadline.TYP_PRVNI) | Q(typ=Deadline.TYP_PRVNI_A_SOUS),
|
|
cislo=self
|
|
).first()
|
|
if prvni_deadline is None:
|
|
posledni_deadline = self.posledni_deadline
|
|
if posledni_deadline is None:
|
|
# TODO promyslet, co se má stát tady
|
|
return Deadline.objects.filter(
|
|
Q(cislo__poradi__lt=self.poradi, cislo__rocnik=self.rocnik) |
|
|
Q(cislo__rocnik__rocnik__lt=self.rocnik.rocnik)
|
|
).order_by("deadline").last()
|
|
return posledni_deadline
|
|
return prvni_deadline
|
|
|
|
@property
|
|
def posledni_deadline(self):
|
|
return self.deadline_v_cisle.all().order_by("deadline").last()
|