From 1f0460e3c91640e2eff4bd1525422fa875cc7104 Mon Sep 17 00:00:00 2001 From: Tomas 'Jethro' Pokorny Date: Tue, 23 Feb 2021 22:18:00 +0100 Subject: [PATCH] vysledkovka | funkce pro vysledkovku presunuty do samostatneho souboru. --- seminar/utils.py | 84 ++++++ seminar/views/views_all.py | 518 +---------------------------------- seminar/views/vysledkovka.py | 437 +++++++++++++++++++++++++++++ 3 files changed, 523 insertions(+), 516 deletions(-) create mode 100644 seminar/views/vysledkovka.py diff --git a/seminar/utils.py b/seminar/utils.py index 39abeae7..47d5027c 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -219,3 +219,87 @@ def viewMethodSwitch(get, post): return NewView.as_view() +def cisla_rocniku(rocnik, jen_verejne=True): + """ + Vrátí všechna čísla daného ročníku. + Parametry: + rocnik (Rocnik): ročník semináře + jen_verejne (bool): zda se mají vrátit jen veřejná, nebo všechna čísla + Vrátí: + seznam objektů typu Cislo + """ + if jen_verejne: + return rocnik.verejna_cisla() + else: + return rocnik.cisla.all() + +def hlavni_problem(problem): + """ Pro daný problém vrátí jeho nejvyšší nadproblém.""" + while not(problem.nadproblem == None): + problem = problem.nadproblem + return problem + +def hlavni_problemy_rocniku(rocnik, jen_verejne=True): + """ Pro zadaný ročník vrátí hlavní problémy ročníku, + tj. ty, které už nemají nadproblém.""" + hlavni_problemy = [] + for cislo in cisla_rocniku(rocnik, jen_verejne): + for problem in hlavni_problemy_cisla(cislo): + hlavni_problemy.append(problem) + hlavni_problemy_set = set(hlavni_problemy) + hlavni_problemy = list(hlavni_problemy_set) + hlavni_problemy.sort(key=lambda k:k.kod_v_rocniku()) # setřídit podle pořadí + + return hlavni_problemy + +def problemy_cisla(cislo): + """ Vrátí seznam všech problémů s body v daném čísle. """ + hodnoceni = cislo.hodnoceni.select_related('problem', 'reseni').all() + # hodnocení, která se vážou k danému číslu + + reseni = [h.reseni for h in hodnoceni] + problemy = [h.problem for h in hodnoceni] + problemy_set = set(problemy) # chceme každý problém unikátně, + problemy = (list(problemy_set)) # převedení na množinu a zpět to zaručí + + return problemy + + +def hlavni_problemy_cisla(cislo, problemy=None): + """ Vrátí seznam všech problémů s body v daném čísle, které již nemají nadproblém. """ + if problemy is None: + problemy = problemy_cisla(cislo) + + # hlavní problémy čísla + # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) + hlavni_problemy = [] + for p in problemy: + hlavni_problemy.append(hlavni_problem(p)) + + # zunikátnění + hlavni_problemy_set = set(hlavni_problemy) + hlavni_problemy = list(hlavni_problemy_set) + hlavni_problemy.sort(key=lambda k: k.kod_v_rocniku()) # setřídit podle t1, t2, c3, ... + + return hlavni_problemy + + +def podproblemy_v_cislu(cislo, problemy=None, hlavni_problemy=None): + """ Vrátí seznam všech problémů s body v daném čísle v poli 'indexovaném' tématy. """ + if problemy is None: + problemy = problemy_cisla(cislo) + if hlavni_problemy is None: + hlavni_problemy = hlavni_problemy_cisla(cislo, problemy) + + podproblemy = dict((hp.id, []) for hp in hlavni_problemy) + hlavni_problemy = set(hlavni_problemy) + podproblemy[-1] = [] + + for problem in problemy: + h_problem = hlavni_problem(problem) + if h_problem in hlavni_problemy: + podproblemy[h_problem.id].append(problem) + else: + podproblemy[-1].append(problem) + + return podproblemy diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index b3e302fa..d4a875fd 100644 --- a/seminar/views/views_all.py +++ b/seminar/views/views_all.py @@ -29,6 +29,7 @@ from seminar.forms import PrihlaskaForm, LoginForm, ProfileEditForm import seminar.forms as f import seminar.templatetags.treenodes as tnltt import seminar.views.views_rest as vr +from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla from datetime import timedelta, date, datetime, MAXYEAR from django.utils import timezone @@ -48,7 +49,7 @@ import csv import logging import time -from seminar.utils import aktivniResitele, resi_v_rocniku +from seminar.utils import aktivniResitele, resi_v_rocniku, hlavni_problemy_rocniku, cisla_rocniku, hlavni_problemy_cisla # ze starého modelu #def verejna_temata(rocnik): @@ -631,267 +632,10 @@ class ArchivView(generic.ListView): return context -### Výsledky - -def sloupec_s_poradim(setrizene_body): - """ - Ze seznamu obsahujícího sestupně setřízené body řešitelů za daný ročník - vytvoří seznam s pořadími (včetně 3.-5. a pak 2 volná místa atp.), - podle toho, jak jdou za sebou ve výsledkovce. - Parametr: - setrizene_body (seznam integerů): sestupně setřízená čísla - - Výstup: - sloupec_s_poradim (seznam stringů) - """ - - # 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(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 setrizene_body[index] == setrizene_body[index + velikost_skupiny]: - velikost_skupiny = velikost_skupiny + 1 - # na konci musíme ošetřit přetečení seznamu - if (index + velikost_skupiny) > len(setrizene_body) - 1: - break - # pokud je velikost skupiny 1, vypíšu pořadí - if velikost_skupiny == 1: - sloupec_s_poradim.append("{}.".format(aktualni_poradi)) - # pokud je skupina větší, vypíšu rozsah - else: - sloupec_s_poradim.append("{}.–{}.".format(aktualni_poradi, - aktualni_poradi+velikost_skupiny-1)) - # zvětšíme aktuální pořadí o tolik, kolik pozic bylo přeskočeno - aktualni_poradi = aktualni_poradi + velikost_skupiny - return sloupec_s_poradim - -def cisla_rocniku(rocnik, jen_verejne=True): - """ - Vrátí všechna čísla daného ročníku. - Parametry: - rocnik (Rocnik): ročník semináře - jen_verejne (bool): zda se mají vrátit jen veřejná, nebo všechna čísla - Vrátí: - seznam objektů typu Cislo - """ - if jen_verejne: - return rocnik.verejna_cisla() - else: - return rocnik.cisla.all() - -def hlavni_problem(problem): - """ Pro daný problém vrátí jeho nejvyšší nadproblém.""" - while not(problem.nadproblem == None): - problem = problem.nadproblem - return problem - -def hlavni_problemy_rocniku(rocnik, jen_verejne=True): - """ Pro zadaný ročník vrátí hlavní problémy ročníku, - tj. ty, které už nemají nadproblém.""" - hlavni_problemy = [] - for cislo in cisla_rocniku(rocnik, jen_verejne): - for problem in hlavni_problemy_cisla(cislo): - hlavni_problemy.append(problem) - hlavni_problemy_set = set(hlavni_problemy) - hlavni_problemy = list(hlavni_problemy_set) - hlavni_problemy.sort(key=lambda k:k.kod_v_rocniku()) # setřídit podle pořadí - - return hlavni_problemy - - -def problemy_cisla(cislo): - """ Vrátí seznam všech problémů s body v daném čísle. """ - hodnoceni = cislo.hodnoceni.select_related('problem', 'reseni').all() - # hodnocení, která se vážou k danému číslu - reseni = [h.reseni for h in hodnoceni] - problemy = [h.problem for h in hodnoceni] - problemy_set = set(problemy) # chceme každý problém unikátně, - problemy = (list(problemy_set)) # převedení na množinu a zpět to zaručí - return problemy -def hlavni_problemy_cisla(cislo, problemy=None): - """ Vrátí seznam všech problémů s body v daném čísle, které již nemají nadproblém. """ - if problemy is None: - problemy = problemy_cisla(cislo) - - # hlavní problémy čísla - # (mají vlastní sloupeček ve výsledkovce, nemají nadproblém) - hlavni_problemy = [] - for p in problemy: - hlavni_problemy.append(hlavni_problem(p)) - - # zunikátnění - hlavni_problemy_set = set(hlavni_problemy) - hlavni_problemy = list(hlavni_problemy_set) - hlavni_problemy.sort(key=lambda k: k.kod_v_rocniku()) # setřídit podle t1, t2, c3, ... - - return hlavni_problemy - - -def podproblemy_v_cislu(cislo, problemy=None, hlavni_problemy=None): - """ Vrátí seznam všech problémů s body v daném čísle v poli 'indexovaném' tématy. """ - if problemy is None: - problemy = problemy_cisla(cislo) - if hlavni_problemy is None: - hlavni_problemy = hlavni_problemy_cisla(cislo, problemy) - - podproblemy = dict((hp.id, []) for hp in hlavni_problemy) - hlavni_problemy = set(hlavni_problemy) - podproblemy[-1] = [] - - for problem in problemy: - h_problem = hlavni_problem(problem) - if h_problem in hlavni_problemy: - podproblemy[h_problem.id].append(problem) - else: - podproblemy[-1].append(problem) - - return podproblemy - -def body_resitelu(resitele, za, odjakziva=True): - """ Funkce počítající počty bodů pro zadané řešitele, - buď odjakživa do daného ročníku/čísla anebo za daný ročník/číslo. - Parametry: - resitele (seznam obsahující položky typu Resitel): aktivní řešitelé - za (Rocnik/Cislo): za co se mají počítat body - (generování starších výsledkovek) - odjakziva (bool): zda se mají počítat body odjakživa, nebo jen za číslo/ročník - zadané v "za" - Výstup: - slovník (Resitel.id):body - """ - resitele_id = [r.id for r in resitele] - # Zjistíme, typ objektu v parametru "za" - if isinstance(za, Rocnik): - cislo = None - rocnik = za - rok = rocnik.prvni_rok - elif isinstance(za, Cislo): - cislo = za - rocnik = None - rok = cislo.rocnik.prvni_rok - else: - assert True, "body_resitelu: za není ani číslo ani ročník." - - - # Kvůli rychlosti používáme sčítáme body už v databázi, viz - # https://docs.djangoproject.com/en/3.0/topics/db/aggregation/, - # sekce Filtering on annotations (protože potřebujeme filtrovat výsledky - # jen do nějakého data, abychom uměli správně nagenerovat výsledkovky i - # za historická čísla. - - # Níže se vytváří dotaz na součet bodů za správně vyfiltrovaná hodnocení, - # který se použije ve výsledném dotazu. - if cislo and odjakziva: # Body se sčítají odjakživa do zadaného čísla. - # Vyfiltrujeme všechna hodnocení, která jsou buď ze starších ročníků, - # anebo ze stejného ročníku, jak je zadané číslo, tam ale sčítáme jen - # pro čísla s pořadím nejvýše stejným, jako má zadané číslo. - body_k_zapocteni = Sum('reseni__hodnoceni__body', - filter=( Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok__lt=rok) | - Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok=rok, - reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi) )) - elif cislo and not odjakziva: # Body se sčítají za dané číslo. - body_k_zapocteni = Sum('reseni__hodnoceni__body', - filter=( Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok=rok, - reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi) )) - elif rocnik and odjakziva: # Spočítáme body za starší ročníky až do zadaného včetně. - body_k_zapocteni = Sum('reseni__hodnoceni__body', - filter= Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok__lte=rok)) - elif rocnik and not odjakziva: # Spočítáme body za daný ročník. - body_k_zapocteni = Sum('reseni__hodnoceni__body', - filter= Q(reseni__hodnoceni__cislo_body__rocnik=rocnik)) - else: - assert True, "body_resitelu: Neplatná kombinace za a odjakživa." - - # Následující řádek přidá ke každému řešiteli údaj ".body" se součtem jejich bodů - resitele_s_body = Resitel.objects.filter(id__in=resitele_id).annotate( - body=body_k_zapocteni) - # 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 0) for res in resitele_s_body} - return slovnik - -class RadekVysledkovkyRocniku(object): - """ 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_sezn, 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_sezn = body_cisla_sezn - self.titul = resitel.get_titul(body_odjakziva) - -def setrid_resitele_a_body(slov_resitel_body): - setrizeni_resitele_id = [dvojice[0] for dvojice in slov_resitel_body] - setrizeni_resitele = [Resitel.objects.get(id=i) for i in setrizeni_resitele_id] - setrizene_body = [dvojice[1] for dvojice in slov_resitel_body] - return setrizeni_resitele_id, setrizeni_resitele, setrizene_body - -def vysledkovka_rocniku(rocnik, jen_verejne=True): - """ Přebírá ročník (např. context["rocnik"]) a vrací výsledkovou listinu ve - formě vhodné pro šablonu "seminar/vysledkovka_rocniku.html" - """ - - ## TODO možná chytřeji vybírat aktivní řešitele - # aktivní řešitelé - chceme letos něco poslal, TODO později vyfiltrujeme ty, kdo mají - # u alespoň jedné hodnoty něco jiného než NULL - aktivni_resitele = list(resi_v_rocniku(rocnik)) - cisla = cisla_rocniku(rocnik, jen_verejne) - body_cisla_slov = {} - for cislo in cisla: - # získáme body za číslo - _, cislobody = secti_body_za_cislo(cislo, aktivni_resitele) - body_cisla_slov[cislo.id] = cislobody - - # získáme body za ročník, seznam obsahuje dvojice (řešitel_id, body) setřízené sestupně - resitel_rocnikbody_sezn = secti_body_za_rocnik(rocnik, aktivni_resitele) - - # setřídíme řešitele podle počtu bodů a získáme seznam s body od nejvyšších po nenižší - setrizeni_resitele_id, setrizeni_resitele, setrizene_body = setrid_resitele_a_body(resitel_rocnikbody_sezn) - poradi = sloupec_s_poradim(setrizene_body) - - # získáme body odjakživa - resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, rocnik) - - # vytvoříme jednotlivé sloupce výsledkovky - radky_vysledkovky = [] - i = 0 - for ar_id in setrizeni_resitele_id: - # seznam počtu bodů daného řešitele pro jednotlivá čísla - body_cisla_sezn = [] - for cislo in cisla: - body_cisla_sezn.append(body_cisla_slov[cislo.id][ar_id]) - - # vytáhneme informace pro daného řešitele - radek = RadekVysledkovkyRocniku( - poradi[i], # pořadí - Resitel.objects.get(id=ar_id), # řešitel (z id) - body_cisla_sezn, # seznam bodů za čísla - setrizene_body[i], # body za ročník (spočítané výše s pořadím) - resitel_odjakzivabody_slov[ar_id], # body odjakživa - rocnik) # ročník semináře pro získání ročníku řešitele - radky_vysledkovky.append(radek) - i += 1 - - return radky_vysledkovky - - class RocnikView(generic.DetailView): model = Rocnik template_name = 'seminar/archiv/rocnik.html' @@ -952,264 +696,6 @@ class ProblemView(generic.DetailView): return context -class RadekVysledkovkyCisla(object): - """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_problemy_sezn, - 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_problemy_sezn = body_problemy_sezn - self.titul = resitel.get_titul(body_odjakziva) - self.body_podproblemy = body_podproblemy - self.body_podproblemy_iter = body_podproblemy_iter # TODELETE - - -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 - -def secti_body_za_rocnik(za, aktivni_resitele): - """ Spočítá body za ročník (celý nebo do daného čísla), - setřídí je sestupně a vrátí jako seznam. - Parametry: - za (typu Rocnik nebo Cislo) spočítá za ročník, nebo za ročník až do - daného čísla - """ - # spočítáme všem řešitelům jejich body za ročník (False => ne odjakživa) - resitel_rocnikbody_slov = body_resitelu(aktivni_resitele, za, False) - # zeptáme se na dvojice (řešitel, body) za ročník a setřídíme sestupně - resitel_rocnikbody_sezn = sorted(resitel_rocnikbody_slov.items(), - key = lambda x: x[1], reverse = True) - return resitel_rocnikbody_sezn - -def secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy=None): - """ Spočítá u řešitelů body za číslo a za jednotlivé hlavní problémy (témata).""" - # TODO setřídit hlavní problémy čísla podle id, ať jsou ve stejném pořadí pokaždé - # pro každý hlavní problém zavedeme slovník s body za daný hlavní problém - # pro jednotlivé řešitele (slovník slovníků hlavních problémů) - if hlavni_problemy is None: - hlavni_problemy = hlavni_problemy_cisla(cislo) - - def ne_clanek_ne_konfera(problem): - return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera)) - - temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy)) - - def cosi(problem): - return problem.id - - hlavni_problemy_slovnik = {} - for hp in temata_a_spol: - hlavni_problemy_slovnik[hp.id] = {} - - hlavni_problemy_slovnik[-1] = {} - - # zakládání prázdných záznamů pro řešitele - cislobody = {} - for ar in aktivni_resitele: - # řešitele převedeme na řetězec pomocí unikátního id - cislobody[ar.id] = "" - for hp in temata_a_spol: - slovnik = hlavni_problemy_slovnik[hp.id] - slovnik[ar.id] = "" - - hlavni_problemy_slovnik[-1][ar.id] = "" - - # vezmeme všechna řešení s body do daného čísla - reseni_do_cisla = Reseni.objects.prefetch_related('problem', 'resitele', - 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) - - # projdeme všechna řešení do čísla a přičteme body každému řešiteli do celkových - # bodů i do bodů za problém - for reseni in reseni_do_cisla: - - # řešení může řešit více problémů - for prob in list(reseni.problem.all()): - nadproblem = hlavni_problem(prob) - if ne_clanek_ne_konfera(nadproblem): - nadproblem_slovnik = hlavni_problemy_slovnik[nadproblem.id] - else: - nadproblem_slovnik = hlavni_problemy_slovnik[-1] - - # a mít více hodnocení - for hodn in list(reseni.hodnoceni_set.all()): - body = hodn.body - - # a mít více řešitelů - for resitel in list(reseni.resitele.all()): - if resitel not in aktivni_resitele: - print("Skipping {}".format(resitel.id)) - continue - pricti_body(cislobody, resitel, body) - pricti_body(nadproblem_slovnik, resitel, body) - return hlavni_problemy_slovnik, cislobody - - -def secti_body_za_cislo_podle_temat(cislo, aktivni_resitele, podproblemy=None, temata=None): - """ Spočítá u řešitelů body za číslo za úlohy v jednotlivých hlavních problémech (témata).""" - if temata is None: - temata = hlavni_problemy_cisla(cislo) - - if podproblemy is None: - podproblemy_v_cislu(cislo, hlavni_problemy=temata) - - body_slovnik = {} - for tema in temata: - body_slovnik[tema.id] = {} - for problem in podproblemy[tema.id]: - body_slovnik[tema.id][problem.id] = {} - body_slovnik[-1] = {} - for problem in podproblemy[-1]: - body_slovnik[-1][problem.id] = {} - - # zakládání prázdných záznamů pro řešitele - for ar in aktivni_resitele: - for tema in temata: - for problem in podproblemy[tema.id]: - body_slovnik[tema.id][problem.id][ar.id] = "" - - for problem in podproblemy[-1]: - body_slovnik[-1][problem.id][ar.id] = "" - - temata = set(t.id for t in temata) - - # vezmeme všechna řešení s body do daného čísla - reseni_do_cisla = Reseni.objects.prefetch_related('problem', 'resitele', - 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) - - # projdeme všechna řešení do čísla a přičteme body každému řešiteli do celkových - # bodů i do bodů za problém - for reseni in reseni_do_cisla: - - # řešení může řešit více problémů - for prob in list(reseni.problem.all()): - nadproblem = hlavni_problem(prob) - if nadproblem.id in temata: - nadproblem_slovnik = body_slovnik[nadproblem.id] - else: - nadproblem_slovnik = body_slovnik[-1] - - problem_slovnik = nadproblem_slovnik[prob.id] - - # a mít více hodnocení - for hodn in list(reseni.hodnoceni_set.all()): - body = hodn.body - - # a mít více řešitelů - for resitel in list(reseni.resitele.all()): - if resitel not in aktivni_resitele: - print("Skipping {}".format(resitel.id)) - continue - pricti_body(problem_slovnik, resitel, body) - return body_slovnik - - -# TODELETE -class FixedIterator: - def next(self): - return self.niter.__next__() - - def __init__(self, niter): - self.niter = niter -# TODELETE - - -def vysledkovka_cisla(cislo, context=None): - if context is None: - context = {} - problemy = problemy_cisla(cislo) - hlavni_problemy = hlavni_problemy_cisla(cislo, problemy) - ## TODO možná chytřeji vybírat aktivní řešitele - # aktivní řešitelé - chceme letos něco poslal, TODO později vyfiltrujeme ty, kdo mají - # u alespoň jedné hodnoty něco jiného než NULL - aktivni_resitele = list(aktivniResitele(cislo)) - - # získáme body za číslo - hlavni_problemy_slovnik, cislobody = secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy) - - # získáme body za ročník, seznam obsahuje dvojice (řešitel_id, body) setřízené sestupně - resitel_rocnikbody_sezn = secti_body_za_rocnik(cislo, aktivni_resitele) - - # získáme body odjakživa - resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, cislo) - - # řešitelé setřídění podle bodů za číslo sestupně - setrizeni_resitele_id = [dvojice[0] for dvojice in resitel_rocnikbody_sezn] - setrizeni_resitele = [Resitel.objects.get(id=i) for i in setrizeni_resitele_id] - - # spočítáme pořadí řešitelů - setrizeni_resitele_body = [dvojice[1] for dvojice in resitel_rocnikbody_sezn] - poradi = sloupec_s_poradim(setrizeni_resitele_body) - - # vytvoříme jednotlivé sloupce výsledkovky - radky_vysledkovky = [] - i = 0 - - def ne_clanek_ne_konfera(problem): - return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera)) - - temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy)) - - # získáme body u jednotlivých témat - podproblemy = podproblemy_v_cislu(cislo, problemy, temata_a_spol) - problemy_slovnik = secti_body_za_cislo_podle_temat(cislo, aktivni_resitele, podproblemy, temata_a_spol) - - # def not_empty(value): - # return value != '' - # - # je_nejake_ostatni = any(filter(not_empty, hlavni_problemy_slovnik[-1].values())) > 0 - - je_nejake_ostatni = len(hlavni_problemy) - len(temata_a_spol) > 0 - - for ar_id in setrizeni_resitele_id: - # získáme seznam bodů za problémy pro daného řešitele - body_problemy = [] - body_podproblemy = [] - for hp in temata_a_spol: - body_problemy.append(hlavni_problemy_slovnik[hp.id][ar_id]) - body_podproblemy.append([problemy_slovnik[hp.id][it.id][ar_id] for it in podproblemy[hp.id]]) - if je_nejake_ostatni: - body_problemy.append(hlavni_problemy_slovnik[-1][ar_id]) - body_podproblemy.append([problemy_slovnik[-1][it.id][ar_id] for it in podproblemy[-1]]) - # vytáhneme informace pro daného řešitele - radek = RadekVysledkovkyCisla( - poradi[i], # pořadí - Resitel.objects.get(id=ar_id), # řešitel (z id) - body_problemy, # seznam bodů za hlavní problémy čísla - cislobody[ar_id], # body za číslo - setrizeni_resitele_body[i], # body za ročník (spočítané výše s pořadím) - resitel_odjakzivabody_slov[ar_id], # body odjakživa - cislo.rocnik, - body_podproblemy, # body všech podproblémů - FixedIterator(body_podproblemy.__iter__()) # TODELETE - ) # ročník semináře pro zjištění ročníku řešitele - radky_vysledkovky.append(radek) - i += 1 - - # vytahané informace předáváme do kontextu - context['cislo'] = cislo - context['radky_vysledkovky'] = radky_vysledkovky - context['problemy'] = temata_a_spol - context['ostatni'] = je_nejake_ostatni - pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] - context['podproblemy'] = pt - context['podproblemy_iter'] = FixedIterator(pt.__iter__()) # TODELETE - #context['v_cisle_zadane'] = TODO - #context['resene_problemy'] = resene_problemy - return context class CisloView(generic.DetailView): # FIXME zobrazování témátek a vůbec, teď je tam jen odkaz na číslo v pdf diff --git a/seminar/views/vysledkovka.py b/seminar/views/vysledkovka.py new file mode 100644 index 00000000..dd8956b9 --- /dev/null +++ b/seminar/views/vysledkovka.py @@ -0,0 +1,437 @@ +import seminar.models as m +from django.db.models import Q, Sum, Count +from seminar.utils import aktivniResitele, resi_v_rocniku, cisla_rocniku, hlavni_problemy_rocniku, hlavni_problem, hlavni_problemy_cisla, problemy_cisla, podproblemy_v_cislu +### Výsledky + +def sloupec_s_poradim(setrizene_body): + """ + Ze seznamu obsahujícího sestupně setřízené body řešitelů za daný ročník + vytvoří seznam s pořadími (včetně 3.-5. a pak 2 volná místa atp.), + podle toho, jak jdou za sebou ve výsledkovce. + Parametr: + setrizene_body (seznam integerů): sestupně setřízená čísla + + Výstup: + sloupec_s_poradim (seznam stringů) + """ + + # 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(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 setrizene_body[index] == setrizene_body[index + velikost_skupiny]: + velikost_skupiny = velikost_skupiny + 1 + # na konci musíme ošetřit přetečení seznamu + if (index + velikost_skupiny) > len(setrizene_body) - 1: + break + # pokud je velikost skupiny 1, vypíšu pořadí + if velikost_skupiny == 1: + sloupec_s_poradim.append("{}.".format(aktualni_poradi)) + # pokud je skupina větší, vypíšu rozsah + else: + sloupec_s_poradim.append("{}.–{}.".format(aktualni_poradi, + aktualni_poradi+velikost_skupiny-1)) + # zvětšíme aktuální pořadí o tolik, kolik pozic bylo přeskočeno + aktualni_poradi = aktualni_poradi + velikost_skupiny + return sloupec_s_poradim + + + + +def body_resitelu(resitele, za, odjakziva=True): + """ Funkce počítající počty bodů pro zadané řešitele, + buď odjakživa do daného ročníku/čísla anebo za daný ročník/číslo. + Parametry: + resitele (seznam obsahující položky typu Resitel): aktivní řešitelé + za (Rocnik/Cislo): za co se mají počítat body + (generování starších výsledkovek) + odjakziva (bool): zda se mají počítat body odjakživa, nebo jen za číslo/ročník + zadané v "za" + Výstup: + slovník (Resitel.id):body + """ + resitele_id = [r.id for r in resitele] + # Zjistíme, typ objektu v parametru "za" + if isinstance(za, m.Rocnik): + cislo = None + rocnik = za + rok = rocnik.prvni_rok + elif isinstance(za, m.Cislo): + cislo = za + rocnik = None + rok = cislo.rocnik.prvni_rok + else: + assert True, "body_resitelu: za není ani číslo ani ročník." + + + # Kvůli rychlosti používáme sčítáme body už v databázi, viz + # https://docs.djangoproject.com/en/3.0/topics/db/aggregation/, + # sekce Filtering on annotations (protože potřebujeme filtrovat výsledky + # jen do nějakého data, abychom uměli správně nagenerovat výsledkovky i + # za historická čísla. + + # Níže se vytváří dotaz na součet bodů za správně vyfiltrovaná hodnocení, + # který se použije ve výsledném dotazu. + if cislo and odjakziva: # Body se sčítají odjakživa do zadaného čísla. + # Vyfiltrujeme všechna hodnocení, která jsou buď ze starších ročníků, + # anebo ze stejného ročníku, jak je zadané číslo, tam ale sčítáme jen + # pro čísla s pořadím nejvýše stejným, jako má zadané číslo. + body_k_zapocteni = Sum('reseni__hodnoceni__body', + filter=( Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok__lt=rok) | + Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok=rok, + reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi) )) + elif cislo and not odjakziva: # Body se sčítají za dané číslo. + body_k_zapocteni = Sum('reseni__hodnoceni__body', + filter=( Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok=rok, + reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi) )) + elif rocnik and odjakziva: # Spočítáme body za starší ročníky až do zadaného včetně. + body_k_zapocteni = Sum('reseni__hodnoceni__body', + filter= Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok__lte=rok)) + elif rocnik and not odjakziva: # Spočítáme body za daný ročník. + body_k_zapocteni = Sum('reseni__hodnoceni__body', + filter= Q(reseni__hodnoceni__cislo_body__rocnik=rocnik)) + else: + assert True, "body_resitelu: Neplatná kombinace za a odjakživa." + + # Následující řádek přidá ke každému řešiteli údaj ".body" se součtem jejich bodů + resitele_s_body = m.Resitel.objects.filter(id__in=resitele_id).annotate( + body=body_k_zapocteni) + # 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 0) for res in resitele_s_body} + return slovnik + +class RadekVysledkovkyRocniku(object): + """ 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_sezn, 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_sezn = body_cisla_sezn + self.titul = resitel.get_titul(body_odjakziva) + +def setrid_resitele_a_body(slov_resitel_body): + setrizeni_resitele_id = [dvojice[0] for dvojice in slov_resitel_body] + setrizeni_resitele = [m.Resitel.objects.get(id=i) for i in setrizeni_resitele_id] + setrizene_body = [dvojice[1] for dvojice in slov_resitel_body] + return setrizeni_resitele_id, setrizeni_resitele, setrizene_body + +def vysledkovka_rocniku(rocnik, jen_verejne=True): + """ Přebírá ročník (např. context["rocnik"]) a vrací výsledkovou listinu ve + formě vhodné pro šablonu "seminar/vysledkovka_rocniku.html" + """ + + ## TODO možná chytřeji vybírat aktivní řešitele + # aktivní řešitelé - chceme letos něco poslal, TODO později vyfiltrujeme ty, kdo mají + # u alespoň jedné hodnoty něco jiného než NULL + aktivni_resitele = list(resi_v_rocniku(rocnik)) + cisla = cisla_rocniku(rocnik, jen_verejne) + body_cisla_slov = {} + for cislo in cisla: + # získáme body za číslo + _, cislobody = secti_body_za_cislo(cislo, aktivni_resitele) + body_cisla_slov[cislo.id] = cislobody + + # získáme body za ročník, seznam obsahuje dvojice (řešitel_id, body) setřízené sestupně + resitel_rocnikbody_sezn = secti_body_za_rocnik(rocnik, aktivni_resitele) + + # setřídíme řešitele podle počtu bodů a získáme seznam s body od nejvyšších po nenižší + setrizeni_resitele_id, setrizeni_resitele, setrizene_body = setrid_resitele_a_body(resitel_rocnikbody_sezn) + poradi = sloupec_s_poradim(setrizene_body) + + # získáme body odjakživa + resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, rocnik) + + # vytvoříme jednotlivé sloupce výsledkovky + radky_vysledkovky = [] + i = 0 + for ar_id in setrizeni_resitele_id: + # seznam počtu bodů daného řešitele pro jednotlivá čísla + body_cisla_sezn = [] + for cislo in cisla: + body_cisla_sezn.append(body_cisla_slov[cislo.id][ar_id]) + + # vytáhneme informace pro daného řešitele + radek = RadekVysledkovkyRocniku( + poradi[i], # pořadí + m.Resitel.objects.get(id=ar_id), # řešitel (z id) + body_cisla_sezn, # seznam bodů za čísla + setrizene_body[i], # body za ročník (spočítané výše s pořadím) + resitel_odjakzivabody_slov[ar_id], # body odjakživa + rocnik) # ročník semináře pro získání ročníku řešitele + radky_vysledkovky.append(radek) + i += 1 + + return radky_vysledkovky +class RadekVysledkovkyCisla(object): + """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_problemy_sezn, + 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_problemy_sezn = body_problemy_sezn + self.titul = resitel.get_titul(body_odjakziva) + self.body_podproblemy = body_podproblemy + self.body_podproblemy_iter = body_podproblemy_iter # TODELETE + + +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 + +def secti_body_za_rocnik(za, aktivni_resitele): + """ Spočítá body za ročník (celý nebo do daného čísla), + setřídí je sestupně a vrátí jako seznam. + Parametry: + za (typu Rocnik nebo Cislo) spočítá za ročník, nebo za ročník až do + daného čísla + """ + # spočítáme všem řešitelům jejich body za ročník (False => ne odjakživa) + resitel_rocnikbody_slov = body_resitelu(aktivni_resitele, za, False) + # zeptáme se na dvojice (řešitel, body) za ročník a setřídíme sestupně + resitel_rocnikbody_sezn = sorted(resitel_rocnikbody_slov.items(), + key = lambda x: x[1], reverse = True) + return resitel_rocnikbody_sezn + +def secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy=None): + """ Spočítá u řešitelů body za číslo a za jednotlivé hlavní problémy (témata).""" + # TODO setřídit hlavní problémy čísla podle id, ať jsou ve stejném pořadí pokaždé + # pro každý hlavní problém zavedeme slovník s body za daný hlavní problém + # pro jednotlivé řešitele (slovník slovníků hlavních problémů) + if hlavni_problemy is None: + hlavni_problemy = hlavni_problemy_cisla(cislo) + + def ne_clanek_ne_konfera(problem): + return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera)) + + temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy)) + + def cosi(problem): + return problem.id + + hlavni_problemy_slovnik = {} + for hp in temata_a_spol: + hlavni_problemy_slovnik[hp.id] = {} + + hlavni_problemy_slovnik[-1] = {} + + # zakládání prázdných záznamů pro řešitele + cislobody = {} + for ar in aktivni_resitele: + # řešitele převedeme na řetězec pomocí unikátního id + cislobody[ar.id] = "" + for hp in temata_a_spol: + slovnik = hlavni_problemy_slovnik[hp.id] + slovnik[ar.id] = "" + + hlavni_problemy_slovnik[-1][ar.id] = "" + + # vezmeme všechna řešení s body do daného čísla + reseni_do_cisla = m.Reseni.objects.prefetch_related('problem', 'resitele', + 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) + + # projdeme všechna řešení do čísla a přičteme body každému řešiteli do celkových + # bodů i do bodů za problém + for reseni in reseni_do_cisla: + + # řešení může řešit více problémů + for prob in list(reseni.problem.all()): + nadproblem = hlavni_problem(prob) + if ne_clanek_ne_konfera(nadproblem): + nadproblem_slovnik = hlavni_problemy_slovnik[nadproblem.id] + else: + nadproblem_slovnik = hlavni_problemy_slovnik[-1] + + # a mít více hodnocení + for hodn in list(reseni.hodnoceni_set.all()): + body = hodn.body + + # a mít více řešitelů + for resitel in list(reseni.resitele.all()): + if resitel not in aktivni_resitele: + print("Skipping {}".format(resitel.id)) + continue + pricti_body(cislobody, resitel, body) + pricti_body(nadproblem_slovnik, resitel, body) + return hlavni_problemy_slovnik, cislobody + + +def secti_body_za_cislo_podle_temat(cislo, aktivni_resitele, podproblemy=None, temata=None): + """ Spočítá u řešitelů body za číslo za úlohy v jednotlivých hlavních problémech (témata).""" + if temata is None: + temata = hlavni_problemy_cisla(cislo) + + if podproblemy is None: + podproblemy_v_cislu(cislo, hlavni_problemy=temata) + + body_slovnik = {} + for tema in temata: + body_slovnik[tema.id] = {} + for problem in podproblemy[tema.id]: + body_slovnik[tema.id][problem.id] = {} + body_slovnik[-1] = {} + for problem in podproblemy[-1]: + body_slovnik[-1][problem.id] = {} + + # zakládání prázdných záznamů pro řešitele + for ar in aktivni_resitele: + for tema in temata: + for problem in podproblemy[tema.id]: + body_slovnik[tema.id][problem.id][ar.id] = "" + + for problem in podproblemy[-1]: + body_slovnik[-1][problem.id][ar.id] = "" + + temata = set(t.id for t in temata) + + # vezmeme všechna řešení s body do daného čísla + reseni_do_cisla = m.Reseni.objects.prefetch_related('problem', 'resitele', + 'hodnoceni_set').filter(hodnoceni__cislo_body=cislo) + + # projdeme všechna řešení do čísla a přičteme body každému řešiteli do celkových + # bodů i do bodů za problém + for reseni in reseni_do_cisla: + + # řešení může řešit více problémů + for prob in reseni.problem.all(): + nadproblem = hlavni_problem(prob) + if nadproblem.id in temata: + nadproblem_slovnik = body_slovnik[nadproblem.id] + else: + nadproblem_slovnik = body_slovnik[-1] + + problem_slovnik = nadproblem_slovnik[prob.id] + + # a mít více hodnocení + for hodn in reseni.hodnoceni_set.all(): + body = hodn.body + + # a mít více řešitelů + for resitel in reseni.resitele.all(): + if resitel not in aktivni_resitele: + print("Skipping {}".format(resitel.id)) + continue + pricti_body(problem_slovnik, resitel, body) + return body_slovnik + + +# TODELETE +class FixedIterator: + def next(self): + return self.niter.__next__() + + def __init__(self, niter): + self.niter = niter +# TODELETE + + +def vysledkovka_cisla(cislo, context=None): + if context is None: + context = {} + problemy = problemy_cisla(cislo) + hlavni_problemy = hlavni_problemy_cisla(cislo, problemy) + ## TODO možná chytřeji vybírat aktivní řešitele + # aktivní řešitelé - chceme letos něco poslal, TODO později vyfiltrujeme ty, kdo mají + # u alespoň jedné hodnoty něco jiného než NULL + aktivni_resitele = list(aktivniResitele(cislo)) + + # získáme body za číslo + hlavni_problemy_slovnik, cislobody = secti_body_za_cislo(cislo, aktivni_resitele, hlavni_problemy) + + # získáme body za ročník, seznam obsahuje dvojice (řešitel_id, body) setřízené sestupně + resitel_rocnikbody_sezn = secti_body_za_rocnik(cislo, aktivni_resitele) + + # získáme body odjakživa + resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, cislo) + + # řešitelé setřídění podle bodů za číslo sestupně + setrizeni_resitele_id = [dvojice[0] for dvojice in resitel_rocnikbody_sezn] + setrizeni_resitele = [m.Resitel.objects.get(id=i) for i in setrizeni_resitele_id] + + # spočítáme pořadí řešitelů + setrizeni_resitele_body = [dvojice[1] for dvojice in resitel_rocnikbody_sezn] + poradi = sloupec_s_poradim(setrizeni_resitele_body) + + # vytvoříme jednotlivé sloupce výsledkovky + radky_vysledkovky = [] + i = 0 + + def ne_clanek_ne_konfera(problem): + return not(isinstance(problem.get_real_instance(), m.Clanek) or isinstance(problem.get_real_instance(), m.Konfera)) + + temata_a_spol = list(filter(ne_clanek_ne_konfera, hlavni_problemy)) + + # získáme body u jednotlivých témat + podproblemy = podproblemy_v_cislu(cislo, problemy, temata_a_spol) + problemy_slovnik = secti_body_za_cislo_podle_temat(cislo, aktivni_resitele, podproblemy, temata_a_spol) + + # def not_empty(value): + # return value != '' + # + # je_nejake_ostatni = any(filter(not_empty, hlavni_problemy_slovnik[-1].values())) > 0 + + je_nejake_ostatni = len(hlavni_problemy) - len(temata_a_spol) > 0 + + for ar_id in setrizeni_resitele_id: + # získáme seznam bodů za problémy pro daného řešitele + body_problemy = [] + body_podproblemy = [] + for hp in temata_a_spol: + body_problemy.append(hlavni_problemy_slovnik[hp.id][ar_id]) + body_podproblemy.append([problemy_slovnik[hp.id][it.id][ar_id] for it in podproblemy[hp.id]]) + if je_nejake_ostatni: + body_problemy.append(hlavni_problemy_slovnik[-1][ar_id]) + body_podproblemy.append([problemy_slovnik[-1][it.id][ar_id] for it in podproblemy[-1]]) + # vytáhneme informace pro daného řešitele + radek = RadekVysledkovkyCisla( + poradi[i], # pořadí + m.Resitel.objects.get(id=ar_id), # řešitel (z id) + body_problemy, # seznam bodů za hlavní problémy čísla + cislobody[ar_id], # body za číslo + setrizeni_resitele_body[i], # body za ročník (spočítané výše s pořadím) + resitel_odjakzivabody_slov[ar_id], # body odjakživa + cislo.rocnik, + body_podproblemy, # body všech podproblémů + FixedIterator(body_podproblemy.__iter__()) # TODELETE + ) # ročník semináře pro zjištění ročníku řešitele + radky_vysledkovky.append(radek) + i += 1 + + # vytahané informace předáváme do kontextu + context['cislo'] = cislo + context['radky_vysledkovky'] = radky_vysledkovky + context['problemy'] = temata_a_spol + context['ostatni'] = je_nejake_ostatni + pt = [podproblemy[it.id] for it in temata_a_spol]+[podproblemy[-1]] + context['podproblemy'] = pt + context['podproblemy_iter'] = FixedIterator(pt.__iter__()) # TODELETE + #context['v_cisle_zadane'] = TODO + #context['resene_problemy'] = resene_problemy + return context