import datetime
import decimal
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import permission_required, \
user_passes_test
from html.parser import HTMLParser
from django import views as DjangoViews
from django.db import transaction
from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
import logging
from personalni.models import Organizator, Resitel, Skola, Prijemce
from tvorba.models import Clanek, Rocnik
from seminar.models.treenode import CisloNode
from seminar.models.soustredeni import Konfery_Ucastnici, Soustredeni_Ucastnici
from odevzdavatko.models import Reseni_Resitele
import treenode.treelib as t
logger = logging.getLogger(__name__)
org_required = permission_required('auth.org')
resitel_required = permission_required('auth.resitel')
# inspirováno django.contrib.auth.decorators permission_required
def check_perms(user):
if user.has_perms(('auth.resitel',)):
return True
if user.has_perms(('auth.org',)):
return True
return False
resitel_or_org_required = user_passes_test(check_perms)
User = get_user_model()
# Není to úplně hezké, ale budeme doufat, že to je funkční...
User.je_org = property(lambda self: self.has_perm('auth.org'))
User.je_resitel = property(lambda self: self.has_perm('auth.resitel'))
AnonymousUser.je_org = False
AnonymousUser.je_resitel = False
def vzorecek_na_prepocet(body, resitelu):
""" Vzoreček na přepočet plných bodů na parciálni, když má řešení více řešitelů. """
return body * 3 / (resitelu + 2)
def inverze_vzorecku_na_prepocet(body: decimal.Decimal, resitelu) -> decimal.Decimal:
""" Vzoreček na přepočet parciálních bodů na plné, když má řešení více řešitelů. """
return round(body * (resitelu + 2) / 3, 1)
class FirstTagParser(HTMLParser):
def __init__(self, *args, **kwargs):
self.firstTag = None
super().__init__(*args, **kwargs)
def handle_data(self, data):
if self.firstTag == None:
self.firstTag = data
def histogram(seznam):
d = {}
for i in seznam:
if i not in d:
d[i] = 0
d[i] += 1
return d
# Pozor: zarovnáno velmi netradičně pro přehlednost
roman_numerals = zip((1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1),
('M', 'CM', 'D', 'CD', 'C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'))
def roman(num):
res = ""
for i, n in roman_numerals:
res += n * (num // i)
num %= i
return res
def from_roman(rom):
if not rom:
return 0
for i, n in roman_numerals:
if rom.upper().startswith(n):
return i + from_roman(rom[len(n):])
raise Exception('Invalid roman numeral: "%s"', rom)
def seznam_problemu():
"""Funkce pro hledání nekonzistencí v databázi a dalších nežádoucích stavů webu/databáze.
Nijak nesouvisí s Problémy zadanými řešitelům."""
# FIXME: přejmenovat funkci?
# FIXME: Tak, jak je napsaná, asi spíš patří někam k views a ne do utils (?)
problemy = []
# Pomocna fce k formatovani problemovych hlasek
def prb(cls, msg, objs=None):
s = '%s: %s' % (cls.__name__, msg)
if objs:
s += ' ['
for o in objs:
try:
url = o.admin_url()
except:
url = None
if url:
s += '%s, ' % (url, o.pk,)
else:
s += '%s, ' % (o.pk,)
s = s[:-2] + ']'
problemy.append(s)
# Duplicita jmen
jmena = {}
for r in Resitel.objects.all():
j = r.osoba.plne_jmeno()
if j not in jmena:
jmena[j] = []
jmena[j].append(r)
for j in jmena:
if len(jmena[j]) > 1:
prb(Resitel, 'Duplicitní jméno "%s"' % (j,), jmena[j])
# Data maturity a narození
for r in Resitel.objects.all():
if not r.rok_maturity:
prb(Resitel, 'Neznámý rok maturity', [r])
if r.rok_maturity and (r.rok_maturity < 1990 or r.rok_maturity > datetime.date.today().year + 10):
prb(Resitel, 'Podezřelé datum maturity', [r])
if r.osoba.datum_narozeni and (
r.osoba.datum_narozeni.year < 1970 or r.osoba.datum_narozeni.year > datetime.date.today().year - 12):
prb(Resitel, 'Podezřelé datum narození', [r])
# if not r.email:
# prb(Resitel, u'Neznámý email', [r])
## Kontroly konzistence databáze a TreeNodů
# Články
for clanek in Clanek.objects.all():
# získáme řešení svázané se článkem a z něj node ve stromě
reseni = clanek.reseni_set
if (reseni.count() != 1):
raise ValueError("Článek k sobě má nejedno řešení!")
r = reseni.first()
clanek_node = r.text_cely # vazba na ReseniNode z Reseni
# content type je věc pomáhající rozeznávat různé typy objektů v django-polymorphic
# protože isinstance vrátí vždy jen TreeNode
# https://django-polymorphic.readthedocs.io/en/stable/migrating.html
cislonode_ct = ContentType.objects.get_for_model(CisloNode)
node = clanek_node
while node is not None:
node_ct = node.polymorphic_ctype
if node_ct == cislonode_ct: # dostali jsme se k CisloNode
# zkontrolujeme, že stromové číslo odpovídá atributu
# .cislonode je opačná vazba k treenode_ptr, abychom z TreeNode dostali
# CisloNode
if clanek.cislo != node.cislonode.cislo:
prb(Clanek, "Číslo otištění uložené u článku nesedí s "
"číslem otištění podle struktury treenodů.", [clanek])
break
node = t.get_parent(node)
return problemy
### Generovani obalek
def resi_v_rocniku(rocnik, cislo=None):
""" Vrátí seznam řešitelů, co vyřešili nějaký problém v daném ročníku, do daného čísla.
Parametry:
rocnik (typu Rocnik) ročník, ze kterého chci řešitele, co něco odevzdali
cislo (typu Cislo) číslo, do kterého včetně se počítá, že v daném
ročníku řešitel něco poslal.
Pokud není zadané, počítají se všechna řešení z daného ročníku.
Výstup:
QuerySet objektů typu Resitel """
if cislo is None:
# filtrujeme pouze podle ročníku
return Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
reseni__hodnoceni__deadline_body__cislo__rocnik=rocnik).distinct()
else: # filtrujeme podle ročníku i čísla
return Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
reseni__hodnoceni__deadline_body__cislo__rocnik=rocnik,
reseni__hodnoceni__deadline_body__cislo__poradi__lte=cislo.poradi).distinct()
def aktivniResitele(cislo, pouze_letosni=False):
""" Vrací QuerySet aktivních řešitelů, což jsou ti, co ještě neodmaturovali
a letos něco poslali (anebo loni něco poslali, pokud jde o první tři čísla).
Parametry:
cislo (typu Cislo) číslo, o které se jedná
pouze_letosni jen řešitelé, kteří tento rok něco poslali
"""
letos = cislo.rocnik
# detekujeme, zda jde o první tři čísla či nikoli (tj. zda spamovat řešitele z minulého roku)
zacatek_rocniku = True
try:
if int(cislo.poradi) > 3:
zacatek_rocniku = False
except ValueError:
# if cislo.poradi != '7-8':
# raise ValueError(f'{cislo} je neplatné číslo čísla (není int a není 7-8)')
zacatek_rocniku = False
# nehledě na číslo chceme jen řešitele, kteří letos něco odevzdali
if pouze_letosni:
zacatek_rocniku = False
try:
loni = Rocnik.objects.get(rocnik=letos.rocnik - 1)
except ObjectDoesNotExist:
# Pro první ročník neexistuje ročník předchozí
zacatek_rocniku = False
if not zacatek_rocniku:
return resi_v_rocniku(letos, cislo).filter(rok_maturity__gte=letos.druhy_rok())
else:
# spojíme querysety s řešiteli loni a letos do daného čísla
return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct().filter(rok_maturity__gte=letos.druhy_rok())
def viewMethodSwitch(get, post):
"""
Vrátí view, který zavolá různé jiné views podle toho, kterou metodou je zavolán.
Inspirováno https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#an-alternative-better-solution, jen jsem to udělal genericky.
Parametry:
post view pro metodu POST
get view pro metodu GET
V obou případech se míní už view jakožto funkce, takže u class-based views se už má použít .as_view()
TODO: Podpora i pro metodu HEAD? A možná i pro FILES?
"""
theGetView = get
thePostView = post
class NewView(DjangoViews.View):
def get(self, request, *args, **kwargs):
return theGetView(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return thePostView(request, *args, **kwargs)
return NewView.as_view()
def sync_skoly(base_url):
"""Stáhne všechny školy z mamwebu na adrese a uloží je do databáze"""
from django.urls import reverse
full_url = base_url.rstrip('/') + reverse('export_skoly')
import requests
from django.core import serializers
json = requests.get(full_url, stream=True).content
for skola in serializers.deserialize('json', json):
skola.save()
@transaction.atomic
def merge_resitele(cilovy, zdrojovy):
"""Spojí dva řešitelské objekty do cílového.
Pojmenování "zdrojový" je silně nepřiléhající, ale co už…"""
# Postup:
# Sjednotit / upravit informace cílového řešitele
print('Upravuji data modelu')
fieldy_shoda = ['skola', 'rok_maturity', 'zasilat', 'zasilat_cislo_emailem', 'zasilat_cislo_papirove']
for f in fieldy_shoda:
zf = getattr(zdrojovy, f)
cf = getattr(cilovy, f)
if cf == zf:
print(f' Údaj {f} je shodný ({zf})')
else:
if zf is None:
print(f' Údaj {f} je pouze v cílovém, používám')
continue
if cf is None:
setattr(cilovy, f, zf)
cilovy.poznamka += f'\nDEBUG: Merge: doplnéný údaj {f} ze zdrojového: {zf}'
print(f" Přiřazuji {f} ze zdrojového: {zf}")
continue
# Jsou fakt různé…
# FIXME: chybí možnost na vlastní úpravu…
verdikt = input(f"\n\n Údaj {f} se u řešitele {cilovy} ({cilovy.id}) liší:\n Zdrojový: {zf}\n Cílový: {cf}\n Který použít, [z]drojový, [c]ílový? ")
verdikt = verdikt[0].casefold()
if verdikt == 'z':
setattr(cilovy, f, zf)
cilovy.poznamka += f'\nDEBUG: Merge: pro {f} použit údaj {zf} (zdrojový), nepoužit {cf} (cílový)'
elif verdikt == 'c':
cilovy.poznamka += f'\nDEBUG: Merge: pro {f} použit údaj {cf} (cílový), nepoužit {zf} (zdrojový)'
else: raise ValueError('Špatná odpověď, řešitel pravděpodobně neuložen')
# poznámku chceme nezahodit…
cilovy.poznamka += f'\nDEBUG: Merge: Původní poznámka: {zdrojovy.poznamka}'
print(f' Výsledný řešitel: {cilovy.__dict__}, ukládám')
cilovy.save()
# Přepojit všechny vazby ze zdrojového na cílového
print('Přepojuji vazby')
# Vazby: Škola (hotovo), Řešení_Řešitelé, Konfery_Účastníci, Soustředění_Účastníci, Osoba (vyřeší se později, nejde přepojit)
ct = Reseni_Resitele.objects.filter(resitele=zdrojovy).update(resitele=cilovy)
print(f' Přepojeno {ct} řešení')
ct = Konfery_Ucastnici.objects.filter(resitel=zdrojovy).update(resitel=cilovy)
print(f' Přepojeno {ct} konfer')
ct = Soustredeni_Ucastnici.objects.filter(resitel=zdrojovy).update(resitel=cilovy)
print(f' Přepojeno {ct} sousů')
# Teď by na zdrojovém řešiteli nemělo nic viset, smazat ho, pamatujíce si jeho Osobu
zdrosoba = zdrojovy.osoba
print(f'Mažu zdrojového řešitele {zdrojovy.__dict__}')
zdrojovy.delete()
# Spojit osoby (separátní funkce).
merge_osoby(cilovy.osoba, zdrosoba)
input("Potvrdit transakci řešitelů (^C pro zrušení) ")
@transaction.atomic
def merge_osoby(cilova, zdrojova):
""" Spojí dvě osoby do cílové
Nehlídá omezení typu "max 1 řešitel na osobu", to by měla hlídat databáze (OneToOneField)."""
# Sjednocení dat
print('Sjednocuji data osob')
# ID, User neřešíme, poznámku vyřešíme separátně.
fieldy = ['datum_narozeni', 'datum_registrace', 'datum_souhlasu_udaje',
'datum_souhlasu_zasilani', 'email', 'foto', 'jmeno', 'mesto',
'pohlavi_muz', 'prezdivka', 'prijmeni', 'psc', 'stat', 'telefon', 'ulice']
for f in fieldy:
zf = getattr(zdrojova, f)
cf = getattr(cilova, f)
if cf == zf:
print(f' Údaj {f} je shodný ({zf})')
else:
if zf is None:
print(f' Údaj {f} je pouze v cílové, používám')
continue
if cf is None:
setattr(cilova, f, zf)
cilova.poznamka += f'\nDEBUG: Merge: doplnéný údaj {f} ze zdrojové: {zf}'
print(f" Přiřazuji {f} ze zdrojové: {zf}")
continue
# Jsou fakt různé…
# FIXME: chybí možnost na vlastní úpravu…
verdikt = input(f"\n\n Údaj {f} se u osoby {cilova} ({cilova.id}) liší:\n Zdrojový: {zf}\n Cílový: {cf}\n Který použít, [z]drojový, [c]ílový? ")
verdikt = verdikt[0].casefold()
if verdikt == 'z':
setattr(cilova, f, zf)
cilova.poznamka += f'\nDEBUG: Merge: pro {f} použit údaj {zf} (zdrojová), nepoužit {cf} (cílová)'
elif verdikt == 'c':
cilova.poznamka += f'\nDEBUG: Merge: pro {f} použit údaj {cf} (cílová), nepoužit {zf} (zdrojová)'
else: raise ValueError('Špatná odpověď, řešitel pravděpodobně neuložen')
# poznámku chceme nezahodit…
cilova.poznamka += f'\nDEBUG: Merge: Původní poznámka: {zdrojova.poznamka}'
print(f' Výsledná osoba: {cilova.__dict__}, ukládám')
cilova.save()
# Vazby: Řešitel, User, Příjemce, Organizátor, Škola.kontaktní_osoba
print('Přepojuji vazby')
ct = Skola.objects.filter(kontaktni_osoba=zdrojova).update(kontaktni_osoba=cilova)
print(f' Přepojeno {ct} kontaktních osob')
# Ostatní vazby vyřeší OneToOneFieldy, ale někdy nemusí existovat…
ct = Resitel.objects.filter(osoba=zdrojova).update(osoba=cilova)
print(f' Přepojeno {ct} řešitelů')
ct = Prijemce.objects.filter(osoba=zdrojova).update(osoba=cilova)
print(f' Přepojeno {ct} příjemců')
ct = Organizator.objects.filter(osoba=zdrojova).update(osoba=cilova)
print(f' Přepojeno {ct} organizátorů')
# Uživatelé vedou opačným směrem, radši chceme zkontrolovat, že jsou různí ručně:
if zdrojova.user != cilova.user:
# Jeden z nich může být nenastavený…
if zdrojova.user is None:
print('Uživatel je již v cílové osobě')
elif cilova.user is None:
print('Používám uživatele zdrojové osoby')
cilova.user = zdrojova.user
# Teď nemůžeme uložit, protože kolize uživatelů. Ukládat cílovou budeme až po smazání zdrojové.
else: raise ValueError('Osoby mají obě uživatele, radši padám')
# Uložení a mazání
print(f'Mažu zdrojovou osobu {zdrojova.__dict__}')
zdrojova.delete()
print(f'Ukládám cílovou osobu {cilova.__dict__}')
cilova.save()
input("Potvrdit transakci osob (^C pro zrušení) ")