Web M&M
https://mam.matfyz.cz
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.
300 lines
12 KiB
300 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)
|
|
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
|
|
|