555 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			555 lines
		
	
	
	
		
			22 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from django.core.exceptions import PermissionDenied
 | |
| from django.views.generic import ListView, DetailView, FormView
 | |
| from django.contrib.auth.mixins import LoginRequiredMixin
 | |
| from django.core.mail import EmailMessage
 | |
| from django.utils import timezone
 | |
| from django.views.generic import ListView, DetailView, FormView, CreateView
 | |
| from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin
 | |
| from django.views.generic.base import View
 | |
| from django.shortcuts import redirect, get_object_or_404, render
 | |
| from django.urls import reverse
 | |
| from django.db import transaction
 | |
| from django.db.models import Q
 | |
| 
 | |
| from dataclasses import dataclass
 | |
| import datetime
 | |
| from decimal import Decimal
 | |
| from itertools import groupby
 | |
| import logging
 | |
| 
 | |
| from . import forms as f
 | |
| from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm
 | |
| from .models import Hodnoceni, Reseni
 | |
| 
 | |
| from personalni.models import Resitel, Osoba, Organizator
 | |
| from tvorba.models import Problem, Deadline, Rocnik
 | |
| from tvorba.utils import resi_v_rocniku
 | |
| from various.models import Nastaveni
 | |
| from various.views.pomocne import formularOKView
 | |
| 
 | |
| 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
 | |
| # - Detail konkrétního problému a řešitele -- přehled všech řešení odevzdaných k tomuto problému
 | |
| # 	- ReseniProblemuView
 | |
| # - Detail konkrétního řešení -- všechny soubory, datum, ...
 | |
| # 	- DetailReseniView
 | |
| # - Pro řešitele: přehled jejich odevzdaných řešení
 | |
| #	- PrehledOdevzdanychReseni
 | |
| #
 | |
| # Taky se může hodit:
 | |
| # - Tabulka všech řešitelů x všech problémů?
 | |
| 
 | |
| class TabulkaOdevzdanychReseniView(ListView):
 | |
| 	template_name = 'odevzdavatko/tabulka.html'
 | |
| 	model = Hodnoceni
 | |
| 
 | |
| 	def inicializuj_osy_tabulky(self):
 | |
| 		"""Vyrobí prvotní querysety pro sloupce a řádky, tj. seznam všech řešitelů a problémů"""
 | |
| 		# FIXME: jméno metody není vypovídající...
 | |
| 		# 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 = Resitel.objects.all()
 | |
| 		self.problemy = Problem.objects.all()
 | |
| 		self.reseni = Reseni.objects.all()
 | |
| 
 | |
| 		self.aktualni_rocnik = Nastaveni.get_solo().aktualni_rocnik	# .get_solo() vrátí tu jedinou instanci
 | |
| 		if 'rocnik' in self.kwargs:
 | |
| 			self.aktualni_rocnik = get_object_or_404(Rocnik, rocnik=self.kwargs['rocnik'])
 | |
| 
 | |
| 		form = FiltrForm(self.request.GET, rocnik=self.aktualni_rocnik)
 | |
| 		if form.is_valid():
 | |
| 			fcd = form.cleaned_data
 | |
| 			resitele = fcd["resitele"]
 | |
| 			problemy = fcd["problemy"]
 | |
| 			reseni_od = fcd["reseni_od"]
 | |
| 			reseni_do = fcd["reseni_do"]
 | |
| 			jen_neobodovane = fcd["neobodovane"]
 | |
| 			self.barvicky = fcd["barvicky"]
 | |
| 		else:
 | |
| 			initial = FiltrForm.gen_initial(self.aktualni_rocnik)
 | |
| 			resitele = initial['resitele']
 | |
| 			problemy = initial['problemy']
 | |
| 			reseni_od = initial['reseni_od'][0]
 | |
| 			reseni_do = initial['reseni_do'][0]
 | |
| 			jen_neobodovane = initial["neobodovane"]
 | |
| 			self.barvicky = initial["barvicky"]
 | |
| 			
 | |
| 
 | |
| 		# Chceme jen letošní problémy
 | |
| 		self.problemy = self.problemy.filter(Q(Tema___rocnik=self.aktualni_rocnik) | Q(Uloha___cislo_zadani__rocnik = self.aktualni_rocnik) | Q(Clanek___cislo__rocnik = self.aktualni_rocnik) | Q(Konfera___soustredeni__rocnik = self.aktualni_rocnik))
 | |
| 
 | |
| 		self.chteni_resitele = resitele	# Zapamatování pro get_context_data
 | |
| 		if resitele == FiltrForm.RESITELE_RELEVANTNI:
 | |
| 			# Nejde použít utils.resi_v_rocniku, protože noví řešitelé mohou mít neobodované řešení a takoví technicky zatím neřeší.
 | |
| 			# Proto používám neodmaturovavší řešitele, TODO: Chceme to takhle nebo jinak?
 | |
| 			self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)	# Prvotní sada, pokud nebude mít body, odstraní se v get_context_data
 | |
| 		elif resitele == FiltrForm.RESITELE_NEODMATUROVAVSI:
 | |
| 			self.resitele = self.resitele.filter(rok_maturity__gt=self.aktualni_rocnik.prvni_rok)
 | |
| 
 | |
| 		if problemy == FiltrForm.PROBLEMY_MOJE:
 | |
| 			org = Organizator.objects.get(osoba__user=self.request.user)
 | |
| 			self.problemy = self.problemy.filter(
 | |
| 					Q(autor=org)|Q(garant=org)|Q(opravovatele=org),
 | |
| 					Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
 | |
| 					)
 | |
| 		elif problemy == FiltrForm.PROBLEMY_LETOSNI:
 | |
| 			self.problemy = self.problemy.filter(
 | |
| 					Q(stav=Problem.STAV_ZADANY)|Q(stav=Problem.STAV_VYRESENY),
 | |
| 					)
 | |
| 			#self.problemy = list(filter(lambda problem: problem.rocnik() == self.aktualni_rocnik, self.problemy)) # DB HOG? # FIXME: některé problémy nemají ročník....
 | |
| 		# 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.reseni = self.reseni.filter(cas_doruceni__date__gt=reseni_od, cas_doruceni__date__lte=reseni_do)
 | |
| 		if jen_neobodovane:
 | |
| 			self.reseni = self.reseni.filter(hodnoceni__body__isnull=True)
 | |
| 		self.jen_neobodovane = jen_neobodovane
 | |
| 
 | |
| 	def get_queryset(self):
 | |
| 		self.inicializuj_osy_tabulky()
 | |
| 		qs = super().get_queryset()
 | |
| 		if self.jen_neobodovane:
 | |
| 			qs = qs.filter(body__isnull=True)
 | |
| 		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.
 | |
| 		self.problemy = self.problemy.filter(id__in=qs.values("problem__id"))
 | |
| 		return qs
 | |
| 
 | |
| 	def get_context_data(self, *args, **kwargs):
 | |
| 		# TODO: refactor asi. Přepisoval jsem to jen syntakticky, nejspíš půlka kódu přestala dávat smysl…
 | |
| 		# self.resitele, self.reseni a self.problemy jsou již nastavené
 | |
| 
 | |
| 		ctx = super().get_context_data(*args, **kwargs)
 | |
| 		ctx['problemy'] = self.problemy
 | |
| 		ctx['resitele'] = self.resitele
 | |
| 		tabulka: dict[Problem, dict[Resitel, list[tuple[Reseni, Hodnoceni]]]] = dict()
 | |
| 		soucty: dict[Problem, dict[Resitel, Decimal]] = dict()
 | |
| 
 | |
| 		def pridej_reseni(resitel, hodnoceni):
 | |
| 			problem = hodnoceni.problem
 | |
| 			body = hodnoceni.body
 | |
| 			cas = hodnoceni.reseni.cas_doruceni
 | |
| 			reseni = hodnoceni.reseni
 | |
| 			if problem not in tabulka:
 | |
| 				tabulka[problem] = dict()
 | |
| 				soucty[problem] = dict()
 | |
| 			if resitel not in tabulka[problem]:
 | |
| 				tabulka[problem][resitel] = [(reseni, hodnoceni)]
 | |
| 				soucty[problem][resitel] = hodnoceni.body or 0 # Neobodované neřešíme
 | |
| 			else:
 | |
| 				tabulka[problem][resitel].append((reseni, hodnoceni))
 | |
| 				soucty[problem][resitel] += hodnoceni.body or 0 # Neobodované neřešíme
 | |
| 		
 | |
| 		for hodnoceni in self.get_queryset():
 | |
| 			for resitel in hodnoceni.reseni.resitele.all():
 | |
| 				pridej_reseni(resitel, hodnoceni)
 | |
| 
 | |
| 		hodnoty: list[list[tuple[Decimal,list[tuple[Reseni, Hodnoceni]]]]] = [] # Seznam řádků výsledné tabulky podle self.resitele, v každém řádku buňky v pořadí podle self.problemy + jejich součty, v každé buňce seznam řešení k danému řešiteli a problému.
 | |
| 		resitele_do_tabulky: list[Resitel] = []
 | |
| 		for resitel in self.resitele:
 | |
| 			dostal_body = False
 | |
| 			resiteluv_radek: list[tuple[Decimal,list[tuple[Reseni, Hodnoceni]]]] = [] # podle pořadí v self.problemy
 | |
| 			for problem in self.problemy:
 | |
| 				if problem in tabulka and resitel in tabulka[problem]:
 | |
| 					resiteluv_radek.append((soucty[problem][resitel], tabulka[problem][resitel]))
 | |
| 					dostal_body = True
 | |
| 				else:
 | |
| 					resiteluv_radek.append((Decimal(0),[]))
 | |
| 			if self.chteni_resitele != FiltrForm.RESITELE_RELEVANTNI or dostal_body:
 | |
| 				hodnoty.append(resiteluv_radek)
 | |
| 				resitele_do_tabulky.append(resitel)
 | |
| 		ctx['radky'] = list(zip(resitele_do_tabulky, hodnoty))
 | |
| 		ctx['filtr'] = FiltrForm(initial=self.request.GET, rocnik=self.aktualni_rocnik)
 | |
| 		# Pro použití hacku na automatické {{form.media}} v template:
 | |
| 		ctx['form'] = ctx['filtr']
 | |
| 		# Pro maximum v přesměrovátku ročníků
 | |
| 		ctx['aktualni_rocnik'] = Nastaveni.get_solo().aktualni_rocnik
 | |
| 		ctx['barvicky'] = self.barvicky
 | |
| 		if 'rocnik' in self.kwargs:
 | |
| 			ctx['rocnik'] = self.kwargs['rocnik']
 | |
| 		else:
 | |
| 			ctx['rocnik'] = ctx['aktualni_rocnik'].rocnik
 | |
| 
 | |
| 		return ctx
 | |
| 
 | |
| # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji?
 | |
| class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View):
 | |
| 	"""Rozskok mezi více řešeními téhož problému od téhož řešitele.
 | |
| 	
 | |
| 	Asi už bude zastaralý v okamžiku, kdy se tenhle komentář nasadí na produkci :-)
 | |
| 
 | |
| 	V případě, že takové řešení existuje jen jedno, tak na něj přesměruje."""
 | |
| 	model = Reseni
 | |
| 	template_name = 'odevzdavatko/seznam.html'
 | |
| 	
 | |
| 	def get_queryset(self):
 | |
| 		qs = super().get_queryset()
 | |
| 		resitel_id = self.kwargs['resitel']
 | |
| 		if resitel_id is None:
 | |
| 			raise ValueError("Nemám řešitele!")
 | |
| 		problem_id = self.kwargs['problem']
 | |
| 		if problem_id is None:
 | |
| 			raise ValueError("Nemám problém! (To je problém!)")
 | |
| 		
 | |
| 		resitel = Resitel.objects.get(id=resitel_id)
 | |
| 		problem = Problem.objects.get(id=problem_id)
 | |
| 		qs = qs.filter(
 | |
| 			problem__in=[problem],
 | |
| 			resitele__in=[resitel],
 | |
| 			)
 | |
| 		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)
 | |
| 
 | |
| 	def get_context_data(self, *args, **kwargs):
 | |
| 		ctx = super().get_context_data(*args, **kwargs)
 | |
| 		# XXX: Předat groupby do template nejde: https://stackoverflow.com/questions/6906593/itertools-groupby-in-a-django-template
 | |
| 		# Django má {% regroup %}, ale ten potřebuje, aby klíč byl atribut položky: https://docs.djangoproject.com/en/3.2/ref/templates/builtins/#regroup
 | |
| 		# Takže rozbalíme groupby do slovníku klíč → seznam sami (dictionary comphrehension)
 | |
| 		ctx['reseni_podle_deadlinu'] = {k: list(v) for k,v in groupby(ctx['object_list'], lambda r: r.deadline_reseni)}
 | |
| 
 | |
| 		# Pro sitetree:
 | |
| 		ctx["resitel_id"] = self.kwargs['resitel']
 | |
| 		ctx["problem_id"] = self.kwargs['problem']
 | |
| 		return ctx
 | |
| 
 | |
| HODNOCENI_INITIAL_DATA = [
 | |
| 	"problem",
 | |
| 	"body",
 | |
| 	"body_celkem",
 | |
| 	"body_neprepocitane",
 | |
| 	"body_neprepocitane_celkem",
 | |
| 	"body_max",
 | |
| 	"body_neprepocitane_max",
 | |
| 	"deadline_body",
 | |
| 	"feedback",
 | |
| ]
 | |
| ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
 | |
| class DetailReseniView(DetailView):
 | |
| 	""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
 | |
| 	model = Reseni
 | |
| 	template_name = 'odevzdavatko/detail.html'
 | |
| 	
 | |
| 	def aktualni_hodnoceni(self):
 | |
| 		self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk'])
 | |
| 		result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
 | |
| 		for hodn in Hodnoceni.objects.filter(reseni=self.reseni):
 | |
| 			result.append({attr: getattr(hodn, attr) for attr in HODNOCENI_INITIAL_DATA})
 | |
| 		return result
 | |
| 
 | |
| 	def get_context_data(self, **kw):
 | |
| 		self.check_access()
 | |
| 		ctx = super().get_context_data(**kw)
 | |
| 		detaily_hodnoceni = self.aktualni_hodnoceni()
 | |
| 		ctx["hodnoceni"] = detaily_hodnoceni
 | |
| 
 | |
| 		# Subject případného mailu (template neumí použitelně spojovat řetězce: https://stackoverflow.com/q/4386168)
 | |
| 		ctx["predmetmailu"] = "Oprava řešení M&M "+self.reseni.problem.first().hlavni_problem.nazev
 | |
| 		ctx["maily_vsech_resitelu"] = [y for x in self.reseni.resitele.all().values_list('osoba__email') for y in x]
 | |
| 		return ctx
 | |
| 
 | |
| 	def get(self, request, *args, **kwargs):
 | |
| 		"""
 | |
| 			Oproti :py:class:`django.views.generic.detail.BaseDetailView`
 | |
| 			kontroluje přístup pomocí :py:meth:`check_access`
 | |
| 		"""
 | |
| 		response = super().get(self, request, *args, **kwargs)
 | |
| 		self.check_access()
 | |
| 		return response
 | |
| 
 | |
| 	def check_access(self):
 | |
| 		""" Řešitel musí být součástí řešení, jinak se na něj nemá co dívat. Případně to může být org."""
 | |
| 		if not self.object.resitele.filter(osoba__user=self.request.user).exists() and not self.request.user.je_org:
 | |
| 			raise PermissionDenied()
 | |
| 
 | |
| 
 | |
| class EditReseniView(DetailReseniView):
 | |
| 	""" Editace (hlavně hodnocení) řešení.  """
 | |
| 	def get_context_data(self, **kw):
 | |
| 		ctx = super().get_context_data(**kw)
 | |
| 		ctx['form'] = f.OhodnoceniReseniFormSet(initial=ctx["hodnoceni"])
 | |
| 		ctx['poznamka_form'] = f.PoznamkaReseniForm(instance=self.reseni)
 | |
| 		ctx['edit'] = True
 | |
| 		return ctx
 | |
| 
 | |
| 	def check_access(self):
 | |
| 		# Na orga máme nároky už v urls.py ale better safe then sorry
 | |
| 		if not self.request.user.je_org:
 | |
| 			raise PermissionDenied()
 | |
| 
 | |
| 
 | |
| def hodnoceniReseniView(request, pk, *args, **kwargs):
 | |
| 	reseni = get_object_or_404(Reseni, pk=pk)
 | |
| 	success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
 | |
| 
 | |
| 	formset = f.OhodnoceniReseniFormSet(request.POST, initial=[
 | |
| 		{k: getattr(h, k) for k in HODNOCENI_INITIAL_DATA} for h in Hodnoceni.objects.filter(reseni=reseni)
 | |
| 	])
 | |
| 	poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni)
 | |
| 	# TODO: Napsat validaci formuláře a formsetu
 | |
| 	if not (formset.is_valid() and poznamka_form.is_valid()):
 | |
| 		raise ValueError(formset.errors, poznamka_form.errors)
 | |
| 
 | |
| 	with transaction.atomic():
 | |
| 		# Poznámka je jednoduchá na zpracování:
 | |
| 		poznamka_form.save()
 | |
| 
 | |
| 		# Smažeme všechna dosavadní hodnocení tohoto řešení
 | |
| 		qs = Hodnoceni.objects.filter(reseni=reseni)
 | |
| 		logger.info(f"Will delete {qs.count()} objects: {qs}")
 | |
| 		qs.delete()
 | |
| 
 | |
| 		# Vyrobíme nová podle formsetu
 | |
| 		notifikace = False
 | |
| 		for form in formset:
 | |
| 			notifikace |= 'feedback' in form.changed_data
 | |
| 			data_for_hodnoceni = form.cleaned_data
 | |
| 			data_for_body = data_for_hodnoceni.copy()
 | |
| 			del(data_for_hodnoceni["body_celkem"])
 | |
| 			del(data_for_hodnoceni["body_neprepocitane"])
 | |
| 			del(data_for_hodnoceni["body_neprepocitane_celkem"])
 | |
| 			hodnoceni = Hodnoceni(
 | |
| 					reseni=reseni,
 | |
| 					**form.cleaned_data,
 | |
| 					)
 | |
| 			logger.info(f"Creating Hodnoceni: {hodnoceni}")
 | |
| 			# FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak
 | |
| 			zmeny_bodu = [it for it in form.changed_data if it.startswith("body")]
 | |
| 			if len(zmeny_bodu) != 0:
 | |
| 				body_nastaveny: None | tuple[str, object] = None
 | |
| 				def nastav_body(jake, na_kolik):
 | |
| 					nonlocal body_nastaveny
 | |
| 					if body_nastaveny is not None:
 | |
| 						logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.")
 | |
| 						hodnoceni.body = -0.1
 | |
| 					else:
 | |
| 						body_nastaveny = (jake, na_kolik)
 | |
| 						hodnoceni.__setattr__(jake, na_kolik)
 | |
| 
 | |
| 				for key, value in data_for_body.items():
 | |
| 					if key.startswith("body") and value is not None:
 | |
| 						nastav_body(key, value)
 | |
| 
 | |
| 				# Něco se změnilo, ale nic není nastavené = něco bylo smazáno
 | |
| 				if body_nastaveny is None:
 | |
| 					hodnoceni.body = None
 | |
| 			hodnoceni.save()
 | |
| 
 | |
| 		adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True)
 | |
| 		if notifikace and adresati:
 | |
| 			email = EmailMessage(
 | |
| 					subject='Změna hodnocení odevzdaného řešení',
 | |
| 					body=f"""Milá řešitelko, milý řešiteli,
 | |
| 
 | |
| došlo ke změně zpětné vazby k Tebou odevzdanému řešení. Zobrazit si ji můžeš na {reseni.resitel_url()}.
 | |
| 
 | |
| Tvoji organizátoři M&M
 | |
| ---
 | |
| Nechceš-li tato upozornění dostávat, můžeš si to nastavit ve svém profilu.""",
 | |
| 					from_email='odevzdavatko@mam.mff.cuni.cz',
 | |
| 					bcc=adresati,
 | |
| 					)
 | |
| 			email.send()
 | |
| 
 | |
| 	return redirect(success_url)
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| class PrehledOdevzdanychReseni(ListView):
 | |
| 	model = Hodnoceni
 | |
| 	template_name = 'odevzdavatko/prehled_reseni.html'
 | |
| 
 | |
| 	def get_queryset(self):
 | |
| 		if not self.request.user.is_authenticated:
 | |
| 			raise RuntimeError("Uživatel měl být přihlášený!")
 | |
| 		# get_or_none, aby neexistence řešitele (např. u orgů) neházela chybu
 | |
| 		resitel = Resitel.objects.filter(osoba__user=self.request.user).first()
 | |
| 		qs = super().get_queryset()
 | |
| 		qs = qs.filter(reseni__resitele__in=[resitel])
 | |
| 		# Setřídíme podle času doručení řešení, aby se netřídily podle okamžiku vyrobení Hodnocení
 | |
| 		qs = qs.order_by('reseni__cas_doruceni')
 | |
| 		return qs
 | |
| 	
 | |
| 	def get_context_data(self, *args, **kwargs):
 | |
| 		ctx = super().get_context_data(*args, **kwargs)
 | |
| 		# Ročník určujeme podle čísla, do jehož deadlinu došlo řešení.
 | |
| 		# 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):
 | |
| 			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
 | |
| 
 | |
| # Přehled všech řešení kvůli debugování
 | |
| 
 | |
| class SeznamReseniView(ListView):
 | |
| 	model = Reseni
 | |
| 	template_name = 'odevzdavatko/seznam.html'
 | |
| 
 | |
| class SeznamAktualnichReseniView(SeznamReseniView):
 | |
| 	def get_queryset(self):
 | |
| 		qs = super().get_queryset()
 | |
| 		akt_rocnik = Nastaveni.get_solo().aktualni_rocnik	# .get_solo() vrátí tu jedinou instanci, asi...
 | |
| 		resitele = resi_v_rocniku(akt_rocnik)
 | |
| 		qs = qs.filter(resitele__in=resitele)	# FIXME: Najde řešení i ze starých ročníků, která odevzdal alespoň jeden aktuální řešitel
 | |
| 		return qs
 | |
| 
 | |
| 
 | |
| class VlozReseniView(LoginRequiredMixin, FormView):
 | |
| 	template_name = 'odevzdavatko/vloz_reseni.html'
 | |
| 	form_class = f.PosliReseniForm
 | |
| 
 | |
| 	def form_valid(self, form):
 | |
| 		data = form.cleaned_data
 | |
| 		nove_reseni = Reseni.objects.create(
 | |
| 			cas_doruceni=data['cas_doruceni'],
 | |
| 			forma=data['forma'],
 | |
| 			poznamka=data['poznamka'],
 | |
| 		)
 | |
| 		nove_reseni.resitele.add(*data['resitel'])
 | |
| 		nove_reseni.problem.add(*data['problem'])
 | |
| 		nove_reseni.save()
 | |
| 
 | |
| 		context = self.get_context_data()
 | |
| 		prilohy = context['prilohy']
 | |
| 		prilohy.instance = nove_reseni
 | |
| 		prilohy.save()
 | |
| 		# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil.
 | |
| 		return redirect(reverse('profil'))
 | |
| 
 | |
| 
 | |
| 	def get_context_data(self,**kwargs):
 | |
| 		data = super().get_context_data(**kwargs)
 | |
| 		if self.request.POST:
 | |
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
 | |
| 		else:
 | |
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet()
 | |
| 		return data
 | |
| 
 | |
| 
 | |
| class NahrajReseniRozcestnikTematekView(LoginRequiredMixin, ListView):
 | |
| 	model = Problem
 | |
| 	template_name = 'odevzdavatko/nahraj_reseni_nadproblem.html'
 | |
| 
 | |
| 	def get_queryset(self):
 | |
| 		return super().get_queryset().filter(stav=Problem.STAV_ZADANY, nadproblem__isnull=True)
 | |
| 
 | |
| 
 | |
| class NahrajReseniView(LoginRequiredMixin, CreateView):
 | |
| 	model = Reseni
 | |
| 	template_name = 'odevzdavatko/nahraj_reseni.html'
 | |
| 	form_class = f.NahrajReseniForm
 | |
| 	nadproblem: Problem
 | |
| 
 | |
| 	def setup(self, request, *args, **kwargs):
 | |
| 		super().setup(request, *args, **kwargs)
 | |
| 		nadproblem_id = self.kwargs["nadproblem_id"]
 | |
| 		self.nadproblem = get_object_or_404(Problem, id=nadproblem_id)
 | |
| 
 | |
| 	def get(self, request, *args, **kwargs):
 | |
| 		# Zaříznutí nezadaných problémů
 | |
| 		if self.nadproblem.stav != Problem.STAV_ZADANY:
 | |
| 			raise PermissionDenied()
 | |
| 
 | |
| 
 | |
| 		# Zaříznutí starých řešitelů:
 | |
| 		# FIXME: Je to tady dost naprasené, mělo by to asi být jinde…
 | |
| 		osoba = Osoba.objects.get(user=self.request.user)
 | |
| 		resitel = osoba.resitel
 | |
| 		if resitel.rok_maturity <= Nastaveni.get_solo().aktualni_rocnik.prvni_rok:
 | |
| 			return render(request, 'universal.html', {
 | |
| 				'title': 'Nelze odevzdat',
 | |
| 				'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.',
 | |
| 				'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.',
 | |
| 			})
 | |
| 		return super().get(request, *args, **kwargs)
 | |
| 
 | |
| 	def get_initial(self):
 | |
| 		nadproblem_id = self.nadproblem.id
 | |
| 		return {
 | |
| 			"nadproblem_id": nadproblem_id,
 | |
| 			"problem": [] if self.nadproblem.podproblem.filter(stav=Problem.STAV_ZADANY).exists() else nadproblem_id
 | |
| 
 | |
| 		}
 | |
| 
 | |
| 	def get_context_data(self,**kwargs):
 | |
| 		data = super().get_context_data(**kwargs)
 | |
| 		if self.request.POST:
 | |
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES)
 | |
| 		else:
 | |
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet()
 | |
| 
 | |
| 		data["nadproblem_id"] = self.nadproblem.id
 | |
| 		data["nadproblem"] = get_object_or_404(Problem, id=self.nadproblem.id)
 | |
| 		return data
 | |
| 
 | |
| 	# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni
 | |
| 	# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset
 | |
| 	def form_valid(self,form):
 | |
| 		context = self.get_context_data()
 | |
| 		prilohy = context['prilohy']
 | |
| 		if not prilohy.is_valid():
 | |
| 			return super().form_invalid(form)
 | |
| 		with transaction.atomic():
 | |
| 			self.object = form.save()
 | |
| 			self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user))
 | |
| 			self.object.resitele.add(*form.cleaned_data["resitele"])
 | |
| 			self.object.cas_doruceni = timezone.now()
 | |
| 			self.object.forma = Reseni.FORMA_UPLOAD
 | |
| 			self.object.save()
 | |
| 
 | |
| 			prilohy.instance = self.object
 | |
| 			prilohy.save()
 | |
| 
 | |
| 		for hodnoceni in self.object.hodnoceni_set.all():
 | |
| 			hodnoceni.deadline_body = Deadline.objects.filter(deadline__gte=self.object.cas_doruceni).first()
 | |
| 			hodnoceni.save()
 | |
| 
 | |
| 		# Pošleme mail opravovatelům a garantovi
 | |
| 		# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí
 | |
| 		prijemci = set()
 | |
| 		problemy = []
 | |
| 		for prob in form.cleaned_data['problem']:
 | |
| 			prijemci.update(prob.opravovatele.all())
 | |
| 			if prob.garant is not None:
 | |
| 				prijemci.add(prob.garant)
 | |
| 			problemy.append(prob)
 | |
| 		# FIXME: Možná poslat mail i relevantním orgům nadproblémů?
 | |
| 		if len(prijemci) < 1:
 | |
| 			logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}")
 | |
| 		# FIXME: Víc informativní obsah mailů, možná vč. příloh?
 | |
| 		prijemci = map(lambda it: it.osoba.email, prijemci)
 | |
| 
 | |
| 		resitel = Osoba.objects.get(user = self.request.user)
 | |
| 
 | |
| 		seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy))
 | |
| 		seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })")
 | |
| 
 | |
| 		EmailMessage(
 | |
| 			subject="Nové řešení k " + seznam_do_subjectu,
 | |
| 			body=f"{resitel} posílá nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }",
 | |
| 			from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení?
 | |
| 			to=list(prijemci),
 | |
| 		).send()
 | |
| 
 | |
| 		return formularOKView(
 | |
| 			self.request,
 | |
| 			text='Řešení úspěšně odevzdáno',
 | |
| 			dalsi_odkazy=[("Odevzdat další řešení", reverse("odevzdavatko_nahraj_reseni"))],
 | |
| 		)
 |