# -*- coding: utf-8 -*- import logging from django.db import models from django.utils import timezone from django.conf import settings from django.core.exceptions import ValidationError from imagekit.models import ImageSpecField, ProcessedImageField from imagekit.processors import ResizeToFit, Transpose from django_countries.fields import CountryField from reversion import revisions as reversion from .base import SeminarModelBase logger = logging.getLogger(__name__) @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, ...)') jak_se_dozvedeli = models.TextField('Jak se dozvěděli', blank=True) 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) prezdivka_resitele = models.CharField('přezdívka řešitele', blank=True, null=True, max_length=256, unique=True) osoba = models.OneToOneField(Osoba, blank=False, null=False, verbose_name='osoba', on_delete=models.PROTECT) 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, 'Nezasílat papírově'), ] zasilat = models.CharField('kam zasílat', max_length=32, choices=ZASILAT_CHOICES, blank=False, default=ZASILAT_DOMU) zasilat_cislo_emailem = models.BooleanField('zasílat číslo emailem', help_text='True pokud chce řešitel dostávat číslo emailem', default=False) zasilat_cislo_papirove = models.BooleanField('zasílat číslo papírově', help_text='True pokud chce řešitel dostávat číslo papírově', default=True) 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() from .odevzdavatko import Hodnoceni vsechna_hodnoceni = Hodnoceni.objects.filter( reseni__in=vsechna_reseni) return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) def get_titul(self, body=None): "Vrati titul jako řetězec." # Nejprve si zadefinujeme titul from enum import Enum from functools import total_ordering @total_ordering class Titul(Enum): """ Třída reprezentující možné tituly. Hodnoty jsou dvojice (dolní hranice, stringifikace). """ nic = (0, '') bc = (20, 'Bc.') mgr = (50, 'Mgr.') dr = (100, 'Dr.') doc = (200, 'Doc.') prof = (500, 'Prof.') akad = (1000, 'Akad.') def __lt__(self, other): return True if self.value[0] < other.value[0] else False def __eq__(self, other): # Měla by být implicitní, ale klidně explicitně. return True if self.value[0] == other.value[0] else False def __str__(self): return self.value[1] @classmethod def z_bodu(cls, body): aktualni = cls.nic # TODO: ověřit, že to funguje for titul in cls: # Kdyžtak použít __members__.items() if titul.value[0] <= body: aktualni = titul else: break return aktualni # Hledáme body v databázi # V listopadu 2020 jsme se na filosofické schůzce shodli o změně hranic titulů: # - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími # - proto se započítávají dvojnásobně a byly posunuté hranice titulů # - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. from .odevzdavatko import Hodnoceni hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all()) novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) def body_z_hodnoceni(hh : list): return sum(h.body for h in hh if h.body is not None) stare_body = body_z_hodnoceni(hodnoceni_do_25_rocniku) if body is None: nove_body = body_z_hodnoceni(novejsi_hodnoceni) else: # Zjistíme, kolik bodů jsou staré, tedy hodnotnější nove_body = max(0, body - stare_body) # Všechny body nad počet původních hodnotnějších stare_body = min(stare_body, body) # Skutečný počet hodnotnějších bodů logicke_body = 2*stare_body + nove_body # Titul se určí následovně: # - Pokud se řeší body, které jsou starší, než do 26 ročníku (včetně), dáváme tituly postaru. # - Jinak dáváme tituly po novu... # - ... ale titul se nesmí odebrat, pokud se zmenšil. def titul_do_26_rocniku(body): """ Původní hranice bodů za tituly """ if body < 10: return Titul.nic elif body < 20: return Titul.bc elif body < 50: return Titul.mgr elif body < 100: return Titul.dr elif body < 200: return Titul.doc elif body < 500: return Titul.prof else: return Titul.akad from .odevzdavatko import Hodnoceni hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(deadline_body__cislo__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all()) novejsi_body = body_z_hodnoceni( Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) .difference(hodnoceni_do_26_rocniku) ) starsi_body = body_z_hodnoceni(hodnoceni_do_26_rocniku) if body is not None: # Ještě z toho vybereme ty správně staré body novejsi_body = max(0, body - starsi_body) starsi_body = min(starsi_body, body) # Titul pro 26. ročník stary_titul = titul_do_26_rocniku(starsi_body) # Titul podle aktuálních pravidel novy_titul = Titul.z_bodu(logicke_body) if novejsi_body == 0: # Žádné nové body -- titul podle starých pravidel return str(stary_titul) return str(max(novy_titul, stary_titul)) def __str__(self): return self.osoba.plne_jmeno() @reversion.register(ignore_duplicates=True) class Organizator(SeminarModelBase): osoba = models.OneToOneField(Osoba, verbose_name='osoba', related_name='org', help_text='osobní údaje organizátora', null=False, blank=False, on_delete=models.PROTECT) 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']