You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

301 lines
12 KiB

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)
4 years ago
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
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):
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))}
# Pro sitetree:
ctx["resitel_id"] = self.kwargs['resitel']
ctx["problem_id"] = self.kwargs['problem']
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ý!")
# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
resitel = m.Resitel.objects.filter(osoba__user=self.request.user).first()
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