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.
240 lines
9.1 KiB
240 lines
9.1 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 dataclasses import dataclass
|
|
import datetime
|
|
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
|
|
|
|
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']
|
|
reseni_do = initial['reseni_do']
|
|
|
|
|
|
# Filtrujeme!
|
|
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci
|
|
if resitele == FiltrForm.RESITELE_RELEVANTNI:
|
|
logger.warning("Někdo chtěl v tabulce jen relevantní řešitele a měl smůlu :-(")
|
|
resitele = FiltrForm.RESITELE_LETOSNI # Fall-through
|
|
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 = []
|
|
for resitel in self.resitele:
|
|
resiteluv_radek = []
|
|
for problem in self.problemy:
|
|
if problem in tabulka and resitel in tabulka[problem]:
|
|
resiteluv_radek.append(tabulka[problem][resitel])
|
|
else:
|
|
resiteluv_radek.append(None)
|
|
hodnoty.append(resiteluv_radek)
|
|
ctx['radky'] = list(zip(self.resitele, 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)
|
|
# Kontext automaticky?
|
|
|
|
## 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
|
|
|