diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 104c2df3..777cc87b 100644 --- a/mamweb/static/css/mamweb.css +++ b/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{ diff --git a/seminar/admin.py b/seminar/admin.py index 4ce6ded5..4da32e96 100644 --- a/seminar/admin.py +++ b/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): diff --git a/seminar/forms.py b/seminar/forms.py index fd380026..c64095c6 100644 --- a/seminar/forms.py +++ b/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, + ) diff --git a/seminar/templates/seminar/archiv/cislo.html b/seminar/templates/seminar/archiv/cislo.html index fc04b439..a875caba 100644 --- a/seminar/templates/seminar/archiv/cislo.html +++ b/seminar/templates/seminar/archiv/cislo.html @@ -84,7 +84,7 @@ # Jméno {% for p in problemy %} - {{ p.kod_v_rocniku }} + {{ p.kod_v_rocniku }} {# TODELETE #} {% for podproblemy in podproblemy_iter.next %} diff --git a/seminar/templates/seminar/odevzdavatko/detail.html b/seminar/templates/seminar/odevzdavatko/detail.html index 6cee990d..6344e0a5 100644 --- a/seminar/templates/seminar/odevzdavatko/detail.html +++ b/seminar/templates/seminar/odevzdavatko/detail.html @@ -2,6 +2,59 @@ {% block content %} +{# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #} + + +

Řešené problémy: {{ object.problem.all | join:", " }}

Řešitelé: {{ object.resitele.all | join:", " }}

@@ -27,21 +80,33 @@ {% endif %} {# Hodnocení: #} -{# FIXME: Udělat jako formulář #}

Hodnocení:

-{% if object.hodnoceni_set.all %} - +
+{% csrf_token %} +{{ form.management_form }} +
-{% for h in object.hodnoceni_set.all %} - - - - +{% for subform in form %} + + + + + + {% endfor %}
ProblémBodyČíslo pro body
{{ h.problem }}{{ h.body }}{{ h.cislo_body }}
{{ subform.problem }}{{ subform.body }}{{ subform.cislo_body }}
-{% else %} -

Ještě nebylo hodnoceno

-{% endif %} + + + + + + + + + + + + {% endblock %} diff --git a/seminar/templates/seminar/orgorozcestnik.html b/seminar/templates/seminar/orgorozcestnik.html index 5f89d36d..4e5fbc78 100644 --- a/seminar/templates/seminar/orgorozcestnik.html +++ b/seminar/templates/seminar/orgorozcestnik.html @@ -20,13 +20,14 @@

Tvorba čísla


diff --git a/seminar/urls.py b/seminar/urls.py index 215d35f6..fe3bbd9b 100644 --- a/seminar/urls.py +++ b/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//', - 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///', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), - path('temp/reseni/', org_required(views.DetailReseniView.as_view()), name='odevzdavatko_detail_reseni'), + path('temp/reseni/', 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())), - ] diff --git a/seminar/utils.py b/seminar/utils.py index bcc67013..39abeae7 100644 --- a/seminar/utils.py +++ b/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í 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() + diff --git a/seminar/views/odevzdavatko.py b/seminar/views/odevzdavatko.py index de8ceec3..e81f119f 100644 --- a/seminar/views/odevzdavatko.py +++ b/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í diff --git a/seminar/views/views_all.py b/seminar/views/views_all.py index 56183114..b3e302fa 100644 --- a/seminar/views/views_all.py +++ b/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'