Merge branch 'odevzdavatko' into data_migrations
Je to WIP, ale jako sneak-peek dobrý :-)
This commit is contained in:
commit
0b32d312da
6 changed files with 209 additions and 43 deletions
|
@ -2,6 +2,7 @@ from django import forms
|
|||
from dal import autocomplete
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.models import User
|
||||
from django.forms import formset_factory
|
||||
from django.forms.models import inlineformset_factory
|
||||
|
||||
from .models import Skola, Resitel, Osoba, Problem
|
||||
|
@ -301,3 +302,17 @@ class NahrajObrazekKTreeNoduForm(forms.ModelForm):
|
|||
model = m.Obrazek
|
||||
fields = ('na_web',)
|
||||
|
||||
|
||||
class JednoHodnoceniForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = m.Hodnoceni
|
||||
fields = ('problem', 'body', 'cislo_body')
|
||||
widgets = {
|
||||
'problem': autocomplete.ModelSelect2(
|
||||
url='autocomplete_problem_odevzdatelny', # FIXME: Dovolit i starší?
|
||||
)
|
||||
}
|
||||
|
||||
OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
|
||||
extra = 0,
|
||||
)
|
||||
|
|
|
@ -2,6 +2,59 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #}
|
||||
<script type='text/javascript'>
|
||||
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
|
||||
function updateElementIndex(el, prefix, ndx) {
|
||||
var id_regex = new RegExp('(' + prefix + '-\\d+)');
|
||||
var replacement = prefix + '-' + ndx;
|
||||
if ($(el).attr("for")) {
|
||||
$(el).attr("for", $(el).attr("for").replace(id_regex, replacement));
|
||||
}
|
||||
if (el.id) {
|
||||
el.id = el.id.replace(id_regex, replacement);
|
||||
}
|
||||
if (el.name) {
|
||||
el.name = el.name.replace(id_regex, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
// Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0
|
||||
function deleteForm(prefix, btn) {
|
||||
var total = parseInt($('#id_' + prefix + '-TOTAL_FORMS').val());
|
||||
if (total >= 1){
|
||||
btn.closest('tr').remove();
|
||||
var forms = $('.hodnoceni');
|
||||
$('#id_' + prefix + '-TOTAL_FORMS').val(forms.length);
|
||||
for (var i=0, formCount=forms.length; i<formCount; i++) {
|
||||
$(forms.get(i)).find(':input').each(function() {
|
||||
updateElementIndex(this, prefix, i);
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Credit: https://simpleit.rocks/python/django/dynamic-add-form-with-add-button-in-django-modelformset-template/
|
||||
$(document).ready(function(){
|
||||
$('#pridat_hodnoceni').click(function() {
|
||||
var form_idx = $('#id_form-TOTAL_FORMS').val();
|
||||
var new_form = $('#empty_form').html().replace(/__prefix__/g, form_idx);
|
||||
$('#form_set').append(new_form);
|
||||
// Newly created form has not the binding between remove button and remove function
|
||||
// We need to add it manually
|
||||
$('.smazat_hodnoceni').click(function(){
|
||||
deleteForm("form",this);
|
||||
});
|
||||
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
|
||||
});
|
||||
$('.smazat_hodnoceni').click(function(){
|
||||
deleteForm("form",this);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<p>Řešené problémy: {{ object.problem.all | join:", " }}</p>
|
||||
|
||||
<p>Řešitelé: {{ object.resitele.all | join:", " }}</p>
|
||||
|
@ -27,21 +80,33 @@
|
|||
{% endif %}
|
||||
|
||||
{# Hodnocení: #}
|
||||
{# FIXME: Udělat jako formulář #}
|
||||
<h3>Hodnocení:</h3>
|
||||
{% if object.hodnoceni_set.all %}
|
||||
<table>
|
||||
<form method=post><table>
|
||||
{% csrf_token %}
|
||||
{{ form.management_form }}
|
||||
<table id="form_set">
|
||||
<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>
|
||||
{% for subform in form %}
|
||||
<tr class="hodnoceni">
|
||||
<td>{{ subform.problem }}</td>
|
||||
<td>{{ subform.body }}</td>
|
||||
<td>{{ subform.cislo_body }}</td>
|
||||
<td><input type=button class="smazat_hodnoceni" value="Smazat" id="id_{{subform.prefix}}-jsremove"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<p>Ještě nebylo hodnoceno</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<input type=button id="pridat_hodnoceni" value="Přidat hodnocení">
|
||||
<input type=submit></form>
|
||||
|
||||
<table id="empty_form" style="display: none;">
|
||||
<tr class="hodnoceni">
|
||||
<td>{{ form.empty_form.problem }}</td>
|
||||
<td>{{ form.empty_form.body }}</td>
|
||||
<td>{{ form.empty_form.cislo_body }}</td>
|
||||
<td><input type=button class="smazat_hodnoceni" value="Smazat" id="id_{{form.empty_form.prefix}}-jsremove"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.urls import path, include, re_path
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from . import views, export
|
||||
from .utils import org_required, resitel_required
|
||||
from .utils import org_required, resitel_required, viewMethodSwitch
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -124,11 +124,6 @@ urlpatterns = [
|
|||
org_required(views.soustredeniObalkyView),
|
||||
name='seminar_soustredeni_obalky'
|
||||
),
|
||||
path(
|
||||
'org/vloz_body/<int:tema>/',
|
||||
org_required(views.VlozBodyView.as_view()),
|
||||
name='seminar_org_vlozbody'
|
||||
),
|
||||
# příprava na nestatický orgorozcestník
|
||||
path(
|
||||
'org/rozcestnik/',
|
||||
|
@ -175,8 +170,7 @@ urlpatterns = [
|
|||
|
||||
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/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
|
||||
path('temp/reseni/all', org_required(views.SeznamReseniView.as_view())),
|
||||
path('temp/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())),
|
||||
|
||||
]
|
||||
|
|
|
@ -5,6 +5,7 @@ import datetime
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from html.parser import HTMLParser
|
||||
from django import views as DjangoViews
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -191,3 +192,30 @@ def aktivniResitele(cislo, pouze_letosni=False):
|
|||
else:
|
||||
# spojíme querysety s řešiteli loni a letos do daného čísla
|
||||
return (resi_v_rocniku(loni) | resi_v_rocniku(letos, cislo)).distinct()
|
||||
|
||||
def viewMethodSwitch(get, post):
|
||||
"""
|
||||
Vrátí view, který zavolá různé jiné views podle toho, kterou metodou je zavolán.
|
||||
|
||||
Inspirováno https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#an-alternative-better-solution, jen jsem to udělal genericky.
|
||||
|
||||
Parametry:
|
||||
post view pro metodu POST
|
||||
get view pro metodu GET
|
||||
|
||||
V obou případech se míní už view jakožto funkce, takže u class-based views se už má použít .as_view()
|
||||
|
||||
TODO: Podpora i pro metodu HEAD? A možná i pro FILES?
|
||||
"""
|
||||
|
||||
theGetView = get
|
||||
thePostView = post
|
||||
|
||||
class NewView(DjangoViews.View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return theGetView(request, *args, **kwargs)
|
||||
def post(self, request, *args, **kwargs):
|
||||
return thePostView(request, *args, **kwargs)
|
||||
|
||||
return NewView.as_view()
|
||||
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
from django.views.generic import ListView, DetailView
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.views.generic import ListView, DetailView, FormView
|
||||
from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin
|
||||
from django.views.generic.base import View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.db import transaction
|
||||
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import seminar.models as m
|
||||
import seminar.forms as f
|
||||
from seminar.utils import aktivniResitele, resi_v_rocniku
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Co chceme?
|
||||
# - "Tabulku" aktuální řešitelé x zveřejněné problémy, v buňkách počet řešení
|
||||
# - TabulkaOdevzdanychReseniView
|
||||
|
@ -30,15 +39,22 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
template_name = 'seminar/odevzdavatko/tabulka.html'
|
||||
model = m.Hodnoceni
|
||||
|
||||
def inicializuj_osy_tabulky(self):
|
||||
"""Vyrobí prvotní querysety pro sloupce a řádky, tj. seznam všech řešitelů a problémů"""
|
||||
# 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()
|
||||
|
||||
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.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.zadane_problemy = m.Problem.objects.filter(stav=m.Problem.STAV_ZADANY).non_polymorphic()
|
||||
self.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')
|
||||
qs = qs.filter(problem__in=self.problemy).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba')
|
||||
return qs
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
|
@ -46,10 +62,10 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
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()
|
||||
self.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['problemy'] = self.problemy
|
||||
ctx['resitele'] = self.resitele
|
||||
tabulka = dict()
|
||||
|
||||
|
@ -76,7 +92,7 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
hodnoty = []
|
||||
for resitel in self.resitele:
|
||||
resiteluv_radek = []
|
||||
for problem in self.zadane_problemy:
|
||||
for problem in self.problemy:
|
||||
if problem in tabulka and resitel in tabulka[problem]:
|
||||
resiteluv_radek.append(tabulka[problem][resitel])
|
||||
else:
|
||||
|
@ -86,7 +102,8 @@ class TabulkaOdevzdanychReseniView(ListView):
|
|||
|
||||
return ctx
|
||||
|
||||
class ReseniProblemuView(ListView):
|
||||
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
|
||||
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
|
||||
model = m.Reseni
|
||||
template_name = 'seminar/odevzdavatko/seznam.html'
|
||||
|
||||
|
@ -107,12 +124,73 @@ class ReseniProblemuView(ListView):
|
|||
)
|
||||
return qs
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
if self.object_list.count() == 1:
|
||||
jedine_reseni = self.object_list.first()
|
||||
return redirect(reverse("odevzdavatko_detail_reseni", kwargs={"pk": jedine_reseni.id}))
|
||||
context = self.get_context_data()
|
||||
return self.render_to_response(context)
|
||||
# Kontext automaticky?
|
||||
|
||||
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
|
||||
class DetailReseniView(DetailView):
|
||||
model = m.Reseni
|
||||
template_name = 'seminar/odevzdavatko/detail.html'
|
||||
# To je všechno? Najde se to podle pk...
|
||||
|
||||
def aktualni_hodnoceni(self):
|
||||
reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk'])
|
||||
result = [] # Slovníky s klíči problem, body, cislo_body -- initial data pro f.OhodnoceniReseniFormSet
|
||||
for hodn in m.Hodnoceni.objects.filter(reseni=reseni):
|
||||
result.append(
|
||||
{"problem": hodn.problem,
|
||||
"body": hodn.body,
|
||||
"cislo_body": hodn.cislo_body,
|
||||
})
|
||||
return result
|
||||
|
||||
def get_context_data(self, **kw):
|
||||
ctx = super().get_context_data(**kw)
|
||||
ctx['form'] = f.OhodnoceniReseniFormSet(
|
||||
initial = self.aktualni_hodnoceni()
|
||||
)
|
||||
return ctx
|
||||
|
||||
|
||||
def hodnoceniReseniView(request, pk, *args, **kwargs):
|
||||
reseni = get_object_or_404(m.Reseni, pk=pk)
|
||||
template_name = 'seminar/odevzdavatko/detail.html'
|
||||
form_class = f.OhodnoceniReseniFormSet
|
||||
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
|
||||
|
||||
# FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
|
||||
# Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm
|
||||
formset = f.OhodnoceniReseniFormSet(request.POST)
|
||||
# TODO: Napsat validaci formuláře a formsetu
|
||||
# TODO: Implementovat větev, kdy formulář validní není.
|
||||
if formset.is_valid():
|
||||
with transaction.atomic():
|
||||
# Smažeme všechna dosavadní hodnocení tohoto řešení
|
||||
qs = m.Hodnoceni.objects.filter(reseni=reseni)
|
||||
logger.info(f"Will delete {qs.count()} objects: {qs}")
|
||||
qs.delete()
|
||||
|
||||
# Vyrobíme nová podle formsetu
|
||||
for form in formset:
|
||||
problem = form.cleaned_data['problem']
|
||||
body = form.cleaned_data['body']
|
||||
cislo_body = form.cleaned_data['cislo_body']
|
||||
hodnoceni = m.Hodnoceni(
|
||||
problem=problem,
|
||||
body=body,
|
||||
cislo_body=cislo_body,
|
||||
reseni=reseni,
|
||||
)
|
||||
logger.info(f"Creating Hodnoceni: {hodnoceni}")
|
||||
hodnoceni.save()
|
||||
|
||||
return redirect(success_url)
|
||||
|
||||
|
||||
# Přehled všech řešení kvůli debugování
|
||||
|
||||
|
|
|
@ -63,20 +63,6 @@ from seminar.utils import aktivniResitele, resi_v_rocniku
|
|||
def get_problemy_k_tematu(tema):
|
||||
return Problem.objects.filter(nadproblem = tema)
|
||||
|
||||
|
||||
class VlozBodyView(generic.ListView):
|
||||
template_name = 'seminar/org/vloz_body.html'
|
||||
|
||||
def get_queryset(self):
|
||||
self.tema = get_object_or_404(Problem,id=self.kwargs['tema'])
|
||||
print(self.tema)
|
||||
self.problemy = Problem.objects.filter(nadproblem = self.tema)
|
||||
print(self.problemy)
|
||||
self.reseni = Reseni.objects.filter(problem__in=self.problemy)
|
||||
print(self.reseni)
|
||||
return self.reseni
|
||||
|
||||
|
||||
class ObalkovaniView(generic.ListView):
|
||||
template_name = 'seminar/org/obalkovani.html'
|
||||
|
||||
|
|
Loading…
Reference in a new issue