Move odevzdavatko do aplikace odevzdavatko
This commit is contained in:
		
							parent
							
								
									fd8ef17959
								
							
						
					
					
						commit
						ef68d3fb75
					
				
					 26 changed files with 599 additions and 534 deletions
				
			
		|  | @ -138,7 +138,7 @@ INSTALLED_APPS = ( | ||||||
|     'various.autentizace', |     'various.autentizace', | ||||||
|     'api', |     'api', | ||||||
|     'aesop', |     'aesop', | ||||||
| 
 |     'odevzdavatko', | ||||||
|     # Admin upravy: |     # Admin upravy: | ||||||
| 
 | 
 | ||||||
| #    'material', | #    'material', | ||||||
|  |  | ||||||
|  | @ -17,6 +17,9 @@ urlpatterns = [ | ||||||
| 	# Seminarova aplikace (ma vlastni podadresare) | 	# Seminarova aplikace (ma vlastni podadresare) | ||||||
| 	path('', include('seminar.urls')), | 	path('', include('seminar.urls')), | ||||||
| 
 | 
 | ||||||
|  | 	# Odevzdavatko (ma vlastni podadresare) | ||||||
|  | 	path('', include('odevzdavatko.urls')), | ||||||
|  | 	 | ||||||
| 	# Korekturovaci aplikace (ma vlastni podadresare) | 	# Korekturovaci aplikace (ma vlastni podadresare) | ||||||
| 	path('', include('korektury.urls')), | 	path('', include('korektury.urls')), | ||||||
| 	 | 	 | ||||||
|  |  | ||||||
							
								
								
									
										11
									
								
								odevzdavatko/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								odevzdavatko/__init__.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | """ | ||||||
|  | Obsahuje vše, co se týká odevzdávání (+ nahrávání) a opravování řešení řešitelů. | ||||||
|  | 
 | ||||||
|  | Slovníček: | ||||||
|  |     Moje řešení = Přehled řešení = Řešení, která odevzdal aktuálního uživatel sám. | ||||||
|  |     Došlá řešení = Tabulka + seznam + detail + ... = Řešení, která poslal někdo jiný. | ||||||
|  |     Poslat řešení = Odevdat mé řešení. (Tj. řešení se vztahem k aktuálnímu uživateli.) | ||||||
|  |     Nahrát řešení = Nahrání řešení bez vztahu k aktuálnímu uživateli. | ||||||
|  | 
 | ||||||
|  | TODO: Místo vložit řešení v nahrávání a posílání řešení dát něco jiného? | ||||||
|  | """ | ||||||
							
								
								
									
										28
									
								
								odevzdavatko/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								odevzdavatko/admin.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | ||||||
|  | from django.contrib import admin | ||||||
|  | from django_reverse_admin import ReverseModelAdmin | ||||||
|  | import seminar.models as m | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PrilohaReseniInline(admin.TabularInline): | ||||||
|  | 	model = m.PrilohaReseni | ||||||
|  | 	extra = 1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Reseni_ResiteleInline(admin.TabularInline): | ||||||
|  | 	model = m.Reseni_Resitele | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @admin.register(m.Reseni) | ||||||
|  | class ReseniAdmin(ReverseModelAdmin): | ||||||
|  | 	base_model = m.Reseni | ||||||
|  | 	inline_type = 'tabular' | ||||||
|  | 	# inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz | ||||||
|  | 	inline_reverse = ['resitele'] | ||||||
|  | 	exclude = ['text_zkraceny', 'text_zkraceny_set'] | ||||||
|  | 	inlines = [PrilohaReseniInline] | ||||||
|  | # FAIL in template | ||||||
|  | #	inlines = [PrilohaReseniInline,Reseni_ResiteleInline] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | admin.site.register(m.PrilohaReseni) | ||||||
|  | admin.site.register(m.Hodnoceni) | ||||||
							
								
								
									
										5
									
								
								odevzdavatko/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								odevzdavatko/apps.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | from django.apps import AppConfig | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class OdevzdavatkoConfig(AppConfig): | ||||||
|  |     name = 'odevzdavatko' | ||||||
							
								
								
									
										218
									
								
								odevzdavatko/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								odevzdavatko/forms.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,218 @@ | ||||||
|  | from django import forms | ||||||
|  | from dal import autocomplete | ||||||
|  | from django.forms import formset_factory | ||||||
|  | from django.forms.models import inlineformset_factory | ||||||
|  | 
 | ||||||
|  | from seminar.models import Resitel | ||||||
|  | import seminar.models as m | ||||||
|  | 
 | ||||||
|  | import logging | ||||||
|  | 
 | ||||||
|  | # pro přidání políčka do formuláře je potřeba | ||||||
|  | # - mít v modelu tu položku, kterou chci upravovat | ||||||
|  | # - přidat do views (prihlaskaView, resitelEditView) | ||||||
|  | # - přidat do forms | ||||||
|  | # - includovat do html | ||||||
|  | 
 | ||||||
|  | class DateInput(forms.DateInput): | ||||||
|  |     # aby se datum dalo vybírat z kalendáře | ||||||
|  |     input_type = 'date'  | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PosliReseniForm(forms.Form): | ||||||
|  | 	#FIXME jen podproblémy daného problému | ||||||
|  | 	problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all()) | ||||||
|  | 	# to_field_name | ||||||
|  | 	#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', | ||||||
|  | 	#	through='Hodnoceni') | ||||||
|  | 
 | ||||||
|  | 	# FIXME pridat vice resitelu | ||||||
|  | 	resitel = forms.ModelChoiceField(label="Řešitel", | ||||||
|  | 		queryset=Resitel.objects.all(), | ||||||
|  | 		widget=autocomplete.ModelSelect2( | ||||||
|  | 			url='autocomplete_resitel', | ||||||
|  | 			attrs = {'data-placeholder--id': '-1', | ||||||
|  | 				'data-placeholder--text' : '---', | ||||||
|  | 				'data-allow-clear': 'true'}) | ||||||
|  |     		) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	#resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', | ||||||
|  | 	#	help_text='Seznam autorů řešení', through='Reseni_Resitele') | ||||||
|  | 	 | ||||||
|  | 	cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení") | ||||||
|  | 
 | ||||||
|  | 	#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) | ||||||
|  | 
 | ||||||
|  | 	forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES) | ||||||
|  | 	#forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, | ||||||
|  | 	#	 default=FORMA_EMAIL) | ||||||
|  | 
 | ||||||
|  | 	poznamka = forms.CharField(label='Neveřejná poznámka', required=False) | ||||||
|  | 	#poznamka = models.TextField('neveřejná poznámka', blank=True, | ||||||
|  | 	#	help_text='Neveřejná poznámka k řešení (plain text)') | ||||||
|  | 
 | ||||||
|  | 	#TODO body do cisla | ||||||
|  | 	#TODO prilohy | ||||||
|  | 
 | ||||||
|  | 	##def __init__(self, *args, **kwargs): | ||||||
|  | 	##	super().__init__(*args, **kwargs) | ||||||
|  | 	##	#self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) | ||||||
|  | 
 | ||||||
|  | class NahrajReseniForm(forms.ModelForm): | ||||||
|  | 	class Meta: | ||||||
|  | 		model = m.Reseni | ||||||
|  | 		fields = ('problem',) | ||||||
|  | 		help_texts = {'problem':''} # Nezobrazovat help text ve formuláři | ||||||
|  | 		 | ||||||
|  | 		widgets = {'problem': | ||||||
|  | 				autocomplete.ModelSelect2Multiple( | ||||||
|  | 					url='autocomplete_problem_odevzdatelny', | ||||||
|  | 					attrs = {'data-placeholder--id': '-1', | ||||||
|  | 						'data-placeholder--text' : '---', | ||||||
|  | 						'data-allow-clear': 'true'}, | ||||||
|  | 				) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,  | ||||||
|  | 		form = NahrajReseniForm, | ||||||
|  | 		fields = ('soubor','res_poznamka'), | ||||||
|  | 		widgets = {'res_poznamka':forms.TextInput()}, | ||||||
|  | 		extra = 1, | ||||||
|  | 		can_delete = False, | ||||||
|  | 
 | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class JednoHodnoceniForm(forms.ModelForm): | ||||||
|  | 	class Meta: | ||||||
|  | 		model = m.Hodnoceni | ||||||
|  | 		fields = ('problem', 'body', 'cislo_body') | ||||||
|  | 		widgets = { | ||||||
|  | 			'problem': autocomplete.ModelSelect2( | ||||||
|  | 				url='autocomplete_problem_odevzdatelny',   # FIXME: Dovolit i starší? | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, | ||||||
|  | 		extra = 0, | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | class PoznamkaReseniForm(forms.ModelForm): | ||||||
|  | 	class Meta: | ||||||
|  | 		model = m.Reseni | ||||||
|  | 		fields = ('poznamka',) | ||||||
|  | 
 | ||||||
|  | # FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat | ||||||
|  | DATE_FORMAT = '%Y-%m-%d' | ||||||
|  | 
 | ||||||
|  | class OdevzdavatkoTabulkaFiltrForm(forms.Form): | ||||||
|  | 	"""Form pro filtrování přehledové odevzdávátkové tabulky | ||||||
|  | 
 | ||||||
|  | 	Inspirováno https://kam.mff.cuni.cz/mffzoom/""" | ||||||
|  | 
 | ||||||
|  | 	# Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices) | ||||||
|  | 
 | ||||||
|  | 	RESITELE_RELEVANTNI = 'relevantni' | ||||||
|  | 	RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi' | ||||||
|  | 	RESITELE_CHOICES = [ | ||||||
|  | 		(RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky | ||||||
|  | 		(RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'), | ||||||
|  | 		# Možná: všechny vč. historických? | ||||||
|  | 		] | ||||||
|  | 
 | ||||||
|  | 	PROBLEMY_MOJE = 'moje' | ||||||
|  | 	PROBLEMY_LETOSNI = 'letosni' | ||||||
|  | 	PROBLEMY_CHOICES = [ | ||||||
|  | 		(PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga | ||||||
|  | 		(PROBLEMY_LETOSNI, 'Všechny letošní'), | ||||||
|  | 		# TODO: *hlavní problémy, možná všechny... | ||||||
|  | 		# XXX: Chtělo by to i "aktuálně zadané... | ||||||
|  | 		] | ||||||
|  | 
 | ||||||
|  | 	# TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)? | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	@classmethod | ||||||
|  | 	def gen_terminy(cls, rocnik=None): | ||||||
|  | 		import datetime | ||||||
|  | 		from time import strftime | ||||||
|  | 		 | ||||||
|  | 		from django.db.utils import OperationalError | ||||||
|  | 		try: | ||||||
|  | 			aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik | ||||||
|  | 			aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo | ||||||
|  | 		except OperationalError: | ||||||
|  | 			# django.db.utils.OperationalError: no such table: seminar_nastaveni | ||||||
|  | 			# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál | ||||||
|  | 			logger = logging.getLogger(__name__) | ||||||
|  | 			logger.error("Rozbitá databáze (před počátečními migracemi?)") | ||||||
|  | 			return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')] | ||||||
|  | 
 | ||||||
|  | 		# FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš. | ||||||
|  | 		if rocnik is not None: | ||||||
|  | 			aktualni_rocnik = rocnik | ||||||
|  | 			aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last() | ||||||
|  | 
 | ||||||
|  | 		result = [] | ||||||
|  | 
 | ||||||
|  | 		for cislo in m.Cislo.objects.filter( | ||||||
|  | 				rocnik=aktualni_rocnik, | ||||||
|  | 				poradi__lte=aktualni_cislo.poradi, | ||||||
|  | 				).reverse():	# Standardně se řadí od nejnovějšího čísla | ||||||
|  | 			# Předem je mi líto kohokoliv, kdo tyhle řádky bude číst... | ||||||
|  | 			if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today(): | ||||||
|  | 				result.append(( | ||||||
|  | 					strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()), | ||||||
|  | 					f"Vydání {cislo.poradi}. čísla")) | ||||||
|  | 			if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today(): | ||||||
|  | 				result.append(( | ||||||
|  | 					strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()), | ||||||
|  | 					f"Předdeadline {cislo.poradi}. čísla")) | ||||||
|  | 			if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today(): | ||||||
|  | 				result.append(( | ||||||
|  | 					strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()), | ||||||
|  | 					f"Sous. deadline {cislo.poradi}. čísla")) | ||||||
|  | 			if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today(): | ||||||
|  | 				result.append(( | ||||||
|  | 					strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()), | ||||||
|  | 					f"Finální deadline {cislo.poradi}. čísla")) | ||||||
|  | 		result.append(( | ||||||
|  | 			strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes")) | ||||||
|  | 
 | ||||||
|  | 		return result | ||||||
|  | 
 | ||||||
|  | 	@classmethod | ||||||
|  | 	def gen_initial(cls, rocnik=None): | ||||||
|  | 		terminy = cls.gen_terminy(rocnik) | ||||||
|  | 		initial = { | ||||||
|  | 			'resitele': cls.RESITELE_RELEVANTNI, | ||||||
|  | 			'problemy': cls.PROBLEMY_MOJE, | ||||||
|  | 			# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… | ||||||
|  | 			'reseni_od': terminy[-2] if rocnik is None else terminy[0], | ||||||
|  | 			'reseni_do': terminy[-1], | ||||||
|  | 			'neobodovane': False, | ||||||
|  | 		} | ||||||
|  | 		return initial | ||||||
|  | 
 | ||||||
|  | 	def __init__(self, *args, rocnik=None, **kwargs): | ||||||
|  | 		if 'initial' not in kwargs: | ||||||
|  | 			super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs) | ||||||
|  | 		else: | ||||||
|  | 			super().__init__(*args, **kwargs) | ||||||
|  | 		# choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem... | ||||||
|  | 		# A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat... | ||||||
|  | 		self.terminy = self.gen_terminy(rocnik) | ||||||
|  | 		self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik)) | ||||||
|  | 		# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… | ||||||
|  | 		self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0] | ||||||
|  | 		self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik)) | ||||||
|  | 		self.fields['reseni_do'].initial = self.terminy[-1] | ||||||
|  | 
 | ||||||
|  | 	# NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views... | ||||||
|  | 	resitele = forms.ChoiceField(choices=RESITELE_CHOICES) | ||||||
|  | 	problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES) | ||||||
|  | 	 | ||||||
|  | 	reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) | ||||||
|  | 	reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) | ||||||
|  | 	neobodovane = forms.BooleanField(required=False) | ||||||
							
								
								
									
										0
									
								
								odevzdavatko/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								odevzdavatko/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							| Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 717 B | 
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB | 
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
| 
 | 
 | ||||||
| {# FIXME: Necopypastovat! Tohle je zkopírované ze static/seminar/dynamic_formsets.js #} | {# FIXME: Necopypastovat! Tohle je zkopírované ze static/odevzdavatko/dynamic_formsets.js #} | ||||||
| <script type='text/javascript'> | <script type='text/javascript'> | ||||||
| // Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 | // Credit https://medium.com/all-about-django/adding-forms-dynamically-to-a-django-formset-375f1090c2b0 | ||||||
| function updateElementIndex(el, prefix, ndx) { | function updateElementIndex(el, prefix, ndx) { | ||||||
|  | @ -104,14 +104,14 @@ $(document).ready(function(){ | ||||||
| 		<td>{{ subform.problem }}</td> | 		<td>{{ subform.problem }}</td> | ||||||
| 		<td>{{ subform.body }}</td> | 		<td>{{ subform.body }}</td> | ||||||
| 		<td>{{ subform.cislo_body }}</td> | 		<td>{{ subform.cislo_body }}</td> | ||||||
| 		<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td> | 		<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> | ||||||
| 	</tr> | 	</tr> | ||||||
|     </tbody> |     </tbody> | ||||||
| {% endfor %} | {% endfor %} | ||||||
| </table> | </table> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| <a href="#"> <img src="{% static "seminar/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br> | <a href="#"> <img src="{% static "odevzdavatko/plus.png" %}" id="pridat_hodnoceni" alt="Přidat hodnocení"></a> </br> | ||||||
| <input type=submit value="Uložit"></form> | <input type=submit value="Uložit"></form> | ||||||
| 
 | 
 | ||||||
| <table id="empty_form" style="display: none;"> | <table id="empty_form" style="display: none;"> | ||||||
|  | @ -119,7 +119,7 @@ $(document).ready(function(){ | ||||||
| 		<td>{{ form.empty_form.problem }}</td> | 		<td>{{ form.empty_form.problem }}</td> | ||||||
| 		<td>{{ form.empty_form.body }}</td> | 		<td>{{ form.empty_form.body }}</td> | ||||||
| 		<td>{{ form.empty_form.cislo_body }}</td> | 		<td>{{ form.empty_form.cislo_body }}</td> | ||||||
| 		<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "seminar/cross.png" %}" alt="Smazat"></a></td> | 		<td><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> | ||||||
| 	</tr> | 	</tr> | ||||||
| </table> | </table> | ||||||
| 
 | 
 | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| {% extends "base.html" %} | {% extends "base.html" %} | ||||||
| {% load staticfiles %} | {% load staticfiles %} | ||||||
| {% block script %} | {% block script %} | ||||||
|     <script src="{% static 'seminar/dynamic_formsets.js' %}"></script> |     <script src="{% static 'odevzdavatko/dynamic_formsets.js' %}"></script> | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
|  | @ -3,7 +3,6 @@ | ||||||
| {% block script %} | {% block script %} | ||||||
|     <!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!--> |     <!--script type="text/javascript" src="{% static 'admin/js/vendor/jquery/jquery.js' %}"></script!--> | ||||||
|     {{form.media}} |     {{form.media}} | ||||||
|     <script src="{% static 'seminar/prihlaska.js' %}"></script> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
| 
 | 
 | ||||||
| <form method=get action=.> | <form method=get action=../odevzdavatko> | ||||||
| {{ filtr.resitele }} | {{ filtr.resitele }} | ||||||
| {{ filtr.problemy }} | {{ filtr.problemy }} | ||||||
| Od: {{ filtr.reseni_od }} | Od: {{ filtr.reseni_od }} | ||||||
							
								
								
									
										18
									
								
								odevzdavatko/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								odevzdavatko/urls.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | ||||||
|  | from django.urls import path | ||||||
|  | 
 | ||||||
|  | from seminar.utils import org_required, resitel_required, viewMethodSwitch, \ | ||||||
|  | 	resitel_or_org_required | ||||||
|  | from . import views | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  | 	path('org/add_solution', org_required(views.PosliReseniView.as_view()), name='seminar_vloz_reseni'), | ||||||
|  | 	path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), | ||||||
|  | 	path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), | ||||||
|  | 
 | ||||||
|  | 	path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), | ||||||
|  | 	path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), | ||||||
|  | 	path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), | ||||||
|  | 	path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), | ||||||
|  | 	path('org/reseni/all', org_required(views.SeznamReseniView.as_view())), | ||||||
|  | 	path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())), | ||||||
|  | ] | ||||||
|  | @ -1,8 +1,10 @@ | ||||||
| from django.views.generic import ListView, DetailView, FormView | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
|  | from django.core.mail import send_mail | ||||||
|  | from django.utils import timezone | ||||||
|  | from django.views.generic import ListView, DetailView, FormView, CreateView | ||||||
| from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin | from django.views.generic.list import MultipleObjectTemplateResponseMixin,MultipleObjectMixin | ||||||
| from django.views.generic.base import View | from django.views.generic.base import View | ||||||
| from django.views.generic.detail import SingleObjectMixin | from django.shortcuts import redirect, get_object_or_404, render | ||||||
| from django.shortcuts import redirect, get_object_or_404 |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
|  | @ -13,9 +15,10 @@ from itertools import groupby | ||||||
| import logging | import logging | ||||||
| 
 | 
 | ||||||
| import seminar.models as m | import seminar.models as m | ||||||
| import seminar.forms as f | from . import forms as f | ||||||
| from seminar.forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm | from .forms import OdevzdavatkoTabulkaFiltrForm as FiltrForm | ||||||
| from seminar.utils import aktivniResitele, resi_v_rocniku, deadline | from seminar.utils import resi_v_rocniku, deadline | ||||||
|  | from seminar.views import formularOKView | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +44,7 @@ class SouhrnReseni: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TabulkaOdevzdanychReseniView(ListView): | class TabulkaOdevzdanychReseniView(ListView): | ||||||
| 	template_name = 'seminar/odevzdavatko/tabulka.html' | 	template_name = 'odevzdavatko/tabulka.html' | ||||||
| 	model = m.Hodnoceni | 	model = m.Hodnoceni | ||||||
| 
 | 
 | ||||||
| 	def inicializuj_osy_tabulky(self): | 	def inicializuj_osy_tabulky(self): | ||||||
|  | @ -161,7 +164,7 @@ class TabulkaOdevzdanychReseniView(ListView): | ||||||
| # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? | # Velmi silně inspirováno zdrojáky, FIXME: Nedá se to udělat smysluplněji? | ||||||
| class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): | class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixin, View): | ||||||
| 	model = m.Reseni | 	model = m.Reseni | ||||||
| 	template_name = 'seminar/odevzdavatko/seznam.html' | 	template_name = 'odevzdavatko/seznam.html' | ||||||
| 	 | 	 | ||||||
| 	def get_queryset(self): | 	def get_queryset(self): | ||||||
| 		qs = super().get_queryset() | 		qs = super().get_queryset() | ||||||
|  | @ -203,7 +206,7 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi | ||||||
| ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex | ## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex | ||||||
| class DetailReseniView(DetailView): | class DetailReseniView(DetailView): | ||||||
| 	model = m.Reseni | 	model = m.Reseni | ||||||
| 	template_name = 'seminar/odevzdavatko/detail.html' | 	template_name = 'odevzdavatko/detail.html' | ||||||
| 	 | 	 | ||||||
| 	def aktualni_hodnoceni(self): | 	def aktualni_hodnoceni(self): | ||||||
| 		self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) | 		self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) | ||||||
|  | @ -227,7 +230,7 @@ class DetailReseniView(DetailView): | ||||||
| 
 | 
 | ||||||
| def hodnoceniReseniView(request, pk, *args, **kwargs): | def hodnoceniReseniView(request, pk, *args, **kwargs): | ||||||
| 	reseni = get_object_or_404(m.Reseni, pk=pk) | 	reseni = get_object_or_404(m.Reseni, pk=pk) | ||||||
| 	template_name = 'seminar/odevzdavatko/detail.html' | 	template_name = 'odevzdavatko/detail.html' | ||||||
| 	form_class = f.OhodnoceniReseniFormSet | 	form_class = f.OhodnoceniReseniFormSet | ||||||
| 	success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) | 	success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk}) | ||||||
| 
 | 
 | ||||||
|  | @ -266,7 +269,7 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | ||||||
| 
 | 
 | ||||||
| class PrehledOdevzdanychReseni(ListView): | class PrehledOdevzdanychReseni(ListView): | ||||||
| 	model = m.Hodnoceni | 	model = m.Hodnoceni | ||||||
| 	template_name = 'seminar/odevzdavatko/resitel_prehled.html' | 	template_name = 'odevzdavatko/prehled_reseni.html' | ||||||
| 
 | 
 | ||||||
| 	def get_queryset(self): | 	def get_queryset(self): | ||||||
| 		if not self.request.user.is_authenticated: | 		if not self.request.user.is_authenticated: | ||||||
|  | @ -292,7 +295,7 @@ class PrehledOdevzdanychReseni(ListView): | ||||||
| 
 | 
 | ||||||
| class SeznamReseniView(ListView): | class SeznamReseniView(ListView): | ||||||
| 	model = m.Reseni | 	model = m.Reseni | ||||||
| 	template_name = 'seminar/odevzdavatko/seznam.html' | 	template_name = 'odevzdavatko/seznam.html' | ||||||
| 
 | 
 | ||||||
| class SeznamAktualnichReseniView(SeznamReseniView): | class SeznamAktualnichReseniView(SeznamReseniView): | ||||||
| 	def get_queryset(self): | 	def get_queryset(self): | ||||||
|  | @ -301,3 +304,94 @@ class SeznamAktualnichReseniView(SeznamReseniView): | ||||||
| 		resitele = resi_v_rocniku(akt_rocnik) | 		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 | 		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 | 		return qs | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PosliReseniView(LoginRequiredMixin, FormView): | ||||||
|  | 	template_name = 'odevzdavatko/posli_reseni.html' | ||||||
|  | 	form_class = f.PosliReseniForm | ||||||
|  | 
 | ||||||
|  | 	def form_valid(self, form): | ||||||
|  | 		data = form.cleaned_data | ||||||
|  | 		nove_reseni = m.Reseni.objects.create( | ||||||
|  | 			cas_doruceni=data['cas_doruceni'], | ||||||
|  | 			forma=data['forma'], | ||||||
|  | 			poznamka=data['poznamka'], | ||||||
|  | 		) | ||||||
|  | 		nove_reseni.resitele.add(data['resitel']) | ||||||
|  | 		nove_reseni.problem.add(data['problem']) | ||||||
|  | 		nove_reseni.save() | ||||||
|  | 		# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil. | ||||||
|  | 		return redirect(reverse('profil')) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class NahrajReseniView(LoginRequiredMixin, CreateView): | ||||||
|  | 	model = m.Reseni | ||||||
|  | 	template_name = 'odevzdavatko/nahraj_reseni.html' | ||||||
|  | 	form_class = f.NahrajReseniForm | ||||||
|  | 
 | ||||||
|  | 	def get(self, request, *args, **kwargs): | ||||||
|  | 		# Zaříznutí starých řešitelů: | ||||||
|  | 		# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… | ||||||
|  | 		osoba = m.Osoba.objects.get(user=self.request.user) | ||||||
|  | 		resitel = osoba.resitel | ||||||
|  | 		if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok: | ||||||
|  | 			return render(request, 'universal.html', { | ||||||
|  | 				'title': 'Nelze odevzdat', | ||||||
|  | 				'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.', | ||||||
|  | 				'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.', | ||||||
|  | 			}) | ||||||
|  | 		return super().get(request, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 	def get_context_data(self,**kwargs): | ||||||
|  | 		data = super().get_context_data(**kwargs) | ||||||
|  | 		if self.request.POST: | ||||||
|  | 			data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) | ||||||
|  | 		else: | ||||||
|  | 			data['prilohy'] = f.ReseniSPrilohamiFormSet() | ||||||
|  | 		return data | ||||||
|  | 
 | ||||||
|  | 	# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni | ||||||
|  | 	# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset | ||||||
|  | 	def form_valid(self,form): | ||||||
|  | 		context = self.get_context_data() | ||||||
|  | 		prilohy = context['prilohy'] | ||||||
|  | 		if not prilohy.is_valid(): | ||||||
|  | 			return super().form_invalid(form) | ||||||
|  | 		with transaction.atomic(): | ||||||
|  | 			self.object = form.save() | ||||||
|  | 			self.object.resitele.add(m.Resitel.objects.get(osoba__user = self.request.user)) | ||||||
|  | 			self.object.cas_doruceni = timezone.now() | ||||||
|  | 			self.object.forma = m.Reseni.FORMA_UPLOAD | ||||||
|  | 			self.object.save() | ||||||
|  | 
 | ||||||
|  | 			prilohy.instance = self.object | ||||||
|  | 			prilohy.save() | ||||||
|  | 
 | ||||||
|  | 		# Pošleme mail opravovatelům a garantovi | ||||||
|  | 		# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí | ||||||
|  | 		prijemci = set() | ||||||
|  | 		problemy = [] | ||||||
|  | 		for prob in form.cleaned_data['problem']: | ||||||
|  | 			prijemci.update(prob.opravovatele.all()) | ||||||
|  | 			if prob.garant is not None: | ||||||
|  | 				prijemci.add(prob.garant) | ||||||
|  | 			problemy.append(prob) | ||||||
|  | 		# FIXME: Možná poslat mail i relevantním orgům nadproblémů? | ||||||
|  | 		if len(prijemci) < 1: | ||||||
|  | 			logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}") | ||||||
|  | 		# FIXME: Víc informativní obsah mailů, možná vč. příloh? | ||||||
|  | 		prijemci = map(lambda it: it.osoba.email, prijemci) | ||||||
|  | 
 | ||||||
|  | 		resitel = m.Osoba.objects.get(user = self.request.user) | ||||||
|  | 
 | ||||||
|  | 		seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy)) | ||||||
|  | 		seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })") | ||||||
|  | 
 | ||||||
|  | 		send_mail( | ||||||
|  | 			subject="Nové řešení k " + seznam_do_subjectu, | ||||||
|  | 			message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }", | ||||||
|  | 			from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení? | ||||||
|  | 			recipient_list=list(prijemci), | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		return formularOKView(self.request, text='Řešení úspěšně odevzdáno') | ||||||
|  | @ -230,27 +230,6 @@ class SoustredeniAdmin(admin.ModelAdmin): | ||||||
| 	inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline] | 	inlines = [SoustredeniUcastniciInline, SoustredeniOrganizatoriInline] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PrilohaReseniInline(admin.TabularInline): |  | ||||||
| 	model = m.PrilohaReseni |  | ||||||
| 	extra = 1 |  | ||||||
| admin.site.register(m.PrilohaReseni) |  | ||||||
| 
 |  | ||||||
| class Reseni_ResiteleInline(admin.TabularInline): |  | ||||||
| 	model = m.Reseni_Resitele |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @admin.register(m.Reseni) |  | ||||||
| class ReseniAdmin(ReverseModelAdmin): |  | ||||||
| 	base_model = m.Reseni |  | ||||||
| 	inline_type = 'tabular' |  | ||||||
| 	# inline_reverse = ['text_cely','resitele'] TODO vrátit zpět a zrychlit dotaz |  | ||||||
| 	inline_reverse = ['resitele'] |  | ||||||
| 	exclude = ['text_zkraceny', 'text_zkraceny_set'] |  | ||||||
| 	inlines = [PrilohaReseniInline] |  | ||||||
| # FAIL in template |  | ||||||
| #	inlines = [PrilohaReseniInline,Reseni_ResiteleInline] |  | ||||||
| 
 |  | ||||||
| admin.site.register(m.Hodnoceni) |  | ||||||
| admin.site.register(m.Pohadka) | admin.site.register(m.Pohadka) | ||||||
| admin.site.register(m.Obrazek) | admin.site.register(m.Obrazek) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										201
									
								
								seminar/forms.py
									
									
									
									
									
								
							
							
						
						
									
										201
									
								
								seminar/forms.py
									
									
									
									
									
								
							|  | @ -3,10 +3,8 @@ from dal import autocomplete | ||||||
| from django.contrib.auth.forms import PasswordResetForm | from django.contrib.auth.forms import PasswordResetForm | ||||||
| from django.core.exceptions import ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.contrib.auth.models import User | from django.contrib.auth.models import User | ||||||
| from django.forms import formset_factory |  | ||||||
| from django.forms.models import inlineformset_factory |  | ||||||
| 
 | 
 | ||||||
| from .models import Skola, Resitel, Osoba, Problem | from .models import Skola, Resitel, Osoba | ||||||
| import seminar.models as m | import seminar.models as m | ||||||
| 
 | 
 | ||||||
| from datetime import date | from datetime import date | ||||||
|  | @ -222,205 +220,8 @@ class PoMaturiteProfileEditForm(ProfileEditForm): | ||||||
| 		label='Rok maturity', | 		label='Rok maturity', | ||||||
| 		required=True) | 		required=True) | ||||||
| 
 | 
 | ||||||
| class VlozReseniForm(forms.Form): |  | ||||||
| 	#FIXME jen podproblémy daného problému |  | ||||||
| 	problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all()) |  | ||||||
| 	# to_field_name |  | ||||||
| 	#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', |  | ||||||
| 	#	through='Hodnoceni') |  | ||||||
| 
 |  | ||||||
| 	# FIXME pridat vice resitelu |  | ||||||
| 	resitel = forms.ModelChoiceField(label="Řešitel", |  | ||||||
| 		queryset=Resitel.objects.all(), |  | ||||||
| 		widget=autocomplete.ModelSelect2( |  | ||||||
| 			url='autocomplete_resitel', |  | ||||||
| 			attrs = {'data-placeholder--id': '-1', |  | ||||||
| 				'data-placeholder--text' : '---', |  | ||||||
| 				'data-allow-clear': 'true'}) |  | ||||||
|     		) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	#resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', |  | ||||||
| 	#	help_text='Seznam autorů řešení', through='Reseni_Resitele') |  | ||||||
| 	 |  | ||||||
| 	cas_doruceni = forms.DateField(widget=DateInput(),label="Čas doručení") |  | ||||||
| 
 |  | ||||||
| 	#cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) |  | ||||||
| 
 |  | ||||||
| 	forma = forms.ChoiceField(label="Forma řešení",choices = m.Reseni.FORMA_CHOICES) |  | ||||||
| 	#forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, |  | ||||||
| 	#	 default=FORMA_EMAIL) |  | ||||||
| 
 |  | ||||||
| 	poznamka = forms.CharField(label='Neveřejná poznámka', required=False) |  | ||||||
| 	#poznamka = models.TextField('neveřejná poznámka', blank=True, |  | ||||||
| 	#	help_text='Neveřejná poznámka k řešení (plain text)') |  | ||||||
| 
 |  | ||||||
| 	#TODO body do cisla |  | ||||||
| 	#TODO prilohy |  | ||||||
| 
 |  | ||||||
| 	##def __init__(self, *args, **kwargs): |  | ||||||
| 	##	super().__init__(*args, **kwargs) |  | ||||||
| 	##	#self.fields['favorite_color'] = forms.ChoiceField(choices=[(color.id, color.name) for color in Resitel.objects.all()]) |  | ||||||
| 
 |  | ||||||
| class NahrajReseniForm(forms.ModelForm): |  | ||||||
| 	class Meta: |  | ||||||
| 		model = m.Reseni |  | ||||||
| 		fields = ('problem',) |  | ||||||
| 		help_texts = {'problem':''} # Nezobrazovat help text ve formuláři |  | ||||||
| 		 |  | ||||||
| 		widgets = {'problem': |  | ||||||
| 				autocomplete.ModelSelect2Multiple( |  | ||||||
| 					url='autocomplete_problem_odevzdatelny', |  | ||||||
| 					attrs = {'data-placeholder--id': '-1', |  | ||||||
| 						'data-placeholder--text' : '---', |  | ||||||
| 						'data-allow-clear': 'true'}, |  | ||||||
| 				) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| ReseniSPrilohamiFormSet = inlineformset_factory(m.Reseni,m.PrilohaReseni,  |  | ||||||
| 		form = NahrajReseniForm, |  | ||||||
| 		fields = ('soubor','res_poznamka'), |  | ||||||
| 		widgets = {'res_poznamka':forms.TextInput()}, |  | ||||||
| 		extra = 1, |  | ||||||
| 		can_delete = False, |  | ||||||
| 
 |  | ||||||
| 		) |  | ||||||
| 
 | 
 | ||||||
| class NahrajObrazekKTreeNoduForm(forms.ModelForm): | class NahrajObrazekKTreeNoduForm(forms.ModelForm): | ||||||
| 	class Meta: | 	class Meta: | ||||||
| 		model = m.Obrazek | 		model = m.Obrazek | ||||||
| 		fields = ('na_web',) | 		fields = ('na_web',) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class JednoHodnoceniForm(forms.ModelForm): |  | ||||||
| 	class Meta: |  | ||||||
| 		model = m.Hodnoceni |  | ||||||
| 		fields = ('problem', 'body', 'cislo_body') |  | ||||||
| 		widgets = { |  | ||||||
| 			'problem': autocomplete.ModelSelect2( |  | ||||||
| 				url='autocomplete_problem_odevzdatelny',   # FIXME: Dovolit i starší? |  | ||||||
| 				) |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm, |  | ||||||
| 		extra = 0, |  | ||||||
| 		) |  | ||||||
| 
 |  | ||||||
| class PoznamkaReseniForm(forms.ModelForm): |  | ||||||
| 	class Meta: |  | ||||||
| 		model = m.Reseni |  | ||||||
| 		fields = ('poznamka',) |  | ||||||
| 
 |  | ||||||
| # FIXME: Ideálně by mělo být součástí třídy níž, ale neumím to udělat |  | ||||||
| DATE_FORMAT = '%Y-%m-%d' |  | ||||||
| 
 |  | ||||||
| class OdevzdavatkoTabulkaFiltrForm(forms.Form): |  | ||||||
| 	"""Form pro filtrování přehledové odevzdávátkové tabulky |  | ||||||
| 
 |  | ||||||
| 	Inspirováno https://kam.mff.cuni.cz/mffzoom/""" |  | ||||||
| 
 |  | ||||||
| 	# Věci definované níž se importují i ve views pro odevzdávátko (Inspirováno https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices) |  | ||||||
| 
 |  | ||||||
| 	RESITELE_RELEVANTNI = 'relevantni' |  | ||||||
| 	RESITELE_NEODMATUROVAVSI = 'neodmaturovavsi' |  | ||||||
| 	RESITELE_CHOICES = [ |  | ||||||
| 		(RESITELE_RELEVANTNI, 'Relevantní řešitelé'), # I.e. nezobrazovat prázdné řádky tabulky |  | ||||||
| 		(RESITELE_NEODMATUROVAVSI, 'Všichni bez maturity'), |  | ||||||
| 		# Možná: všechny vč. historických? |  | ||||||
| 		] |  | ||||||
| 
 |  | ||||||
| 	PROBLEMY_MOJE = 'moje' |  | ||||||
| 	PROBLEMY_LETOSNI = 'letosni' |  | ||||||
| 	PROBLEMY_CHOICES = [ |  | ||||||
| 		(PROBLEMY_MOJE, 'Moje problémy'), # Letošní problémy, které mají v sobě nebo v nadproblémech přiřazeného daného orga |  | ||||||
| 		(PROBLEMY_LETOSNI, 'Všechny letošní'), |  | ||||||
| 		# TODO: *hlavní problémy, možná všechny... |  | ||||||
| 		# XXX: Chtělo by to i "aktuálně zadané... |  | ||||||
| 		] |  | ||||||
| 
 |  | ||||||
| 	# TODO: Typy problémů (problémy, úlohy, ostatní, všechny)? Jen některá řešení (obodovaná/neobodovaná, víc řešitelů, ...)? |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	@classmethod |  | ||||||
| 	def gen_terminy(cls, rocnik=None): |  | ||||||
| 		import datetime |  | ||||||
| 		from time import strftime |  | ||||||
| 		 |  | ||||||
| 		from django.db.utils import OperationalError |  | ||||||
| 		try: |  | ||||||
| 			aktualni_rocnik = m.Nastaveni.get_solo().aktualni_rocnik |  | ||||||
| 			aktualni_cislo = m.Nastaveni.get_solo().aktualni_cislo |  | ||||||
| 		except OperationalError: |  | ||||||
| 			# django.db.utils.OperationalError: no such table: seminar_nastaveni |  | ||||||
| 			# Nemáme databázi, takže to selhalo. Pro jistotu vrátíme aspoň dvě možnosti, ať to nepadá dál |  | ||||||
| 			logger = logging.getLogger(__name__) |  | ||||||
| 			logger.error("Rozbitá databáze (před počátečními migracemi?)") |  | ||||||
| 			return [('broken', 'Je to rozbitý'), ('fubar', 'Nefunguje to')] |  | ||||||
| 
 |  | ||||||
| 		# FIXME: Tohle je hnusný monkey patch, mělo by to být nějak zahrnuto výš. |  | ||||||
| 		if rocnik is not None: |  | ||||||
| 			aktualni_rocnik = rocnik |  | ||||||
| 			aktualni_cislo = m.Cislo.objects.filter(rocnik=rocnik).order_by('poradi').last() |  | ||||||
| 
 |  | ||||||
| 		result = [] |  | ||||||
| 
 |  | ||||||
| 		for cislo in m.Cislo.objects.filter( |  | ||||||
| 				rocnik=aktualni_rocnik, |  | ||||||
| 				poradi__lte=aktualni_cislo.poradi, |  | ||||||
| 				).reverse():	# Standardně se řadí od nejnovějšího čísla |  | ||||||
| 			# Předem je mi líto kohokoliv, kdo tyhle řádky bude číst... |  | ||||||
| 			if cislo.datum_vydani is not None and cislo.datum_vydani <= datetime.date.today(): |  | ||||||
| 				result.append(( |  | ||||||
| 					strftime(DATE_FORMAT, cislo.datum_vydani.timetuple()), |  | ||||||
| 					f"Vydání {cislo.poradi}. čísla")) |  | ||||||
| 			if cislo.datum_preddeadline is not None and cislo.datum_preddeadline <= datetime.date.today(): |  | ||||||
| 				result.append(( |  | ||||||
| 					strftime(DATE_FORMAT, cislo.datum_preddeadline.timetuple()), |  | ||||||
| 					f"Předdeadline {cislo.poradi}. čísla")) |  | ||||||
| 			if cislo.datum_deadline_soustredeni is not None and cislo.datum_deadline_soustredeni <= datetime.date.today(): |  | ||||||
| 				result.append(( |  | ||||||
| 					strftime(DATE_FORMAT, cislo.datum_deadline_soustredeni.timetuple()), |  | ||||||
| 					f"Sous. deadline {cislo.poradi}. čísla")) |  | ||||||
| 			if cislo.datum_deadline is not None and cislo.datum_deadline <= datetime.date.today(): |  | ||||||
| 				result.append(( |  | ||||||
| 					strftime(DATE_FORMAT, cislo.datum_deadline.timetuple()), |  | ||||||
| 					f"Finální deadline {cislo.poradi}. čísla")) |  | ||||||
| 		result.append(( |  | ||||||
| 			strftime(DATE_FORMAT, datetime.date.today().timetuple()), f"Dnes")) |  | ||||||
| 
 |  | ||||||
| 		return result |  | ||||||
| 
 |  | ||||||
| 	@classmethod |  | ||||||
| 	def gen_initial(cls, rocnik=None): |  | ||||||
| 		terminy = cls.gen_terminy(rocnik) |  | ||||||
| 		initial = { |  | ||||||
| 			'resitele': cls.RESITELE_RELEVANTNI, |  | ||||||
| 			'problemy': cls.PROBLEMY_MOJE, |  | ||||||
| 			# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… |  | ||||||
| 			'reseni_od': terminy[-2] if rocnik is None else terminy[0], |  | ||||||
| 			'reseni_do': terminy[-1], |  | ||||||
| 			'neobodovane': False, |  | ||||||
| 		} |  | ||||||
| 		return initial |  | ||||||
| 
 |  | ||||||
| 	def __init__(self, *args, rocnik=None, **kwargs): |  | ||||||
| 		if 'initial' not in kwargs: |  | ||||||
| 			super().__init__(initial=self.gen_initial(rocnik), *args, **kwargs) |  | ||||||
| 		else: |  | ||||||
| 			super().__init__(*args, **kwargs) |  | ||||||
| 		# choices jako parametr Select widgetu neumí brát callable, jen iterable, takže si pro jednoduchost můžu rovnou uložit výsledek sem... |  | ||||||
| 		# A "sem" znamená do libovolné metody, protože jinak se jedná o kód, který django spustí při inicializaci a protože potřebujeme databázi, tak by spadnul při vyrábění testdat... |  | ||||||
| 		self.terminy = self.gen_terminy(rocnik) |  | ||||||
| 		self.fields['reseni_od'].widget = forms.Select(choices=self.gen_terminy(rocnik)) |  | ||||||
| 		# Pokud chceme neaktuální ročník, tak nás nejspíš zajímají všechna řešení… |  | ||||||
| 		self.fields['reseni_od'].initial = self.terminy[-2] if rocnik is None else self.terminy[0] |  | ||||||
| 		self.fields['reseni_do'].widget = forms.Select(choices=self.gen_terminy(rocnik)) |  | ||||||
| 		self.fields['reseni_do'].initial = self.terminy[-1] |  | ||||||
| 
 |  | ||||||
| 	# NOTE: Initial definuji pro jednotlivé fieldy, aby to bylo tady a nebylo potřeba to řešit ve views... |  | ||||||
| 	resitele = forms.ChoiceField(choices=RESITELE_CHOICES) |  | ||||||
| 	problemy = forms.ChoiceField(choices=PROBLEMY_CHOICES) |  | ||||||
| 	 |  | ||||||
| 	reseni_od = forms.DateField(input_formats=[DATE_FORMAT]) |  | ||||||
| 	reseni_do = forms.DateField(input_formats=[DATE_FORMAT]) |  | ||||||
| 	neobodovane = forms.BooleanField(required=False) |  | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								seminar/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								seminar/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | from .models_all import * | ||||||
|  | from .odevzdavatko import * | ||||||
|  | @ -9,7 +9,6 @@ import logging | ||||||
| from django.contrib.sites.shortcuts import get_current_site | from django.contrib.sites.shortcuts import get_current_site | ||||||
| from django.db import models | from django.db import models | ||||||
| from django.contrib import auth | from django.contrib import auth | ||||||
| from django.db.models import Sum |  | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils.encoding import force_text | from django.utils.encoding import force_text | ||||||
|  | @ -316,6 +315,7 @@ class Resitel(SeminarModelBase): | ||||||
| 	def vsechny_body(self): | 	def vsechny_body(self): | ||||||
| 		"Spočítá body odjakživa." | 		"Spočítá body odjakživa." | ||||||
| 		vsechna_reseni = self.reseni_set.all() | 		vsechna_reseni = self.reseni_set.all() | ||||||
|  | 		from .odevzdavatko import Hodnoceni | ||||||
| 		vsechna_hodnoceni = Hodnoceni.objects.filter( | 		vsechna_hodnoceni = Hodnoceni.objects.filter( | ||||||
| 			reseni__in=vsechna_reseni) | 			reseni__in=vsechna_reseni) | ||||||
| 		return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) | 		return sum(h.body for h in list(vsechna_hodnoceni) if h.body is not None) | ||||||
|  | @ -362,6 +362,7 @@ class Resitel(SeminarModelBase): | ||||||
| 		#  - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími | 		#  - body z 25. ročníku a dříve byly shledány dvakrát hodnotnějšími | ||||||
| 		#  - proto se započítávají dvojnásobně a byly posunuté hranice titulů | 		#  - proto se započítávají dvojnásobně a byly posunuté hranice titulů | ||||||
| 		#  - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. | 		#  - staré tituly se ale nemají odebrat, pokud řešitel v t.č. minulém (26.) ročníku měl titul, má ho mít pořád. | ||||||
|  | 		from .odevzdavatko import Hodnoceni | ||||||
| 		hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all()) | 		hodnoceni_do_25_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=25,reseni__in=self.reseni_set.all()) | ||||||
| 		novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) | 		novejsi_hodnoceni = Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()).difference(hodnoceni_do_25_rocniku) | ||||||
| 
 | 
 | ||||||
|  | @ -399,6 +400,7 @@ class Resitel(SeminarModelBase): | ||||||
| 			else: | 			else: | ||||||
| 				return Titul.akad | 				return Titul.akad | ||||||
| 
 | 
 | ||||||
|  | 		from .odevzdavatko import Hodnoceni | ||||||
| 		hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all()) | 		hodnoceni_do_26_rocniku = Hodnoceni.objects.filter(cislo_body__rocnik__rocnik__lte=26,reseni__in=self.reseni_set.all()) | ||||||
| 		novejsi_body = body_z_hodnoceni( | 		novejsi_body = body_z_hodnoceni( | ||||||
| 			Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) | 			Hodnoceni.objects.filter(reseni__in=self.reseni_set.all()) | ||||||
|  | @ -1084,101 +1086,6 @@ class Uloha(Problem): | ||||||
| 		zadani_node = self.ulohazadaninode  | 		zadani_node = self.ulohazadaninode  | ||||||
| 		return treelib.get_upper_node_of_type(zadani_node, CisloNode) | 		return treelib.get_upper_node_of_type(zadani_node, CisloNode) | ||||||
| 
 | 
 | ||||||
| @reversion.register(ignore_duplicates=True) |  | ||||||
| class Reseni(SeminarModelBase): |  | ||||||
| 
 |  | ||||||
| 	class Meta: |  | ||||||
| 		db_table = 'seminar_reseni' |  | ||||||
| 		verbose_name = 'Řešení' |  | ||||||
| 		verbose_name_plural = 'Řešení' |  | ||||||
| 		#ordering = ['-problem', 'resitele']	# FIXME: Takhle to chceme, ale nefunguje to. |  | ||||||
| 		ordering = ['-cas_doruceni'] |  | ||||||
| 
 |  | ||||||
| 	# Interní ID |  | ||||||
| 	id = models.AutoField(primary_key = True) |  | ||||||
| 
 |  | ||||||
| 	# Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. |  | ||||||
| 	problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém', |  | ||||||
| 		through='Hodnoceni') |  | ||||||
| 
 |  | ||||||
| 	resitele = models.ManyToManyField(Resitel, verbose_name='autoři řešení', |  | ||||||
| 		help_text='Seznam autorů řešení', through='Reseni_Resitele') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) |  | ||||||
| 
 |  | ||||||
| 	FORMA_PAPIR = 'papir' |  | ||||||
| 	FORMA_EMAIL = 'email' |  | ||||||
| 	FORMA_UPLOAD = 'upload' |  | ||||||
| 	FORMA_CHOICES = [ |  | ||||||
| 		(FORMA_PAPIR, 'Papírové řešení'), |  | ||||||
| 		(FORMA_EMAIL, 'Emailem'), |  | ||||||
| 		(FORMA_UPLOAD, 'Upload přes web'), |  | ||||||
| 		] |  | ||||||
| 	forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, |  | ||||||
| 		 default=FORMA_EMAIL) |  | ||||||
| 
 |  | ||||||
| 	text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení',  |  | ||||||
| 		blank=True, null=True, related_name="reseni_cely_set", |  | ||||||
| 		on_delete=models.PROTECT) |  | ||||||
| 
 |  | ||||||
| 	poznamka = models.TextField('neveřejná poznámka', blank=True, |  | ||||||
| 		help_text='Neveřejná poznámka k řešení (plain text)') |  | ||||||
| 
 |  | ||||||
| 	zverejneno = models.BooleanField('řešení zveřejněno', default=False,  |  | ||||||
| 		help_text='Udává, zda je řešení zveřejněno') |  | ||||||
| 
 |  | ||||||
| 	def verejne_url(self): |  | ||||||
| 		return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) |  | ||||||
| 
 |  | ||||||
| 	def absolute_url(self): |  | ||||||
| 		return "https://" + str(get_current_site(None)) + self.verejne_url() |  | ||||||
| 
 |  | ||||||
| 	# má OneToOneField s: |  | ||||||
| 	# Konfera |  | ||||||
| 
 |  | ||||||
| 	# má ForeignKey s: |  | ||||||
| 	# Hodnoceni |  | ||||||
| 
 |  | ||||||
| 	def sum_body(self): |  | ||||||
| 		return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] |  | ||||||
| 
 |  | ||||||
| 	def __str__(self): |  | ||||||
| 		return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) |  | ||||||
| 		# NOTE: Potenciální DB HOG (bez select_related) |  | ||||||
| 
 |  | ||||||
| ## Pravdepodobne uz nebude potreba: |  | ||||||
| #	def save(self, *args, **kwargs): |  | ||||||
| #		if ((self.cislo_body is None) and (self.problem.cislo_reseni) and |  | ||||||
| #				(self.problem.typ == Problem.TYP_ULOHA)): |  | ||||||
| #			self.cislo_body = self.problem.cislo_reseni |  | ||||||
| #		super(Reseni, self).save(*args, **kwargs) |  | ||||||
| 
 |  | ||||||
| class Hodnoceni(SeminarModelBase): |  | ||||||
| 	class Meta: |  | ||||||
| 		db_table = 'seminar_hodnoceni' |  | ||||||
| 		verbose_name = 'Hodnocení' |  | ||||||
| 		verbose_name_plural = 'Hodnocení' |  | ||||||
| 	 |  | ||||||
| 	# Interní ID |  | ||||||
| 	id = models.AutoField(primary_key = True) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 	body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body',  |  | ||||||
| 		blank=True, null=True) |  | ||||||
| 
 |  | ||||||
| 	cislo_body = models.ForeignKey(Cislo, verbose_name='číslo pro body',  |  | ||||||
| 		related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) |  | ||||||
| 
 |  | ||||||
| 	reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) |  | ||||||
| 
 |  | ||||||
| 	problem = models.ForeignKey(Problem, verbose_name='problém',  |  | ||||||
| 		related_name='hodnoceni', on_delete=models.PROTECT) |  | ||||||
| 
 |  | ||||||
| 	def __str__(self): |  | ||||||
| 		return "{}, {}, {}".format(self.problem, self.reseni, self.body) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| def aux_generate_filename(self, filename): | def aux_generate_filename(self, filename): | ||||||
| 	"""Pomocná funkce generující ošetřený název souboru v adresáři s datem""" | 	"""Pomocná funkce generující ošetřený název souboru v adresáři s datem""" | ||||||
|  | @ -1204,45 +1111,6 @@ def generate_filename_konfera(self, filename): | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| ## | ## | ||||||
| def generate_filename(self, filename): |  | ||||||
| 	return os.path.join( |  | ||||||
| 		settings.SEMINAR_RESENI_DIR, |  | ||||||
| 		aux_generate_filename(self, filename) |  | ||||||
| 	) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| @reversion.register(ignore_duplicates=True) |  | ||||||
| class PrilohaReseni(SeminarModelBase): |  | ||||||
| 
 |  | ||||||
| 	class Meta: |  | ||||||
| 		db_table = 'seminar_priloha_reseni' |  | ||||||
| 		verbose_name = 'Příloha řešení' |  | ||||||
| 		verbose_name_plural = 'Přílohy řešení' |  | ||||||
| 		ordering = ['reseni', 'vytvoreno'] |  | ||||||
| 
 |  | ||||||
| 	# Interní ID |  | ||||||
| 	id = models.AutoField(primary_key = True) |  | ||||||
| 
 |  | ||||||
| 	reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', |  | ||||||
| 		on_delete=models.CASCADE) |  | ||||||
| 
 |  | ||||||
| 	vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) |  | ||||||
| 
 |  | ||||||
| 	soubor = models.FileField('soubor', upload_to = generate_filename) |  | ||||||
| 
 |  | ||||||
| 	poznamka = models.TextField('neveřejná poznámka', blank=True, |  | ||||||
| 		help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') |  | ||||||
| 	 |  | ||||||
| 	res_poznamka = models.TextField('poznámka řešitele', blank=True, |  | ||||||
| 		help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') |  | ||||||
| 
 |  | ||||||
| 	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): | class Pohadka(SeminarModelBase): | ||||||
|  | @ -1385,29 +1253,6 @@ class Konfera(Problem): | ||||||
| 	def cislo_node(self): | 	def cislo_node(self): | ||||||
| 		return None | 		return None | ||||||
| 
 | 
 | ||||||
| # Vazebna tabulka. Mozna se generuje automaticky. |  | ||||||
| @reversion.register(ignore_duplicates=True) |  | ||||||
| class Reseni_Resitele(models.Model): |  | ||||||
| 
 |  | ||||||
| 	class Meta: |  | ||||||
| 		db_table = 'seminar_reseni_resitele' |  | ||||||
| 		verbose_name = 'Řešení řešitelů' |  | ||||||
| 		verbose_name_plural = 'Řešení řešitelů' |  | ||||||
| 		ordering = ['reseni', 'resitele'] |  | ||||||
| 
 |  | ||||||
| 	# Interní ID |  | ||||||
| 	id = models.AutoField(primary_key = True) |  | ||||||
| 
 |  | ||||||
| 	resitele = models.ForeignKey(Resitel, verbose_name='řešitel', on_delete=models.PROTECT) |  | ||||||
| 
 |  | ||||||
| 	reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) |  | ||||||
| 
 |  | ||||||
| 	# podil - jakou merou se ktery resitel podilel na danem reseni |  | ||||||
| 	#	- pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni |  | ||||||
| 
 |  | ||||||
| 	def __str__(self): |  | ||||||
| 		return '{} od {}'.format(self.reseni, self.resitel) |  | ||||||
| 		# NOTE: Poteciální DB HOG bez select_related |  | ||||||
| 
 | 
 | ||||||
| @reversion.register(ignore_duplicates=True) | @reversion.register(ignore_duplicates=True) | ||||||
| class Konfery_Ucastnici(models.Model): | class Konfery_Ucastnici(models.Model): | ||||||
|  | @ -1705,21 +1550,6 @@ class CastNode(TreeNode): | ||||||
| 	def getOdkazStr(self): | 	def getOdkazStr(self): | ||||||
| 		return str(self.nadpis) | 		return str(self.nadpis) | ||||||
| 
 | 
 | ||||||
| class ReseniNode(TreeNode): |  | ||||||
| 	class Meta: |  | ||||||
| 		db_table = 'seminar_nodes_otistene_reseni' |  | ||||||
| 		verbose_name = 'Otištěné řešení (Node)' |  | ||||||
| 		verbose_name_plural = 'Otištěná řešení (Node)' |  | ||||||
| 	reseni = models.ForeignKey(Reseni, |  | ||||||
| 		on_delete=models.PROTECT, |  | ||||||
| 		verbose_name = 'reseni') |  | ||||||
| 	 |  | ||||||
| 	def aktualizuj_nazev(self): |  | ||||||
| 		self.nazev = "ReseniNode: "+str(self.reseni) |  | ||||||
| 
 |  | ||||||
| 	def getOdkazStr(self): |  | ||||||
| 		return str(self.reseni) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| @reversion.register(ignore_duplicates=True) | @reversion.register(ignore_duplicates=True) | ||||||
| class Nastaveni(SingletonModel): | class Nastaveni(SingletonModel): | ||||||
							
								
								
									
										188
									
								
								seminar/models/odevzdavatko.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								seminar/models/odevzdavatko.py
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,188 @@ | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | import reversion | ||||||
|  | 
 | ||||||
|  | from django.contrib.sites.shortcuts import get_current_site | ||||||
|  | from django.db import models | ||||||
|  | from django.db.models import Sum | ||||||
|  | from django.urls import reverse_lazy | ||||||
|  | from django.utils import timezone | ||||||
|  | from django.conf import settings | ||||||
|  | 
 | ||||||
|  | from seminar.models import models_all as am | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @reversion.register(ignore_duplicates=True) | ||||||
|  | class Reseni(am.SeminarModelBase): | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         db_table = 'seminar_reseni' | ||||||
|  |         verbose_name = 'Řešení' | ||||||
|  |         verbose_name_plural = 'Řešení' | ||||||
|  |         #ordering = ['-problem', 'resitele']	# FIXME: Takhle to chceme, ale nefunguje to. | ||||||
|  |         ordering = ['-cas_doruceni'] | ||||||
|  | 
 | ||||||
|  |     # Interní ID | ||||||
|  |     id = models.AutoField(primary_key = True) | ||||||
|  | 
 | ||||||
|  |     # Ke každé dvojici řešní a problém existuje nanejvýš jedno hodnocení, doplnění vazby. | ||||||
|  |     problem = models.ManyToManyField(am.Problem, verbose_name='problém', help_text='Problém', | ||||||
|  |                                      through='Hodnoceni') | ||||||
|  | 
 | ||||||
|  |     resitele = models.ManyToManyField(am.Resitel, verbose_name='autoři řešení', | ||||||
|  |                                       help_text='Seznam autorů řešení', through='Reseni_Resitele') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     cas_doruceni = models.DateTimeField('čas_doručení', default=timezone.now, blank=True) | ||||||
|  | 
 | ||||||
|  |     FORMA_PAPIR = 'papir' | ||||||
|  |     FORMA_EMAIL = 'email' | ||||||
|  |     FORMA_UPLOAD = 'upload' | ||||||
|  |     FORMA_CHOICES = [ | ||||||
|  |         (FORMA_PAPIR, 'Papírové řešení'), | ||||||
|  |         (FORMA_EMAIL, 'Emailem'), | ||||||
|  |         (FORMA_UPLOAD, 'Upload přes web'), | ||||||
|  |     ] | ||||||
|  |     forma = models.CharField('forma řešení', max_length=16, choices=FORMA_CHOICES, blank=False, | ||||||
|  |                              default=FORMA_EMAIL) | ||||||
|  | 
 | ||||||
|  |     text_cely = models.OneToOneField('ReseniNode', verbose_name='Plná verze textu řešení', | ||||||
|  |                                      blank=True, null=True, related_name="reseni_cely_set", | ||||||
|  |                                      on_delete=models.PROTECT) | ||||||
|  | 
 | ||||||
|  |     poznamka = models.TextField('neveřejná poznámka', blank=True, | ||||||
|  |                                 help_text='Neveřejná poznámka k řešení (plain text)') | ||||||
|  | 
 | ||||||
|  |     zverejneno = models.BooleanField('řešení zveřejněno', default=False, | ||||||
|  |                                      help_text='Udává, zda je řešení zveřejněno') | ||||||
|  | 
 | ||||||
|  |     def verejne_url(self): | ||||||
|  |         return str(reverse_lazy('odevzdavatko_detail_reseni', args=[self.id])) | ||||||
|  | 
 | ||||||
|  |     def absolute_url(self): | ||||||
|  |         return "https://" + str(get_current_site(None)) + self.verejne_url() | ||||||
|  | 
 | ||||||
|  |     # má OneToOneField s: | ||||||
|  |     # Konfera | ||||||
|  | 
 | ||||||
|  |     # má ForeignKey s: | ||||||
|  |     # Hodnoceni | ||||||
|  | 
 | ||||||
|  |     def sum_body(self): | ||||||
|  |         return self.hodnoceni_set.all().aggregate(Sum('body'))["body__sum"] | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return "{}({}): {}({})".format(self.resitele.first(),len(self.resitele.all()), self.problem.first() ,len(self.problem.all())) | ||||||
|  |     # NOTE: Potenciální DB HOG (bez select_related) | ||||||
|  | 
 | ||||||
|  | ## Pravdepodobne uz nebude potreba: | ||||||
|  | #	def save(self, *args, **kwargs): | ||||||
|  | #		if ((self.cislo_body is None) and (self.problem.cislo_reseni) and | ||||||
|  | #				(self.problem.typ == Problem.TYP_ULOHA)): | ||||||
|  | #			self.cislo_body = self.problem.cislo_reseni | ||||||
|  | #		super(Reseni, self).save(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | class Hodnoceni(am.SeminarModelBase): | ||||||
|  |     class Meta: | ||||||
|  |         db_table = 'seminar_hodnoceni' | ||||||
|  |         verbose_name = 'Hodnocení' | ||||||
|  |         verbose_name_plural = 'Hodnocení' | ||||||
|  | 
 | ||||||
|  |     # Interní ID | ||||||
|  |     id = models.AutoField(primary_key = True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     body = models.DecimalField(max_digits=8, decimal_places=1, verbose_name='body', | ||||||
|  |                                blank=True, null=True) | ||||||
|  | 
 | ||||||
|  |     cislo_body = models.ForeignKey(am.Cislo, verbose_name='číslo pro body', | ||||||
|  |                                    related_name='hodnoceni', blank=True, null=True, on_delete=models.PROTECT) | ||||||
|  | 
 | ||||||
|  |     reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) | ||||||
|  | 
 | ||||||
|  |     problem = models.ForeignKey(am.Problem, verbose_name='problém', | ||||||
|  |                                 related_name='hodnoceni', on_delete=models.PROTECT) | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return "{}, {}, {}".format(self.problem, self.reseni, self.body) | ||||||
|  | 
 | ||||||
|  | def generate_filename(self, filename): | ||||||
|  |     return os.path.join( | ||||||
|  |         settings.SEMINAR_RESENI_DIR, | ||||||
|  |         am.aux_generate_filename(self, filename) | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @reversion.register(ignore_duplicates=True) | ||||||
|  | class PrilohaReseni(am.SeminarModelBase): | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         db_table = 'seminar_priloha_reseni' | ||||||
|  |         verbose_name = 'Příloha řešení' | ||||||
|  |         verbose_name_plural = 'Přílohy řešení' | ||||||
|  |         ordering = ['reseni', 'vytvoreno'] | ||||||
|  | 
 | ||||||
|  |     # Interní ID | ||||||
|  |     id = models.AutoField(primary_key = True) | ||||||
|  | 
 | ||||||
|  |     reseni = models.ForeignKey(Reseni, verbose_name='řešení', related_name='prilohy', | ||||||
|  |                                on_delete=models.CASCADE) | ||||||
|  | 
 | ||||||
|  |     vytvoreno = models.DateTimeField('vytvořeno', default=timezone.now, blank=True, editable=False) | ||||||
|  | 
 | ||||||
|  |     soubor = models.FileField('soubor', upload_to = generate_filename) | ||||||
|  | 
 | ||||||
|  |     poznamka = models.TextField('neveřejná poznámka', blank=True, | ||||||
|  |                                 help_text='Neveřejná poznámka k příloze řešení (plain text), např. o původu') | ||||||
|  | 
 | ||||||
|  |     res_poznamka = models.TextField('poznámka řešitele', blank=True, | ||||||
|  |                                     help_text='Poznámka k příloze řešení, např. co daný soubor obsahuje') | ||||||
|  | 
 | ||||||
|  |     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('/') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Vazebna tabulka. Mozna se generuje automaticky. | ||||||
|  | @reversion.register(ignore_duplicates=True) | ||||||
|  | class Reseni_Resitele(models.Model): | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         db_table = 'seminar_reseni_resitele' | ||||||
|  |         verbose_name = 'Řešení řešitelů' | ||||||
|  |         verbose_name_plural = 'Řešení řešitelů' | ||||||
|  |         ordering = ['reseni', 'resitele'] | ||||||
|  | 
 | ||||||
|  |     # Interní ID | ||||||
|  |     id = models.AutoField(primary_key = True) | ||||||
|  | 
 | ||||||
|  |     resitele = models.ForeignKey(am.Resitel, verbose_name='řešitel', on_delete=models.PROTECT) | ||||||
|  | 
 | ||||||
|  |     reseni = models.ForeignKey(Reseni, verbose_name='řešení', on_delete=models.CASCADE) | ||||||
|  | 
 | ||||||
|  |     # podil - jakou merou se ktery resitel podilel na danem reseni | ||||||
|  |     #	- pouziti v budoucnu, pokud by resitele nemeli dostat vsichni stejne bodu za spolecne reseni | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         return '{} od {}'.format(self.reseni, self.resitel) | ||||||
|  |     # NOTE: Poteciální DB HOG bez select_related | ||||||
|  | 
 | ||||||
|  | class ReseniNode(am.TreeNode): | ||||||
|  |     class Meta: | ||||||
|  |         db_table = 'seminar_nodes_otistene_reseni' | ||||||
|  |         verbose_name = 'Otištěné řešení (Node)' | ||||||
|  |         verbose_name_plural = 'Otištěná řešení (Node)' | ||||||
|  |     reseni = models.ForeignKey(Reseni, | ||||||
|  |                                on_delete=models.PROTECT, | ||||||
|  |                                verbose_name = 'reseni') | ||||||
|  | 
 | ||||||
|  |     def aktualizuj_nazev(self): | ||||||
|  |         self.nazev = "ReseniNode: "+str(self.reseni) | ||||||
|  | 
 | ||||||
|  |     def getOdkazStr(self): | ||||||
|  |         return str(self.reseni) | ||||||
|  | 
 | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| from django.urls import path, include, re_path | from django.urls import path, include, re_path | ||||||
| from django.contrib.auth.decorators import login_required | from django.contrib.auth.decorators import login_required | ||||||
| from . import views | from . import views | ||||||
| from .utils import org_required, resitel_required, viewMethodSwitch, resitel_or_org_required | from .utils import org_required | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
| #	path('aktualni/temata/', views.TemataRozcestnikView), | #	path('aktualni/temata/', views.TemataRozcestnikView), | ||||||
|  | @ -116,8 +116,6 @@ urlpatterns = [ | ||||||
| 
 | 
 | ||||||
| 	path('prihlaska/',views.prihlaskaView, name='seminar_prihlaska'), | 	path('prihlaska/',views.prihlaskaView, name='seminar_prihlaska'), | ||||||
| 
 | 
 | ||||||
| 	path('resitel/odevzdana_reseni/', resitel_or_org_required(views.PrehledOdevzdanychReseni.as_view()), name='seminar_resitel_odevzdana_reseni'), |  | ||||||
| 
 |  | ||||||
| 	path( | 	path( | ||||||
| 		'resitel/osobni-udaje/', | 		'resitel/osobni-udaje/', | ||||||
| 		login_required(views.resitelEditView), | 		login_required(views.resitelEditView), | ||||||
|  | @ -127,19 +125,9 @@ urlpatterns = [ | ||||||
| 	# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku | 	# Obecný view na profil -- orgům dá rozcestník, řešitelům jejich stránku | ||||||
| 	path('profil/', views.profilView, name='profil'), | 	path('profil/', views.profilView, name='profil'), | ||||||
| 
 | 
 | ||||||
| 	path('org/add_solution', org_required(views.AddSolutionView.as_view()), name='seminar_vloz_reseni'), |  | ||||||
| 	path('resitel/nahraj_reseni', resitel_required(views.NahrajReseniView.as_view()), name='seminar_nahraj_reseni'), |  | ||||||
| 
 |  | ||||||
| 	re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'), | 	re_path(r'^temp/vue/.*$',views.VueTestView.as_view(),name='vue_test_view'), | ||||||
| 	path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()), | 	path('temp/image_upload/', views.NahrajObrazekKTreeNoduView.as_view()), | ||||||
| 
 | 
 | ||||||
| 	path('', views.TitulniStranaView.as_view(), name='titulni_strana'), | 	path('', views.TitulniStranaView.as_view(), name='titulni_strana'), | ||||||
| 	path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'), | 	path('jak-resit/', views.JakResitView.as_view(), name='jak_resit'), | ||||||
| 
 |  | ||||||
| 	path('org/reseni/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), |  | ||||||
| 	path('org/reseni/rocnik/<int:rocnik>/', org_required(views.TabulkaOdevzdanychReseniView.as_view()), name='odevzdavatko_tabulka'), |  | ||||||
| 	path('org/reseni/<int:problem>/<int:resitel>/', org_required(views.ReseniProblemuView.as_view()), name='odevzdavatko_reseni_resitele_k_problemu'), |  | ||||||
| 	path('org/reseni/<int:pk>', org_required(viewMethodSwitch(get=views.DetailReseniView.as_view(), post=views.hodnoceniReseniView)), name='odevzdavatko_detail_reseni'), |  | ||||||
| 	path('org/reseni/all', org_required(views.SeznamReseniView.as_view())), |  | ||||||
| 	path('org/reseni/akt', org_required(views.SeznamAktualnichReseniView.as_view())), |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -1,6 +1,5 @@ | ||||||
| from .views_all import * | from .views_all import * | ||||||
| from .views_rest import * | from .views_rest import * | ||||||
| from .odevzdavatko import * |  | ||||||
| 
 | 
 | ||||||
| # Dočsasné views | # Dočsasné views | ||||||
| from .docasne import * | from .docasne import * | ||||||
|  |  | ||||||
|  | @ -1,26 +1,22 @@ | ||||||
| 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, JsonResponse | ||||||
| from django.urls import reverse,reverse_lazy | from django.urls import reverse | ||||||
| from django.core.exceptions import PermissionDenied, ObjectDoesNotExist | from django.core.exceptions import ObjectDoesNotExist | ||||||
| from django.core.mail import send_mail |  | ||||||
| from django.views import generic | from django.views import generic | ||||||
| from django.utils.translation import ugettext as _ | from django.utils.translation import ugettext as _ | ||||||
| from django.http import Http404,HttpResponseBadRequest,HttpResponseRedirect | from django.http import Http404 | ||||||
| from django.db.models import Q, Sum, Count | from django.db.models import Q, Sum, Count | ||||||
| from django.views.decorators.csrf import ensure_csrf_cookie |  | ||||||
| from django.views.decorators.debug import sensitive_post_parameters | from django.views.decorators.debug import sensitive_post_parameters | ||||||
| from django.views.generic.edit import FormView, CreateView | from django.views.generic.edit import CreateView | ||||||
| from django.views.generic.base import TemplateView, RedirectView | from django.views.generic.base import TemplateView, RedirectView | ||||||
| from django.contrib.auth.models import User, Permission, Group | from django.contrib.auth.models import User, Permission, Group | ||||||
| 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.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from django.forms.models import model_to_dict |  | ||||||
| 
 | 
 | ||||||
| import seminar.models as s | import seminar.models as s | ||||||
| import seminar.models as m | import seminar.models as m | ||||||
| from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Pohadka, Tema, Clanek, Osoba, Skola # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci | from seminar.models import Problem, Cislo, Reseni, Nastaveni, Rocnik, Soustredeni, Organizator, Resitel, Novinky, Soustredeni_Ucastnici, Tema, Clanek, Osoba # Tohle je stare a chceme se toho zbavit. Pouzivejte s.ToCoChci | ||||||
| #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva | #from .models import VysledkyZaCislo, VysledkyKCisluZaRocnik, VysledkyKCisluOdjakziva | ||||||
| from seminar import utils, treelib | from seminar import utils, treelib | ||||||
| from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm | from seminar.forms import PrihlaskaForm, ProfileEditForm, PoMaturiteProfileEditForm | ||||||
|  | @ -29,7 +25,7 @@ import seminar.templatetags.treenodes as tnltt | ||||||
| import seminar.views.views_rest as vr | import seminar.views.views_rest as vr | ||||||
| from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu | from seminar.views.vysledkovka import vysledkovka_rocniku, vysledkovka_cisla, body_resitelu | ||||||
| 
 | 
 | ||||||
| from datetime import timedelta, date, datetime, MAXYEAR | from datetime import date, datetime | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from itertools import groupby | from itertools import groupby | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
|  | @ -40,14 +36,11 @@ import os | ||||||
| import os.path as op | import os.path as op | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| import unicodedata | import unicodedata | ||||||
| import json |  | ||||||
| import traceback |  | ||||||
| import sys |  | ||||||
| import csv | import csv | ||||||
| import logging | import logging | ||||||
| import time | import time | ||||||
| 
 | 
 | ||||||
| from seminar.utils import aktivniResitele, resi_v_rocniku, problemy_rocniku, cisla_rocniku, hlavni_problemy_f | from seminar.utils import aktivniResitele, problemy_rocniku, cisla_rocniku, hlavni_problemy_f | ||||||
| from various.autentizace.views import LoginView | from various.autentizace.views import LoginView | ||||||
| from various.autentizace.utils import posli_reset_hesla | from various.autentizace.utils import posli_reset_hesla | ||||||
| 
 | 
 | ||||||
|  | @ -1040,97 +1033,6 @@ class ResitelView(LoginRequiredMixin,generic.DetailView): | ||||||
| # - přidat do forms | # - přidat do forms | ||||||
| # - includovat do html | # - includovat do html | ||||||
| 
 | 
 | ||||||
| class AddSolutionView(LoginRequiredMixin, FormView): |  | ||||||
| 	template_name = 'seminar/org/vloz_reseni.html' |  | ||||||
| 	form_class = f.VlozReseniForm |  | ||||||
| 
 |  | ||||||
| 	def form_valid(self, form): |  | ||||||
| 		data = form.cleaned_data |  | ||||||
| 		nove_reseni = m.Reseni.objects.create( |  | ||||||
| 			cas_doruceni=data['cas_doruceni'], |  | ||||||
| 			forma=data['forma'], |  | ||||||
| 			poznamka=data['poznamka'], |  | ||||||
| 			) |  | ||||||
| 		nove_reseni.resitele.add(data['resitel']) |  | ||||||
| 		nove_reseni.problem.add(data['problem']) |  | ||||||
| 		nove_reseni.save() |  | ||||||
| 		# Chtěl jsem, aby bylo vidět, že se to uložilo, tak přesměrovávám na profil. |  | ||||||
| 		return redirect(reverse('profil')) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class NahrajReseniView(LoginRequiredMixin, CreateView): |  | ||||||
| 	model = s.Reseni |  | ||||||
| 	template_name = 'seminar/profil/nahraj_reseni.html' |  | ||||||
| 	form_class = f.NahrajReseniForm |  | ||||||
| 
 |  | ||||||
| 	def get(self, request, *args, **kwargs): |  | ||||||
| 		# Zaříznutí starých řešitelů: |  | ||||||
| 		# FIXME: Je to tady dost naprasené, mělo by to asi být jinde… |  | ||||||
| 		osoba = m.Osoba.objects.get(user=self.request.user) |  | ||||||
| 		resitel = osoba.resitel |  | ||||||
| 		if resitel.rok_maturity <= m.Nastaveni.get_solo().aktualni_rocnik.prvni_rok: |  | ||||||
| 			return render(request, 'universal.html', { |  | ||||||
| 				'title': 'Nelze odevzdat', |  | ||||||
| 				'error': 'Zdá se, že jsi již odmaturoval/a, a tedy nemůžeš odevzdat do našeho semináře řešení.', |  | ||||||
| 				'text': 'Pokud se ti zdá, že to je chyba, napiš nám prosím e-mail. Díky.', |  | ||||||
| 				}) |  | ||||||
| 		return super().get(request, *args, **kwargs) |  | ||||||
| 
 |  | ||||||
| 	def get_context_data(self,**kwargs): |  | ||||||
| 		data = super().get_context_data(**kwargs) |  | ||||||
| 		if self.request.POST: |  | ||||||
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet(self.request.POST,self.request.FILES) |  | ||||||
| 		else: |  | ||||||
| 			data['prilohy'] = f.ReseniSPrilohamiFormSet() |  | ||||||
| 		return data |  | ||||||
| 
 |  | ||||||
| 	# FIXME prepsat tak, aby form_valid se volalo jen tehdy, kdyz je form i formset validni |  | ||||||
| 	# Inspirace: https://stackoverflow.com/questions/41599809/using-a-django-filefield-in-an-inline-formset |  | ||||||
| 	def form_valid(self,form): |  | ||||||
| 		context = self.get_context_data() |  | ||||||
| 		prilohy = context['prilohy'] |  | ||||||
| 		if not prilohy.is_valid(): |  | ||||||
| 			return super().form_invalid(form) |  | ||||||
| 		with transaction.atomic(): |  | ||||||
| 			self.object = form.save() |  | ||||||
| 			self.object.resitele.add(Resitel.objects.get(osoba__user = self.request.user)) |  | ||||||
| 			self.object.cas_doruceni = timezone.now() |  | ||||||
| 			self.object.forma = s.Reseni.FORMA_UPLOAD |  | ||||||
| 			self.object.save() |  | ||||||
| 
 |  | ||||||
| 			prilohy.instance = self.object |  | ||||||
| 			prilohy.save() |  | ||||||
| 
 |  | ||||||
| 		# Pošleme mail opravovatelům a garantovi |  | ||||||
| 		# FIXME: Nechat spočítat databázi? Je to pár dotazů (pravděpodobně), takže to za to možná nestojí |  | ||||||
| 		prijemci = set() |  | ||||||
| 		problemy = [] |  | ||||||
| 		for prob in form.cleaned_data['problem']: |  | ||||||
| 			prijemci.update(prob.opravovatele.all()) |  | ||||||
| 			if prob.garant is not None: |  | ||||||
| 				prijemci.add(prob.garant) |  | ||||||
| 			problemy.append(prob) |  | ||||||
| 			# FIXME: Možná poslat mail i relevantním orgům nadproblémů? |  | ||||||
| 		if len(prijemci) < 1: |  | ||||||
| 			logger.warning(f"Pozor, neposílám e-mail nikomu. Problémy: {problemy}") |  | ||||||
| 		# FIXME: Víc informativní obsah mailů, možná vč. příloh? |  | ||||||
| 		prijemci = map(lambda it: it.osoba.email, prijemci) |  | ||||||
| 
 |  | ||||||
| 		resitel = Osoba.objects.get(user = self.request.user) |  | ||||||
| 
 |  | ||||||
| 		seznam = "problému " + str(problemy[0]) if len(problemy) == 1 else 'následujícím problémům:\n' + ', \n'.join(map(str, problemy)) |  | ||||||
| 		seznam_do_subjectu = "problému " + str(problemy[0]) + ("" if len(problemy) == 1 else f" (a dalším { len(problemy) - 1 })") |  | ||||||
| 
 |  | ||||||
| 		send_mail( |  | ||||||
| 			subject="Nové řešení k " + seznam_do_subjectu, |  | ||||||
| 			message=f"Řešitel{ '' if resitel.pohlavi_muz else 'ka' } { resitel } právě nahrál{'' if resitel.pohlavi_muz else 'a' } nové řešení k { seznam }.\n\nHurá do opravování: { self.object.absolute_url() }", |  | ||||||
| 			from_email="submitovatko@mam.mff.cuni.cz", # FIXME: Chceme to mít radši tady, nebo v nastavení? |  | ||||||
| 			recipient_list=list(prijemci), |  | ||||||
| 			) |  | ||||||
| 
 |  | ||||||
| 		return formularOKView(self.request, text='Řešení úspěšně odevzdáno') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data): | def prihlaska_log_gdpr_safe(logger, gdpr_logger, msg, form_data): | ||||||
| 	msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items))) | 	msg = "{}, form_hash:{}".format(msg,hash(frozenset(form_data.items))) | ||||||
| 	logger.warn(msg) | 	logger.warn(msg) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue