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
4a521a34fa
  1. 4
      mamweb/static/css/mamweb.css
  2. 27
      seminar/admin.py
  3. 15
      seminar/forms.py
  4. 2
      seminar/templates/seminar/archiv/cislo.html
  5. 87
      seminar/templates/seminar/odevzdavatko/detail.html
  6. 3
      seminar/templates/seminar/orgorozcestnik.html
  7. 10
      seminar/urls.py
  8. 28
      seminar/utils.py
  9. 98
      seminar/views/odevzdavatko.py
  10. 14
      seminar/views/views_all.py

4
mamweb/static/css/mamweb.css

@ -395,8 +395,8 @@ input[type="file"] {
border-width:1px;
border-radius: 5px;
padding:3px;
top:20px;
left:20px;
top:50px;
left:10px;
}
.field-with-comment:hover span.field-comment{

27
seminar/admin.py

@ -46,11 +46,11 @@ class OsobaAdmin(admin.ModelAdmin):
@admin.register(m.Organizator)
class OrganizatorAdmin(admin.ModelAdmin):
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'prezdivka']
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
@admin.register(m.Resitel)
class ResitelAdmin(admin.ModelAdmin):
search_fields = ['jmeno', 'prijmeni', 'prezdivka']
search_fields = ['osoba__jmeno', 'osoba__prijmeni', 'osoba__prezdivka']
ordering = ('osoba__jmeno','osoba__prijmeni')
@admin.register(m.Problem)
@ -65,29 +65,28 @@ class ProblemAdmin(PolymorphicParentModelAdmin):
# Pokud chceme orezavat na aktualni rocnik, musime do modelu pridat odkaz na rocnik. Zatim bere vse.
search_fields = ['nazev']
# V ProblemAdmin to nejde, protoze se to nepropise do deti
class ProblemAdminMixin(object):
show_in_index = True
autocomplete_fields = ['nadproblem','autor','garant']
filter_horizontal = ['opravovatele']
@admin.register(m.Tema)
class TemaAdmin(PolymorphicChildModelAdmin):
class TemaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
base_model = m.Tema
show_in_index = True
autocomplete_fields = ['nadproblem']
@admin.register(m.Clanek)
class ClanekAdmin(PolymorphicChildModelAdmin):
class ClanekAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
base_model = m.Clanek
show_in_index = True
autocomplete_fields = ['nadproblem']
@admin.register(m.Uloha)
class UlohaAdmin(PolymorphicChildModelAdmin):
class UlohaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
base_model = m.Uloha
show_in_index = True
autocomplete_fields = ['nadproblem']
@admin.register(m.Konfera)
class KonferaAdmin(PolymorphicChildModelAdmin):
class KonferaAdmin(ProblemAdminMixin,PolymorphicChildModelAdmin):
base_model = m.Konfera
show_in_index = True
autocomplete_fields = ['nadproblem']
class TextAdminInline(admin.TabularInline):

15
seminar/forms.py

@ -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
seminar/templates/seminar/archiv/cislo.html

@ -84,7 +84,7 @@
<th class='border-r'>#
<th class='border-r'>Jméno
{% for p in problemy %}
<th class='border-r' id="problem{{ p.kod_v_rocniku }}"><a href="{{ p.verejne_url }}">{{ p.kod_v_rocniku }}</a>
<th class='border-r' id="problem{{ forloop.counter }}"><a href="{{ p.verejne_url }}">{{ p.kod_v_rocniku }}</a>
{# TODELETE #}
{% for podproblemy in podproblemy_iter.next %}

87
seminar/templates/seminar/odevzdavatko/detail.html

@ -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 %}

3
seminar/templates/seminar/orgorozcestnik.html

@ -20,13 +20,14 @@
<h2><strong>Tvorba čísla</strong></h2>
<ul>
<li><a href="/admin/seminar/problemnavrh/add/"><strong>přidat téma</strong></a></li>
<li><a href="/admin/seminar/problem/add/"><strong>přidat téma</strong></a></li>
<li><strong>korektury</strong>
<ul>
<li><a href="/korektury/">korekturování</a></li>
<li><a href="/admin/korektury/korekturovanepdf/add/">přidat pdf k opravám</a></li>
</ul>
</li>
<li><a href="{% url 'odevzdavatko_tabulka' %}"><strong>zadávání bodů</strong></a></li>
<li><a href='{{ posledni_cislo_url }}'><strong>poslední vydané číslo </strong></a></li>
</ul>
<hr />

10
seminar/urls.py

@ -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())),
]

28
seminar/utils.py

@ -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í view jakožto funkce, takže u class-based views se 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()

98
seminar/views/odevzdavatko.py

@ -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í

14
seminar/views/views_all.py

@ -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…
Cancel
Save