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 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
#
# 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 ( )
form = FiltrForm ( self . request . GET )
if form . is_valid ( ) :
fcd = form . cleaned_data
resitele = fcd [ " resitele " ]
problemy = fcd [ " problemy " ]
reseni_od = fcd [ " reseni_od " ]
reseni_do = fcd [ " reseni_do " ]
else :
initial = FiltrForm . gen_initial ( )
resitele = initial [ ' resitele ' ]
problemy = initial [ ' problemy ' ]
reseni_od = initial [ ' reseni_od ' ] [ 0 ]
reseni_do = initial [ ' reseni_do ' ] [ 0 ]
# Filtrujeme!
aktualni_rocnik = m . Nastaveni . get_solo ( ) . aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
self . chteni_resitele = resitele # Zapamatování pro get_context_data
if resitele == FiltrForm . RESITELE_RELEVANTNI :
# TODO: Zkontrolovat, že resi_v_rocniku vrací QuerySet (jinak asi bude žrát spoustu zdrojů zbytečně)
self . resitele = resi_v_rocniku ( aktualni_rocnik ) # Prvotní sada, pokud nebude mít body, odstraní se v get_context_data
elif resitele == FiltrForm . RESITELE_LETOSNI :
self . resitele = resi_v_rocniku ( aktualni_rocnik )
if problemy == FiltrForm . PROBLEMY_MOJE :
org = m . Organizator . objects . get ( osoba__user = self . request . user )
from django . db . models import Q
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() == 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 )
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 )
# Pro použití hacku na automatické {{form.media}} v template:
ctx [ ' form ' ] = ctx [ ' filtr ' ]
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 )
# 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