from django . core . exceptions import PermissionDenied
from django . views . generic import ListView , DetailView , FormView
from django . contrib . auth . mixins import LoginRequiredMixin
from django . core . mail import EmailMessage
from django . utils import timezone
from django . views . generic import ListView , DetailView , FormView , CreateView
from django . views . generic . list import MultipleObjectTemplateResponseMixin , MultipleObjectMixin
from django . views . generic . base import View
from django . shortcuts import redirect , get_object_or_404 , render
from django . urls import reverse
from django . db import transaction
from django . db . models import Q
from dataclasses import dataclass
import datetime
from decimal import Decimal
from itertools import groupby
import logging
from . import forms as f
from . forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from . models import Hodnoceni , Reseni
from personalni . models import Resitel , Osoba , Organizator
from tvorba . models import Problem , Deadline , Rocnik
from tvorba . utils import resi_v_rocniku
from various . models import Nastaveni
from various . views . pomocne import formularOKView
logger = logging . getLogger ( __name__ )
# Co chceme?
# - "Tabulku" aktuální řešitelé x zveřejněné problémy, v buňkách počet řešení
# - TabulkaOdevzdanychReseniView
# - Detail konkrétního problému a řešitele -- přehled všech řešení odevzdaných k tomuto problému
# - ReseniProblemuView
# - Detail konkrétního řešení -- všechny soubory, datum, ...
# - DetailReseniView
# - Pro řešitele: přehled jejich odevzdaných řešení
# - PrehledOdevzdanychReseni
#
# Taky se může hodit:
# - Tabulka všech řešitelů x všech problémů?
class TabulkaOdevzdanychReseniView ( ListView ) :
template_name = ' odevzdavatko/tabulka.html '
model = Hodnoceni
def inicializuj_osy_tabulky ( self ) :
""" Vyrobí prvotní querysety pro sloupce a řádky, tj. seznam všech řešitelů a problémů """
# FIXME: jméno metody není vypovídající...
# NOTE: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistují ty objekty (?). TODO: Otestovat
# TODO: Prefetches, Select related, ...
self . resitele = Resitel . objects . all ( )
self . problemy = Problem . objects . all ( )
self . reseni = Reseni . objects . all ( )
self . aktualni_rocnik = Nastaveni . get_solo ( ) . aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
if ' rocnik ' in self . kwargs :
self . aktualni_rocnik = get_object_or_404 ( Rocnik , rocnik = self . kwargs [ ' rocnik ' ] )
form = FiltrForm ( self . request . GET , rocnik = self . aktualni_rocnik )
if form . is_valid ( ) :
fcd = form . cleaned_data
resitele = fcd [ " resitele " ]
problemy = fcd [ " problemy " ]
reseni_od = fcd [ " reseni_od " ]
reseni_do = fcd [ " reseni_do " ]
jen_neobodovane = fcd [ " neobodovane " ]
self . barvicky = fcd [ " barvicky " ]
else :
initial = FiltrForm . gen_initial ( self . aktualni_rocnik )
resitele = initial [ ' resitele ' ]
problemy = initial [ ' problemy ' ]
reseni_od = initial [ ' reseni_od ' ] [ 0 ]
reseni_do = initial [ ' reseni_do ' ] [ 0 ]
jen_neobodovane = initial [ " neobodovane " ]
self . barvicky = initial [ " barvicky " ]
# Chceme jen letošní problémy
self . problemy = self . problemy . filter ( Q ( Tema___rocnik = self . aktualni_rocnik ) | Q ( Uloha___cislo_zadani__rocnik = self . aktualni_rocnik ) | Q ( Clanek___cislo__rocnik = self . aktualni_rocnik ) | Q ( Konfera___soustredeni__rocnik = self . aktualni_rocnik ) )
self . chteni_resitele = resitele # Zapamatování pro get_context_data
if resitele == FiltrForm . RESITELE_RELEVANTNI :
# Nejde použít utils.resi_v_rocniku, protože noví řešitelé mohou mít neobodované řešení a takoví technicky zatím neřeší.
# Proto používám neodmaturovavší řešitele, TODO: Chceme to takhle nebo jinak?
self . resitele = self . resitele . filter ( rok_maturity__gt = self . aktualni_rocnik . prvni_rok ) # Prvotní sada, pokud nebude mít body, odstraní se v get_context_data
elif resitele == FiltrForm . RESITELE_NEODMATUROVAVSI :
self . resitele = self . resitele . filter ( rok_maturity__gt = self . aktualni_rocnik . prvni_rok )
if problemy == FiltrForm . PROBLEMY_MOJE :
org = Organizator . objects . get ( osoba__user = self . request . user )
self . problemy = self . problemy . filter (
Q ( autor = org ) | Q ( garant = org ) | Q ( opravovatele = org ) ,
Q ( stav = Problem . STAV_ZADANY ) | Q ( stav = Problem . STAV_VYRESENY ) ,
)
elif problemy == FiltrForm . PROBLEMY_LETOSNI :
self . problemy = self . problemy . filter (
Q ( stav = Problem . STAV_ZADANY ) | Q ( stav = Problem . STAV_VYRESENY ) ,
)
#self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník....
# NOTE: Protože řešení odkazuje přímo na Problém a QuerySet na Hodnocení je nepolymorfní, musíme porovnávat taky s nepolymorfními Problémy.
self . problemy = self . problemy . non_polymorphic ( ) . distinct ( )
self . reseni = self . reseni . filter ( cas_doruceni__date__gt = reseni_od , cas_doruceni__date__lte = reseni_do )
if jen_neobodovane :
self . reseni = self . reseni . filter ( hodnoceni__body__isnull = True )
self . jen_neobodovane = jen_neobodovane
def get_queryset ( self ) :
self . inicializuj_osy_tabulky ( )
qs = super ( ) . get_queryset ( )
if self . jen_neobodovane :
qs = qs . filter ( body__isnull = True )
qs = qs . filter ( problem__in = self . problemy , reseni__in = self . reseni , reseni__resitele__in = self . resitele ) . select_related ( ' reseni ' , ' problem ' ) . prefetch_related ( ' reseni__resitele__osoba ' ) . distinct ( )
# FIXME tohle je ošklivé, na špatném místě a pomalé. Ale moc mě štvalo, že musím hledat správná místa v tabulce.
self . problemy = self . problemy . filter ( id__in = qs . values ( " problem__id " ) )
return qs
def get_context_data ( self , * args , * * kwargs ) :
# TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl…
# self.resitele, self.reseni a self.problemy jsou již nastavené
ctx = super ( ) . get_context_data ( * args , * * kwargs )
ctx [ ' problemy ' ] = self . problemy
ctx [ ' resitele ' ] = self . resitele
tabulka : dict [ Problem , dict [ Resitel , list [ tuple [ Reseni , Hodnoceni ] ] ] ] = dict ( )
soucty : dict [ Problem , dict [ Resitel , Decimal ] ] = dict ( )
def pridej_reseni ( resitel , hodnoceni ) :
problem = hodnoceni . problem
body = hodnoceni . body
cas = hodnoceni . reseni . cas_doruceni
reseni = hodnoceni . reseni
if problem not in tabulka :
tabulka [ problem ] = dict ( )
soucty [ problem ] = dict ( )
if resitel not in tabulka [ problem ] :
tabulka [ problem ] [ resitel ] = [ ( reseni , hodnoceni ) ]
soucty [ problem ] [ resitel ] = hodnoceni . body or 0 # Neobodované neřešíme
else :
tabulka [ problem ] [ resitel ] . append ( ( reseni , hodnoceni ) )
soucty [ problem ] [ resitel ] + = hodnoceni . body or 0 # Neobodované neřešíme
for hodnoceni in self . get_queryset ( ) :
for resitel in hodnoceni . reseni . resitele . all ( ) :
pridej_reseni ( resitel , hodnoceni )
hodnoty : list [ list [ tuple [ Decimal , list [ tuple [ Reseni , Hodnoceni ] ] ] ] ] = [ ] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
resitele_do_tabulky : list [ Resitel ] = [ ]
for resitel in self . resitele :
dostal_body = False
resiteluv_radek : list [ tuple [ Decimal , list [ tuple [ Reseni , Hodnoceni ] ] ] ] = [ ] # podle pořadí v self.problemy
for problem in self . problemy :
if problem in tabulka and resitel in tabulka [ problem ] :
resiteluv_radek . append ( ( soucty [ problem ] [ resitel ] , tabulka [ problem ] [ resitel ] ) )
dostal_body = True
else :
resiteluv_radek . append ( ( Decimal ( 0 ) , [ ] ) )
if self . chteni_resitele != FiltrForm . RESITELE_RELEVANTNI or dostal_body :
hodnoty . append ( resiteluv_radek )
resitele_do_tabulky . append ( resitel )
ctx [ ' radky ' ] = list ( zip ( resitele_do_tabulky , hodnoty ) )
ctx [ ' filtr ' ] = FiltrForm ( initial = self . request . GET , rocnik = self . aktualni_rocnik )
# Pro použití hacku na automatické {{form.media}} v template:
ctx [ ' form ' ] = ctx [ ' filtr ' ]
# Pro maximum v přesměrovátku ročníků
ctx [ ' aktualni_rocnik ' ] = Nastaveni . get_solo ( ) . aktualni_rocnik
ctx [ ' barvicky ' ] = self . barvicky
if ' rocnik ' in self . kwargs :
ctx [ ' rocnik ' ] = self . kwargs [ ' rocnik ' ]
else :
ctx [ ' rocnik ' ] = ctx [ ' aktualni_rocnik ' ] . rocnik
return ctx
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView ( MultipleObjectTemplateResponseMixin , MultipleObjectMixin , View ) :
""" Rozskok mezi více řešeními téhož problému od téhož řešitele.
Asi už bude zastaralý v okamžiku , kdy se tenhle komentář nasadí na produkci : - )
V případě , že takové řešení existuje jen jedno , tak na něj přesměruje . """
model = Reseni
template_name = ' odevzdavatko/seznam.html '
def get_queryset ( self ) :
qs = super ( ) . get_queryset ( )
resitel_id = self . kwargs [ ' resitel ' ]
if resitel_id is None :
raise ValueError ( " Nemám řešitele! " )
problem_id = self . kwargs [ ' problem ' ]
if problem_id is None :
raise ValueError ( " Nemám problém! (To je problém!) " )
resitel = Resitel . objects . get ( id = resitel_id )
problem = Problem . objects . get ( id = problem_id )
qs = qs . filter (
problem__in = [ problem ] ,
resitele__in = [ resitel ] ,
)
return qs
def get ( self , request , * args , * * kwargs ) :
self . object_list = self . get_queryset ( )
if self . object_list . count ( ) == 1 :
jedine_reseni = self . object_list . first ( )
return redirect ( reverse ( " odevzdavatko_detail_reseni " , kwargs = { " pk " : jedine_reseni . id } ) )
context = self . get_context_data ( )
return self . render_to_response ( context )
def get_context_data ( self , * args , * * kwargs ) :
ctx = super ( ) . get_context_data ( * args , * * kwargs )
# XXX: Předat groupby do template nejde: https://stackoverflow.com/questions/6906593/itertools-groupby-in-a-django-template
# Django má {% regroup %}, ale ten potřebuje, aby klíč byl atribut položky: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#regroup
# Takže rozbalíme groupby do slovníku klíč → seznam sami (dictionary comphrehension)
ctx [ ' reseni_podle_deadlinu ' ] = { k : list ( v ) for k , v in groupby ( ctx [ ' object_list ' ] , lambda r : r . deadline_reseni ) }
# Pro sitetree:
ctx [ " resitel_id " ] = self . kwargs [ ' resitel ' ]
ctx [ " problem_id " ] = self . kwargs [ ' problem ' ]
return ctx
HODNOCENI_INITIAL_DATA = [
" problem " ,
" body " ,
" body_celkem " ,
" body_neprepocitane " ,
" body_neprepocitane_celkem " ,
" body_max " ,
" body_neprepocitane_max " ,
" deadline_body " ,
" feedback " ,
]
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView ( DetailView ) :
""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
model = Reseni
template_name = ' odevzdavatko/detail.html '
def aktualni_hodnoceni ( self ) :
self . reseni = get_object_or_404 ( Reseni , id = self . kwargs [ ' pk ' ] )
result = [ ] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in Hodnoceni . objects . filter ( reseni = self . reseni ) :
result . append ( { attr : getattr ( hodn , attr ) for attr in HODNOCENI_INITIAL_DATA } )
return result
def get_context_data ( self , * * kw ) :
self . check_access ( )
ctx = super ( ) . get_context_data ( * * kw )
detaily_hodnoceni = self . aktualni_hodnoceni ( )
ctx [ " hodnoceni " ] = detaily_hodnoceni
# Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168)
ctx [ " predmetmailu " ] = " Oprava řešení M&M " + self . reseni . problem . first ( ) . hlavni_problem . nazev
ctx [ " maily_vsech_resitelu " ] = [ y for x in self . reseni . resitele . all ( ) . values_list ( ' osoba__email ' ) for y in x ]
return ctx
def get ( self , request , * args , * * kwargs ) :
"""
Oproti : py : class : ` django . views . generic . detail . BaseDetailView `
kontroluje přístup pomocí : py : meth : ` check_access `
"""
response = super ( ) . get ( self , request , * args , * * kwargs )
self . check_access ( )
return response
def check_access ( self ) :
""" Řešitel musí být součástí řešení, jinak se na něj nemá co dívat. Případně to může být org. """
if not self . object . resitele . filter ( osoba__user = self . request . user ) . exists ( ) and not self . request . user . je_org :
raise PermissionDenied ( )
class EditReseniView ( DetailReseniView ) :
""" Editace (hlavně hodnocení) řešení. """
def get_context_data ( self , * * kw ) :
ctx = super ( ) . get_context_data ( * * kw )
ctx [ ' form ' ] = f . OhodnoceniReseniFormSet ( initial = ctx [ " hodnoceni " ] )
ctx [ ' poznamka_form ' ] = f . PoznamkaReseniForm ( instance = self . reseni )
ctx [ ' edit ' ] = True
return ctx
def check_access ( self ) :
# Na orga máme nároky už v urls.py ale better safe then sorry
if not self . request . user . je_org :
raise PermissionDenied ( )
def hodnoceniReseniView ( request , pk , * args , * * kwargs ) :
reseni = get_object_or_404 ( Reseni , pk = pk )
success_url = reverse ( ' odevzdavatko_detail_reseni ' , kwargs = { ' pk ' : pk } )
formset = f . OhodnoceniReseniFormSet ( request . POST , initial = [
{ k : getattr ( h , k ) for k in HODNOCENI_INITIAL_DATA } for h in Hodnoceni . objects . filter ( reseni = reseni )
] )
poznamka_form = f . PoznamkaReseniForm ( request . POST , instance = reseni )
# TODO: Napsat validaci formuláře a formsetu
if not ( formset . is_valid ( ) and poznamka_form . is_valid ( ) ) :
raise ValueError ( formset . errors , poznamka_form . errors )
with transaction . atomic ( ) :
# Poznámka je jednoduchá na zpracování:
poznamka_form . save ( )
# Smažeme všechna dosavadní hodnocení tohoto řešení
qs = Hodnoceni . objects . filter ( reseni = reseni )
logger . info ( f " Will delete { qs . count ( ) } objects: { qs } " )
qs . delete ( )
# Vyrobíme nová podle formsetu
notifikace = False
for form in formset :
notifikace | = ' feedback ' in form . changed_data
data_for_hodnoceni = form . cleaned_data
data_for_body = data_for_hodnoceni . copy ( )
del ( data_for_hodnoceni [ " body_celkem " ] )
del ( data_for_hodnoceni [ " body_neprepocitane " ] )
del ( data_for_hodnoceni [ " body_neprepocitane_celkem " ] )
hodnoceni = Hodnoceni (
reseni = reseni ,
* * form . cleaned_data ,
)
logger . info ( f " Creating Hodnoceni: { hodnoceni } " )
zmeny_bodu = [ it for it in form . changed_data if it . startswith ( " body " ) ]
if len ( zmeny_bodu ) == 1 :
hodnoceni . __setattr__ ( zmeny_bodu [ 0 ] , data_for_body [ zmeny_bodu [ 0 ] ] )
# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno
if len ( zmeny_bodu ) > 1 and len ( zmeny_bodu ) != 4 and len ( zmeny_bodu ) != 2 :
# 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
logger . warning ( f " Hodnocení { hodnoceni } mělo mít nastavené víc různých bodů: { zmeny_bodu } . Nastavuji -0.1. " )
hodnoceni . body = - 0.1
hodnoceni . save ( )
adresati = reseni . resitele . filter ( upozorneni = True ) . values_list ( ' osoba__email ' , flat = True )
if notifikace and adresati :
email = EmailMessage (
subject = ' Změna hodnocení odevzdaného řešení ' ,
body = f """ Milá řešitelko, milý řešiteli,
došlo ke změně zpětné vazby k Tebou odevzdanému řešení . Zobrazit si ji můžeš na { reseni . resitel_url ( ) } .
Tvoji organizátoři M & M
- - -
Nechceš - li tato upozornění dostávat , můžeš si to nastavit ve svém profilu . """ ,
from_email = ' odevzdavatko@mam.mff.cuni.cz ' ,
bcc = adresati ,
)
email . send ( )
return redirect ( success_url )
class PrehledOdevzdanychReseni ( ListView ) :
model = Hodnoceni
template_name = ' odevzdavatko/prehled_reseni.html '
def get_queryset ( self ) :
if not self . request . user . is_authenticated :
raise RuntimeError ( " Uživatel měl být přihlášený! " )
# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
resitel = Resitel . objects . filter ( osoba__user = self . request . user ) . first ( )
qs = super ( ) . get_queryset ( )
qs = qs . filter ( reseni__resitele__in = [ resitel ] )
# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení
qs = qs . order_by ( ' reseni__cas_doruceni ' )
return qs
def get_context_data ( self , * args , * * kwargs ) :
ctx = super ( ) . get_context_data ( * args , * * kwargs )
# Ročník určujeme podle čísla, do jehož deadlinu došlo řešení.
# Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/
podle_rocniku = [ ]
for rocnik , hodnoceni in groupby ( ctx [ ' object_list ' ] , lambda ho : ho . deadline_body . cislo . rocnik if ho . deadline_body is not None else None ) :
suma_bodu = 0
hodnoceni = list ( hodnoceni )
for i in hodnoceni :
if i . body != None : suma_bodu + = i . body
podle_rocniku . append ( ( rocnik , hodnoceni , suma_bodu ) )
ctx [ ' podle_rocniku ' ] = reversed ( podle_rocniku ) # Od nejnovějšího ročníku
# TODO: Umožnit stažení / zobrazení řešení
return ctx
# Přehled všech řešení kvůli debugování
class SeznamReseniView ( ListView ) :
model = Reseni
template_name = ' odevzdavatko/seznam.html '
class SeznamAktualnichReseniView ( SeznamReseniView ) :
def get_queryset ( self ) :
qs = super ( ) . get_queryset ( )
akt_rocnik = Nastaveni . get_solo ( ) . aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
resitele = resi_v_rocniku ( akt_rocnik )
qs = qs . filter ( resitele__in = resitele ) # FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
return qs
class VlozReseniView ( LoginRequiredMixin , FormView ) :
template_name = ' odevzdavatko/vloz_reseni.html '
form_class = f . PosliReseniForm
def form_valid ( self , form ) :
data = form . cleaned_data
nove_reseni = Reseni . objects . create (
cas_doruceni = data [ ' cas_doruceni ' ] ,
forma = data [ ' forma ' ] ,
poznamka = data [ ' poznamka ' ] ,
)
nove_reseni . resitele . add ( * data [ ' resitel ' ] )
nove_reseni . problem . add ( * data [ ' problem ' ] )
nove_reseni . save ( )
context = self . get_context_data ( )
prilohy = context [ ' prilohy ' ]
prilohy . instance = nove_reseni
prilohy . save ( )
# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil.
return redirect ( reverse ( ' profil ' ) )
def get_context_data ( self , * * kwargs ) :
data = super ( ) . get_context_data ( * * kwargs )
if self . request . POST :
data [ ' prilohy ' ] = f . ReseniSPrilohamiFormSet ( self . request . POST , self . request . FILES )
else :
data [ ' prilohy ' ] = f . ReseniSPrilohamiFormSet ( )
return data
class NahrajReseniRozcestnikTematekView ( LoginRequiredMixin , ListView ) :
model = Problem
template_name = ' odevzdavatko/nahraj_reseni_nadproblem.html '
def get_queryset ( self ) :
return super ( ) . get_queryset ( ) . filter ( stav = Problem . STAV_ZADANY , nadproblem__isnull = True )
class NahrajReseniView ( LoginRequiredMixin , CreateView ) :
model = Reseni
template_name = ' odevzdavatko/nahraj_reseni.html '
form_class = f . NahrajReseniForm
nadproblem : Problem
def setup ( self , request , * args , * * kwargs ) :
super ( ) . setup ( request , * args , * * kwargs )
nadproblem_id = self . kwargs [ " nadproblem_id " ]
self . nadproblem = get_object_or_404 ( Problem , id = nadproblem_id )
def get ( self , request , * args , * * kwargs ) :
# Zaříznutí nezadaných problémů
if self . nadproblem . stav != Problem . STAV_ZADANY :
raise PermissionDenied ( )
# Zaříznutí starých řešitelů:
# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
osoba = Osoba . objects . get ( user = self . request . user )
resitel = osoba . resitel
if resitel . rok_maturity < = Nastaveni . get_solo ( ) . aktualni_rocnik . prvni_rok :
return render ( request , ' universal.html ' , {
' title ' : ' Nelze odevzdat ' ,
' error ' : ' Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení. ' ,
' text ' : ' Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky. ' ,
} )
return super ( ) . get ( request , * args , * * kwargs )
def get_initial ( self ) :
nadproblem_id = self . nadproblem . id
return {
" nadproblem_id " : nadproblem_id ,
" problem " : [ ] if self . nadproblem . podproblem . filter ( stav = Problem . STAV_ZADANY ) . exists ( ) else nadproblem_id
}
def get_context_data ( self , * * kwargs ) :
data = super ( ) . get_context_data ( * * kwargs )
if self . request . POST :
data [ ' prilohy ' ] = f . ReseniSPrilohamiFormSet ( self . request . POST , self . request . FILES )
else :
data [ ' prilohy ' ] = f . ReseniSPrilohamiFormSet ( )
data [ " nadproblem_id " ] = self . nadproblem . id
data [ " nadproblem " ] = get_object_or_404 ( Problem , id = self . nadproblem . id )
return data
# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
def form_valid ( self , form ) :
context = self . get_context_data ( )
prilohy = context [ ' prilohy ' ]
if not prilohy . is_valid ( ) :
return super ( ) . form_invalid ( form )
with transaction . atomic ( ) :
self . object = form . save ( )
self . object . resitele . add ( Resitel . objects . get ( osoba__user = self . request . user ) )
self . object . resitele . add ( * form . cleaned_data [ " resitele " ] )
self . object . cas_doruceni = timezone . now ( )
self . object . forma = Reseni . FORMA_UPLOAD
self . object . save ( )
prilohy . instance = self . object
prilohy . save ( )
for hodnoceni in self . object . hodnoceni_set . all ( ) :
hodnoceni . deadline_body = Deadline . objects . filter ( deadline__gte = self . object . cas_doruceni ) . first ( )
hodnoceni . save ( )
# Pošleme mail opravovatelům a garantovi
# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí
prijemci = set ( )
problemy = [ ]
for prob in form . cleaned_data [ ' problem ' ] :
prijemci . update ( prob . opravovatele . all ( ) )
if prob . garant is not None :
prijemci . add ( prob . garant )
problemy . append ( prob )
# FIXME: Možná poslat mail i relevantním orgům nadproblémů?
if len ( prijemci ) < 1 :
logger . warning ( f " Pozor, neposílám e-mail nikomu. Problémy: { problemy } " )
# FIXME: Víc informativní obsah mailů, možná vč. příloh?
prijemci = map ( lambda it : it . osoba . email , prijemci )
resitel = Osoba . objects . get ( user = self . request . user )
seznam = " problému " + str ( problemy [ 0 ] ) if len ( problemy ) == 1 else ' následujícím problémům: \n ' + ' , \n ' . join ( map ( str , problemy ) )
seznam_do_subjectu = " problému " + str ( problemy [ 0 ] ) + ( " " if len ( problemy ) == 1 else f " (a dalším { len ( problemy ) - 1 } ) " )
EmailMessage (
subject = " Nové řešení k " + seznam_do_subjectu ,
body = f " { resitel } posílá nové řešení k { seznam } . \n \n Hurá do opravování: { self . object . absolute_url ( ) } " ,
from_email = " submitovatko@mam.mff.cuni.cz " , # FIXME: Chceme to mít radši tady, nebo v nastavení?
to = list ( prijemci ) ,
) . send ( )
return formularOKView (
self . request ,
text = ' Řešení úspěšně odevzdáno ' ,
dalsi_odkazy = [ ( " Odevzdat další řešení " , reverse ( " odevzdavatko_nahraj_reseni " ) ) ] ,
)