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.
492 lines
16 KiB
492 lines
16 KiB
import abc
|
|
from functools import cached_property
|
|
from typing import Union, Iterable # TODO: s pythonem 3.10 přepsat na '|'
|
|
|
|
from odevzdavatko.models import Hodnoceni
|
|
from personalni.models import Resitel
|
|
from soustredeni.models import Konfera
|
|
from tvorba.models import Cislo, Rocnik, Deadline, Problem, Clanek
|
|
from django.db.models import Q, Sum
|
|
from tvorba.utils import resi_v_rocniku
|
|
|
|
ROCNIK_ZRUSENI_TEMAT = 25
|
|
|
|
|
|
class FixedIterator:
|
|
def next(self):
|
|
return self.niter.__next__()
|
|
|
|
def __init__(self, niter):
|
|
self.niter = niter
|
|
|
|
|
|
def body_resitelu(
|
|
za: Union[Cislo, Rocnik, None] = None,
|
|
do: Deadline = None,
|
|
od: Deadline = None,
|
|
jen_verejne: bool = True,
|
|
resitele: Iterable[Resitel] = None,
|
|
null=0 # Výchozí hodnota, pokud pro daného řešitele nejsou body
|
|
) -> dict[int, int]:
|
|
filtr = Q()
|
|
|
|
if jen_verejne:
|
|
filtr &= Q(reseni__hodnoceni__deadline_body__verejna_vysledkovka=True)
|
|
|
|
# Zjistíme, typ objektu v parametru "za"
|
|
if isinstance(za, Rocnik):
|
|
filtr &= Q(reseni__hodnoceni__deadline_body__cislo__rocnik=za)
|
|
elif isinstance(za, Cislo):
|
|
filtr &= Q(reseni__hodnoceni__deadline_body__cislo=za)
|
|
|
|
if do:
|
|
filtr &= Q(reseni__hodnoceni__deadline_body__deadline__lte=do.deadline)
|
|
|
|
if od:
|
|
filtr &= Q(reseni__hodnoceni__deadline_body__deadline__gte=od.deadline)
|
|
|
|
resiteleQuery = Resitel.objects.all()
|
|
|
|
if resitele is not None:
|
|
resitele_id = [r.id for r in resitele]
|
|
resiteleQuery = resiteleQuery.filter(id__in=resitele_id)
|
|
|
|
# Přidáme ke každému řešiteli údaj ".body" se součtem jejich bodů
|
|
resitele_s_body = resiteleQuery.annotate(
|
|
body=Sum('reseni__hodnoceni__body', filter=filtr))
|
|
|
|
# Teď jen z QuerySetu řešitelů anotovaných body vygenerujeme slovník
|
|
# indexovaný řešitelským id obsahující body.
|
|
# Pokud jsou body None, nahradíme za 0.
|
|
slovnik = {
|
|
int(res.id): (res.body if res.body else null) for res in resitele_s_body
|
|
}
|
|
return slovnik
|
|
|
|
|
|
class Vysledkovka(abc.ABC):
|
|
jen_verejne: bool
|
|
rocnik: Rocnik
|
|
do_deadlinu: Deadline
|
|
|
|
@property
|
|
@abc.abstractmethod
|
|
def aktivni_resitele(self) -> list[Resitel]:
|
|
...
|
|
|
|
@cached_property
|
|
def resitele_s_body_za_rocnik_setrizeny_seznam(self) -> list[tuple[int, int]]:
|
|
# spočítáme všem řešitelům jejich body za ročník
|
|
resitel_body_za_rocnik_slovnik = body_resitelu(
|
|
resitele=self.aktivni_resitele,
|
|
za=self.rocnik,
|
|
jen_verejne=self.jen_verejne,
|
|
do=self.do_deadlinu
|
|
)
|
|
|
|
# zeptáme se na dvojice (řešitel, body) za ročník a setřídíme sestupně
|
|
resitele_s_body_za_rocnik_setrizeny_seznam = sorted(
|
|
resitel_body_za_rocnik_slovnik.items(),
|
|
key=lambda x: x[1], reverse=True
|
|
)
|
|
|
|
return resitele_s_body_za_rocnik_setrizeny_seznam
|
|
|
|
@cached_property
|
|
def body_za_rocnik_seznamy(self) -> tuple[list[int], list[int]]:
|
|
if len(self.resitele_s_body_za_rocnik_setrizeny_seznam) == 0:
|
|
return [], []
|
|
return tuple(zip(*self.resitele_s_body_za_rocnik_setrizeny_seznam))
|
|
|
|
@property
|
|
def setrizeni_resitele_id(self) -> list[int]:
|
|
return self.body_za_rocnik_seznamy[0]
|
|
|
|
@property
|
|
def setrizene_body(self) -> list[int]:
|
|
return self.body_za_rocnik_seznamy[1]
|
|
|
|
@cached_property
|
|
def resitel_body_odjakziva_slovnik(self) -> dict[int, int]:
|
|
return body_resitelu(jen_verejne=self.jen_verejne, do=self.do_deadlinu)
|
|
|
|
@cached_property
|
|
def poradi(self):
|
|
# ze seznamu obsahujícího setřízené body spočítáme sloupec s pořadím
|
|
aktualni_poradi = 1
|
|
sloupec_s_poradim = []
|
|
|
|
# seskupíme seznam všech bodů podle hodnot
|
|
for index in range(0, len(self.setrizene_body)):
|
|
# pokud je pořadí větší než číslo řádku, tak jsme vypsali větší rozsah
|
|
# a chceme vypsat už jen prázdné místo, než dojdeme na správný řádek
|
|
if (index + 1) < aktualni_poradi:
|
|
sloupec_s_poradim.append("")
|
|
continue
|
|
velikost_skupiny = 0
|
|
# zjistíme počet po sobě jdoucích stejných hodnot
|
|
while self.setrizene_body[index] == self.setrizene_body[
|
|
index + velikost_skupiny]:
|
|
velikost_skupiny += 1
|
|
# na konci musíme ošetřit přetečení seznamu
|
|
if (index + velikost_skupiny) > len(self.setrizene_body) - 1:
|
|
break
|
|
# pokud je velikost skupiny 1, vypíšu pořadí
|
|
if velikost_skupiny == 1:
|
|
sloupec_s_poradim.append(f"{aktualni_poradi}.")
|
|
# pokud je skupina větší, vypíšu rozsah
|
|
else:
|
|
sloupec_s_poradim.append(
|
|
f"{aktualni_poradi}.–{aktualni_poradi + velikost_skupiny - 1}."
|
|
)
|
|
# zvětšíme aktuální pořadí o tolik, kolik pozic bylo přeskočeno
|
|
aktualni_poradi += velikost_skupiny
|
|
return sloupec_s_poradim
|
|
|
|
|
|
class VysledkovkaRocniku(Vysledkovka):
|
|
|
|
def __init__(self, rocnik: Rocnik, jen_verejne: bool = True):
|
|
self.rocnik = rocnik
|
|
self.jen_verejne = jen_verejne
|
|
deadliny = Deadline.objects.filter(cislo__rocnik=rocnik)
|
|
if jen_verejne:
|
|
deadliny = deadliny.filter(verejna_vysledkovka=True)
|
|
self.do_deadlinu = deadliny.order_by("deadline").last()
|
|
|
|
@cached_property
|
|
def aktivni_resitele(self) -> list[Resitel]:
|
|
return list(resi_v_rocniku(self.rocnik))
|
|
|
|
@cached_property
|
|
def cisla_rocniku(self) -> list[Cislo]:
|
|
""" Vrátí všechna čísla daného ročníku. """
|
|
if self.jen_verejne:
|
|
return self.rocnik.verejne_vysledkovky_cisla()
|
|
else:
|
|
return self.rocnik.cisla.all().order_by('poradi')
|
|
|
|
@cached_property
|
|
def body_za_cisla_slovnik(self) -> dict[int, dict[int, int]]: # Výstup: Cislo.id → ( Resitel.id → body )
|
|
# TODO: Body jsou decimal!
|
|
body_cisla_slovnik = dict()
|
|
for cislo in self.cisla_rocniku:
|
|
# získáme body za číslo
|
|
body_za_cislo = body_resitelu(
|
|
za=cislo,
|
|
resitele=self.aktivni_resitele,
|
|
jen_verejne=self.jen_verejne,
|
|
null=""
|
|
)
|
|
body_cisla_slovnik[cislo.id] = body_za_cislo
|
|
return body_cisla_slovnik
|
|
|
|
class RadekVysledkovkyRocniku:
|
|
# TODO: přepsat na dataclass
|
|
""" Obsahuje věci, které se hodí vědět při konstruování výsledkovky.
|
|
Umožňuje snazší práci v templatu (lepší, než seznam)."""
|
|
|
|
def __init__(self, poradi, resitel, body_cisla_seznam, body_rocnik, body_odjakziva, rok):
|
|
self.poradi = poradi
|
|
self.resitel = resitel
|
|
self.rocnik_resitele = resitel.rocnik(rok)
|
|
self.body_rocnik = body_rocnik
|
|
self.body_celkem_odjakziva = body_odjakziva
|
|
self.body_cisla_seznam = body_cisla_seznam
|
|
self.titul = resitel.get_titul(body_odjakziva)
|
|
|
|
@cached_property
|
|
def radky_vysledkovky(self) -> list[RadekVysledkovkyRocniku]:
|
|
radky_vysledkovky = []
|
|
|
|
setrizeni_resitele_dict = dict()
|
|
for r in Resitel.objects.filter(
|
|
id__in=self.setrizeni_resitele_id
|
|
).select_related('osoba'):
|
|
setrizeni_resitele_dict[r.id] = r
|
|
|
|
for i, ar_id in enumerate(self.setrizeni_resitele_id):
|
|
if self.setrizene_body[i] > 0:
|
|
# seznam počtu bodů daného řešitele pro jednotlivá čísla
|
|
body_cisla_seznam = []
|
|
for cislo in self.cisla_rocniku:
|
|
body_cisla_seznam.append(self.body_za_cisla_slovnik[cislo.id][ar_id])
|
|
|
|
# Pokud řešitel dostal nějaké body
|
|
if self.resitele_s_body_za_rocnik_setrizeny_seznam[i] != 0:
|
|
# vytáhneme informace pro daného řešitele
|
|
radek = self.RadekVysledkovkyRocniku(
|
|
poradi=self.poradi[i],
|
|
resitel=setrizeni_resitele_dict[ar_id],
|
|
body_cisla_seznam=body_cisla_seznam,
|
|
body_rocnik=self.setrizene_body[i],
|
|
body_odjakziva=self.resitel_body_odjakziva_slovnik[ar_id],
|
|
rok=self.rocnik) # ročník semináře pro získání ročníku řešitele
|
|
radky_vysledkovky.append(radek)
|
|
|
|
return radky_vysledkovky
|
|
|
|
|
|
class VysledkovkaCisla(Vysledkovka):
|
|
def __init__(
|
|
self,
|
|
cislo: Cislo,
|
|
jen_verejne: bool = True,
|
|
do_deadlinu: Deadline = None
|
|
):
|
|
self.cislo = cislo
|
|
self.rocnik = cislo.rocnik
|
|
self.jen_verejne = jen_verejne
|
|
if do_deadlinu is None:
|
|
do_deadlinu = Deadline.objects.filter(cislo=cislo).last()
|
|
self.do_deadlinu = do_deadlinu
|
|
|
|
@cached_property
|
|
def aktivni_resitele(self) -> list[Resitel]:
|
|
# TODO možná chytřeji vybírat aktivní řešitele
|
|
return list(resi_v_rocniku(self.rocnik))
|
|
|
|
@cached_property
|
|
def problemy(self) -> list[Problem]:
|
|
""" Vrátí seznam všech problémů s body v daném čísle. """
|
|
return Problem.objects.filter(
|
|
hodnoceni__in=Hodnoceni.objects.filter(deadline_body__cislo=self.cislo)
|
|
).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
|
|
|
@cached_property
|
|
def hlavni_problemy(self) -> list[Problem]:
|
|
""" Vrátí seznam všech problémů, které již nemají nadproblém. """
|
|
# hlavní problémy čísla
|
|
# (mají vlastní sloupeček ve výsledkovce, nemají nadproblém)
|
|
hlavni_problemy = set()
|
|
for p in self.problemy:
|
|
hlavni_problemy.add(p.hlavni_problem) # FIXME: proč tohle nemůže obsahovat reálné instance? Ve výsledkovce by se pak zobrazovaly správné kódy…
|
|
|
|
# zunikátnění
|
|
hlavni_problemy = list(hlavni_problemy)
|
|
hlavni_problemy.sort(
|
|
key=lambda k: k.kod_v_rocniku) # setřídit podle t1, t2, c3, ...
|
|
|
|
return hlavni_problemy
|
|
|
|
# Není cached, protože si myslím, že queryset lze použít ve for jen jednou.
|
|
@property
|
|
def hodnoceni_do_cisla(self):
|
|
hodnoceni = Hodnoceni.objects.prefetch_related('reseni__resitele').select_related('problem', 'reseni')
|
|
if self.jen_verejne:
|
|
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
|
return hodnoceni.filter(
|
|
deadline_body__cislo=self.cislo,
|
|
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
|
body__isnull=False,
|
|
)
|
|
|
|
@cached_property
|
|
def sectene_body(self):
|
|
"""
|
|
Sečte body za číslo, hlavní problémy a podproblémy.
|
|
|
|
Problém s ID '-1' znamená problémy bez nadproblémů, jež nejsou témata, tj. články, úlohy, konfery, …
|
|
"""
|
|
|
|
# Body za číslo
|
|
body_za_cislo = {ar.id: "" for ar in self.aktivni_resitele}
|
|
|
|
# Body za hlavní problémy
|
|
body_za_temata = {
|
|
hp.id: {ar.id: "" for ar in self.aktivni_resitele}
|
|
for hp in self.temata_a_spol
|
|
}
|
|
# Ostatní body
|
|
body_za_temata[-1] = {ar.id: "" for ar in self.aktivni_resitele}
|
|
|
|
# Body za podproblémy
|
|
body_za_problemy = {
|
|
tema.id: {
|
|
problem.id: {ar.id: "" for ar in self.aktivni_resitele}
|
|
for problem in self.podproblemy[tema.id]
|
|
}
|
|
for tema in self.temata_a_spol
|
|
}
|
|
# Ostatní body
|
|
body_za_problemy[-1] = {
|
|
problem.id: {ar.id: "" for ar in self.aktivni_resitele}
|
|
for problem in self.podproblemy[-1]
|
|
}
|
|
|
|
# Sečteme hodnocení
|
|
for hodnoceni in self.hodnoceni_do_cisla:
|
|
prob = hodnoceni.problem.get_real_instance()
|
|
nadproblem = prob.hlavni_problem.id
|
|
|
|
# Když nadproblém není "téma", pak je "Ostatní"
|
|
if nadproblem not in body_za_temata:
|
|
nadproblem = -1
|
|
|
|
problem_slovnik = body_za_problemy[nadproblem][prob.id]
|
|
nadproblem_slovnik = body_za_temata[nadproblem]
|
|
|
|
body = hodnoceni.body
|
|
|
|
# Může mít více řešitelů
|
|
for resitel in hodnoceni.reseni.resitele.all():
|
|
if resitel not in self.aktivni_resitele:
|
|
continue
|
|
self.pricti_body(body_za_cislo, resitel, body)
|
|
self.pricti_body(nadproblem_slovnik, resitel, body)
|
|
self.pricti_body(problem_slovnik, resitel, body)
|
|
return body_za_cislo, body_za_temata, body_za_problemy
|
|
|
|
@cached_property
|
|
def body_za_temata(self) -> dict[int, dict[int, str]]:
|
|
return self.sectene_body[1]
|
|
|
|
@cached_property
|
|
def body_za_cislo(self) -> dict[int, str]:
|
|
return self.sectene_body[0]
|
|
|
|
@cached_property
|
|
def problemy_slovnik(self):
|
|
return self.sectene_body[2]
|
|
|
|
@cached_property
|
|
def temata_a_spol(self) -> list[Problem]:
|
|
if self.rocnik.rocnik < ROCNIK_ZRUSENI_TEMAT:
|
|
return self.hlavni_problemy
|
|
else:
|
|
return list(filter(self.ne_clanek_ne_konfera, self.hlavni_problemy))
|
|
|
|
@cached_property
|
|
def je_nejake_ostatni(self):
|
|
return len(self.hlavni_problemy) - len(self.temata_a_spol) > 0
|
|
|
|
@cached_property
|
|
def podproblemy(self) -> dict[int, list[Problem]]:
|
|
podproblemy = {hp.id: [] for hp in self.temata_a_spol}
|
|
temata_a_spol = set(self.temata_a_spol)
|
|
podproblemy[-1] = []
|
|
|
|
for problem in self.problemy:
|
|
h_problem = problem.hlavni_problem
|
|
if h_problem in temata_a_spol:
|
|
podproblemy[h_problem.id].append(problem.get_real_instance())
|
|
else:
|
|
podproblemy[-1].append(problem.get_real_instance())
|
|
|
|
for podproblem in podproblemy.keys():
|
|
podproblemy[podproblem] = sorted(podproblemy[podproblem], key=lambda p: p.kod_v_rocniku)
|
|
return podproblemy
|
|
|
|
@cached_property
|
|
def podproblemy_seznam(self) -> list[list[Problem]]:
|
|
return [self.podproblemy[it.id] for it in self.temata_a_spol] + [self.podproblemy[-1]]
|
|
|
|
@cached_property
|
|
def podproblemy_iter(self) -> FixedIterator:
|
|
return FixedIterator(self.podproblemy_seznam.__iter__())
|
|
|
|
class RadekVysledkovkyCisla(object):
|
|
# TODO: Přepsat na dataclass
|
|
"""Obsahuje věci, které se hodí vědět při konstruování výsledkovky.
|
|
Umožňuje snazší práci v templatu (lepší, než seznam)."""
|
|
|
|
def __init__(self, poradi, resitel, temata_seznamk, body_cislo, body_rocnik, body_odjakziva, rok, body_podproblemy, body_podproblemy_iter):
|
|
self.resitel = resitel
|
|
self.rocnik_resitele = resitel.rocnik(rok)
|
|
self.body_cislo = body_cislo
|
|
self.body_rocnik = body_rocnik
|
|
self.body_celkem_odjakziva = body_odjakziva
|
|
self.poradi = poradi
|
|
self.body_za_temata_seznam = temata_seznamk
|
|
self.titul = resitel.get_titul(body_odjakziva)
|
|
self.body_podproblemy = body_podproblemy
|
|
self.body_podproblemy_iter = body_podproblemy_iter
|
|
|
|
@cached_property
|
|
def radky_vysledkovky(self) -> list[RadekVysledkovkyCisla]:
|
|
# vytvoříme jednotlivé sloupce výsledkovky
|
|
radky_vysledkovky = []
|
|
|
|
setrizeni_resitele_slovnik = {}
|
|
setrizeni_resitele = Resitel.objects.filter(id__in=self.setrizeni_resitele_id).select_related('osoba')
|
|
|
|
for r in setrizeni_resitele:
|
|
setrizeni_resitele_slovnik[r.id] = r
|
|
|
|
for i, ar_id in enumerate(self.setrizeni_resitele_id):
|
|
if self.setrizene_body[i] > 0:
|
|
# získáme seznam bodů za problémy pro daného řešitele
|
|
body_problemy = []
|
|
body_podproblemy = []
|
|
for hp in self.temata_a_spol:
|
|
body_problemy.append(self.body_za_temata[hp.id][ar_id])
|
|
body_podproblemy.append([
|
|
self.problemy_slovnik[hp.id][it.id][ar_id]
|
|
for it in self.podproblemy[hp.id]
|
|
])
|
|
if self.je_nejake_ostatni:
|
|
body_problemy.append(self.body_za_temata[-1][ar_id])
|
|
body_podproblemy.append(
|
|
[self.problemy_slovnik[-1][it.id][ar_id] for it in self.podproblemy[-1]])
|
|
# vytáhneme informace pro daného řešitele
|
|
radek = self.RadekVysledkovkyCisla(
|
|
poradi=self.poradi[i],
|
|
resitel=setrizeni_resitele_slovnik[ar_id],
|
|
temata_seznamk=body_problemy,
|
|
body_cislo=self.body_za_cislo[ar_id],
|
|
body_rocnik=self.setrizene_body[i],
|
|
body_odjakziva=self.resitel_body_odjakziva_slovnik[ar_id],
|
|
rok=self.rocnik,
|
|
body_podproblemy=body_podproblemy, # body všech podproblémů
|
|
body_podproblemy_iter=FixedIterator(body_podproblemy.__iter__())
|
|
) # ročník semináře pro zjištění ročníku řešitele
|
|
radky_vysledkovky.append(radek)
|
|
return radky_vysledkovky
|
|
|
|
@staticmethod
|
|
def pricti_body(slovnik, resitel, body):
|
|
""" Přiřazuje danému řešiteli body do slovníku. """
|
|
# testujeme na None (""), pokud je to první řešení
|
|
# daného řešitele, předěláme na 0
|
|
# (v dalším kroku přičteme reálný počet bodů),
|
|
# rozlišujeme tím mezi 0 a neodevzdaným řešením
|
|
|
|
if slovnik[resitel.id] == "":
|
|
slovnik[resitel.id] = 0
|
|
|
|
slovnik[resitel.id] += body
|
|
|
|
@staticmethod
|
|
def ne_clanek_ne_konfera(problem):
|
|
inst = problem.get_real_instance()
|
|
return not (isinstance(inst, Clanek) or isinstance(inst, Konfera))
|
|
|
|
|
|
class VysledkovkaDoTeXu(VysledkovkaCisla):
|
|
def __init__(
|
|
self,
|
|
nejake_cislo: Cislo,
|
|
od_vyjma: Deadline,
|
|
do_vcetne: Deadline
|
|
):
|
|
super().__init__(nejake_cislo, False, do_vcetne)
|
|
self.od_deadlinu = od_vyjma
|
|
|
|
@cached_property
|
|
def problemy(self) -> list[Problem]:
|
|
return Problem.objects.filter(hodnoceni__in=Hodnoceni.objects.filter(
|
|
deadline_body__deadline__gt=self.od_deadlinu.deadline,
|
|
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
|
)).distinct().non_polymorphic().select_related('nadproblem').select_related('nadproblem__nadproblem')
|
|
|
|
@property
|
|
def hodnoceni_do_cisla(self):
|
|
hodnoceni = Hodnoceni.objects.prefetch_related(
|
|
'problem', 'reseni', 'reseni__resitele')
|
|
if self.jen_verejne:
|
|
hodnoceni = hodnoceni.filter(deadline_body__verejna_vysledkovka=True)
|
|
return hodnoceni.filter(
|
|
deadline_body__deadline__gt=self.od_deadlinu.deadline,
|
|
deadline_body__deadline__lte=self.do_deadlinu.deadline,
|
|
body__isnull=False,
|
|
)
|
|
|