WIP: Stáhnout řešení jako ZIP #84

Draft
ledoian wants to merge 2 commits from stahnout_reseni_jako_zip into master
2 changed files with 54 additions and 0 deletions

View file

@ -11,7 +11,9 @@ urlpatterns = [
path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='odevzdavatko_resitel_odevzdana_reseni'), path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='odevzdavatko_resitel_odevzdana_reseni'),
path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/zip/', org_required(views.OdevzdanaReseniVZipuView.as_view()), name='odevzdavatko_zip'),
path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'),
path('org/reseni/rocnik/<int:rocnik>/zip/', org_required(views.OdevzdanaReseniVZipuView.as_view()), name='odevzdavatko_zip'),
path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'),
path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.EditReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.EditReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'),
path('org/reseni/all', org_required(views.SeznamReseniView.as_view())), path('org/reseni/all', org_required(views.SeznamReseniView.as_view())),

View file

@ -10,17 +10,22 @@ from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
from decimal import Decimal from decimal import Decimal
from itertools import groupby from itertools import groupby
import logging import logging
import os
import tempfile
import zipfile
from . import forms as f from . import forms as f
from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
from .models import Hodnoceni, Reseni from .models import Hodnoceni, Reseni
from odevzdavatko.templatetags.jmena import jmeno_jako_prefix
from personalni.models import Resitel, Osoba, Organizator from personalni.models import Resitel, Osoba, Organizator
from tvorba.models import Problem, Deadline, Rocnik from tvorba.models import Problem, Deadline, Rocnik
from tvorba.utils import resi_v_rocniku from tvorba.utils import resi_v_rocniku
@ -103,6 +108,11 @@ class TabulkaOdevzdanychReseniView(ListView):
# 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. # 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.problemy = self.problemy.non_polymorphic().distinct() self.problemy = self.problemy.non_polymorphic().distinct()
# self.problemy jsou teď už správně, zrelevantníme self.reseni a self.resitele
self.reseni = self.reseni.filter(problem__in=self.problemy).distinct()
if resitele == FiltrForm.RESITELE_RELEVANTNI:
self.resitele = self.resitele.filter(reseni__in=self.reseni).distinct()
self.reseni = self.reseni.filter(cas_doruceni__date__gt=reseni_od, cas_doruceni__date__lte=reseni_do) self.reseni = self.reseni.filter(cas_doruceni__date__gt=reseni_od, cas_doruceni__date__lte=reseni_do)
if jen_neobodovane: if jen_neobodovane:
self.reseni = self.reseni.filter(hodnoceni__body__isnull=True) self.reseni = self.reseni.filter(hodnoceni__body__isnull=True)
@ -116,6 +126,7 @@ class TabulkaOdevzdanychReseniView(ListView):
qs = qs.filter(problem__in=self.problemy, reseni__in=self.reseni, reseni__resitele__in=self.resitele).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba').distinct() qs = qs.filter(problem__in=self.problemy, reseni__in=self.reseni, reseni__resitele__in=self.resitele).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba').distinct()
# FIXME tohle je ošklivé, na špatném místě a pomalé. Ale moc mě štvalo, že musím hledat správná místa v tabulce. # FIXME tohle je ošklivé, na špatném místě a pomalé. Ale moc mě štvalo, že musím hledat správná místa v tabulce.
self.problemy = self.problemy.filter(id__in=qs.values("problem__id")) self.problemy = self.problemy.filter(id__in=qs.values("problem__id"))
# TODO: liší se nějak od `self.problemy = self.problemy.filter(hodnoceni__in=qs)`?
return qs return qs
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
@ -175,6 +186,47 @@ class TabulkaOdevzdanychReseniView(ListView):
return ctx return ctx
# Intuitivně se mi dědičnost nelíbí, neumím říct přesně proč… (Zhruba:
# vykoná/přikládá se spousta kódu, který ale souvisí jen s HTML, tím, že si
# píšeme vlastní .get(). Lepší by bylo společné ne-HTML části (e.g.
# inicializuj_osy_tabulky) vyrazit ven a v obou Views jen použít…)
class OdevzdanaReseniVZipuView(TabulkaOdevzdanychReseniView):
def get(self, request, *args, **kwargs):
# Inspirováno implementací django.views.generic.list.BaseListView
self.object_list = self.get_queryset()
# Teď už máme i `self.problemy`.
with tempfile.TemporaryDirectory() as d:
print(f'DBG: {d=}')
zfname = f"{d}/reseni.zip"
with zipfile.ZipFile(zfname, 'w', compression=zipfile.ZIP_LZMA) as zf: # `zip` is builtin :-/
print(f'DBG: .{self.reseni.count()=}')
# TODO: data z tabulky
for r in self.reseni:
if len(r.resitele.all()) < 1:
logger.error(f'Řešení {r.id} nemá řešitele??!!')
continue
# DBG!
if len(r.prilohy.all()) < 1: continue
# TODO: komentáře jmen souborů
# Pro konzistenci imitujeme `data-alt-filename="{{object.resitele.first.osoba | jmeno_jako_prefix }}_{{ object.id }}_{{ priloha.split | last}}"` z templates/odevzdavatko/detail.html.
jmeno_slozky = f'{jmeno_jako_prefix(r.resitele.first().osoba)}_{r.id}'
zf.mkdir(jmeno_slozky)
slozka = zipfile.Path(zf, jmeno_slozky)
print(f'DBG: .({jmeno_slozky=}, {r.prilohy.count()=})')
for pr in r.prilohy.all():
jmeno_souboru = f'{jmeno_slozky}_{pr.split()[-1]}'
print(f'DBG: .({os.path.join(jmeno_slozky, jmeno_souboru)=})')
zf.write(pr.soubor.path, os.path.join(jmeno_slozky, jmeno_souboru))
print(f'DBG: Done, sending')
# close&open k provedení všech zápisů
with open(zfname, 'rb') as zf:
print(f'DBG: .')
response = HttpResponse(zf.read(), content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename="reseni.zip"'
print(f'DBG: .')
return response
# Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
"""Rozskok mezi více řešeními téhož problému od téhož řešitele. """Rozskok mezi více řešeními téhož problému od téhož řešitele.