Browse Source

Merge branch 'data_migrations' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into data_migrations

export_seznamu_prednasek
Kateřina Č 4 years ago
parent
commit
ad2a93117f
  1. 4
      data/sitetree.json
  2. 1
      seminar/.~lock.profile_vysledkovka.txt#
  3. 37
      seminar/admin.py
  4. 82
      seminar/forms.py
  5. 51
      seminar/models.py
  6. 8
      seminar/templates/seminar/odevzdavatko/tabulka.html
  7. 2
      seminar/templates/seminar/zadani/AktualniVysledkovka.html
  8. 4
      seminar/utils.py
  9. 54
      seminar/views/odevzdavatko.py
  10. 6
      seminar/views/views_all.py
  11. 24
      seminar/views/vysledkovka.py

4
data/sitetree.json

@ -337,8 +337,8 @@
"sort_order": 33,
"title": "Výsledková listina",
"tree": 1,
"url": "zadani/vysledkova-listina/",
"urlaspattern": false
"url": "seminar_aktualni_vysledky",
"urlaspattern": true
},
"model": "sitetree.treeitem",
"pk": 16

1
seminar/.~lock.profile_vysledkovka.txt#

@ -1 +0,0 @@
,anet,erebus,25.03.2020 22:21,file:///home/anet/.config/libreoffice/4;

37
seminar/admin.py

@ -1,7 +1,8 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from django.db import models
from django.forms import widgets
from django.forms import widgets, ModelForm
from django.core.exceptions import ValidationError
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin, PolymorphicChildModelFilter
from reversion.admin import VersionAdmin
@ -12,11 +13,43 @@ from solo.admin import SingletonModelAdmin
# Todo: reversion
import seminar.models as m
import seminar.treelib as tl
admin.site.register(m.Skola)
admin.site.register(m.Prijemce)
admin.site.register(m.Rocnik)
admin.site.register(m.Cislo)
class CisloForm(ModelForm):
class Meta:
model = m.Cislo
fields = '__all__'
def clean(self):
print("Cleaning...")
print(self.cleaned_data)
if self.cleaned_data.get('verejne_db') == False:
return self.cleaned_data
cn = m.CisloNode.objects.get(cislo=self.instance)
for ch in tl.all_children(cn):
if isinstance(ch, m.TemaVCisleNode):
if ch.tema.stav not in \
(m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
raise ValidationError('Téma %(tema)s není zadané ani vyřešené', params={'tema':ch.tema})
if isinstance(ch, m.UlohaZadaniNode) or isinstance(ch, m.UlohaVzorakNode):
if ch.uloha.stav not in \
(m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
raise ValidationError('Úloha %(uloha)s není zadaná ani vyřešená', params={'uloha':ch.uloha})
if isinstance(ch, m.ReseniNode):
for problem in ch.reseni.problem_set:
if problem not in \
(m.Problem.STAV_ZADANY, m.Problem.STAV_VYRESENY):
raise ValidationError('Problém %s není zadaný ani vyřešený', code=problem)
return self.cleaned_data
@admin.register(m.Cislo)
class CisloAdmin(admin.ModelAdmin):
form = CisloForm
@admin.register(m.Osoba)
class OsobaAdmin(admin.ModelAdmin):

82
seminar/forms.py

@ -316,3 +316,85 @@ class JednoHodnoceniForm(forms.ModelForm):
OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
extra = 0,
)
# FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat
DATE_FORMAT = '%Y-%m-%d'
class OdevzdavatkoTabulkaFiltrForm(forms.Form):
"""Form pro filtrování přehledové odevzdávátkové tabulky
Inspirováno https://kam.mff.cuni.cz/mffzoom/"""
# Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices)
RESITELE_RELEVANTNI = 'relevantni'
RESITELE_LETOSNI = 'letosni'
RESITELE_CHOICES = [
(RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky
(RESITELE_LETOSNI, 'Všichni letošní'),
# Možná: všechny vč. historických?
]
PROBLEMY_MOJE = 'moje'
PROBLEMY_LETOSNI = 'letosni'
PROBLEMY_CHOICES = [
(PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga
(PROBLEMY_LETOSNI, 'Všechny letošní'),
# TODO: *hlavní problémy, možná všechny...
# XXX: Chtělo by to i "aktuálně zadané...
]
# TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)?
def gen_terminy():
import datetime
from time import strftime
from django.db.utils import OperationalError
try:
aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik
aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo
except OperationalError:
# django.db.utils.OperationalError: no such table: seminar_nastaveni
# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál
logger = logging.getLogger(__name__)
logger.error("Rozbitá databáze (před počátečními migracemi?)")
return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')]
result = []
for cislo in m.Cislo.objects.filter(
rocnik=aktualni_rocnik,
poradi__lte=aktualni_cislo.poradi,
).reverse(): # Standardně se řadí od nejnovějšího čísla
# Předem je mi líto kohokoliv, kdo tyhle řádky bude číst...
if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()),
f"Vydání {cislo.poradi}. čísla"))
if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()),
f"Předdeadline {cislo.poradi}. čísla"))
if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()),
f"Sous. deadline {cislo.poradi}. čísla"))
if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today():
result.append((
strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()),
f"Finální deadline {cislo.poradi}. čísla"))
result.append((
strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes"))
return result
# NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views...
resitele = forms.ChoiceField(choices=RESITELE_CHOICES, initial=RESITELE_RELEVANTNI)
problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES, initial=PROBLEMY_MOJE)
# choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem...
terminy = gen_terminy()
reseni_od = forms.DateField(input_formats=[DATE_FORMAT], widget=forms.Select(choices=terminy), initial=terminy[-2])
reseni_do = forms.DateField(input_formats=[DATE_FORMAT], widget=forms.Select(choices=terminy), initial=terminy[-1])

51
seminar/models.py

@ -864,31 +864,31 @@ class Problem(SeminarModelBase,PolymorphicModel):
return str(self.kod)
return '<Není zadaný>'
def verejne(self):
# aktuálně podle stavu problému
# FIXME pro některé problémy možná chceme override
# FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.
# Je to tak správně? Podle aktuální představy ano.
stav_verejny = False
if self.stav == 'zadany' or self.stav == 'vyreseny':
stav_verejny = True
print("stav_verejny: {}".format(stav_verejny))
cislo_verejne = False
cislonode = self.cislo_node()
if cislonode is None:
# problém nemá vlastní node, veřejnost posuzujeme jen podle stavu
print("empty node")
return stav_verejny
else:
cislo_zadani = cislonode.cislo
if (cislo_zadani and cislo_zadani.verejne()):
print("cislo: {}".format(cislo_zadani))
cislo_verejne = True
print("stav_verejny: {}".format(stav_verejny))
print("cislo_verejne: {}".format(cislo_verejne))
return (stav_verejny and cislo_verejne)
verejne.boolean = True
# def verejne(self):
# # aktuálně podle stavu problému
# # FIXME pro některé problémy možná chceme override
# # FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.
# # Je to tak správně? Podle aktuální představy ano.
# stav_verejny = False
# if self.stav == 'zadany' or self.stav == 'vyreseny':
# stav_verejny = True
# print("stav_verejny: {}".format(stav_verejny))
#
# cislo_verejne = False
# cislonode = self.cislo_node()
# if cislonode is None:
# # problém nemá vlastní node, veřejnost posuzujeme jen podle stavu
# print("empty node")
# return stav_verejny
# else:
# cislo_zadani = cislonode.cislo
# if (cislo_zadani and cislo_zadani.verejne()):
# print("cislo: {}".format(cislo_zadani))
# cislo_verejne = True
# print("stav_verejny: {}".format(stav_verejny))
# print("cislo_verejne: {}".format(cislo_verejne))
# return (stav_verejny and cislo_verejne)
# verejne.boolean = True
def verejne_url(self):
return reverse('seminar_problem', kwargs={'pk': self.id})
@ -962,6 +962,7 @@ class Clanek(Problem):
cislo = models.ForeignKey(Cislo, blank=True, null=True, on_delete=models.PROTECT,
verbose_name='číslo vydání', related_name='vydane_clanky')
@cached_property
def kod_v_rocniku(self):
if self.stav == 'zadany':
# Nemělo by být potřeba

8
seminar/templates/seminar/odevzdavatko/tabulka.html

@ -4,6 +4,14 @@
{% block content %}
<form method=get action=.>
{{ filtr.resitele }}
{{ filtr.problemy }}
Od: {{ filtr.reseni_od }}
Do: {{ filtr.reseni_do }}
<input type=submit value="→">
</form>
<table>
<tr>
<td></td> {# Prázdná buňka v levém horním rohu #}

2
seminar/templates/seminar/zadani/AktualniVysledkovka.html

@ -24,8 +24,10 @@
<div class='mam-org-only'>
<h1>Výsledky včetně neveřejných</h1>
{% with vysledkovka_s_neverejnymi as radky_vysledkovky %}
{% with cisla_s_neverejnymi as cisla %}
{% include "seminar/vysledkovka_rocnik.html" %}
{% endwith %}
{% endwith %}
</div>
{% endif %}

4
seminar/utils.py

@ -230,9 +230,9 @@ def cisla_rocniku(rocnik, jen_verejne=True):
seznam objektů typu Cislo
"""
if jen_verejne:
return rocnik.verejna_cisla()
return rocnik.verejne_vysledkovky_cisla()
else:
return rocnik.cisla.all()
return rocnik.cisla.all().order_by('poradi')
def hlavni_problem(problem):
""" Pro daný problém vrátí jeho nejvyšší nadproblém."""

54
seminar/views/odevzdavatko.py

@ -12,6 +12,7 @@ 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__)
@ -41,28 +42,55 @@ class TabulkaOdevzdanychReseniView(ListView):
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(FormFiltr.resitele, "resitele")
problemy = FiltrForm.get_initial_for_field(FormFiltr.problemy, "problemy")
resitele_od = FiltrForm.get_initial_for_field(FormFiltr.resitele_od, "resitele_od")
resitele_do = FiltrForm.get_initial_for_field(FormFiltr.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()
self.akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
self.resitele = resi_v_rocniku(self.akt_rocnik)
# 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 = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
qs = super().get_queryset()
qs = qs.filter(problem__in=self.problemy).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba')
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):
# FIXME: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistuje Nastavení.
self.akt_rocnik = m.Nastaveni.get_solo().aktualni_rocnik # .get_solo() vrátí tu jedinou instanci, asi...
self.resitele = resi_v_rocniku(self.akt_rocnik)
# 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 = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
# self.resitele, self.reseni a self.problemy jsou již nastavené
ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.problemy
@ -100,6 +128,10 @@ class TabulkaOdevzdanychReseniView(ListView):
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?

6
seminar/views/views_all.py

@ -29,7 +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 seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu
from datetime import timedelta, date, datetime, MAXYEAR
from django.utils import timezone
@ -185,7 +185,7 @@ class TNLData(object):
return [cls.from_treenode(treenode)]
else:
found = []
for tn in all_children(treenode):
for tn in treelib.all_children(treenode):
result = cls.filter_treenode(tn, predicate)
# Result by v tuhle chvíli měl být seznam TNLDat odpovídající treenodům, jež matchnuly predikát.
for tnl in result:
@ -503,6 +503,7 @@ def ZadaniAktualniVysledkovkaView(request):
pass
# vysledkovka s neverejnyma vysledkama
vysledkovka_s_neverejnymi = vysledkovka_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False)
cisla_s_neverejnymi = cisla_rocniku(nastaveni.aktualni_rocnik, jen_verejne=False)
return render(
request,
'seminar/zadani/AktualniVysledkovka.html',
@ -511,6 +512,7 @@ def ZadaniAktualniVysledkovkaView(request):
'radky_vysledkovky': vysledkovka,
'cisla': cisla,
'vysledkovka_s_neverejnymi': vysledkovka_s_neverejnymi,
'cisla_s_neverejnymi': cisla_s_neverejnymi,
}
)

24
seminar/views/vysledkovka.py

@ -48,7 +48,7 @@ def sloupec_s_poradim(setrizene_body):
def body_resitelu(resitele, za, odjakziva=True):
def body_resitelu(resitele, za, odjakziva=True, jen_verejne=False):
""" 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:
@ -95,9 +95,19 @@ def body_resitelu(resitele, za, odjakziva=True):
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ě.
if jen_verejne:
body_k_zapocteni = Sum('reseni__hodnoceni__body',
filter= Q(reseni__hodnoceni__cislo_body__rocnik__prvni_rok__lte=rok,
reseni__hodnoceni__cislo_body__verejna_vysledkovka=True))
else:
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.
if jen_verejne:
body_k_zapocteni = Sum('reseni__hodnoceni__body',
filter=Q(reseni__hodnoceni__cislo_body__rocnik=rocnik,
reseni__hodnoceni__cislo_body__verejna_vysledkovka=True))
else:
body_k_zapocteni = Sum('reseni__hodnoceni__body',
filter=Q(reseni__hodnoceni__cislo_body__rocnik=rocnik))
else:
@ -149,14 +159,14 @@ def vysledkovka_rocniku(rocnik, jen_verejne=True):
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)
resitel_rocnikbody_sezn = secti_body_za_rocnik(rocnik, aktivni_resitele, jen_verejne=jen_verejne)
# setřídíme řešitele podle počtu bodů a získáme seznam s body od nejvyšších po nenižší
setrizeni_resitele_id, 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)
resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, rocnik, jen_verejne=jen_verejne)
# vytvoříme jednotlivé sloupce výsledkovky
radky_vysledkovky = []
@ -216,7 +226,7 @@ def pricti_body(slovnik, resitel, body):
slovnik[resitel.id] += body
def secti_body_za_rocnik(za, aktivni_resitele):
def secti_body_za_rocnik(za, aktivni_resitele, jen_verejne):
""" Spočítá body za ročník (celý nebo do daného čísla),
setřídí je sestupně a vrátí jako seznam.
Parametry:
@ -224,7 +234,7 @@ def secti_body_za_rocnik(za, aktivni_resitele):
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)
resitel_rocnikbody_slov = body_resitelu(aktivni_resitele, za, False, jen_verejne=jen_verejne)
# 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)
@ -380,10 +390,10 @@ def vysledkovka_cisla(cislo, context=None):
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)
resitel_rocnikbody_sezn = secti_body_za_rocnik(cislo, aktivni_resitele, jen_verejne=True)
# získáme body odjakživa
resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, cislo)
resitel_odjakzivabody_slov = body_resitelu(aktivni_resitele, cislo, jen_verejne=True)
# řešitelé setřídění podle bodů za číslo sestupně
setrizeni_resitele_id = [dvojice[0] for dvojice in resitel_rocnikbody_sezn]

Loading…
Cancel
Save