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
SEMINAR_RESENI_DIR = os.path.join('reseni')

18
seminar/models.py

@ -747,15 +747,18 @@ class Problem(SeminarModelBase,PolymorphicModel):
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ě?
stav_verejny = False
if self.stav == 'zadany' or self.stav == 'vyreseny':
stav_verejny = True
return stav_verejny
cislo_verejne = False
if (self.cislo_zadani and self.cislo_zadani.verejne()):
cislo_verejne = True
#cislo_verejne = False
#if (self.cislo_zadani and self.cislo_zadani.verejne()):
# cislo_verejne = True
return (stav_verejny and cislo_verejne)
#return (stav_verejny and cislo_verejne)
verejne.boolean = True
def verejne_url(self):
@ -993,7 +996,7 @@ def aux_generate_filename(self, filename):
unidecode(filename.replace('/', '-').replace('\0', ''))
)
datedir = timezone.now().strftime('%Y-%m')
fname = "{}_{}".format(
fname = "{}/{}".format(
timezone.now().strftime('%Y-%m-%d-%H:%M'),
clean)
return os.path.join(datedir, fname)
@ -1046,6 +1049,11 @@ class PrilohaReseni(SeminarModelBase):
def __str__(self):
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):
"""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()),
# 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:
# 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
letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik,
hodnoceni__cislo_body__poradi__lte=cislo.poradi)
# 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()
return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(),
reseni__hodnoceni__cislo_body__rocnik=rocnik,
reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi).distinct()
def aktivniResitele(cislo, pouze_letosni=False):

1
seminar/views/__init__.py

@ -1,3 +1,4 @@
from .views_all import *
from .autocomplete 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.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.db import transaction
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.forms.models import model_to_dict
import seminar.models as s
@ -120,15 +121,57 @@ class TNLData(object):
self.appendable_siblings = tnltt.appendableChildren(self.parent)
else:
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
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)
for (idx,ch) in enumerate(treelib.all_children(anode)):
# FIXME přidat filtrování na veřejnost
outitem = cls.from_treenode(ch,out,idx)
else:
raise PermissionDenied()
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.add_edit_options()
return out
@ -195,7 +238,7 @@ class TreeNodeView(generic.DetailView):
def get_context_data(self,**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
class TreeNodeJSONView(generic.DetailView):
@ -203,7 +246,7 @@ class TreeNodeJSONView(generic.DetailView):
def get(self,request,*args, **kwargs):
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)
@ -332,6 +375,7 @@ class ProblemView(generic.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Teď potřebujeme doplnit tnldata do kontextu.
# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME.
if False:
@ -339,11 +383,11 @@ class ProblemView(generic.DetailView):
pass
elif isinstance(self.object, s.Clanek) or isinstance(self.object, s.Konfera):
# 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):
# 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_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode)
tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode,user)
tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode,user)
context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak])
elif isinstance(self.object, s.Tema):
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
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):
"""
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)
if nazev is not None:
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)
if nadproblem is not None:
queryset = queryset.filter(nadproblem__pk = nadproblem)

2
vue_frontend/src/router/index.js

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

Loading…
Cancel
Save