Toto se rozbíjí, když dojde ke smazání hodnocení v pořadí dříve, než nějaké hodnocení s neprázdnou zpětnou vazbou, neboť řádky formsetu jsou přečíslovány a pak špatně spárovány s původními hodnotami, takže se nesprávně detekuje změna.
		
			
				
	
	
		
			242 lines
		
	
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			242 lines
		
	
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 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 tvorba.models import Problem, Deadline, Cislo, Uloha, aux_generate_filename
 | |
| from various.models import SeminarModelBase
 | |
| 
 | |
| from odevzdavatko.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet
 | |
| from personalni.models import Resitel
 | |
| 
 | |
| @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('treenode.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()
 | |
| 
 | |
| 	def resitel_url(self):
 | |
| 		return f'https://{get_current_site(None)}{reverse_lazy("odevzdavatko_resitel_reseni", args=[self.id])}'
 | |
| 
 | |
| 	# 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)
 | |
| 
 | |
| 	def deadline_reseni(self):
 | |
| 		return Deadline.objects.filter(deadline__gte=self.cas_doruceni).order_by("deadline").first()
 | |
| 
 | |
| ## 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)
 | |
| 
 | |
| 	# V ročníku < 26 nastaveno na deadline vygenerovaný pro původní cislo_body
 | |
| 	deadline_body = models.ForeignKey(Deadline, verbose_name='deadline 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)
 | |
| 
 | |
| 	feedback = models.TextField('zpětná vazba', blank=True, default='', help_text='Zpětná vazba řešiteli (plain text)')
 | |
| 
 | |
| 	@property
 | |
| 	def body_celkem(self):
 | |
| 		# FIXME řeším jen prvního řešitele.
 | |
| 		return Hodnoceni.objects.filter(problem=self.problem, reseni__resitele=self.reseni.resitele.first(), body__isnull=False).aggregate(Sum("body"))["body__sum"]
 | |
| 
 | |
| 	@body_celkem.setter
 | |
| 	def body_celkem(self, value):
 | |
| 		if value is None:
 | |
| 			self.body = None
 | |
| 		else:
 | |
| 			if self.body is None:
 | |
| 				self.body = 0
 | |
| 			if self.body_celkem is None:
 | |
| 				self.body += value
 | |
| 			else:
 | |
| 				self.body += value - self.body_celkem
 | |
| 
 | |
| 	@property
 | |
| 	def body_neprepocitane(self):
 | |
| 		if self.body is None:
 | |
| 			return None
 | |
| 		return inverze_vzorecku_na_prepocet(self.body, self.reseni.resitele.count())
 | |
| 
 | |
| 	@body_neprepocitane.setter
 | |
| 	def body_neprepocitane(self, value):
 | |
| 		if value is None:
 | |
| 			self.body = None
 | |
| 		else:
 | |
| 			self.body = vzorecek_na_prepocet(value, self.reseni.resitele.count())
 | |
| 
 | |
| 	@property
 | |
| 	def body_neprepocitane_celkem(self):
 | |
| 		if self.body_celkem is None:
 | |
| 			return None
 | |
| 		return inverze_vzorecku_na_prepocet(self.body_celkem, self.reseni.resitele.count())
 | |
| 
 | |
| 	@body_neprepocitane_celkem.setter
 | |
| 	def body_neprepocitane_celkem(self, value):
 | |
| 		if value is None:
 | |
| 			self.body = None
 | |
| 		else:
 | |
| 			self.body_celkem = vzorecek_na_prepocet(value, self.reseni.resitele.count())
 | |
| 
 | |
| 	@property
 | |
| 	def body_max(self):
 | |
| 		if self.body_neprepocitane_max is None:
 | |
| 			return None
 | |
| 		return vzorecek_na_prepocet(self.body_neprepocitane_max, self.reseni.resitele.count())
 | |
| 
 | |
| 	@property
 | |
| 	def body_neprepocitane_max(self):
 | |
| 		if not isinstance(self.problem.get_real_instance(), Uloha):
 | |
| 			return None
 | |
| 		return self.problem.uloha.max_body
 | |
| 
 | |
| 	def __str__(self):
 | |
| 		return "{}, {}, {}".format(self.problem, self.reseni, self.body)
 | |
| 
 | |
| 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('/')
 | |
| 
 | |
| 
 | |
| # 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
 |