Merge branch 'odevzdavatko' into data_migrations
Je to WIP, ale jako sneak-peek dobrý :-)
This commit is contained in:
		
						commit
						0b32d312da
					
				
					 6 changed files with 209 additions and 43 deletions
				
			
		|  | @ -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,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 %} | ||||
|  |  | |||
|  | @ -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())), | ||||
| 
 | ||||
| ] | ||||
|  |  | |||
|  | @ -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() | ||||
| 
 | ||||
|  |  | |||
|  | @ -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í | ||||
| 
 | ||||
|  |  | |||
|  | @ -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…
	
		Reference in a new issue
	
	 Pavel "LEdoian" Turinsky
						Pavel "LEdoian" Turinsky