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: resitele = FiltrForm.get_initial_for_field(FiltrForm.resitele, "resitele") problemy = FiltrForm.get_initial_for_field(FiltrForm.problemy, "problemy") resitele_od = FiltrForm.get_initial_for_field(FiltrForm.resitele_od, "resitele_od") resitele_do = FiltrForm.get_initial_for_field(FiltrForm.resitele_do, "resitele_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