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