Merge branch 'master' into korekturovatko
This commit is contained in:
		
						commit
						8ec06d0c90
					
				
					 18 changed files with 313 additions and 93 deletions
				
			
		|  | @ -42,7 +42,11 @@ | |||
| 
 | ||||
|   window.addEventListener("load", _ => { | ||||
|     aktualizuj_vse({}, true, () => { | ||||
|       if (location.hash !== "") location.hash = location.hash; // Po rozházení korektur sescrollujeme na kotvu v URL | ||||
|       if (location.hash !== "") { // Po rozházení korektur sescrollujeme na kotvu v URL | ||||
|         const h = location.hash.substring(1); | ||||
|         location.hash = "HACK"; | ||||
|         location.hash = h; | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,9 +26,10 @@ | |||
| 
 | ||||
| 	.tres { | ||||
| 		flex: 1; | ||||
| 		text-align: end; | ||||
| 	} | ||||
| 
 | ||||
| 	.grey { | ||||
| 	.half-opacity { | ||||
| 		opacity: 0.5; | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ | |||
|   {% for osoba in object_list %} | ||||
|   <div class="osoba"> | ||||
|     <div class="uno">{{ osoba.jmeno }} {{ osoba.prijmeni }}</div> | ||||
|     <div class="dos {% if not osoba.jak_se_dozvedeli %}grey{% endif %}">{% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}</div> | ||||
|     <div class="dos {% if not osoba.jak_se_dozvedeli %}half-opacity{% endif %}">{% if osoba.jak_se_dozvedeli %} {{osoba.jak_se_dozvedeli}} {% else %} NEZADÁNO {% endif %}</div> | ||||
|     <div class="tres">{{ osoba.datum_registrace }}</div> | ||||
|   </div> | ||||
|   {% endfor %} | ||||
|  |  | |||
							
								
								
									
										88
									
								
								personalni/templates/personalni/profil/export_lidi.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								personalni/templates/personalni/profil/export_lidi.html
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| {% extends "base.html" %} | ||||
| 
 | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
| <h2><strong>Export lidí</strong></h2> | ||||
| 
 | ||||
| <select name="select-one" id="select-one"> | ||||
|   <option value="0">---</option> | ||||
|   <option value="1">Řešitelé čísla</option> | ||||
|   <option value="2">Řešitelé ročníku</option> | ||||
|   <option value="3">Všichni řešitelé, kteří ještě neodmaturovali</option> | ||||
|   <option value="4">Organizátoři soustředění</option> | ||||
| </select> | ||||
| 
 | ||||
| <select name="select-two" id="select-two"> | ||||
| <!-- will be filled with ajax --> | ||||
| </select> | ||||
| 
 | ||||
| <button id="download-button">Stáhnout</button> | ||||
| 
 | ||||
| <script defer> | ||||
|   const select_one = document.getElementById("select-one") | ||||
|   const select_two = document.getElementById("select-two") | ||||
|   const download_button = document.getElementById("download-button") | ||||
| 
 | ||||
|   download_button.style.display = 'none' | ||||
|   select_two.style.display = 'none' | ||||
| 
 | ||||
|   const fetch_dict_string = '{{ typy_exportu|safe }}' | ||||
|   const fetch_dict = JSON.parse(fetch_dict_string) | ||||
| 
 | ||||
| 
 | ||||
|   select_one.addEventListener('change', (e) => { | ||||
|     value = e.target.value | ||||
|     select_two.style.display = 'none' | ||||
|     select_two.innerHTML = '' | ||||
|     // puvodni stav | ||||
|     if (value == 0) { | ||||
|       download_button.style.display = 'none' | ||||
|       select_two.style.display = 'none' | ||||
|       return | ||||
|     } | ||||
|     // v tomto pripade muzeme rovnou stahnout | ||||
|     if (!(value in fetch_dict)) { | ||||
|       download_button.style.display = 'block' | ||||
|       select_two.style.display = 'none' | ||||
|       return | ||||
|     } | ||||
|     download_button.style.display = 'none' | ||||
|     fetch("/profil/exporty_lidi/get/" + value) | ||||
|       .then(response => response.json()) | ||||
|       .then(data => { | ||||
|         const option = document.createElement('option') | ||||
|         option.value = 0 | ||||
|         option.text = '---' | ||||
|         select_two.appendChild(option) | ||||
|         for (const [key, value] of Object.entries(data)) { | ||||
|           const option = document.createElement('option') | ||||
|           option.value = value["id"] | ||||
|           option.text = value["display"] | ||||
|           select_two.appendChild(option) | ||||
|         } | ||||
|         select_two.style.display = 'block' | ||||
|       }) | ||||
| 
 | ||||
|   }) | ||||
| 
 | ||||
|   select_two.addEventListener('change', (e) => { | ||||
|     value = e.target.value | ||||
|     if (value == 0) { | ||||
|       download_button.style.display = 'none' | ||||
|       return | ||||
|     } | ||||
|     download_button.style.display = 'block' | ||||
|   }) | ||||
| 
 | ||||
|   download_button.addEventListener('click', (e) => { | ||||
|     if (select_two.innerHTML == '') { | ||||
|       window.location.href = "/profil/exporty_lidi/get_csv_only_one_step/" + select_one.value | ||||
|     } else { | ||||
|       window.location.href = "/profil/exporty_lidi/get_csv/" + select_one.value + "/" + select_two.value | ||||
|     } | ||||
|      | ||||
|   }) | ||||
| </script> | ||||
| 
 | ||||
| {% endblock %} | ||||
|  | @ -107,6 +107,13 @@ | |||
|     </li> | ||||
| </ul> | ||||
| 
 | ||||
| <hr /> | ||||
| <h2><strong>Exporty dat lidí v semináří</strong></h2> | ||||
| 
 | ||||
| <ul> | ||||
|   <li><a href="{% url 'exporty_lidi' %}">dostupné exporty</a></li> | ||||
| </ul> | ||||
| 
 | ||||
| <hr /> | ||||
| <p>Nemůžeš najít, co hledáš? Může to být v <a href="{% url 'admin:index' %}">administračním rozhraní webu</a>.</p> | ||||
| {% endblock content %} | ||||
|  |  | |||
|  | @ -38,6 +38,28 @@ urlpatterns = [ | |||
|         'org/propagace/jak-se-dozvedeli/', | ||||
|         org_required(views.JakSeDozvedeliView.as_view()), | ||||
|         name='jak_se_dozvedeli' | ||||
| 	), | ||||
|      | ||||
| 	# export dat o řešitelích | ||||
|     path( | ||||
|         'profil/exporty_lidi', | ||||
|         org_required(views.ExportLidiView.as_view()), | ||||
|         name='exporty_lidi',      | ||||
| 	), | ||||
|     path( | ||||
|         'profil/exporty_lidi/get/<int:type>', | ||||
|         org_required(views.get_export_options), | ||||
|         name='exporty_lidi_options', | ||||
|     ), | ||||
|     path( | ||||
| 		'profil/exporty_lidi/get_csv_only_one_step/<int:type>', | ||||
| 		org_required(views.download_export_csv_only_first_step), | ||||
| 		name='exporty_lidi_data', | ||||
| 	), | ||||
|     path( | ||||
|         'profil/exporty_lidi/get_csv/<int:type>/<int:id>', | ||||
|         org_required(views.download_export_csv), | ||||
|         name='exporty_lidi_download', | ||||
| 	) | ||||
| 
 | ||||
| ] | ||||
|  |  | |||
|  | @ -20,13 +20,16 @@ from django.utils import timezone | |||
| import personalni.models as m | ||||
| from soustredeni.models import Soustredeni | ||||
| from odevzdavatko.models import Hodnoceni | ||||
| from tvorba.models import Clanek, Uloha, Tema | ||||
| from tvorba.models import Clanek, Uloha, Tema, Cislo, Rocnik | ||||
| import tvorba.utils as tvorba_utils | ||||
| from various.models import Nastaveni | ||||
| from .forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm | ||||
| 
 | ||||
| from datetime import date | ||||
| import logging | ||||
| import csv | ||||
| from enum import Enum | ||||
| import json | ||||
| 
 | ||||
| from various.views.pomocne import formularOKView | ||||
| from various.autentizace.views import LoginView | ||||
|  | @ -141,6 +144,54 @@ class OrgoRozcestnikView(TemplateView): | |||
| 		#content_type = 'text/plain; charset=UTF8' | ||||
| 	#XXX | ||||
| 	 | ||||
| class PrvniTypExportu(Enum): | ||||
| 	CISLA = 1 | ||||
| 	ROCNIKU = 2 | ||||
| 	SOUSTREDENI = 4 | ||||
| 
 | ||||
| class ExportLidiView(TemplateView): | ||||
| 	template_name = 'personalni/profil/export_lidi.html' | ||||
| 
 | ||||
| 	def get_context_data(self, **kwargs): | ||||
| 		context = super().get_context_data(**kwargs) | ||||
| 		context['typy_exportu'] = json.dumps({member.value: member.name.lower().capitalize() for member in PrvniTypExportu}) | ||||
| 		return context | ||||
| 	 | ||||
| 
 | ||||
| def get_export_options(request, type): | ||||
| 	if type == PrvniTypExportu.CISLA.value: | ||||
| 		data = [{"id": c.id, "display": str(c)} for c in Cislo.objects.all()] | ||||
| 	if type == PrvniTypExportu.ROCNIKU.value: | ||||
| 		data = [{"id": r.id, "display": str(r)} for r in Rocnik.objects.all()] | ||||
| 	if type == PrvniTypExportu.SOUSTREDENI.value: | ||||
| 		data = [{"id": s.id, "display": str(s)} for s in Soustredeni.objects.all()] | ||||
| 	return HttpResponse(json.dumps(data), content_type='application/json') | ||||
| 
 | ||||
| def download_export_csv_only_first_step(request, type): | ||||
| 	if type == 3: | ||||
| 		response = dataResiteluCsvResponse(tvorba_utils.resitele_co_neodmaturovali()) | ||||
| 		response['Content-Disposition'] = 'attachment; filename="resitele_co_neodmaturovali.csv"' | ||||
| 		return response | ||||
| 
 | ||||
| def download_export_csv(request, type, id): | ||||
| 	if type == PrvniTypExportu.CISLA.value: | ||||
| 		response = dataResiteluCsvResponse(tvorba_utils.resi_cislo(Cislo.objects.get(id=id))) | ||||
| 		name = str(Cislo.objects.get(id=id)).replace(" ", "_") + "_resitele_cisla.csv" | ||||
| 		response['Content-Disposition'] = 'attachment; filename="' + name + '"' | ||||
| 		return response | ||||
| 	if type == PrvniTypExportu.ROCNIKU.value: | ||||
| 		response = dataResiteluCsvResponse(tvorba_utils.resi_v_rocniku(Rocnik.objects.get(id=id))) | ||||
| 		name = str(Rocnik.objects.get(id=id)).replace(" ", "_") + "_resitele_rocniku.csv" | ||||
| 		response['Content-Disposition'] = 'attachment; filename="' + name + '"' | ||||
| 		return response | ||||
| 	if type == PrvniTypExportu.SOUSTREDENI.value: | ||||
| 		soustredeni = Soustredeni.objects.get(id=id) | ||||
| 		organizatori = soustredeni.organizatori.all() | ||||
| 		organizatoriOsoby = Osoba.objects.filter(org__in=organizatori) | ||||
| 		response = dataOsobCsvResponse(organizatoriOsoby, columns=("jmeno", "prijmeni", "email", "telefon",)) | ||||
| 		name = str(soustredeni).replace(" ", "_") + "_organizatori_soustredeni.csv" | ||||
| 		response['Content-Disposition'] = 'attachment; filename="' + name + '"' | ||||
| 		return response | ||||
| 
 | ||||
| class ResitelView(LoginRequiredMixin,generic.DetailView): | ||||
| 	model = m.Resitel | ||||
|  | @ -470,3 +521,46 @@ def dataResiteluCsvResponse(queryset, columns=None, with_header=True): | |||
| 	writer.writerows(queryset_list) | ||||
| 
 | ||||
| 	return response | ||||
| 
 | ||||
| def dataOsobCsvResponse(queryset, columns=None, with_header=True): | ||||
| 	"""Pomocná funkce pro vracení dat osob jako CSV. Musí dostat správný QuerySet, který dává Ososby""" | ||||
| 
 | ||||
| 	default_columns = ( | ||||
| 		'id', | ||||
| 		'jmeno', | ||||
| 		'prijmeni', | ||||
| 		'prezdivka', | ||||
| 		'email', | ||||
| 		'telefon', | ||||
| 		'datum_narozeni', | ||||
| 		'osloveni', | ||||
| 		'ulice', | ||||
| 		'mesto', | ||||
| 		'psc', | ||||
| 		'stat', | ||||
| 		'jak_se_dozvedeli', | ||||
| 		'poznamka', | ||||
| 		'datum_registrace', | ||||
| 		'datum_souhlasu_udaje', | ||||
| 		'datum_souhlasu_zasilani', | ||||
| 	) | ||||
| 
 | ||||
| 	if columns is None: columns = default_columns | ||||
| 
 | ||||
| 	def get_field_name(column_name): | ||||
| 		return column_name | ||||
| 	 | ||||
| 	response = HttpResponse(content_type='text/csv') | ||||
| 	writer = csv.writer(response) | ||||
| 	 | ||||
| 	# První řádek je záhlaví | ||||
| 	if with_header: | ||||
| 		writer.writerow(map(get_field_name, columns)) | ||||
| 	 | ||||
| 	# Data: | ||||
| 	queryset_list = queryset.values_list(*columns) | ||||
| 	writer.writerows(queryset_list) | ||||
| 
 | ||||
| 	return response | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from soustredeni.models import Soustredeni | |||
| 
 | ||||
| class Seznam_PrednaskaInline(admin.TabularInline): | ||||
| 	""" | ||||
| 		Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Přednášky <prednasky.models.Prednaska>` | ||||
| 		:py:class:`Inline <django.contrib.admin.TabularInline>` pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující :py:class:`Přednášky <prednasky.models.Prednaska>` | ||||
| 		v adminu :py:class:`Seznamu <prednasky.models.Seznam>`. | ||||
| 	""" | ||||
| 	model = Prednaska.seznamy.through | ||||
|  | @ -60,7 +60,7 @@ class Seznam_PrednaskaInline(admin.TabularInline): | |||
| 
 | ||||
| class Seznam_ZnalostInline(admin.TabularInline): | ||||
| 	""" | ||||
| 		Pomůcka pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující hezky :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 		:py:class:`Inline <django.contrib.admin.TabularInline>` pro :py:class:`prednasky.admin.SeznamAdmin` zobrazující :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 		v adminu :py:class:`Seznamu <prednasky.models.Seznam>`. | ||||
| 	""" | ||||
| 	model = Znalost.seznamy.through | ||||
|  | @ -97,7 +97,7 @@ admin.site.register(Seznam, SeznamAdmin) | |||
| 
 | ||||
| 
 | ||||
| class PrednaskaAdmin(VersionAdmin): | ||||
| 	""" Admin pro :py:class:`Přednášku <prednasky.models.Prednaska> """ | ||||
| 	""" Admin pro :py:class:`Přednášku <prednasky.models.Prednaska>` """ | ||||
| 	list_display = ['nazev', 'org', 'obor'] | ||||
| 	list_filter = ['org', 'obor'] | ||||
| 	search_fields = ['nazev'] | ||||
|  | @ -138,7 +138,7 @@ admin.site.register(Prednaska, PrednaskaAdmin) | |||
| 
 | ||||
| class ZnalostAdmin(PrednaskaAdmin): # Trochu hack, ať nemusím vypisovat všechno znovu | ||||
| 	""" | ||||
| 		Admin pro :py:class:`Znalost <prednasky.models.Znalost> | ||||
| 		Admin pro :py:class:`Znalost <prednasky.models.Znalost>` | ||||
| 		TODO předělat, aby nedědila z :py:class:`prednasky.admin.PrednaskaAdmin`, ale společné věci byly zvlášť | ||||
| 	""" | ||||
| 	list_display = ("__str__",) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ from django import forms | |||
| from .models import Hlasovani, HlasovaniOZnalostech | ||||
| 
 | ||||
| class HlasovaniPrednaskaForm(forms.Form): | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o jedné :py:class:`Přednášce <prednasky.models.Prednaska>` | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro :py:class:`Hlasování <prednasky.models.Hlasovani>` o jedné :py:class:`Přednášce <prednasky.models.Prednaska>` | ||||
| 	(neobsahuje téměř nic, většina se musí doplnit jiným způsobem) | ||||
| 	""" | ||||
| 
 | ||||
|  | @ -17,7 +17,7 @@ class HlasovaniPrednaskaForm(forms.Form): | |||
| HlasovaniPrednaskaFormSet = forms.formset_factory(HlasovaniPrednaskaForm, extra=0) | ||||
| 
 | ||||
| class HlasovaniZnalostiForm(forms.Form): | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o jedné :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 	""" :py:class:`Formulář <django.forms.Form>` pro :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>` o jedné :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 	(neobsahuje téměř nic, většina se musí doplnit jiným způsobem) | ||||
| 	""" | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										20
									
								
								prednasky/migrations/0023_hlasovani_ucastnik_osoba.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								prednasky/migrations/0023_hlasovani_ucastnik_osoba.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| # Generated by Django 4.2.16 on 2025-02-19 17:31 | ||||
| 
 | ||||
| from django.db import migrations, models | ||||
| import django.db.models.deletion | ||||
| 
 | ||||
| 
 | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ('personalni', '0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni'), | ||||
|         ('prednasky', '0022_preklep_u_odpovedi_hlasovanioznalostech'), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='hlasovani', | ||||
|             name='ucastnik_osoba', | ||||
|             field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='personalni.osoba'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -6,7 +6,7 @@ from personalni.models import Organizator, Osoba | |||
| 
 | ||||
| class Seznam(models.Model): | ||||
| 	""" | ||||
| 		Spojuje :py:class:`Přednášky <prednasky.models.Prednaska>` | ||||
| 		Spojuje :py:class:`Přednášky <prednasky.models.Prednaska>` a :py:class:`Znalosti <prednasky.models.Znalost> | ||||
| 		se :py:class:`Soustředěními <soustredeni.models.Soustredeni>`, | ||||
| 		kde by mohly zaznít, nebo zazní/zazněly. | ||||
| 	""" | ||||
|  | @ -19,8 +19,8 @@ class Seznam(models.Model): | |||
| 
 | ||||
| 	class Stav(models.IntegerChoices): | ||||
| 		""" Stav seznamu přednášek (NAVRH se používá k hlasování viz :py:func:`daný view <prednasky.views.newPrednaska>`). """ | ||||
| 		NAVRH = 1, "Návrh" | ||||
| 		BUDE = 2, "Bude" | ||||
| 		NAVRH = 1, "Návrh" #: odpovídá před-soustřeďkové představě o tom, jaké přednášky dělat (dá se o nich třeba hlasovat ap.) | ||||
| 		BUDE = 2, "Bude" #: odpovídá definitivní představě o tom, co bude/bylo a dá se porovnávat s novými návrhy | ||||
| 
 | ||||
| 	id = models.AutoField(primary_key=True) | ||||
| 	soustredeni = models.ForeignKey(Soustredeni, null=True, default=None, on_delete=models.PROTECT) | ||||
|  | @ -88,6 +88,7 @@ class Hlasovani(models.Model): | |||
| 	#: že všechna předchozí hlasování zde mají náhodný string…) | ||||
| 	#: TODO Změnit to na Osobu* | ||||
| 	ucastnik = models.CharField("Účastník", max_length=100) | ||||
| 	ucastnik_osoba = models.ForeignKey(Osoba, on_delete=models.CASCADE, blank=False, null=True) | ||||
| 	seznam = models.ForeignKey(Seznam, null=True, on_delete=models.SET_NULL) | ||||
| 
 | ||||
| 	def __str__(self): | ||||
|  | @ -98,6 +99,9 @@ class Znalost(models.Model): | |||
| 	""" | ||||
| 		Reprezentuje znalost, na kterou se můžeme účastníka ptát (nechat je hlasovat). | ||||
| 		(Viz :py:class:`HlasováníOZnalostech <prednasky.models.HlasovaniOZnalostech>`.) | ||||
| 
 | ||||
| 		(V podstatě :py:class:`Přednáška <prednasky.models.Prednaska>, jen neobsahuje | ||||
| 		tolik detailů a v hlasování má jiné odpovědi.) | ||||
| 	""" | ||||
| 	class Meta: | ||||
| 		db_table = "prednasky_znalost" | ||||
|  | @ -117,6 +121,9 @@ class HlasovaniOZnalostech(models.Model): | |||
| 		Reprezentuje hlasování jednoho účastníka | ||||
| 		o jedné :py:class:`Znalosti <prednasky.models.Znalost>` | ||||
| 		v jednom :py:class:`Seznamu <prednasky.models.Seznam>` (účastníkův pohled se totiž mezi sousy změnit) | ||||
| 
 | ||||
| 		(V podstatě totéž, co :py:class:`Hlasování <prednasky.models.Hlasovani>`, jen má jiné komentáře | ||||
| 		u odpovědí a místo přednášky odkazuje na znalost.) | ||||
| 	""" | ||||
| 	class Odpoved(models.IntegerChoices): | ||||
| 		""" Na kolik danou znalost účastník ovládá v daném Hlasování (větší číslo = víc zná) """ | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
| <p>Obtížnost 1 je nejlehčí, 3 nejtěžší.</p> | ||||
|   {{ form_set_prednasky.management_form }} | ||||
|   {% for f, p in formy_a_prednasky %} | ||||
|     <div class="hlasovani-prednaska"> | ||||
|     <h4>{{p.nazev}} ({{p.org}})</h4> | ||||
|     <p class="textprednasky">{{p.anotace | linebreaksbr}}</p> | ||||
|     <label>Obor: </label> {{p.obor}}<br> | ||||
|  | @ -22,17 +23,20 @@ | |||
|     <br> | ||||
|     {{ f }} | ||||
|     <br> | ||||
|     </div> | ||||
|   {% empty %} | ||||
|     Nejsou žádné přednášky o kterých by šlo hlasovat. | ||||
|   {% endfor %} | ||||
| 
 | ||||
|   {{ form_set_znalosti.management_form }} | ||||
|   {% for f, z in formy_a_znalosti %} | ||||
|     <div class="hlasovani-znalost"> | ||||
|     {% if forloop.first %}<hr/><h3>Jak moc znáš následující?</h3>{% endif %} | ||||
|     <h4>{{z.nazev}}</h4> | ||||
|     <p class="textznalosti">{{z.text | linebreaksbr}}</p> | ||||
|     {{ f }} | ||||
|     <br> | ||||
|     </div> | ||||
|   {% endfor %} | ||||
|   <input type="submit" value="Odeslat"/> | ||||
| </form> | ||||
|  |  | |||
|  | @ -2,19 +2,19 @@ | |||
| 
 | ||||
| {% block content %} | ||||
|   <h1>{% block nadpis1a %} | ||||
| 	  Hlasování o přednáškách | ||||
| 	  Výsledky hlasování o přednáškách | ||||
| 	  {% endblock %}</h1> | ||||
|   {# Projdi vsechny seznamy #} | ||||
|   <div class="mam-org-only"> | ||||
|   <ul> | ||||
|   {% for seznam in object_list %} | ||||
|     <li> | ||||
|     {% if seznam.stav == 1 %} {# STAV_NAHRH = 1 #}  | ||||
|         <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Návrh přednášek na soustředění {{seznam.soustredeni.misto}} </a> | ||||
|     {% if seznam.stav == seznam.Stav.NAVRH %} | ||||
|       Návrh přednášek na soustředění {{seznam.soustredeni.misto}} | ||||
|     {% else %} | ||||
|         <a href="/prednasky/seznam_prednasek/{{seznam.id}}">Seznam přednášek na soustředění {{seznam.soustredeni.misto}} </a> | ||||
|       Seznam přednášek na soustředění {{seznam.soustredeni.misto}} | ||||
|     {% endif %} | ||||
|     <a href="/prednasky/seznam_prednasek/{{seznam.id}}/hlasovani.csv">Export</a> | ||||
|       (<a href='{% url "seznam-export-csv" seznam=seznam.id %}'>CSV</a>) | ||||
|     </li> | ||||
|   {% endfor %} | ||||
|   </ul> | ||||
|  |  | |||
|  | @ -12,19 +12,9 @@ urlpatterns = [ | |||
| 		'prednasky/metaseznam_prednasek', | ||||
| 		org_required(views.MetaSeznamListView.as_view()), | ||||
| 		name='metaseznam-list'), | ||||
| 	# path( | ||||
| 	# 	'prednasky/seznam_prednasek/<int:seznam>/export', | ||||
| 	# 	org_required(views.SeznamExportView), | ||||
| 	# 	name='seznam-export' | ||||
| 	# ), | ||||
| 	path( | ||||
| 		'prednasky/seznam_prednasek/<int:seznam>/hlasovani.csv', | ||||
| 		org_required(views.PrednaskyExportView), | ||||
| 		name='seznam-export-csv' | ||||
| 	), | ||||
| 	path( | ||||
| 		'prednasky/seznam_prednasek/<int:seznam>/', | ||||
| 		org_required(views.SeznamListView.as_view()), | ||||
| 		name='seznam-list' | ||||
| 	), | ||||
| ] | ||||
|  |  | |||
|  | @ -64,6 +64,7 @@ def newPrednaska(request: HttpRequest) -> HttpResponse: | |||
| 						prednaska=prednaska, | ||||
| 						body=form.cleaned_data['body'], | ||||
| 						ucastnik=ucastnik, | ||||
| 						ucastnik_osoba=osoba, | ||||
| 						seznam=seznam, | ||||
| 					) | ||||
| 
 | ||||
|  | @ -116,8 +117,8 @@ def newPrednaska(request: HttpRequest) -> HttpResponse: | |||
| 		'prednasky/base.html', | ||||
| 		{ | ||||
| 			'form_set_prednasky': form_set_prednasky, 'form_set_znalosti': form_set_znalosti, | ||||
| 			'formy_a_prednasky': zip(form_set_prednasky, prednasky), | ||||
| 			'formy_a_znalosti': zip(form_set_znalosti, znalosti), | ||||
| 			'formy_a_prednasky': list(zip(form_set_prednasky, prednasky)), | ||||
| 			'formy_a_znalosti': list(zip(form_set_znalosti, znalosti)), | ||||
| 		} | ||||
| 	) | ||||
| 
 | ||||
|  | @ -132,64 +133,6 @@ class MetaSeznamListView(generic.ListView): | |||
| 	template_name = 'prednasky/metaseznam_prednasek.html' | ||||
| 
 | ||||
| 
 | ||||
| class SeznamListView(generic.ListView): | ||||
| 	""" | ||||
| 		Náhled na to, kolik má která přednáška v :py:class:`Seznamu <prednasky.models.Seznam>` :py:class:`hlasů <prednasky.models.Hlasovani.Body>`. | ||||
| 		(Je otázka, zda tento View vůbec chceme. Pokud ano, hodilo by se do něj přidat i znalosti.) | ||||
| 	""" | ||||
| 	template_name = 'prednasky/seznam_prednasek.html' | ||||
| 
 | ||||
| 	def get_queryset(self): | ||||
| 		self.seznam = get_object_or_404(Seznam, id=self.kwargs["seznam"]) | ||||
| 		prednasky = Prednaska.objects.filter(seznamy=self.seznam).order_by( | ||||
| 			'org__osoba__user__first_name', 'org__osoba__user__last_name' | ||||
| 		) | ||||
| 		return prednasky | ||||
| 
 | ||||
| 	# FIXME nahradit anotaci s filtrem po prechodu na Django 2.2 | ||||
| 	def get_context_data(self,**kwargs): | ||||
| 		context = super(SeznamListView, self).get_context_data(**kwargs) | ||||
| 
 | ||||
| 		# hlasovani se vztahuje k nejnovejsimu soustredeni | ||||
| 		sous = Soustredeni.objects.first() | ||||
| 		seznam = Seznam.objects.filter(soustredeni = sous, stav=Seznam.Stav.NAVRH).first() | ||||
| 	 | ||||
| 		for obj in self.object_list: | ||||
| 			hlasovani_set = obj.hlasovani_set.filter(seznam=seznam).only('body') | ||||
| 			obj.body = sum(map(lambda x: x.body,hlasovani_set)) | ||||
| 
 | ||||
| 		return context | ||||
| 
 | ||||
| 
 | ||||
| # def SeznamExportView(request, seznam): | ||||
| # 	"""Vypíše výsledky hlasování ve formátu pro prologovský optimalizátor""" | ||||
| # 	# TODO zřejmě se nepoužívá, časem vyřadit? nahradit tabulkou vhodnější pro | ||||
| # 	# lidi? | ||||
| # 	hlasovani = Hlasovani.objects.filter(seznam=seznam) | ||||
| # 	prednasky = Prednaska.objects.filter(seznamy=seznam) | ||||
| # 	orgove = set(p.org for p in prednasky) | ||||
| # 	ucastnici = set(h.ucastnik for h in hlasovani) | ||||
| # | ||||
| # 	for p in prednasky: | ||||
| # 		p.body = [] | ||||
| # 		for u in ucastnici: | ||||
| # 			try: | ||||
| # 				p.body.append(hlasovani.get(ucastnik=u, prednaska=p).body) | ||||
| # 			except ObjectDoesNotExist: | ||||
| # 				# účastník nehlasoval | ||||
| # 				p.body.append("?") | ||||
| # | ||||
| # 	for h in hlasovani: | ||||
| # 		h.ucastnik = hash(h.ucastnik) | ||||
| # | ||||
| # 	return render( | ||||
| # 		request, | ||||
| # 		'prednasky/seznam_prednasek_export.txt', | ||||
| # 		{"hlasovani": hlasovani, "prednasky": prednasky, "orgove": orgove}, | ||||
| # 		content_type="text/plain" | ||||
| # 	) | ||||
| 
 | ||||
| 
 | ||||
| def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResponse: | ||||
| 	""" | ||||
| 		Vrátí všechna :py:class:`Hlasování <prednasky.models.Hlasovani>` | ||||
|  | @ -214,6 +157,8 @@ def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResp | |||
| 	# A po inicializaci sloupců vyplníme tabulku | ||||
| 	table: [str, list[str|Prednaska|Znalost,]] = {} | ||||
| 
 | ||||
| 	errors = [] | ||||
| 
 | ||||
| 	for h in hlasovani: | ||||
| 		if h.ucastnik not in table: # Pokud jsme účastníka ještě neviděli, předgenerujeme si jeho řádek | ||||
| 			table[h.ucastnik] = [h.ucastnik] + ([""] * width) | ||||
|  | @ -221,7 +166,7 @@ def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResp | |||
| 		if h.prednaska.id in prednasky_map: | ||||
| 			table[h.ucastnik][prednasky_map[h.prednaska.id]] = h.body | ||||
| 		else: | ||||
| 			pass # TODO Padat hlasitě? | ||||
| 			errors.append(f"Přednáška {h.prednaska.id} ({h.prednaska}) dostala od Účastníka {h.ucastnik} následující hodnocení: {h.body}") | ||||
| 
 | ||||
| 	for h in hlasovani_o_znalostech: | ||||
| 		ucastnik = str(h.ucastnik) + ' ' + str(h.ucastnik.id) # id, kvůli kolizi jmen | ||||
|  | @ -231,8 +176,10 @@ def PrednaskyExportView(request: HttpRequest, seznam: int, **kwargs) -> HttpResp | |||
| 		if h.znalost.id in znalosti_map: | ||||
| 			table[ucastnik][znalosti_map[h.znalost.id]] = h.odpoved | ||||
| 		else: | ||||
| 			pass # TODO Padat hlasitě? | ||||
| 			errors.append(f"Znalost {h.znalost.id} ({h.znalost}) dostala od Účastníka {h.ucastnik.id} následující odpověď: {h.odpoved}") | ||||
| 
 | ||||
| 	if len(errors) > 0: | ||||
| 		logger.error("Při exportování hlasování o přednáškách a znalostech se neexportovali hodnocení a přednášky (pravděpodobně se od hlasování vyškrtla nějaká znalost/přednáška ze seznamu):\n" + "\n".join(errors)) | ||||
| 
 | ||||
| 	response = HttpResponse(content_type="text/csv", charset="utf-8") | ||||
| 	response["Content-Disposition"] = 'attachment; filename="hlasovani.csv"' | ||||
|  |  | |||
|  | @ -16,4 +16,11 @@ | |||
| 	  {% endfor %} | ||||
|   </ul> | ||||
| 
 | ||||
|   <h2>Seznam účastníků – červená znamená že jim nechodí fyzické číslo</h2> | ||||
|   <ul> | ||||
|     {% for resitel in resitele %} | ||||
|       <li {% if resitel.neposilame %}style="color: white; background-color: red;"{% endif %}>{{ resitel.jmeno }}: {% if resitel.bodydiff > 3 %}🧦{% endif %} {% if resitel.ttitul != resitel.ftitul %} {{resitel.ftitul}} → {{resitel.ttitul}} {% endif %}</li> | ||||
|     {% endfor %} | ||||
|   </ul> | ||||
| 
 | ||||
| {% endblock content %} | ||||
|  |  | |||
|  | @ -28,6 +28,30 @@ def resi_v_rocniku(rocnik, cislo=None): | |||
| 			reseni__hodnoceni__deadline_body__cislo__poradi__lte=cislo.poradi | ||||
| 		).distinct() | ||||
| 	 | ||||
| def resi_cislo(cislo): | ||||
| 	""" Vrátí seznam řešitelů, co vyřešili nějaký problém v daném čísle. | ||||
| 	Parametry: | ||||
| 		cislo (typu Cislo)	číslo, ve kterém chci řešitele, co něco odevzdali | ||||
| 	Výstup: | ||||
| 		QuerySet objektů typu Resitel  | ||||
| 	""" | ||||
| 
 | ||||
| 	return personalni.models.Resitel.objects.filter( | ||||
| 		reseni__hodnoceni__deadline_body__cislo=cislo | ||||
| 	).distinct() | ||||
| 
 | ||||
| def resitele_co_neodmaturovali(): | ||||
| 	""" Vrátí seznam řešitelů, co ještě neodmaturovali. | ||||
| 	Pokud ještě není srpen, tak zahrnuje i ty, kteří odmaturovali letos. | ||||
| 
 | ||||
| 	Výstup: | ||||
| 		QuerySet objektů typu Resitel """ | ||||
| 	from datetime import datetime | ||||
| 	current_year = datetime.now().year | ||||
| 	if datetime.now().month < 8: | ||||
| 		current_year -= 1 | ||||
| 	return personalni.models.Resitel.objects.filter(rok_maturity__gte=current_year) | ||||
| 
 | ||||
| 
 | ||||
| def aktivniResitele(cislo, pouze_letosni=False): | ||||
| 	""" Vrací QuerySet aktivních řešitelů, což jsou ti, co ještě neodmaturovali | ||||
|  |  | |||
|  | @ -375,7 +375,8 @@ class OdmenyView(generic.TemplateView): | |||
| 		tocislo = get_object_or_404(Cislo, rocnik=self.kwargs.get('trocnik'), poradi=self.kwargs.get('tcislo')) | ||||
| 		resitele = utils.aktivniResitele(tocislo) | ||||
| 
 | ||||
| 		def get_diff(from_deadline: Deadline, to_deadline: Deadline): | ||||
| 		def get_diff(from_deadline: Deadline, to_deadline: Deadline, probody=False): | ||||
| 			"""Co je probody? pokud True, funkce vrací všechny rešitele a k nim potřebné informace, pokud False, vrací jen ty, kteří mají změnu v titulu.""" | ||||
| 			frombody = body_resitelu(resitele=resitele, jen_verejne=False, do=from_deadline) | ||||
| 			tobody = body_resitelu(resitele=resitele, jen_verejne=False, do=to_deadline) | ||||
| 			outlist = [] | ||||
|  | @ -384,6 +385,9 @@ class OdmenyView(generic.TemplateView): | |||
| 				tbody = tobody.get(resitel.id, 0) | ||||
| 				ftitul = resitel.get_titul(fbody) | ||||
| 				ttitul = resitel.get_titul(tbody) | ||||
| 				if probody: | ||||
| 					outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'fbody': fbody, 'tbody': tbody, 'ftitul': ftitul, 'ttitul': ttitul, 'bodydiff': tbody - fbody, "neposilame": not(resitel.zasilat_cislo_papirove)}) | ||||
| 				else: | ||||
| 					if ftitul != ttitul: | ||||
| 						outlist.append({'jmeno': resitel.osoba.plne_jmeno(), 'ftitul': ftitul, 'ttitul': ttitul}) | ||||
| 			return outlist | ||||
|  | @ -401,6 +405,7 @@ class OdmenyView(generic.TemplateView): | |||
| 		context["from_deadline"] = from_deadline | ||||
| 		context["to_deadline"] = to_deadline | ||||
| 		context["zmeny"] = get_diff(from_deadline, to_deadline) | ||||
| 		context["resitele"] = get_diff(from_deadline, to_deadline, probody=resitele.order_by("osoba__prijmeni")) | ||||
| 
 | ||||
| 		return context | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue