Merge remote-tracking branch 'origin/data_migrations' into treenode_editor
This commit is contained in:
		
						commit
						c11abf3c7a
					
				
					 14 changed files with 1166 additions and 63 deletions
				
			
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -295,6 +295,9 @@ LOGGING = { | ||||||
|             }, |             }, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | # Permissions for uploads | ||||||
|  | FILE_UPLOAD_PERMISSIONS = 0o0644 | ||||||
|  | 
 | ||||||
| # MaM specific | # MaM specific | ||||||
| 
 | 
 | ||||||
| SEMINAR_RESENI_DIR = os.path.join('reseni') | SEMINAR_RESENI_DIR = os.path.join('reseni') | ||||||
|  |  | ||||||
|  | @ -747,15 +747,18 @@ class Problem(SeminarModelBase,PolymorphicModel): | ||||||
| 	def verejne(self): | 	def verejne(self): | ||||||
| 		# aktuálně podle stavu problému | 		# aktuálně podle stavu problému | ||||||
| 		# FIXME pro některé problémy možná chceme override | 		# FIXME pro některé problémy možná chceme override | ||||||
|  | 		# FIXME vrací veřejnost čistě problému, nezávisle na čísle, ve kterém je.  | ||||||
|  | 		# Je to tak správně? | ||||||
| 		stav_verejny = False | 		stav_verejny = False | ||||||
| 		if self.stav == 'zadany' or self.stav == 'vyreseny': | 		if self.stav == 'zadany' or self.stav == 'vyreseny': | ||||||
| 			stav_verejny = True | 			stav_verejny = True | ||||||
|  | 		return stav_verejny | ||||||
| 		 | 		 | ||||||
| 		cislo_verejne = False | 		#cislo_verejne = False | ||||||
| 		if (self.cislo_zadani and self.cislo_zadani.verejne()): | 		#if (self.cislo_zadani and self.cislo_zadani.verejne()): | ||||||
| 			cislo_verejne = True | 		#	cislo_verejne = True | ||||||
| 		 | 		 | ||||||
| 		return (stav_verejny and cislo_verejne) | 		#return (stav_verejny and cislo_verejne) | ||||||
| 	verejne.boolean = True | 	verejne.boolean = True | ||||||
| 
 | 
 | ||||||
| 	def verejne_url(self): | 	def verejne_url(self): | ||||||
|  | @ -993,7 +996,7 @@ def aux_generate_filename(self, filename): | ||||||
| 		unidecode(filename.replace('/', '-').replace('\0', '')) | 		unidecode(filename.replace('/', '-').replace('\0', '')) | ||||||
| 	) | 	) | ||||||
| 	datedir = timezone.now().strftime('%Y-%m') | 	datedir = timezone.now().strftime('%Y-%m') | ||||||
| 	fname = "{}_{}".format( | 	fname = "{}/{}".format( | ||||||
| 		timezone.now().strftime('%Y-%m-%d-%H:%M'), | 		timezone.now().strftime('%Y-%m-%d-%H:%M'), | ||||||
| 		clean) | 		clean) | ||||||
| 	return os.path.join(datedir, fname) | 	return os.path.join(datedir, fname) | ||||||
|  | @ -1046,6 +1049,11 @@ class PrilohaReseni(SeminarModelBase): | ||||||
| 	def __str__(self): | 	def __str__(self): | ||||||
| 		return str(self.soubor) | 		return str(self.soubor) | ||||||
| 
 | 
 | ||||||
|  | 	def split(self): | ||||||
|  | 		"Vrátí cestu rozsekanou po složkách. To se hodí v templatech" | ||||||
|  | 		# Věřím, že tohle funguje, případně použít os.path nebo pathlib. | ||||||
|  | 		return self.soubor.url.split('/') | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class Pohadka(SeminarModelBase): | class Pohadka(SeminarModelBase): | ||||||
| 	"""Kus pohádky před/za úlohou v čísle""" | 	"""Kus pohádky před/za úlohou v čísle""" | ||||||
|  |  | ||||||
							
								
								
									
										47
									
								
								seminar/templates/seminar/odevzdavatko/detail.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								seminar/templates/seminar/odevzdavatko/detail.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | ||||||
|  | {% extends "base.html" %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | 
 | ||||||
|  | <p>Řešené problémy: {{ object.problem.all | join:", " }}</p> | ||||||
|  | 
 | ||||||
|  | <p>Řešitelé: {{ object.resitele.all | join:", " }}</p> | ||||||
|  | 
 | ||||||
|  | {# https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.get_FOO_display #} | ||||||
|  | <p>Forma: {{ object.get_forma_display }}, doručeno {{ object.cas_doruceni }}</p> | ||||||
|  | 
 | ||||||
|  | {# Soubory: #} | ||||||
|  | <h3>Přílohy:</h3> | ||||||
|  | {% if object.prilohy.all %} | ||||||
|  | <table> | ||||||
|  | <tr><th>Soubor</th><th>Řešitelova poznámka</th><th>Datum</th></tr> | ||||||
|  | {% for priloha in object.prilohy.all %} | ||||||
|  | <tr> | ||||||
|  | 	<td><a href="{{ priloha.soubor.url }}" download>{{ priloha.split | last }}</a></td> | ||||||
|  | 	<td>{{ priloha.res_poznamka }}</td> | ||||||
|  | 	<td>{{ priloha.vytvoreno }}</td></tr> | ||||||
|  | 	{# TODO: Orgo-poznámka, ideálně jako formulář #} | ||||||
|  | {% endfor %} | ||||||
|  | </table> | ||||||
|  | {% else %} | ||||||
|  | <p>Žádné přílohy</p> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | {# Hodnocení: #} | ||||||
|  | {# FIXME: Udělat jako formulář #} | ||||||
|  | <h3>Hodnocení:</h3> | ||||||
|  | {% if object.hodnoceni_set.all %} | ||||||
|  | <table> | ||||||
|  | <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> | ||||||
|  | {% endfor %} | ||||||
|  | </table> | ||||||
|  | {% else %} | ||||||
|  | <p>Ještě nebylo hodnoceno</p> | ||||||
|  | {% endif %} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										11
									
								
								seminar/templates/seminar/odevzdavatko/seznam.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								seminar/templates/seminar/odevzdavatko/seznam.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | {% extends "base.html" %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | 
 | ||||||
|  | <ul> | ||||||
|  | 	{% for obj in object_list %} | ||||||
|  | 	<li><a href="{% url 'odevzdavatko_detail_reseni' pk=obj.id %}">{{ obj }}</a> ({{ obj.get_forma_display }} {{ obj.cas_doruceni }}) | ||||||
|  | 	{% endfor %} | ||||||
|  | </ul> | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										36
									
								
								seminar/templates/seminar/odevzdavatko/tabulka.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								seminar/templates/seminar/odevzdavatko/tabulka.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | {% extends "base.html" %} | ||||||
|  | 
 | ||||||
|  | {% load utils %} {# Možná by mohlo být někde výš v hierarchii templatů... #} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | 
 | ||||||
|  | <table> | ||||||
|  | 	<tr> | ||||||
|  | 		<td></td> {# Prázdná buňka v levém horním rohu #} | ||||||
|  | 		{% for p in problemy %} | ||||||
|  | 		<th> | ||||||
|  | 			{# TODO: Přehled řešení k problému, odkázaný odsud? #} | ||||||
|  | 			{{ p }} | ||||||
|  | 		</th> | ||||||
|  | 		{% endfor %} | ||||||
|  | 	</tr> | ||||||
|  | 	{% for resitel,hodnoty in radky%} | ||||||
|  | 	<tr> | ||||||
|  | 		<td> | ||||||
|  | 			{# TODO: Chceme mít view i na řešení konkrétního řešitele ke všem problémům? #} | ||||||
|  | 			{{ resitel }} | ||||||
|  | 		</td> | ||||||
|  | 		{% for hodn in hodnoty %} | ||||||
|  | 			<td> | ||||||
|  | 			{% if hodn %} | ||||||
|  | 			<a href="{% url 'odevzdavatko_reseni_resitele_k_problemu' problem=hodn.problem_id resitel=hodn.resitel_id %}"> | ||||||
|  | 				{{ hodn.pocet_reseni }} řeš.<br>{{ hodn.body }} b<br>{{ hodn.posledni_odevzdani|kratke_datum|default_if_none:"Nikdy"|default:"???"}} | ||||||
|  | 			</a> | ||||||
|  | 			{% endif %} | ||||||
|  | 			</td> | ||||||
|  | 		{% endfor %} | ||||||
|  | 	</tr> | ||||||
|  | 	{% endfor %} | ||||||
|  | </table> | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										27
									
								
								seminar/templatetags/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								seminar/templatetags/utils.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | from django import template | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from pytz import timezone | ||||||
|  | from mamweb.settings import TIME_ZONE | ||||||
|  | import logging | ||||||
|  | register = template.Library() | ||||||
|  | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | @register.filter(name='kratke_datum', expects_localtime=True) | ||||||
|  | def kratke_datum(dt): | ||||||
|  | 	# None dává None, ne-datum dává False, aby se daly použít filtry typu "default". | ||||||
|  | 	if dt is None: | ||||||
|  | 		return None | ||||||
|  | 	if not isinstance(dt, datetime): | ||||||
|  | 		logger.warning(f"Špatné volání filtru {__name__}: {dt}") | ||||||
|  | 		return False | ||||||
|  | 	naive_now = datetime.now() | ||||||
|  | 	tz = timezone(TIME_ZONE) | ||||||
|  | 	now = tz.localize(naive_now) | ||||||
|  | 	delta = now - dt | ||||||
|  | 	if delta <= timedelta(days=1): | ||||||
|  | 		return dt.strftime("%k:%M") | ||||||
|  | 	if delta <= timedelta(days=365):	# Timedelta neumí vyjádřit 1 rok | ||||||
|  | 		return dt.strftime("%d. %m.") | ||||||
|  | 	return dt.strftime("%d. %m. %Y") | ||||||
|  | 
 | ||||||
|  | @ -168,5 +168,10 @@ urlpatterns = [ | ||||||
| 	# org_member_required(views.OrganizatorAutocomplete.as_view()), | 	# org_member_required(views.OrganizatorAutocomplete.as_view()), | ||||||
| 	# name='seminar_autocomplete_organizator') | 	# name='seminar_autocomplete_organizator') | ||||||
| 
 | 
 | ||||||
|  | 	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/all', org_required(views.SeznamReseniView.as_view())), | ||||||
|  | 	path('temp/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())), | ||||||
| 
 | 
 | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -148,16 +148,12 @@ def resi_v_rocniku(rocnik, cislo=None): | ||||||
| 
 | 
 | ||||||
| 	if cislo is None: | 	if cislo is None: | ||||||
| 		# filtrujeme pouze podle ročníku | 		# filtrujeme pouze podle ročníku | ||||||
| 		letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik) | 		return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(), | ||||||
|  | 										reseni__hodnoceni__cislo_body__rocnik=rocnik).distinct() | ||||||
| 	else:  # filtrujeme podle ročníku i čísla | 	else:  # filtrujeme podle ročníku i čísla | ||||||
| 		letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik, | 		return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(), | ||||||
| 												 hodnoceni__cislo_body__poradi__lte=cislo.poradi) | 										reseni__hodnoceni__cislo_body__rocnik=rocnik, | ||||||
| 
 | 										reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi).distinct() | ||||||
| 	# vygenerujeme queryset řešitelů, co letos něco poslali |  | ||||||
| 	letosni_resitele = m.Resitel.objects.none() |  | ||||||
| 	for reseni in letosni_reseni: |  | ||||||
| 		letosni_resitele = letosni_resitele | reseni.resitele.filter(rok_maturity__gte=rocnik.druhy_rok()) |  | ||||||
| 	return letosni_resitele.distinct() |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def aktivniResitele(cislo, pouze_letosni=False): | def aktivniResitele(cislo, pouze_letosni=False): | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
| from .views_all import * | from .views_all import * | ||||||
| from .autocomplete import * | from .autocomplete import * | ||||||
| from .views_rest import * | from .views_rest import * | ||||||
|  | from .odevzdavatko import * | ||||||
|  |  | ||||||
							
								
								
									
										129
									
								
								seminar/views/odevzdavatko.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								seminar/views/odevzdavatko.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | ||||||
|  | from django.views.generic import ListView, DetailView | ||||||
|  | from django.views.generic.base import TemplateView | ||||||
|  | 
 | ||||||
|  | from dataclasses import dataclass | ||||||
|  | import datetime | ||||||
|  | 
 | ||||||
|  | import seminar.models as m | ||||||
|  | from seminar.utils import aktivniResitele, resi_v_rocniku | ||||||
|  | 
 | ||||||
|  | # 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 | ||||||
|  | # | ||||||
|  | # Taky se může hodit: | ||||||
|  | # - Tabulka všech řešitelů x všech problémů? | ||||||
|  | 
 | ||||||
|  | @dataclass | ||||||
|  | class SouhrnReseni: | ||||||
|  | 	"""Dataclass reprezentující data o odevzdaných řešeních pro zobrazení v tabulce.""" | ||||||
|  | 	pocet_reseni : int | ||||||
|  | 	posledni_odevzdani : datetime.datetime | ||||||
|  | 	body : float | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TabulkaOdevzdanychReseniView(ListView): | ||||||
|  | 	template_name = 'seminar/odevzdavatko/tabulka.html' | ||||||
|  | 	model = m.Hodnoceni | ||||||
|  | 
 | ||||||
|  | 	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.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() | ||||||
|  | 
 | ||||||
|  | 		qs = super().get_queryset() | ||||||
|  | 		qs = qs.filter(problem__in=self.zadane_problemy).select_related('reseni', 'problem').prefetch_related('reseni__resitele__osoba') | ||||||
|  | 		return qs | ||||||
|  | 
 | ||||||
|  | 	def get_context_data(self, *args, **kwargs): | ||||||
|  | 		# FIXME: Tenhle blok nemůže být přímo ve třídě, protože před vyrobením databáze neexistuje Nastavení. | ||||||
|  | 		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() | ||||||
|  | 
 | ||||||
|  | 		ctx = super().get_context_data(*args, **kwargs) | ||||||
|  | 		ctx['problemy'] = self.zadane_problemy | ||||||
|  | 		ctx['resitele'] = self.resitele | ||||||
|  | 		tabulka = dict() | ||||||
|  | 
 | ||||||
|  | 		def pridej_reseni(problem, resitel, body, cas): | ||||||
|  | 			if problem not in tabulka: | ||||||
|  | 				tabulka[problem] = dict() | ||||||
|  | 			if resitel not in tabulka[problem]: | ||||||
|  | 				tabulka[problem][resitel] = SouhrnReseni(pocet_reseni=1, posledni_odevzdani=cas, body=body) | ||||||
|  | 			else: | ||||||
|  | 				tabulka[problem][resitel].posledni_odevzdani = max(tabulka[problem][resitel].posledni_odevzdani, cas) | ||||||
|  | 				tabulka[problem][resitel].body = max(tabulka[problem][resitel].body, body, | ||||||
|  | 					key=lambda x: x if x is not None else -1 # None je malé číslo | ||||||
|  | 					# FIXME: Možná dává smysl i mít None jako velké číslo -- jakože "TODO: zadat body" | ||||||
|  | 					) | ||||||
|  | 				tabulka[problem][resitel].pocet_reseni += 1 | ||||||
|  | 			# Pro jednoduchost template si ještě poznamenáme ID problému a řešitele | ||||||
|  | 			tabulka[problem][resitel].problem_id = problem.id | ||||||
|  | 			tabulka[problem][resitel].resitel_id = resitel.id | ||||||
|  | 		 | ||||||
|  | 		for hodnoceni in self.get_queryset(): | ||||||
|  | 			for resitel in hodnoceni.reseni.resitele.all(): | ||||||
|  | 				pridej_reseni(hodnoceni.problem, resitel, hodnoceni.body, hodnoceni.reseni.cas_doruceni) | ||||||
|  | 
 | ||||||
|  | 		hodnoty = [] | ||||||
|  | 		for resitel in self.resitele: | ||||||
|  | 			resiteluv_radek = [] | ||||||
|  | 			for problem in self.zadane_problemy: | ||||||
|  | 				if problem in tabulka and resitel in tabulka[problem]: | ||||||
|  | 					resiteluv_radek.append(tabulka[problem][resitel]) | ||||||
|  | 				else: | ||||||
|  | 					resiteluv_radek.append(None) | ||||||
|  | 			hodnoty.append(resiteluv_radek) | ||||||
|  | 		ctx['radky'] = list(zip(self.resitele, hodnoty)) | ||||||
|  | 
 | ||||||
|  | 		return ctx | ||||||
|  | 
 | ||||||
|  | class ReseniProblemuView(ListView): | ||||||
|  | 	model = m.Reseni | ||||||
|  | 	template_name = 'seminar/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 = m.Resitel.objects.get(id=resitel_id) | ||||||
|  | 		problem = m.Problem.objects.get(id=problem_id) | ||||||
|  | 		qs = qs.filter( | ||||||
|  | 			problem__in=[problem], | ||||||
|  | 			resitele__in=[resitel], | ||||||
|  | 			) | ||||||
|  | 		return qs | ||||||
|  | 	 | ||||||
|  | 	# Kontext automaticky? | ||||||
|  | 
 | ||||||
|  | class DetailReseniView(DetailView): | ||||||
|  | 	model = m.Reseni | ||||||
|  | 	template_name = 'seminar/odevzdavatko/detail.html' | ||||||
|  | 	# To je všechno? Najde se to podle pk... | ||||||
|  | 
 | ||||||
|  | # Přehled všech řešení kvůli debugování | ||||||
|  | 
 | ||||||
|  | class SeznamReseniView(ListView): | ||||||
|  | 	model = m.Reseni | ||||||
|  | 	template_name = 'seminar/odevzdavatko/seznam.html' | ||||||
|  | 
 | ||||||
|  | class SeznamAktualnichReseniView(SeznamReseniView): | ||||||
|  | 	def get_queryset(self): | ||||||
|  | 		qs = super().get_queryset() | ||||||
|  | 		akt_rocnik = m.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 | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| # coding:utf-8 | 
 | ||||||
| 
 | 
 | ||||||
| from django.shortcuts import get_object_or_404, render, redirect | from django.shortcuts import get_object_or_404, render, redirect | ||||||
| from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse | from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, JsonResponse | ||||||
|  | @ -17,6 +17,7 @@ from django.contrib.auth.models import User, Permission | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.core import serializers | from django.core import serializers | ||||||
|  | from django.core.exceptions import PermissionDenied | ||||||
| from django.forms.models import model_to_dict | from django.forms.models import model_to_dict | ||||||
| 
 | 
 | ||||||
| import seminar.models as s | import seminar.models as s | ||||||
|  | @ -120,15 +121,57 @@ class TNLData(object): | ||||||
| 			self.appendable_siblings = tnltt.appendableChildren(self.parent) | 			self.appendable_siblings = tnltt.appendableChildren(self.parent) | ||||||
| 		else: | 		else: | ||||||
| 			self.appendable_siblings = [] | 			self.appendable_siblings = [] | ||||||
|  | 	@classmethod | ||||||
|  | 	def public_above(cls, anode): | ||||||
|  | 		""" Returns output of verejne for closest Rocnik, Cislo or Problem above. | ||||||
|  | 		(All of them have method verejne.)""" | ||||||
|  | 		parent = anode # chceme začít už od konkrétního node včetně | ||||||
|  | 		while True: | ||||||
|  | 			rocnik = isinstance(parent, s.RocnikNode) | ||||||
|  | 			cislo = isinstance(parent, s.CisloNode) | ||||||
|  | 			uloha = (isinstance(parent, s.UlohaVzorakNode) or  | ||||||
|  | 				isinstance(parent, s.UlohaZadaniNode)) | ||||||
|  | 			tema = isinstance(parent, s.TemaVCisleNode) | ||||||
| 
 | 
 | ||||||
| 
 | 			if (rocnik or cislo or uloha or tema) or parent==None: | ||||||
|  | 				break | ||||||
|  | 			else: | ||||||
|  | 				parent = treelib.get_parent(parent) | ||||||
|  | 		if rocnik: | ||||||
|  | 			return parent.rocnik.verejne() | ||||||
|  | 		elif cislo: | ||||||
|  | 			return parent.cislo.verejne() | ||||||
|  | 		elif uloha: | ||||||
|  | 			return parent.uloha.verejne() | ||||||
|  | 		elif tema: | ||||||
|  | 			return parent.tema.verejne() | ||||||
|  | 		elif None: | ||||||
|  | 			print("Existuje TreeNode, který není pod číslem, ročníkem, úlohou" | ||||||
|  | 			"ani tématem. {}".format(anode)) | ||||||
|  | 			return False | ||||||
| 	 | 	 | ||||||
| 	@classmethod | 	@classmethod | ||||||
| 	def from_treenode(cls,anode,parent=None,index=None): | 	def all_public_children(cls, anode): | ||||||
|  | 		for ch in treelib.all_children(anode): | ||||||
|  | 			if TNLData.public_above(ch): | ||||||
|  | 				yield ch | ||||||
|  | 			else: | ||||||
|  | 				continue | ||||||
|  | 
 | ||||||
|  | 	@classmethod | ||||||
|  | 	def from_treenode(cls, anode, user, parent=None, index=None): | ||||||
|  | 		if TNLData.public_above(anode) or user.has_perm('auth.org'): | ||||||
| 			out = cls(anode,parent,index) | 			out = cls(anode,parent,index) | ||||||
| 		for (idx,ch) in enumerate(treelib.all_children(anode)): | 		else: | ||||||
| 			# FIXME přidat filtrování na veřejnost | 			raise PermissionDenied() | ||||||
| 			outitem = cls.from_treenode(ch,out,idx) | 
 | ||||||
|  | 		if user.has_perm('auth.org'): | ||||||
|  | 			enum_children = enumerate(treelib.all_children(anode)) | ||||||
|  | 		else: | ||||||
|  | 			enum_children = enumerate(TNLData.all_public_children(anode))	 | ||||||
|  | 	 | ||||||
|  | 		for (idx,ch) in enum_children: | ||||||
|  | 			outitem = cls.from_treenode(ch, user, out,  idx) | ||||||
| 			out.children.append(outitem) | 			out.children.append(outitem) | ||||||
| 		out.add_edit_options() | 		out.add_edit_options() | ||||||
| 		return out | 		return out | ||||||
|  | @ -195,7 +238,7 @@ class TreeNodeView(generic.DetailView): | ||||||
| 
 | 
 | ||||||
| 	def get_context_data(self,**kwargs): | 	def get_context_data(self,**kwargs): | ||||||
| 		context = super().get_context_data(**kwargs) | 		context = super().get_context_data(**kwargs) | ||||||
| 		context['tnldata'] = TNLData.from_treenode(self.object) | 		context['tnldata'] = TNLData.from_treenode(self.object,self.request.user) | ||||||
| 		return context | 		return context | ||||||
| 
 | 
 | ||||||
| class TreeNodeJSONView(generic.DetailView): | class TreeNodeJSONView(generic.DetailView): | ||||||
|  | @ -203,7 +246,7 @@ class TreeNodeJSONView(generic.DetailView): | ||||||
| 
 | 
 | ||||||
| 	def get(self,request,*args, **kwargs): | 	def get(self,request,*args, **kwargs): | ||||||
| 		self.object = self.get_object() | 		self.object = self.get_object() | ||||||
| 		data = TNLData.from_treenode(self.object).to_json() | 		data = TNLData.from_treenode(self.object,self.request.user).to_json() | ||||||
| 		return JsonResponse(data) | 		return JsonResponse(data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -332,6 +375,7 @@ class ProblemView(generic.DetailView): | ||||||
| 
 | 
 | ||||||
| 	def get_context_data(self, **kwargs): | 	def get_context_data(self, **kwargs): | ||||||
| 		context = super().get_context_data(**kwargs) | 		context = super().get_context_data(**kwargs) | ||||||
|  | 		user = self.request.user | ||||||
| 		# Teď potřebujeme doplnit tnldata do kontextu. | 		# Teď potřebujeme doplnit tnldata do kontextu. | ||||||
| 		# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME. | 		# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME. | ||||||
| 		if False: | 		if False: | ||||||
|  | @ -339,11 +383,11 @@ class ProblemView(generic.DetailView): | ||||||
| 			pass | 			pass | ||||||
| 		elif isinstance(self.object, s.Clanek) or  isinstance(self.object, s.Konfera): | 		elif isinstance(self.object, s.Clanek) or  isinstance(self.object, s.Konfera): | ||||||
| 			# Tyhle Problémy mají ŘešeníNode | 			# Tyhle Problémy mají ŘešeníNode | ||||||
| 			context['tnldata'] = TNLData.from_treenode(self.object.reseninode) | 			context['tnldata'] = TNLData.from_treenode(self.object.reseninode,user) | ||||||
| 		elif isinstance(self.object, s.Uloha): | 		elif isinstance(self.object, s.Uloha): | ||||||
| 			# FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever | 			# FIXME: Teď vždycky zobrazujeme i vzorák! Možná by bylo hezčí/lepší mít to stejně jako pro Téma: procházet jen dosažitelné z Ročníku / čísla / whatever | ||||||
| 			tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode) | 			tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode,user) | ||||||
| 			tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode) | 			tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode,user) | ||||||
| 			context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak]) | 			context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak]) | ||||||
| 		elif isinstance(self.object, s.Tema): | 		elif isinstance(self.object, s.Tema): | ||||||
| 			rocniknode = self.object.rocnik.rocniknode | 			rocniknode = self.object.rocnik.rocniknode | ||||||
|  |  | ||||||
|  | @ -21,31 +21,6 @@ class PermissionMixin(object): | ||||||
| 		# návštěvník nemusí být zalogován, aby si prohlížel obsah | 		# návštěvník nemusí být zalogován, aby si prohlížel obsah | ||||||
| 		return [permission() for permission in permission_classes] | 		return [permission() for permission in permission_classes] | ||||||
| 
 | 
 | ||||||
| 	def verejne_nad(self, node): |  | ||||||
| 		""" Returns output of verejne for closest Rocnik, Cislo or Problem above. |  | ||||||
| 		(All of them have method verejne.)""" |  | ||||||
| 		parent = get_parent(node) |  | ||||||
| 		while True: |  | ||||||
| 			rocnik = isinstance(parent, RocnikNode) |  | ||||||
| 			cislo = isinstance(parent, CisloNode) |  | ||||||
| 			problem = isinstance(parent, ProblemNode) |  | ||||||
| 
 |  | ||||||
| 			if (rocnik or cislo or problem): |  | ||||||
| 				break |  | ||||||
| 			else: |  | ||||||
| 				parent = get_parent(parent) |  | ||||||
| 		if rocnik: |  | ||||||
| 			return parent.rocnik.verejne() |  | ||||||
| 		elif cislo: |  | ||||||
| 			return parent.cislo.verejne() |  | ||||||
| 		elif problem: |  | ||||||
| 			return parent.problem.verjne() |  | ||||||
| 
 |  | ||||||
| 	def has_object_permission(self, request, view, obj): |  | ||||||
| 		# test that obj is Node |  | ||||||
| 		assert isinstance(obj, Node) |  | ||||||
| 		return verejne_nad(node) |  | ||||||
| 
 |  | ||||||
| class ReadWriteSerializerMixin(object): | class ReadWriteSerializerMixin(object): | ||||||
| 	""" | 	""" | ||||||
| 	Overrides get_serializer_class to choose the read serializer | 	Overrides get_serializer_class to choose the read serializer | ||||||
|  | @ -124,6 +99,12 @@ class UlohaVzorakNodeViewSet(PermissionMixin, ReadWriteSerializerMixin, viewsets | ||||||
| 		nazev = self.request.query_params.get('nazev',None) | 		nazev = self.request.query_params.get('nazev',None) | ||||||
| 		if nazev is not None: | 		if nazev is not None: | ||||||
| 			queryset = queryset.filter(nazev__contains=nazev) | 			queryset = queryset.filter(nazev__contains=nazev) | ||||||
|  | 
 | ||||||
|  | 		if self.request.user.has_perm('auth.org'): | ||||||
|  | 			return queryset | ||||||
|  | 		else: # pro neorgy jen zveřejněné vzoráky | ||||||
|  | 			return queryset.filter(uloha__cislo_reseni__verejne_db=True) | ||||||
|  | 
 | ||||||
| 		nadproblem = self.request.query_params.get('nadproblem',None) | 		nadproblem = self.request.query_params.get('nadproblem',None) | ||||||
| 		if nadproblem is not None: | 		if nadproblem is not None: | ||||||
| 			queryset = queryset.filter(nadproblem__pk = nadproblem) | 			queryset = queryset.filter(nadproblem__pk = nadproblem) | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ export default new Router({ | ||||||
| 	}, { | 	}, { | ||||||
| 		path: '/zadani/aktualni', | 		path: '/zadani/aktualni', | ||||||
| 		name: 'treenode_zadani', | 		name: 'treenode_zadani', | ||||||
| 		props: {'tnid': 23}, | 		props: {'tnid': 1655}, | ||||||
| 		component: TreeNodeRoot | 		component: TreeNodeRoot | ||||||
| 	}, { | 	}, { | ||||||
| 		path: '/cislo/:cislo', | 		path: '/cislo/:cislo', | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue