269 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			269 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from django import forms
 | |
| from dal import autocomplete
 | |
| from django.forms import formset_factory
 | |
| from django.forms.models import inlineformset_factory
 | |
| from django.utils import timezone
 | |
| 
 | |
| from personalni.models import Resitel
 | |
| from tvorba.models import Problem, Deadline
 | |
| from various.models import Nastaveni
 | |
| 
 | |
| from odevzdavatko.models import Reseni, PrilohaReseni, Hodnoceni
 | |
| 
 | |
| 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):
 | |
| 	problem = forms.ModelMultipleChoiceField(
 | |
| 		queryset=Problem.objects.all(),
 | |
| 		label="Problémy",
 | |
| 		widget=autocomplete.ModelSelect2Multiple(
 | |
| 			url='autocomplete_problem',
 | |
| 			attrs={
 | |
| 				'data-placeholder--id': '-1',
 | |
| 				'data-placeholder--text': '---',
 | |
| 				'data-close-on-select': 'false',
 | |
| 				'data-dropdown-css-class': 's2m-se-zaskrtavatky',
 | |
| 				'data-allow-clear': 'true'
 | |
| 				},
 | |
| 			),
 | |
| 	)
 | |
| 	# to_field_name
 | |
| 	#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém',
 | |
| 	#	through='Hodnoceni')
 | |
| 
 | |
| 	resitel = forms.ModelMultipleChoiceField(label="Řešitelé",
 | |
| 		queryset=Resitel.objects.all(),
 | |
| 		widget=autocomplete.ModelSelect2Multiple(
 | |
| 			url='autocomplete_resitel',
 | |
| 			attrs = {'data-placeholder--id': '-1',
 | |
| 				'data-placeholder--text' : '---',
 | |
| 				'data-close-on-select': 'false',
 | |
| 				'data-dropdown-css-class': 's2m-se-zaskrtavatky',
 | |
| 				'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 = 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)')
 | |
| 
 | |
| 
 | |
| class NahrajReseniForm(forms.ModelForm):
 | |
| 	class Meta:
 | |
| 		model = Reseni
 | |
| 		fields = ('problem', 'resitele')
 | |
| 		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-close-on-select': 'false',
 | |
| 						'data-dropdown-css-class': 's2m-se-zaskrtavatky',
 | |
| 						'data-allow-clear': 'true'},
 | |
| 					forward=["nadproblem_id"],
 | |
| 				),
 | |
| 				'resitele':
 | |
| 				autocomplete.ModelSelect2Multiple(
 | |
| 					url='autocomplete_resitel_public',
 | |
| 					attrs = {'data-placeholder--id': '-1',
 | |
| 						'data-placeholder--text' : '---',
 | |
| 						'data-close-on-select': 'false',
 | |
| 						'data-dropdown-css-class': 's2m-se-zaskrtavatky',
 | |
| 						'data-allow-clear': 'true'},
 | |
| 				)
 | |
| 		}
 | |
| 
 | |
| 	nadproblem_id = forms.IntegerField(required=False, disabled=True, widget=forms.HiddenInput())
 | |
| 
 | |
| 	def __init__(self, *args, **kwargs):
 | |
| 		super().__init__(*args, **kwargs)
 | |
| 		# FIXME Z nějakého důvodu se do této třídy dostaneme i bez resitele
 | |
| 		if 'resitele' in self.fields:
 | |
| 			# FIXME Mnohem hezčí by to bylo u definice resitele výše, ale nepodařilo se mi to.
 | |
| 			self.fields['resitele'].required = False
 | |
| 			self.fields['resitele'].label = "Další autoři"
 | |
| 		if 'problem' in self.fields:
 | |
| 			self.fields['problem'].label = "Všechny řešené problémy"
 | |
| 
 | |
| 	def clean_problem(self):
 | |
| 		problem = self.cleaned_data.get('problem')
 | |
| 		for p in problem:
 | |
| 			if p.stav != Problem.STAV_ZADANY:
 | |
| 				raise forms.ValidationError("Problém " + str(p) + " již nelze řešit!")
 | |
| 		return problem
 | |
| 
 | |
| ReseniSPrilohamiFormSet = inlineformset_factory(Reseni, PrilohaReseni,
 | |
| 		form = NahrajReseniForm,
 | |
| 		fields = ('soubor','res_poznamka'),
 | |
| 		widgets = {'res_poznamka':forms.TextInput()},
 | |
| 		extra = 1,
 | |
| 		can_delete = False,
 | |
| 
 | |
| 		)
 | |
| 
 | |
| 
 | |
| class JednoHodnoceniForm(forms.ModelForm):
 | |
| 	class Meta:
 | |
| 		model = Hodnoceni
 | |
| 		fields = ('problem', 'body', 'deadline_body', 'feedback',)
 | |
| 		widgets = {
 | |
| 			'problem': autocomplete.ModelSelect2(
 | |
| 				url='autocomplete_problem',
 | |
| 				),
 | |
| 			'feedback': forms.Textarea(attrs={'rows': 1, 'cols': 30, 'class': 'feedback'}),
 | |
| 			}
 | |
| 
 | |
| 	body_celkem = forms.DecimalField(required=False, decimal_places=1)
 | |
| 	body_neprepocitane = forms.DecimalField(required=False, decimal_places=1)
 | |
| 	body_neprepocitane_celkem = forms.DecimalField(required=False, decimal_places=1)
 | |
| 
 | |
| 	def __init__(self, *args, initial=None, **kwargs):
 | |
| 		if initial is not None:
 | |
| 			body_max = initial["body_max"]
 | |
| 			body_neprepocitane_max = initial["body_neprepocitane_max"]
 | |
| 			del(initial["body_max"])
 | |
| 			del(initial["body_neprepocitane_max"])
 | |
| 		super().__init__(*args, initial=initial, **kwargs)
 | |
| 		if initial is not None:
 | |
| 			self.fields['body'].widget.attrs['placeholder'] = body_max
 | |
| 			self.fields['body_celkem'].widget.attrs['placeholder'] = body_max
 | |
| 			self.fields['body_neprepocitane'].widget.attrs['placeholder'] = body_neprepocitane_max
 | |
| 			self.fields['body_neprepocitane_celkem'].widget.attrs['placeholder'] = body_neprepocitane_max
 | |
| 
 | |
| 
 | |
| OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
 | |
| 		extra = 0,
 | |
| 		)
 | |
| 
 | |
| class PoznamkaReseniForm(forms.ModelForm):
 | |
| 	class Meta:
 | |
| 		model = 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 = Nastaveni.get_solo().aktualni_rocnik
 | |
| 		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
 | |
| 
 | |
| 		result = []
 | |
| 
 | |
| 		result.append(("0001-01-01", f"Odjakživa"))
 | |
| 
 | |
| 		for deadline in Deadline.objects.filter(
 | |
| 				deadline__lte=timezone.now(),
 | |
| 				cislo__rocnik=aktualni_rocnik
 | |
| 				).order_by("deadline"):
 | |
| 
 | |
| 			result.append((
 | |
| 				strftime(DATE_FORMAT, deadline.deadline.timetuple()),
 | |
| 				str(deadline)))
 | |
| 
 | |
| 		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,
 | |
| 			'barvicky': True,
 | |
| 		}
 | |
| 		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)
 | |
| 	barvicky = forms.BooleanField(required=False)
 |