mamweb/tvorba/models/cislo.py

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