from django . views . generic import ListView , DetailView , FormView
from django . views . generic . list import MultipleObjectTemplateResponseMixin , MultipleObjectMixin
from django . views . generic . base import View
from django . views . generic . detail import SingleObjectMixin
from django . shortcuts import redirect , get_object_or_404
from django . urls import reverse
from django . db import transaction
from django . db . models import Q
from dataclasses import dataclass
import datetime
from itertools import groupby
import logging
import seminar . models as m
import seminar . forms as f
from seminar . forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from seminar . utils import aktivniResitele , resi_v_rocniku , deadline
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ů?
@dataclass
class SouhrnReseni :
""" Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce. """
pocet_reseni : int
posledni_odevzdani : datetime . datetime
body : float
class TabulkaOdevzdanychReseniView ( ListView ) :
template_name = ' seminar/odevzdavatko/tabulka.html '
model = m . 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 = m . Resitel . objects . all ( )
self . problemy = m . Problem . objects . all ( )
self . reseni = m . Reseni . objects . all ( )
self . aktualni_rocnik = m . Nastaveni . get_solo ( ) . aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
if ' rocnik ' in self . kwargs :
self . aktualni_rocnik = m . Rocnik . objects . get ( 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 " ]
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 " ]
# Chceme jen letošní problémy
# FIXME: Neexistuje metoda, jak dostat starší 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 = m . Organizator . objects . get ( osoba__user = self . request . user )
self . problemy = self . problemy . filter ( Q ( autor = org ) | Q ( garant = org ) | Q ( opravovatele = org ) , stav = m . Problem . STAV_ZADANY )
elif problemy == FiltrForm . PROBLEMY_LETOSNI :
self . problemy = self . problemy . filter ( stav = m . Problem . STAV_ZADANY )
#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 ( )
self . reseni = self . reseni . filter ( cas_doruceni__date__gte = reseni_od , cas_doruceni__date__lte = reseni_do )
if jen_neobodovane :
self . reseni = self . reseni . filter ( hodnoceni__body__isnull = True )
def get_queryset ( self ) :
self . inicializuj_osy_tabulky ( )
qs = super ( ) . get_queryset ( )
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 ' )
return qs
def get_context_data ( self , * args , * * kwargs ) :
# 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 ( )
def pridej_reseni ( problem , resitel , body , cas ) :
if problem not in tabulka :
tabulka [ problem ] = dict ( )
if resitel not in tabulka [ problem ] :
tabulka [ problem ] [ resitel ] = SouhrnReseni ( pocet_reseni = 1 , posledni_odevzdani = cas , body = body )
else :
tabulka [ problem ] [ resitel ] . posledni_odevzdani = max ( tabulka [ problem ] [ resitel ] . posledni_odevzdani , cas )
tabulka [ problem ] [ resitel ] . body = max ( tabulka [ problem ] [ resitel ] . body , body ,
key = lambda x : x if x is not None else - 1 # None je malé číslo
# FIXME: Možná dává smysl i mít None jako velké číslo -- jakože "TODO: zadat body"
)
tabulka [ problem ] [ resitel ] . pocet_reseni + = 1
# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele
tabulka [ problem ] [ resitel ] . problem_id = problem . id
tabulka [ problem ] [ resitel ] . resitel_id = resitel . id
for hodnoceni in self . get_queryset ( ) :
for resitel in hodnoceni . reseni . resitele . all ( ) :
pridej_reseni ( hodnoceni . problem , resitel , hodnoceni . body , hodnoceni . reseni . cas_doruceni )
hodnoty = [ ]
resitele_do_tabulky = [ ]
for resitel in self . resitele :
dostal_body = False
resiteluv_radek = [ ]
for problem in self . problemy :
if problem in tabulka and resitel in tabulka [ problem ] :
resiteluv_radek . append ( tabulka [ problem ] [ resitel ] )
dostal_body = True
else :
resiteluv_radek . append ( None )
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 ' ] = m . Nastaveni . get_solo ( ) . aktualni_rocnik
return ctx
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView ( MultipleObjectTemplateResponseMixin , MultipleObjectMixin , View ) :
model = m . Reseni
template_name = ' seminar/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 = m . Resitel . objects . get ( id = resitel_id )
problem = m . 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 : deadline ( r . cas_doruceni ) ) }
return ctx
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView ( DetailView ) :
model = m . Reseni
template_name = ' seminar/odevzdavatko/detail.html '
def aktualni_hodnoceni ( self ) :
reseni = get_object_or_404 ( m . Reseni , id = self . kwargs [ ' pk ' ] )
result = [ ] # Slovníky s klíči problem, body, cislo_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in m . Hodnoceni . objects . filter ( reseni = reseni ) :
result . append (
{ " problem " : hodn . problem ,
" body " : hodn . body ,
" cislo_body " : hodn . cislo_body ,
} )
return result
def get_context_data ( self , * * kw ) :
ctx = super ( ) . get_context_data ( * * kw )
ctx [ ' form ' ] = f . OhodnoceniReseniFormSet (
initial = self . aktualni_hodnoceni ( )
)
return ctx
def hodnoceniReseniView ( request , pk , * args , * * kwargs ) :
reseni = get_object_or_404 ( m . Reseni , pk = pk )
template_name = ' seminar/odevzdavatko/detail.html '
form_class = f . OhodnoceniReseniFormSet
success_url = reverse ( ' odevzdavatko_detail_reseni ' , kwargs = { ' pk ' : pk } )
# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
# Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm
formset = f . OhodnoceniReseniFormSet ( request . POST )
# TODO: Napsat validaci formuláře a formsetu
# TODO: Implementovat větev, kdy formulář validní není.
if formset . is_valid ( ) :
with transaction . atomic ( ) :
# Smažeme všechna dosavadní hodnocení tohoto řešení
qs = m . Hodnoceni . objects . filter ( reseni = reseni )
logger . info ( f " Will delete { qs . count ( ) } objects: { qs } " )
qs . delete ( )
# Vyrobíme nová podle formsetu
for form in formset :
problem = form . cleaned_data [ ' problem ' ]
body = form . cleaned_data [ ' body ' ]
cislo_body = form . cleaned_data [ ' cislo_body ' ]
hodnoceni = m . Hodnoceni (
problem = problem ,
body = body ,
cislo_body = cislo_body ,
reseni = reseni ,
)
logger . info ( f " Creating Hodnoceni: { hodnoceni } " )
hodnoceni . save ( )
return redirect ( success_url )
class PrehledOdevzdanychReseni ( ListView ) :
model = m . Hodnoceni
template_name = ' seminar/odevzdavatko/resitel_prehled.html '
def get_queryset ( self ) :
if not self . request . user . is_authenticated :
raise RuntimeError ( " Uživatel měl být přihlášený! " )
resitel = m . Resitel . objects . get ( osoba__user = self . request . user )
qs = super ( ) . get_queryset ( )
qs = qs . filter ( reseni__resitele__in = [ resitel ] )
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 : deadline ( ho . reseni . cas_doruceni ) [ 1 ] . rocnik if deadline ( ho . reseni . cas_doruceni ) is not None else None ) :
podle_rocniku . append ( ( rocnik , list ( hodnoceni ) ) )
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 = m . Reseni
template_name = ' seminar/odevzdavatko/seznam.html '
class SeznamAktualnichReseniView ( SeznamReseniView ) :
def get_queryset ( self ) :
qs = super ( ) . get_queryset ( )
akt_rocnik = m . 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