Merge branch 'data_migrations' of gimli.ms.mff.cuni.cz:/akce/mam/git/mamweb into data_migrations
This commit is contained in:
		
						commit
						d5c4884a35
					
				
					 19 changed files with 1341 additions and 46 deletions
				
			
		
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -123,6 +123,7 @@ INSTALLED_APPS = ( | |||
|      | ||||
|     'webpack_loader', | ||||
|     'rest_framework', | ||||
|     'rest_framework.authtoken', | ||||
| 
 | ||||
|     # MaMweb | ||||
|     'mamweb', | ||||
|  | @ -294,6 +295,9 @@ LOGGING = { | |||
|             }, | ||||
|         } | ||||
| 
 | ||||
| # Permissions for uploads | ||||
| FILE_UPLOAD_PERMISSIONS = 0o0644 | ||||
| 
 | ||||
| # MaM specific | ||||
| 
 | ||||
| SEMINAR_RESENI_DIR = os.path.join('reseni') | ||||
|  |  | |||
							
								
								
									
										1
									
								
								seminar/.~lock.profile_vysledkovka.txt#
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								seminar/.~lock.profile_vysledkovka.txt#
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| ,anet,erebus,25.03.2020 22:21,file:///home/anet/.config/libreoffice/4; | ||||
|  | @ -743,12 +743,20 @@ class Problem(SeminarModelBase,PolymorphicModel): | |||
| 		return '<Není zadaný>' | ||||
| 
 | ||||
| 	def verejne(self): | ||||
| 		# FIXME: Tohle se liší podle typu problému, má se udělat polymorfně. | ||||
| 		# Zatím je tu jen dummy fail-safe default: nic není veřejné. | ||||
| 		# Doporučené řešení: dělat tohle podle stavu problému a veřejnosti čísla, ve kterém je | ||||
| 		return False | ||||
| 		# FIXME: Tohle je blbost | ||||
| 		return (self.cislo_zadani and self.cislo_zadani.verejne()) | ||||
| 		# aktuálně podle stavu problému | ||||
| 		# 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 | ||||
| 		if self.stav == 'zadany' or self.stav == 'vyreseny': | ||||
| 			stav_verejny = True | ||||
| 		return stav_verejny | ||||
| 		 | ||||
| 		#cislo_verejne = False | ||||
| 		#if (self.cislo_zadani and self.cislo_zadani.verejne()): | ||||
| 		#	cislo_verejne = True | ||||
| 		 | ||||
| 		#return (stav_verejny and cislo_verejne) | ||||
| 	verejne.boolean = True | ||||
| 
 | ||||
| 	def verejne_url(self): | ||||
|  | @ -986,7 +994,7 @@ def aux_generate_filename(self, filename): | |||
| 		unidecode(filename.replace('/', '-').replace('\0', '')) | ||||
| 	) | ||||
| 	datedir = timezone.now().strftime('%Y-%m') | ||||
| 	fname = "{}_{}".format( | ||||
| 	fname = "{}/{}".format( | ||||
| 		timezone.now().strftime('%Y-%m-%d-%H:%M'), | ||||
| 		clean) | ||||
| 	return os.path.join(datedir, fname) | ||||
|  | @ -1039,6 +1047,11 @@ class PrilohaReseni(SeminarModelBase): | |||
| 	def __str__(self): | ||||
| 		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): | ||||
| 	"""Kus pohádky před/za úlohou v čísle""" | ||||
|  |  | |||
							
								
								
									
										7
									
								
								seminar/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								seminar/permissions.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| from rest_framework.permissions import BasePermission | ||||
| 
 | ||||
| class AllowWrite(BasePermission): | ||||
| 
 | ||||
|          def has_permission(self, request, view): | ||||
|                   return request.user.has_perm('auth.org') | ||||
| 
 | ||||
							
								
								
									
										97
									
								
								seminar/templates/seminar/archiv/cislo-normal.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								seminar/templates/seminar/archiv/cislo-normal.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,97 @@ | |||
| {% extends "seminar/archiv/base_cisla.html" %} | ||||
| 
 | ||||
| {# {% block content %} | ||||
|  <div> | ||||
| 
 | ||||
|   <h1> | ||||
|     {% block nadpis1a %}{% block nadpis1b %} | ||||
|       Číslo {{ cislo }} | ||||
|     {% endblock %}{% endblock %} | ||||
|   </h1> | ||||
| 
 | ||||
|   {% if cislo.pdf %} | ||||
|     <p><a href='{{ cislo.pdf.url }}'>Číslo v pdf</a> | ||||
|   {% endif %} | ||||
|   <p><a href='{{ cislo.rocnik.verejne_url }}'>Ročník {{ cislo.rocnik }}</a> | ||||
| 
 | ||||
|   {% if v_cisle_zadane %} | ||||
|     <h2>Zadané problémy</h2> | ||||
|     <ul> | ||||
|     {% for p in v_cisle_zadane %} | ||||
|       <li{% if user.is_staff and not cislo.verejne %} class='mam-org-only'{% endif %}> | ||||
|         {% if user.is_staff or cislo.verejne %} | ||||
|           <a href='{{ p.verejne_url }}'>{% endif %}{{ p.kod_v_rocniku }} {{ p.nazev }} {{ p.body_v_zavorce }}{% if user.is_staff or cislo.verejne %}</a>{% endif %} | ||||
|     {% endfor %} | ||||
|     </ul> | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {% if resene_problemy %} | ||||
|     <h2>Řešené problémy</h2> | ||||
|     <ul> | ||||
|     {% for p in resene_problemy %} | ||||
|       <li{% if user.is_staff and not cislo.verejne %} class='mam-org-only'{% endif %}> | ||||
|         {% if user.is_staff or cislo.verejne %} | ||||
|           <a href='{{ p.verejne_url }}'>{% endif %}{{ p.kod_v_rocniku }} {{ p.nazev }} {{ p.body_v_zavorce }}{% if user.is_staff or cislo.verejne %}</a>{% endif %} | ||||
|     {% endfor %} | ||||
|     </ul> | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {% if user.is_staff %} | ||||
|       <div class="mam-org-only"> | ||||
|         <h2> Orgovské odkazy </h2> | ||||
|         <ul> | ||||
|           <li><a href="obalky.pdf">Obálky (PDF)</a></li> | ||||
|           <li><a href="tituly.tex">Tituly (TeX)</a></li> | ||||
|           <li><a href="vysledkovka.tex">Výsledkovka (TeX)</a></li> | ||||
|           <li><a href="obalkovani">Obálkování</a></li> | ||||
|         </ul> | ||||
|       </div> | ||||
|   {% endif %}  | ||||
| 
 | ||||
|   {% if cislo.verejna_vysledkovka %} | ||||
|   <h2>Výsledkovka</h2> | ||||
|   {% else %} | ||||
|     {% if user.is_staff %} | ||||
|       <div class='mam-org-only'> | ||||
|       <h2>Výsledkovka (neveřejná)</h2> | ||||
|     {% endif %} | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {% if cislo.verejna_vysledkovka or user.is_staff %} | ||||
|     <table class='vysledkovka'> | ||||
|       <tr class='border-b'> | ||||
|         <th class='border-r'># | ||||
|         <th class='border-r'>Jméno #} | ||||
|         {# problémy by měly být veřejné, když je veřejná výsledkovka #} | ||||
| {#        {% for p in problemy %} | ||||
|         <th class='border-r'><a href="{{ p.verejne_url }}">{{ p.kod_v_rocniku }}</a> | ||||
|         {% endfor %} | ||||
|         <th class='border-r'>Za číslo</sup> | ||||
|         <th class='border-r'>Za ročník | ||||
|         <th class='border-r'>Odjakživa | ||||
|     {% for rv in radky_vysledkovky %} | ||||
|       <tr> | ||||
|         <td class='border-r'>{% autoescape off %}{{ rv.poradi }}{% endautoescape %} | ||||
|             <th class='border-r'> | ||||
|             {% if rv.resitel.titul != "" %} | ||||
|               {{ rv.resitel.titul }}<sup>MM</sup> | ||||
|             {% endif %} | ||||
|             {{ rv.resitel.osoba.plne_jmeno }} | ||||
|         {% for b in rv.hlavni_problemy_body %} | ||||
|         <td class='border-r'>{{ b }} | ||||
|         {% endfor %} | ||||
|         <td class='border-r'>{{ rv.body_cislo }} | ||||
|         <td class='border-r'><b>{{ rv.body_rocnik }}</b> | ||||
|         <td class='border-r'>{{ rv.body_celkem_odjakziva }} | ||||
|       </tr> | ||||
|     {% endfor %} | ||||
|     </table> | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {% if not cislo.verejna_vysledkovka and user.is_staff %} | ||||
|       </div> | ||||
|   {% endif %} | ||||
| 
 | ||||
| </div>  | ||||
| {% endblock content %} #} | ||||
| 
 | ||||
							
								
								
									
										19
									
								
								seminar/templates/seminar/archiv/problem_tema.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								seminar/templates/seminar/archiv/problem_tema.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| {% extends "seminar/archiv/problem.html" %} | ||||
| 
 | ||||
| {% block problem %} | ||||
|     <h1> | ||||
|       {% block nadpis1a %}{% block nadpis1b %} | ||||
|         {{ problem.nazev_typu }} {{ problem.kod_v_rocniku }}: {{ problem.nazev }} | ||||
|       {% endblock %}{% endblock %} | ||||
|     </h1> | ||||
| 
 | ||||
|   <h2>Zadání</h2> | ||||
|   {{ problem.text_zadani |safe }} | ||||
|   {% if problem.text_reseni %} | ||||
|     <h2>Řešení</h2> | ||||
|     {{ problem.text_reseni |safe }} | ||||
|   {% endif %} | ||||
| 
 | ||||
|   {# TODO vysledkovka tematu #} | ||||
| 
 | ||||
| {% endblock %} | ||||
							
								
								
									
										23
									
								
								seminar/templates/seminar/archiv/problem_uloha.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								seminar/templates/seminar/archiv/problem_uloha.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| {% extends "seminar/archiv/problem.html" %} | ||||
| 
 | ||||
| {% block problem %} | ||||
|     <h1> | ||||
|       {% block nadpis1a %}{% block nadpis1b %} | ||||
|         {{ problem.nazev_typu }} {{ problem.kod_v_rocniku }}: {{ problem.nazev }} {{ problem.body_v_zavorce }} | ||||
|       {% endblock %}{% endblock %} | ||||
|     </h1> | ||||
|   {% if problem.cislo_zadani %} | ||||
|     <p>Zadáno v čísle <a href='{{ problem.cislo_zadani.verejne_url }}'>{{ problem.cislo_zadani.kod }}</a>. | ||||
|   {% endif %} | ||||
|   {% if problem.cislo_reseni %} | ||||
|     <p>Řešeno v čísle <a href='{{ problem.cislo_reseni.verejne_url }}'>{{ problem.cislo_reseni.kod }}</a>. | ||||
|   {% endif %} | ||||
| 
 | ||||
|   <h2>Zadání</h2> | ||||
|   {{ problem.text_zadani |safe }} | ||||
|   {% if problem.text_reseni %} | ||||
|     <h2>Řešení</h2> | ||||
|     {{ problem.text_reseni |safe }} | ||||
|   {% endif %} | ||||
| 
 | ||||
| {% endblock %} | ||||
							
								
								
									
										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") | ||||
| 
 | ||||
|  | @ -140,7 +140,7 @@ def gen_resitele(rnd, osoby, skoly): | |||
| 				x += 1 | ||||
| 				os.user = user | ||||
| 				os.save() | ||||
| 			os.user.user_permissions.add(resitel_perm) | ||||
| 				os.user.user_permissions.add(resitel_perm) | ||||
| 			resitele.append(Resitel.objects.create(osoba=os, skola=rnd.choice(skoly), | ||||
| 				rok_maturity=rnd.randint(2019, 2029), | ||||
| 				zasilat=rnd.choice(Resitel.ZASILAT_CHOICES)[0])) | ||||
|  | @ -199,7 +199,7 @@ def gen_organizatori(rnd, osoby, last_rocnik): | |||
| 				x += 1 | ||||
| 				os.user = user | ||||
| 				os.save() | ||||
| 			os.user.user_permissions.add(org_perm) | ||||
| 				os.user.user_permissions.add(org_perm) | ||||
| 			organizatori.append(Organizator.objects.create(osoba=os, | ||||
| 				organizuje_od=od, organizuje_do=do, strucny_popis_organizatora = popis_orga)) | ||||
| 	return organizatori | ||||
|  |  | |||
|  | @ -168,5 +168,10 @@ urlpatterns = [ | |||
| 	# org_member_required(views.OrganizatorAutocomplete.as_view()), | ||||
| 	# 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: | ||||
| 		# 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 | ||||
| 		letosni_reseni = m.Reseni.objects.filter(hodnoceni__cislo_body__rocnik=rocnik, | ||||
| 												 hodnoceni__cislo_body__poradi__lte=cislo.poradi) | ||||
| 
 | ||||
| 	# 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() | ||||
| 		return m.Resitel.objects.filter(rok_maturity__gte=rocnik.druhy_rok(), | ||||
| 										reseni__hodnoceni__cislo_body__rocnik=rocnik, | ||||
| 										reseni__hodnoceni__cislo_body__poradi__lte=cislo.poradi).distinct() | ||||
| 
 | ||||
| 
 | ||||
| def aktivniResitele(cislo, pouze_letosni=False): | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| from .views_all import * | ||||
| from .autocomplete 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.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.db import transaction | ||||
| from django.core import serializers | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.forms.models import model_to_dict | ||||
| 
 | ||||
| import seminar.models as s | ||||
|  | @ -120,14 +121,57 @@ class TNLData(object): | |||
| 			self.appendable_siblings = tnltt.appendableChildren(self.parent) | ||||
| 		else: | ||||
| 			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 | ||||
| 	def from_treenode(cls,anode,parent=None,index=None): | ||||
| 		out = cls(anode,parent,index) | ||||
| 		for (idx,ch) in enumerate(treelib.all_children(anode)): | ||||
| 			outitem = cls.from_treenode(ch,out,idx) | ||||
| 	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) | ||||
| 		else: | ||||
| 			raise PermissionDenied() | ||||
| 
 | ||||
| 		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.add_edit_options() | ||||
| 		return out | ||||
|  | @ -194,7 +238,7 @@ class TreeNodeView(generic.DetailView): | |||
| 
 | ||||
| 	def get_context_data(self,**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 | ||||
| 
 | ||||
| class TreeNodeJSONView(generic.DetailView): | ||||
|  | @ -202,7 +246,7 @@ class TreeNodeJSONView(generic.DetailView): | |||
| 
 | ||||
| 	def get(self,request,*args, **kwargs): | ||||
| 		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) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -331,6 +375,7 @@ class ProblemView(generic.DetailView): | |||
| 
 | ||||
| 	def get_context_data(self, **kwargs): | ||||
| 		context = super().get_context_data(**kwargs) | ||||
| 		user = self.request.user | ||||
| 		# Teď potřebujeme doplnit tnldata do kontextu. | ||||
| 		# Ošklivý type switch, hezčí by bylo udělat to polymorfni. FIXME. | ||||
| 		if False: | ||||
|  | @ -338,11 +383,11 @@ class ProblemView(generic.DetailView): | |||
| 			pass | ||||
| 		elif isinstance(self.object, s.Clanek) or  isinstance(self.object, s.Konfera): | ||||
| 			# 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): | ||||
| 			# 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_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode) | ||||
| 			tnl_zadani = TNLData.from_treenode(self.object.ulohazadaninode,user) | ||||
| 			tnl_vzorak = TNLData.from_treenode(self.object.ulohavzoraknode,user) | ||||
| 			context['tnldata'] = TNLData.from_tnldata_list([tnl_zadani, tnl_vzorak]) | ||||
| 		elif isinstance(self.object, s.Tema): | ||||
| 			rocniknode = self.object.rocnik.rocniknode | ||||
|  | @ -384,16 +429,16 @@ class AktualniZadaniView(generic.TemplateView): | |||
| #			) | ||||
| # | ||||
| def ZadaniTemataView(request): | ||||
|         nastaveni = get_object_or_404(Nastaveni) | ||||
|         verejne = nastaveni.aktualni_cislo.verejne() | ||||
|         akt_rocnik = nastaveni.aktualni_cislo.rocnik | ||||
|         temata = s.Tema.objects.filter(rocnik=akt_rocnik, stav='zadany') | ||||
|         return render(request, 'seminar/tematka/rozcestnik.html', | ||||
|                         { | ||||
|                          'tematka': temata, | ||||
|                          'verejne': verejne, | ||||
|                                 }, | ||||
|                         ) | ||||
| 	nastaveni = get_object_or_404(Nastaveni) | ||||
| 	verejne = nastaveni.aktualni_cislo.verejne() | ||||
| 	akt_rocnik = nastaveni.aktualni_cislo.rocnik | ||||
| 	temata = s.Tema.objects.filter(rocnik=akt_rocnik, stav='zadany') | ||||
| 	return render(request, 'seminar/tematka/rozcestnik.html', | ||||
| 			{ | ||||
| 			 'tematka': temata, | ||||
| 			 'verejne': verejne, | ||||
| 				}, | ||||
| 			) | ||||
| 
 | ||||
| 
 | ||||
| #	nastaveni = get_object_or_404(Nastaveni) | ||||
|  |  | |||
|  | @ -1,7 +1,23 @@ | |||
| from rest_framework import viewsets,filters | ||||
| from rest_framework.permissions import BasePermission, AllowAny | ||||
| from . import models as m | ||||
| from . import views | ||||
| 
 | ||||
| from seminar.permissions import AllowWrite  | ||||
| 
 | ||||
| class PermissionMixin(object): | ||||
| 	""" Redefines get_permissions so that only organizers can make changes. """ | ||||
| 
 | ||||
| 	def get_permissions(self): | ||||
| 		permission_classes = [] | ||||
| 		print("get_permissions have been called.") | ||||
| 		if self.action in ["create", "update", "partial_update", "destroy"]: | ||||
| 			permission_classes = [AllowWrite] # speciální permission na zápis - orgové | ||||
| 		else: | ||||
| 			permission_classes = [AllowAny]  | ||||
| 		# návštěvník nemusí být zalogován, aby si prohlížel obsah | ||||
| 		return [permission() for permission in permission_classes] | ||||
| 
 | ||||
| class ReadWriteSerializerMixin(object): | ||||
| 	""" | ||||
| 	Overrides get_serializer_class to choose the read serializer | ||||
|  | @ -46,27 +62,27 @@ class ReadWriteSerializerMixin(object): | |||
| 		) | ||||
| 		return self.create_serializer_class | ||||
| 
 | ||||
| class UlohaVzorakNodeViewSet(viewsets.ModelViewSet): | ||||
| class UlohaVzorakNodeViewSet(PermissionMixin, viewsets.ModelViewSet): | ||||
| 	queryset = m.UlohaVzorakNode.objects.all() | ||||
| 	serializer_class = views.UlohaVzorakNodeSerializer | ||||
| 
 | ||||
| class TextViewSet(viewsets.ModelViewSet): | ||||
| class TextViewSet(PermissionMixin, viewsets.ModelViewSet): | ||||
| 	queryset = m.Text.objects.all() | ||||
| 	serializer_class = views.TextSerializer | ||||
| 
 | ||||
| class TextNodeViewSet(ReadWriteSerializerMixin,viewsets.ModelViewSet): | ||||
| class TextNodeViewSet(PermissionMixin, ReadWriteSerializerMixin,viewsets.ModelViewSet): | ||||
| 	queryset = m.TextNode.objects.all() | ||||
| 	read_serializer_class = views.TextNodeSerializer | ||||
| 	write_serializer_class = views.TextNodeWriteSerializer | ||||
| 	create_serializer_class = views.TextNodeCreateSerializer | ||||
| 
 | ||||
| class CastNodeViewSet(ReadWriteSerializerMixin,viewsets.ModelViewSet): | ||||
| class CastNodeViewSet(PermissionMixin, ReadWriteSerializerMixin,viewsets.ModelViewSet): | ||||
| 	queryset = m.CastNode.objects.all() | ||||
| 	read_serializer_class = views.CastNodeSerializer | ||||
| 	write_serializer_class = views.CastNodeSerializer | ||||
| 	create_serializer_class = views.CastNodeCreateSerializer | ||||
| 
 | ||||
| class UlohaVzorakNodeViewSet(viewsets.ModelViewSet): | ||||
| class UlohaVzorakNodeViewSet(PermissionMixin, viewsets.ModelViewSet): | ||||
| 	serializer_class = views.UlohaVzorakNodeSerializer | ||||
| 
 | ||||
| 	def get_queryset(self): | ||||
|  | @ -74,4 +90,7 @@ class UlohaVzorakNodeViewSet(viewsets.ModelViewSet): | |||
| 		nazev = self.request.query_params.get('nazev',None) | ||||
| 		if nazev is not None: | ||||
| 			queryset = queryset.filter(nazev__contains=nazev) | ||||
| 		return queryset | ||||
| 		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) | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue
	
	 Kateřina Č
						Kateřina Č