diff --git a/seminar/models.py b/mamweb/vsechno.py similarity index 100% rename from seminar/models.py rename to mamweb/vsechno.py diff --git a/odevzdavatko/templates/odevzdavatko/prehled_reseni.html b/odevzdavatko/templates/odevzdavatko/prehled_reseni.html index 0700e3fe..f58acc80 100644 --- a/odevzdavatko/templates/odevzdavatko/prehled_reseni.html +++ b/odevzdavatko/templates/odevzdavatko/prehled_reseni.html @@ -12,7 +12,7 @@
-{% for rocnik, hodnoceni in podle_rocniku %} +{% for rocnik, hodnoceni, suma_bodu in podle_rocniku %}

Ročník {{ rocnik }}

@@ -33,7 +33,7 @@ {% endfor %}
- +

Celkový počet bodů {{suma_bodu}}


{% endfor %} diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 652570e0..2a213a2c 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -356,7 +356,12 @@ class PrehledOdevzdanychReseni(ListView): # Chceme to mít seřazené, takže místo comphrerehsion ručně postavíme pole polí. Django templates neumí použít OrderedDict :-/ podle_rocniku = [] for rocnik, hodnoceni in groupby(ctx['object_list'], lambda ho: ho.deadline_body.cislo.rocnik if ho.deadline_body is not None else None): - podle_rocniku.append((rocnik, list(hodnoceni))) + suma_bodu = 0 + hodnoceni = list(hodnoceni) + for i in hodnoceni : + if i.body != None : suma_bodu += i.body + podle_rocniku.append((rocnik, hodnoceni, suma_bodu)) + ctx['podle_rocniku'] = reversed(podle_rocniku) # Od nejnovějšího ročníku # TODO: Umožnit stažení / zobrazení řešení return ctx diff --git a/soustredeni/templates/soustredeni/stvrzenky.tex b/soustredeni/templates/soustredeni/stvrzenky.tex index 78752866..6a22fbaa 100644 --- a/soustredeni/templates/soustredeni/stvrzenky.tex +++ b/soustredeni/templates/soustredeni/stvrzenky.tex @@ -29,7 +29,7 @@ } {% for u in ucastnici %} - {% with o=u.osoba %} + {% with o=u.resitel.osoba %} \stvrzenka{{o.jmeno|sloz}}{{o.prijmeni|sloz}} {% endwith %} {% endfor %} diff --git a/soustredeni/urls.py b/soustredeni/urls.py index 6e7f25e5..78fc2f12 100644 --- a/soustredeni/urls.py +++ b/soustredeni/urls.py @@ -26,12 +26,12 @@ urlpatterns = [ ), path( 'export_ucastniku', - org_required(views.soustredeniUcastniciExportView), + org_required(views.SoustredeniUcastniciExportView.as_view()), name='soustredeni_ucastnici_export' ), path( 'stvrzenky.pdf', - org_required(views.soustredeniStvrzenkyView), + org_required(views.SoustredeniStvrzenkyView.as_view()), name='soustredeni_ucastnici_stvrzenky' ), path( diff --git a/soustredeni/views.py b/soustredeni/views.py index fcd2bf01..3cb129f7 100644 --- a/soustredeni/views.py +++ b/soustredeni/views.py @@ -1,4 +1,4 @@ -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404 from django.http import HttpResponse from django.views import generic from django.contrib.staticfiles.finders import find @@ -6,13 +6,9 @@ from django.http import Http404 from django.core.exceptions import PermissionDenied import csv -import tempfile -import shutil -import subprocess -from pathlib import Path -import http -import personalni.views +import various.views.generic +from personalni.views import obalkyView from .models import Soustredeni, Soustredeni_Ucastnici from various.models import Nastaveni @@ -36,73 +32,78 @@ class SoustredeniListView(generic.ListView): ) -def soustredeniObalkyView(request, soustredeni): - soustredeni = get_object_or_404(Soustredeni, id=soustredeni) - return personalni.views.obalkyView(request, soustredeni.ucastnici.all()) +class KonkretniSoustredeniMixin: + """ Přidá k View s parametrem `soustredeni` atribut `self.soustredeni` """ + def setup(self, request, *args, **kwargs): + super().setup(request, *args, **kwargs) + soustredeni_id = self.kwargs["soustredeni"] + self.soustredeni = get_object_or_404(Soustredeni, id=soustredeni_id) -class SoustredeniUcastniciBaseView(generic.ListView): +class SoustredeniUcastniciBaseView( + KonkretniSoustredeniMixin, + various.views.generic.NeprazdnyListView, +): + """ + Slouží jako ListView účastníků soustředění + + háže inteligentní chybu při soustředění bez účastníků + """ model = Soustredeni_Ucastnici + if_prazdny_title = "K soustředění nejsou přidaní žádní účastníci" + if_prazdny_text = "K tebou zvolenému soustředění nejsou přidaní žádní účastníci, tedy není co zobrazit. Můžeš to zkusit změnit v adminu, případně se zeptej webařů :-)" def get_queryset(self): - soustredeni = get_object_or_404( - Soustredeni, - pk=self.kwargs["soustredeni"] - ) return Soustredeni_Ucastnici.objects.filter( - soustredeni=soustredeni).select_related('resitel') + soustredeni=self.soustredeni).select_related('resitel', 'resitel__osoba') + + +# FIXME předělat jako ostatní (vyžaduje předělání `obalkyView`) +def soustredeniObalkyView(request, soustredeni): + soustredeni = get_object_or_404(Soustredeni, id=soustredeni) + return obalkyView(request, soustredeni.ucastnici.all()) class SoustredeniMailyUcastnikuView(SoustredeniUcastniciBaseView): """ Seznam e-mailů řešitelů oddělených čárkami. """ - model = Soustredeni_Ucastnici template_name = 'soustredeni/maily_ucastniku.txt' class SoustredeniUcastniciView(SoustredeniUcastniciBaseView): """ HTML tabulka účastníků pro tisk. """ - model = Soustredeni_Ucastnici template_name = 'soustredeni/seznam_ucastniku.html' -def soustredeniUcastniciExportView(request, soustredeni): - soustredeni = get_object_or_404(Soustredeni, id=soustredeni) - ucastnici = soustredeni.ucastnici.all() - response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="ucastnici.csv"' - - writer = csv.writer(response) - writer.writerow(["jmeno", "prijmeni", "rok_maturity", "telefon", "email", "ulice", "mesto", "psc","stat"]) - for u in ucastnici: - o = u.osoba - writer.writerow([o.jmeno, o.prijmeni, str(u.rok_maturity), o.telefon, o.email, o.ulice, o.mesto, o.psc, o.stat.name]) - return response +class SoustredeniUcastniciExportView(SoustredeniUcastniciBaseView): + """ CSV tabulka účastníků. """ + def render(self, request, *args, **kwargs): + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = 'attachment; filename="ucastnici.csv"' -def soustredeniStvrzenkyView(request, soustredeni): - soustredeni = get_object_or_404(Soustredeni, id=soustredeni) - ucastnici = soustredeni.ucastnici.all() - if ucastnici.count() == 0: - return HttpResponse( - render(request, 'universal.html', { - 'title': 'Není pro koho vyrobit stvrzenky.', - 'text': 'Právě ses pokusil/a vygenerovat stvrzenky pro prázdnou množinu lidí. Můžeš to zkusit změnit, případně se zeptej webařů :-)', - }), - status=http.HTTPStatus.NOT_FOUND, - ) - castka = Nastaveni.get_solo().cena_sous - tex = render(request, 'soustredeni/stvrzenky.tex', {'ucastnici': ucastnici, 'soustredeni': soustredeni, 'castka': castka}).content + writer = csv.writer(response) + writer.writerow(["jmeno", "prijmeni", "rok_maturity", "telefon", "email", "ulice", "mesto", "psc","stat"]) + for u in self.object_list: + o = u.resitel.osoba + writer.writerow([o.jmeno, o.prijmeni, str(u.resitel.rok_maturity), o.telefon, o.email, o.ulice, o.mesto, o.psc, o.stat.name]) + return response - with tempfile.TemporaryDirectory() as tempdirfn: - tempdir = Path(tempdirfn) - with open(tempdir / "stvrzenky.tex", "w") as texfile: - texfile.write(tex.decode()) - shutil.copy(find('soustredeni/logomm.pdf'), tempdir) - subprocess.call(["pdflatex", "stvrzenky.tex"], cwd = tempdir, stdout=subprocess.DEVNULL) +class SoustredeniStvrzenkyView( + various.views.generic.TeXResponseMixin, + SoustredeniUcastniciBaseView, +): + template_name = 'soustredeni/stvrzenky.tex' + dalsi_potrebne_soubory = [find('soustredeni/logomm.pdf')] - with open(tempdir / "stvrzenky.pdf", "rb") as pdffile: - response = HttpResponse(pdffile.read(), content_type='application/pdf') - return response + if_prazdny_title = "Není pro koho vyrobit stvrzenky." + if_prazdny_text = "Právě ses pokusil/a vygenerovat stvrzenky pro prázdnou množinu lidí. Můžeš to zkusit změnit, případně se zeptej webařů :-)" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["castka"] = Nastaveni.get_solo().cena_sous + context["soustredeni"] = self.soustredeni + context["ucastnici"] = self.object_list + return context class SoustredeniAbstraktyView(generic.DetailView): model = Soustredeni diff --git a/various/views/generic.py b/various/views/generic.py index b18178fb..98c6b880 100644 --- a/various/views/generic.py +++ b/various/views/generic.py @@ -1,5 +1,22 @@ +""" +Stejně jako je `django.views.generic` jsou zde generické Views +a pár mixinů, které upravují chování Views. +""" + +import http +import shutil +import subprocess +import tempfile + +from pathlib import Path + import django.views +from django.http import HttpResponse +from django.shortcuts import render +from django.template.loader import render_to_string +from django.views import generic + def viewMethodSwitch(get, post): """ @@ -27,3 +44,65 @@ def viewMethodSwitch(get, post): return thePostView(request, *args, **kwargs) return NewView.as_view() + + +class NeprazdnyListView(generic.ListView): + """ + Použití jako generic.ListView, jen při prázdném listu vyhodí M&M stránku + s titlem `self.if_prazdny_title` a textem `self.if_prazdny_text` + a způsob renderování (např. CSV) lze změnit přepsáním metody render. + """ + allow_empty = False # Interní djangová věc + if_prazdny_title = "V seznamu nic není" + if_prazdny_text = "V seznamu nic není. Zkus to napravit v adminu, nebo se zeptej webařů." + + # Skoro copy-paste generic.list.ListView.get, + # protože nemůžu chytat 404, neboť může nastat i v get_context_data + def get(self, request, *args, **kwargs): + self.object_list = self.get_queryset() + + if self.get_paginate_by(self.object_list) is not None and hasattr( + self.object_list, "exists" + ): + is_empty = not self.object_list.exists() + else: + is_empty = not self.object_list + if is_empty: + return render(request, 'universal.html', { + 'title': self.if_prazdny_title, + 'text': self.if_prazdny_text, + }, status=http.HTTPStatus.NOT_FOUND) + + return self.render(request, *args, **kwargs) + + # Tohle jsem vyčlenil, aby šlo generovat i něco jiného než template + def render(self, request, *args, **kwargs): + context = self.get_context_data() + return self.render_to_response(context) + + +class TeXResponseMixin: + """ + Mixin pro TemplateView, aby výsledek projel TeXem a vrátil rovnou PDF. + Obrázky a jiné soubory lze přidat nastavením `dalsi_potrebne_soubory` + (např. na `[django.contrib.staticfiles.finders.find('bla')]`, + nebo jiný seznam absolutních cest). + """ + dalsi_potrebne_soubory = [] + tex_prikaz = "pdflatex" + + def render_to_response(self, context, **response_kwargs): + zdrojak = render_to_string(self.get_template_names(), context) + + with tempfile.TemporaryDirectory() as tempdirfn: + tempdir = Path(tempdirfn) + with open(tempdir / "main.tex", "w") as texfile: + texfile.write(zdrojak) + for file in self.dalsi_potrebne_soubory: + shutil.copy(file, tempdir) + subprocess.call([self.tex_prikaz, "main.tex"], cwd=tempdir, stdout=subprocess.DEVNULL) + + with open(tempdir / "main.pdf", "rb") as pdffile: + response = HttpResponse(pdffile.read(), content_type='application/pdf', **response_kwargs) + return response +