Browse Source

Merge remote-tracking branch 'origin/data_migrations' into treenode_editor

export_seznamu_prednasek
parent
commit
c11abf3c7a
  1. 817
      data/sitetree_new.json
  2. 3
      mamweb/settings_common.py
  3. 18
      seminar/models.py
  4. 47
      seminar/templates/seminar/odevzdavatko/detail.html
  5. 11
      seminar/templates/seminar/odevzdavatko/seznam.html
  6. 36
      seminar/templates/seminar/odevzdavatko/tabulka.html
  7. 27
      seminar/templatetags/utils.py
  8. 5
      seminar/urls.py
  9. 14
      seminar/utils.py
  10. 1
      seminar/views/__init__.py
  11. 129
      seminar/views/odevzdavatko.py
  12. 66
      seminar/views/views_all.py
  13. 31
      seminar/viewsets.py
  14. 2
      vue_frontend/src/router/index.js

817
data/sitetree_new.json

File diff suppressed because one or more lines are too long

3
mamweb/settings_common.py

@ -295,6 +295,9 @@ LOGGING = {
}, },
} }
# Permissions for uploads
FILE_UPLOAD_PERMISSIONS = 0o0644
# MaM specific # MaM specific
SEMINAR_RESENI_DIR = os.path.join('reseni') SEMINAR_RESENI_DIR = os.path.join('reseni')

18
seminar/models.py

@ -747,15 +747,18 @@ class Problem(SeminarModelBase,PolymorphicModel):
def verejne(self): def verejne(self):
# aktuálně podle stavu problému # aktuálně podle stavu problému
# FIXME pro některé problémy možná chceme override # 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ě?
stav_verejny = False stav_verejny = False
if self.stav == 'zadany' or self.stav == 'vyreseny': if self.stav == 'zadany' or self.stav == 'vyreseny':
stav_verejny = True stav_verejny = True
return stav_verejny
cislo_verejne = False #cislo_verejne = False
if (self.cislo_zadani and self.cislo_zadani.verejne()): #if (self.cislo_zadani and self.cislo_zadani.verejne()):
cislo_verejne = True # cislo_verejne = True
return (stav_verejny and cislo_verejne) #return (stav_verejny and cislo_verejne)
verejne.boolean = True verejne.boolean = True
def verejne_url(self): def verejne_url(self):
@ -993,7 +996,7 @@ def aux_generate_filename(self, filename):
unidecode(filename.replace('/', '-').replace('\0', '')) unidecode(filename.replace('/', '-').replace('\0', ''))
) )
datedir = timezone.now().strftime('%Y-%m') datedir = timezone.now().strftime('%Y-%m')
fname = "{}_{}".format( fname = "{}/{}".format(
timezone.now().strftime('%Y-%m-%d-%H:%M'), timezone.now().strftime('%Y-%m-%d-%H:%M'),
clean) clean)
return os.path.join(datedir, fname) return os.path.join(datedir, fname)
@ -1046,6 +1049,11 @@ class PrilohaReseni(SeminarModelBase):
def __str__(self): def __str__(self):
return str(self.soubor) return str(self.soubor)
def split(self):
"Vrátí cestu rozsekanou po složkách. To se hodí v templatech"
# Věřím, že tohle funguje, případně použít os.path nebo pathlib.
return self.soubor.url.split('/')
class Pohadka(SeminarModelBase): class Pohadka(SeminarModelBase):
"""Kus pohádky před/za úlohou v čísle""" """Kus pohádky před/za úlohou v čísle"""

47
seminar/templates/seminar/odevzdavatko/detail.html

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block content %}
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
<p>Řešitelé: {{ object.resitele.all | join:", " }}</p>
{# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #}
<p>Forma: {{ object.get_forma_display }}, doručeno {{ object.cas_doruceni }}</p>
{# Soubory: #}
<h3>Přílohy:</h3>
{% if object.prilohy.all %}
<table>
<tr><th>Soubor</th><th>Řešitelova poznámka</th><th>Datum</th></tr>
{% for priloha in object.prilohy.all %}
<tr>
<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td>
<td>{{ priloha.res_poznamka }}</td>
<td>{{ priloha.vytvoreno }}</td></tr>
{# TODO: Orgo-poznámka, ideálně jako formulář #}
{% endfor %}
</table>
{% else %}
<p>Žádné přílohy</p>
{% endif %}
{# Hodnocení: #}
{# FIXME: Udělat jako formulář #}
<h3>Hodnocení:</h3>
{% if object.hodnoceni_set.all %}
<table>
<tr><th>Problém</th><th>Body</th><th>Číslo pro body</th></tr>
{% for h in object.hodnoceni_set.all %}
<tr>
<td>{{ h.problem }}</a></td>
<td>{{ h.body }}</td>
<td>{{ h.cislo_body }}</td></tr>
{% endfor %}
</table>
{% else %}
<p>Ještě nebylo hodnoceno</p>
{% endif %}
{% endblock %}

11
seminar/templates/seminar/odevzdavatko/seznam.html

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
<ul>
{% for obj in object_list %}
<li><a href="{% url 'odevzdavatko_detail_reseni' pk=obj.id %}">{{ obj }}</a> ({{ obj.get_forma_display }} {{ obj.cas_doruceni }})
{% endfor %}
</ul>
{% endblock %}

36
seminar/templates/seminar/odevzdavatko/tabulka.html

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #}
{% block content %}
<table>
<tr>
<td></td> {# Prázdná buňka v levém horním rohu #}
{% for p in problemy %}
<th>
{# TODO: Přehled řešení k problému, odkázaný odsud? #}
{{ p }}
</th>
{% endfor %}
</tr>
{% for resitel,hodnoty in radky%}
<tr>
<td>
{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #}
{{ resitel }}
</td>
{% for hodn in hodnoty %}
<td>
{% if hodn %}
<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}">
{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}}
</a>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endblock %}

27
seminar/templatetags/utils.py

@ -0,0 +1,27 @@
from django import template
from datetime import datetime, timedelta
from pytz import timezone
from mamweb.settings import TIME_ZONE
import logging
register = template.Library()
logger = logging.getLogger(__name__)
@register.filter(name='kratke_datum', expects_localtime=True)
def kratke_datum(dt):
# None dává None, ne-datum dává False, aby se daly použít filtry typu "default".
if dt is None:
return None
if not isinstance(dt, datetime):
logger.warning(f"Špatné volání filtru {__name__}: {dt}")
return False
naive_now = datetime.now()
tz = timezone(TIME_ZONE)
now = tz.localize(naive_now)
delta = now - dt
if delta <= timedelta(days=1):
return dt.strftime("%k:%M")
if delta <= timedelta(days=365): # Timedelta neumí vyjádřit 1 rok
return dt.strftime("%d. %m.")
return dt.strftime("%d. %m. %Y")

5
seminar/urls.py

@ -168,5 +168,10 @@ urlpatterns = [
# org_member_required(views.OrganizatorAutocomplete.as_view()), # org_member_required(views.OrganizatorAutocomplete.as_view()),
# name='seminar_autocomplete_organizator') # name='seminar_autocomplete_organizator')
path('temp/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('temp/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('temp/reseni/<int:pk>', org_required(views.DetailReseniView.as_view()), name='odevzdavatko_detail_reseni'),
path('temp/reseni/all', org_required(views.SeznamReseniView.as_view())),
path('temp/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
] ]

14
seminar/utils.py

@ -148,16 +148,12 @@ def resi_v_rocniku(rocnik, cislo=None):
if cislo is None: if cislo is None:
# filtrujeme pouze podle ročníku # filtrujeme pouze podle ročníku
letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik) return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
reseni__hodnoceni__cislo_body__rocnik=rocnik).distinct()
else: # filtrujeme podle ročníku i čísla else: # filtrujeme podle ročníku i čísla
letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik, return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
hodnoceni__cislo_body__poradi__lte=cislo.poradi) reseni__hodnoceni__cislo_body__rocnik=rocnik,
reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi).distinct()
# vygenerujeme queryset řešitelů, co letos něco poslali
letosni_resitele = m.Resitel.objects.none()
for reseni in letosni_reseni:
letosni_resitele = letosni_resitele | reseni.resitele.filter(rok_maturity__gte=rocnik.druhy_rok())
return letosni_resitele.distinct()
def aktivniResitele(cislo, pouze_letosni=False): def aktivniResitele(cislo, pouze_letosni=False):

1
seminar/views/__init__.py

@ -1,3 +1,4 @@
from .views_all import * from .views_all import *
from .autocomplete import * from .autocomplete import *
from .views_rest import * from .views_rest import *
from .odevzdavatko import *

129
seminar/views/odevzdavatko.py

@ -0,0 +1,129 @@
from django.views.generic import ListView, DetailView
from django.views.generic.base import TemplateView
from dataclasses import dataclass
import datetime
import seminar.models as m
from seminar.utils import aktivniResitele, resi_v_rocniku
# 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 get_queryset(self):
# 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.zadane_problemy = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
qs = super().get_queryset()
qs = qs.filter(problem__in=self.zadane_problemy).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.zadane_problemy = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
ctx = super().get_context_data(*args, **kwargs)
ctx['problemy'] = self.zadane_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.zadane_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))
return ctx
class ReseniProblemuView(ListView):
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
# Kontext automaticky?
class DetailReseniView(DetailView):
model = m.Reseni
template_name = 'seminar/odevzdavatko/detail.html'
# To je všechno? Najde se to podle pk...
# 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

66
seminar/views/views_all.py

@ -1,4 +1,4 @@
# coding:utf-8
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse
@ -17,6 +17,7 @@ from django.contrib.auth.models import User, Permission
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction from django.db import transaction
from django.core import serializers from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
import seminar.models as s import seminar.models as s
@ -120,15 +121,57 @@ class TNLData(object):
self.appendable_siblings = tnltt.appendableChildren(self.parent) self.appendable_siblings = tnltt.appendableChildren(self.parent)
else: else:
self.appendable_siblings = [] self.appendable_siblings = []
@classmethod
def public_above(cls, anode):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = anode # chceme začít už od konkrétního node včetně
while True:
rocnik = isinstance(parent, s.RocnikNode)
cislo = isinstance(parent, s.CisloNode)
uloha = (isinstance(parent, s.UlohaVzorakNode) or
isinstance(parent, s.UlohaZadaniNode))
tema = isinstance(parent, s.TemaVCisleNode)
if (rocnik or cislo or uloha or tema) or parent==None:
break
else:
parent = treelib.get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif uloha:
return parent.uloha.verejne()
elif tema:
return parent.tema.verejne()
elif None:
print("Existuje TreeNode, který není pod číslem, ročníkem, úlohou"
"ani tématem. {}".format(anode))
return False
@classmethod
def all_public_children(cls, anode):
for ch in treelib.all_children(anode):
if TNLData.public_above(ch):
yield ch
else:
continue
@classmethod @classmethod
def from_treenode(cls,anode,parent=None,index=None): def from_treenode(cls, anode, user, parent=None, index=None):
if TNLData.public_above(anode) or user.has_perm('auth.org'):
out = cls(anode,parent,index) out = cls(anode,parent,index)
for (idx,ch) in enumerate(treelib.all_children(anode)): else:
# FIXME přidat filtrování na veřejnost raise PermissionDenied()
outitem = cls.from_treenode(ch,out,idx)
if user.has_perm('auth.org'):
enum_children = enumerate(treelib.all_children(anode))
else:
enum_children = enumerate(TNLData.all_public_children(anode))
for (idx,ch) in enum_children:
outitem = cls.from_treenode(ch, user, out, idx)
out.children.append(outitem) out.children.append(outitem)
out.add_edit_options() out.add_edit_options()
return out return out
@ -195,7 +238,7 @@ class TreeNodeView(generic.DetailView):
def get_context_data(self,**kwargs): def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['tnldata'] = TNLData.from_treenode(self.object) context['tnldata'] = TNLData.from_treenode(self.object,self.request.user)
return context return context
class TreeNodeJSONView(generic.DetailView): class TreeNodeJSONView(generic.DetailView):
@ -203,7 +246,7 @@ class TreeNodeJSONView(generic.DetailView):
def get(self,request,*args, **kwargs): def get(self,request,*args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
data = TNLData.from_treenode(self.object).to_json() data = TNLData.from_treenode(self.object,self.request.user).to_json()
return JsonResponse(data) return JsonResponse(data)
@ -332,6 +375,7 @@ class ProblemView(generic.DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = self.request.user
# Teď potřebujeme doplnit tnldata do kontextu. # Teď potřebujeme doplnit tnldata do kontextu.
# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME. # Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME.
if False: if False:
@ -339,11 +383,11 @@ class ProblemView(generic.DetailView):
pass pass
elif isinstance(self.object, s.Clanek) or isinstance(self.object, s.Konfera): elif isinstance(self.object, s.Clanek) or isinstance(self.object, s.Konfera):
# Tyhle Problémy mají ŘešeníNode # Tyhle Problémy mají ŘešeníNode
context['tnldata'] = TNLData.from_treenode(self.object.reseninode) context['tnldata'] = TNLData.from_treenode(self.object.reseninode,user)
elif isinstance(self.object, s.Uloha): elif isinstance(self.object, s.Uloha):
# FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever # FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever
tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode) tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode,user)
tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode) tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode,user)
context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak]) context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak])
elif isinstance(self.object, s.Tema): elif isinstance(self.object, s.Tema):
rocniknode = self.object.rocnik.rocniknode rocniknode = self.object.rocnik.rocniknode

31
seminar/viewsets.py

@ -21,31 +21,6 @@ class PermissionMixin(object):
# návštěvník nemusí být zalogován, aby si prohlížel obsah # návštěvník nemusí být zalogován, aby si prohlížel obsah
return [permission() for permission in permission_classes] return [permission() for permission in permission_classes]
def verejne_nad(self, node):
""" Returns output of verejne for closest Rocnik, Cislo or Problem above.
(All of them have method verejne.)"""
parent = get_parent(node)
while True:
rocnik = isinstance(parent, RocnikNode)
cislo = isinstance(parent, CisloNode)
problem = isinstance(parent, ProblemNode)
if (rocnik or cislo or problem):
break
else:
parent = get_parent(parent)
if rocnik:
return parent.rocnik.verejne()
elif cislo:
return parent.cislo.verejne()
elif problem:
return parent.problem.verjne()
def has_object_permission(self, request, view, obj):
# test that obj is Node
assert isinstance(obj, Node)
return verejne_nad(node)
class ReadWriteSerializerMixin(object): class ReadWriteSerializerMixin(object):
""" """
Overrides get_serializer_class to choose the read serializer Overrides get_serializer_class to choose the read serializer
@ -124,6 +99,12 @@ class UlohaVzorakNodeViewSet(PermissionMixin, ReadWriteSerializerMixin, viewsets
nazev = self.request.query_params.get('nazev',None) nazev = self.request.query_params.get('nazev',None)
if nazev is not None: if nazev is not None:
queryset = queryset.filter(nazev__contains=nazev) queryset = queryset.filter(nazev__contains=nazev)
if self.request.user.has_perm('auth.org'):
return queryset
else: # pro neorgy jen zveřejněné vzoráky
return queryset.filter(uloha__cislo_reseni__verejne_db=True)
nadproblem = self.request.query_params.get('nadproblem',None) nadproblem = self.request.query_params.get('nadproblem',None)
if nadproblem is not None: if nadproblem is not None:
queryset = queryset.filter(nadproblem__pk = nadproblem) queryset = queryset.filter(nadproblem__pk = nadproblem)

2
vue_frontend/src/router/index.js

@ -20,7 +20,7 @@ export default new Router({
}, { }, {
path: '/zadani/aktualni', path: '/zadani/aktualni',
name: 'treenode_zadani', name: 'treenode_zadani',
props: {'tnid': 23}, props: {'tnid': 1655},
component: TreeNodeRoot component: TreeNodeRoot
}, { }, {
path: '/cislo/:cislo', path: '/cislo/:cislo',

Loading…
Cancel
Save