Merge pull request 'Vylepšení hodnotítka fix #1354 fix #1237' (!20) from vylepseni_odevzdavatka into master
Reviewed-on: #20
This commit is contained in:
		
						commit
						2ab6e76fbe
					
				
					 7 changed files with 204 additions and 11 deletions
				
			
		|  | @ -1255,3 +1255,8 @@ div.gdpr { | |||
| label[for=id_skola] { | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| /* detail řešení */ | ||||
| .bodovani>input { | ||||
| 	width: 4em; | ||||
| } | ||||
|  |  | |||
|  | @ -119,6 +119,24 @@ class JednoHodnoceniForm(forms.ModelForm): | |||
| 			'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, | ||||
| 		) | ||||
|  |  | |||
|  | @ -49,8 +49,18 @@ $(document).ready(function(){ | |||
|             $('#id_form-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value | ||||
|         } | ||||
| 		$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1); | ||||
| 
 | ||||
| 		$('.bodovani').children().change(function(){ | ||||
| 			$(this).parent().parent().children(".bodovani").children().attr("disabled", true); | ||||
| 			$(this).attr("disabled", false); | ||||
| 		}) | ||||
| 	}); | ||||
| 	$('.smazat_hodnoceni').click(function(){ | ||||
| 		deleteForm("form",this); | ||||
| 	}); | ||||
| 
 | ||||
| 	$('.bodovani').children().change(function(){ | ||||
| 		$(this).parent().parent().children(".bodovani").children().attr("disabled", true); | ||||
| 		$(this).attr("disabled", false); | ||||
| 	}) | ||||
| }); | ||||
|  |  | |||
|  | @ -4,6 +4,13 @@ | |||
| {% load mail %} | ||||
| {% load jmena %} | ||||
| 
 | ||||
| {# Přišlo mi to hezčí, než psát všude if. #} | ||||
| {% block custom_css %} | ||||
|   {% if object.resitele.count == 1 %} | ||||
|     <style>.teamovaCast {display: none}</style> | ||||
|   {% endif %} | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| 
 | ||||
|   {% if edit %} | ||||
|  | @ -76,6 +83,22 @@ | |||
| <h3>Neveřejná poznámka:</h3> | ||||
| <p>{{ poznamka_form.poznamka }}</p> | ||||
| 
 | ||||
| 
 | ||||
| <script>vporadku=true;</script> | ||||
| {% for h in hodnoceni %}{% if h.body < 0.0 %} | ||||
|   <script> | ||||
|     if(vporadku){ | ||||
|       vporadku=false; | ||||
|       alert( | ||||
|         "Pozor! Některé hodnocení má záporné body.\n\n" + | ||||
|         "Buď jde o záměr, nebo o špatné zadáný počet bodů (např. součet bodů za úlohu) nebo se něco pokazilo.\n\n" + | ||||
|         "Pokud se to děje neočekávaně a opakovaně, napiš webařům :)" | ||||
|       ); | ||||
|     } | ||||
|   </script> | ||||
| {% endif %}{% endfor %} | ||||
| 
 | ||||
| 
 | ||||
| {# Hodnocení: #} | ||||
| <h3>Hodnocení:</h3> | ||||
| <table> | ||||
|  | @ -83,12 +106,15 @@ | |||
| {{ form.management_form }} | ||||
| </table> | ||||
| <table id="form_set"> | ||||
| <tr><th>Problém</th><th>Body</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr> | ||||
| <tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑🤝🧑</th><th class="teamovaCast">{# ❤ #}🧑🤝🧑∑</th><th>Deadline pro body</th><th>Zpětná vazba pro řešitele</th></tr> | ||||
| {% for subform in form %} | ||||
|     <tbody> | ||||
| 	<tr class="hodnoceni"> | ||||
| 		<td>{{ subform.problem }}</td> | ||||
| 		<td>{{ subform.body }}</td> | ||||
| 		<td class="bodovani">{{ subform.body }}</td> | ||||
| 		<td class="bodovani">{{ subform.body_celkem }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ subform.body_neprepocitane }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ subform.body_neprepocitane_celkem }}</td> | ||||
| 		<td>{{ subform.deadline_body }}</td> | ||||
| 		<td>{{ subform.feedback }}</td> | ||||
| 		<td class="has_smazat_hodnoceni"><a href="#" class="smazat_hodnoceni" id="id_{{subform.prefix}}-jsremove" title="Smazat hodnocení"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> | ||||
|  | @ -104,7 +130,10 @@ | |||
| <table id="empty_form" style="display: none;"> | ||||
| 	<tr class="hodnoceni"> | ||||
| 		<td>{{ form.empty_form.problem }}</td> | ||||
| 		<td>{{ form.empty_form.body }}</td> | ||||
| 		<td class="bodovani">{{ form.empty_form.body }}</td> | ||||
| 		<td class="bodovani">{{ form.empty_form.body_celkem }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ form.empty_form.body_neprepocitane }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ form.empty_form.body_neprepocitane_celkem }}</td> | ||||
| 		<td>{{ form.empty_form.deadline_body }}</td> | ||||
| 		<td>{{ form.empty_form.feedback }}</td> | ||||
| 		<td class="has_smazat_hodnoceni"><a href="#" class="smazat_hodnoceni" id="id_{{form.empty_form.prefix}}-jsremove" title="Smazat hodnocení"><img src="{% static "odevzdavatko/cross.png" %}" alt="Smazat"></a></td> | ||||
|  | @ -114,16 +143,61 @@ | |||
|   {% else %} | ||||
| <h3>Hodnocení:</h3> | ||||
| <table class="dosla_reseni"> | ||||
| <tr><th>Problém</th><th>Body</th><th>Zpětná vazba od opravovatele</th></tr> | ||||
| <tr><th>Problém</th><th>{# 📖 #}🧍</th><th>{# 🔵 #}🧍∑</th><th class="teamovaCast">{# 💪 #}🧑🤝🧑</th><th class="teamovaCast">{# ❤ #}🧑🤝🧑∑</th><th>Zpětná vazba od opravovatele</th></tr> | ||||
| {% for h in hodnoceni %} | ||||
| 	<tr class="hodnoceni"> | ||||
| 		<td>{{ h.problem }}</td> | ||||
| 		<td>{{ h.body }}</td> | ||||
| 		<td class="bodovani">{{ h.body }}</td> | ||||
| 		<td class="bodovani">{{ h.body_celkem }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ h.body_neprepocitane }}</td> | ||||
| 		<td class="bodovani teamovaCast">{{ h.body_neprepocitane_celkem }}</td> | ||||
| 		<td>{{ h.feedback | linebreaks }}</td> | ||||
| 	</tr> | ||||
| {% endfor %} | ||||
| </table> | ||||
|   {% endif %} | ||||
| 
 | ||||
| <h3>Vysvětlivky:</h3> | ||||
| <dl> | ||||
|   <dt>{# 📖 #}🧍</dt> | ||||
|   <dd>Body za toto řešení.</dd> | ||||
| 
 | ||||
|   <dt>{# 🔵 #}🧍∑</dt> | ||||
|   <dd>Body za tento problém/úlohu (součet za všechna řešení).</dd> | ||||
| 
 | ||||
|   <dt class="teamovaCast">{# 💪 #}🧑🤝🧑</dt> | ||||
|   <dd class="teamovaCast">Body, které by dostal tým, kdyby to řešil jako jeden řešitel, za toto řešení.</dd> | ||||
| 
 | ||||
|   <dt class="teamovaCast">{# ❤ #}🧑🤝🧑∑</dt> | ||||
|   <dd class="teamovaCast">Body, které by dostal tým, kdyby to řešil jako jeden řešitel, za tento problém/úlohu (součet za všechna řešení).</dd> | ||||
| </dl> | ||||
| 
 | ||||
| 
 | ||||
| {% if edit %} | ||||
| <h3>Návod pro hodnocení:</h3> | ||||
| Sloupce: | ||||
| <ol> | ||||
|   <li>Pokud to neudělal řešitel, je třeba pomocí pluska přidat řádky (případně křížkem smazat) a vyplnit problémy tak, aby zde byly všechny, které řešení řeší (body zadáváme přímo k úlohám, ne k témátku samotnému).</li> | ||||
|   <li>Pak je třeba do jednoho ze 2 nebo 4 sloupců vyplnit body (lze udělovat desetiny, setiny už udělovat nejde): | ||||
|     <ul> | ||||
|       <li>TLDR: pokud si počítáš a kontroluješ vše sám, vyplňuj do nejlevějšího. Pokud naopak vždy vyplňuješ to, kolik řešení má dostat bodů (bez ohledu na počet řešitelů a předchozí odevzdání), vyplňuj nejpravější.</li> | ||||
|       <li>Zaprvé je třeba dávat pozor, že řešitel už mohl dostat body za danou úlohu (to je rozdíl mezi lichými a sudými sloupci).</li> | ||||
|       <li>Zadruhé řešení, na kterém se spolupracovalo, dostává body přepočítané podle vzorečku <a href="https://mam.matfyz.cz/jak-resit/podrobneji/">zde dole</a>. To dělá rozdíl mezi prvními a druhými dvěma sloupci, pokud se oboje zobrazují.</li> | ||||
|       <li>Co který sloupec znamená, je napsáno výše ve vysvětlivkách. | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Pokud nemáš důvod, deadline neměň. Sloupeček s deadlinem znamená, do kterého deadlinu se započítají body (nemusí se shodovat s deadlinem řešení).</li> | ||||
|   <li>Poslední sloupec je na zpětnou vazbu řešiteli, tedy (na rozdíl od Neveřejné poznámky, která je určena pro synchronizaci orgů) ji uvidí řešitelé. Zatím jen pasivně (nechodí e-mail). Pohled řešitele si můžete prohlédnout <a href="{% url 'odevzdavatko_resitel_reseni' reseni.id %}">zde</a>. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.</li> | ||||
| </ol> | ||||
| 
 | ||||
| Další poznámky | ||||
| <ul> | ||||
|   <li>Pokud chceš zadané body smazat (rozmyslel sis to a ohodnotíš to později), smaž body v libovolném sloupeci.</li> | ||||
|   <li>Ne, soubory si zatím nejde stáhnout lépe než proklikáním všech řešeních. Stejně tak nejde hromadně bodovat. Třeba někdy půjde.</li> | ||||
|   <li>Pokud řešitel odevzdal něco nesouvisejícího, nebo něco duplicitně, tak mu za to dejte nulu a jako problém nastavte něco, co odevzdal (ať se mu ve výsledkovce nezobrazuje 0 na špatném místě). A upozorni ho.</li> | ||||
|   <li>Ano, lze zadávat záporné body (např. za podvádění), web vás bude silně upozorňovat, ale jinak mu to nevadí.</li> | ||||
|   <li>Libovolné problémy s hodnotítkem řeš s {% maillink 'webaři' to='web@mam.mff.cuni.cz' subject='Hodnotítko' %}.</li> | ||||
| </ul> | ||||
| {% endif %} | ||||
| 
 | ||||
| {% endblock %} | ||||
|  |  | |||
|  | @ -224,12 +224,18 @@ class DetailReseniView(DetailView): | |||
| 		self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) | ||||
| 		result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet | ||||
| 		for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni): | ||||
| 			result.append({ | ||||
| 				"problem": hodn.problem, | ||||
| 				"body": hodn.body, | ||||
| 				"deadline_body": hodn.deadline_body, | ||||
| 				"feedback": hodn.feedback, | ||||
| 				}) | ||||
| 			seznam_atributu = [ | ||||
| 				"problem", | ||||
| 				"body", | ||||
| 				"body_celkem", | ||||
| 				"body_neprepocitane", | ||||
| 				"body_neprepocitane_celkem", | ||||
| 				"body_max", | ||||
| 				"body_neprepocitane_max", | ||||
| 				"deadline_body", | ||||
| 				"feedback", | ||||
| 			] | ||||
| 			result.append({attr: getattr(hodn, attr) for attr in seznam_atributu}) | ||||
| 		return result | ||||
| 
 | ||||
| 	def get_context_data(self, **kw): | ||||
|  | @ -296,11 +302,23 @@ def hodnoceniReseniView(request, pk, *args, **kwargs): | |||
| 
 | ||||
| 		# Vyrobíme nová podle formsetu | ||||
| 		for form in formset: | ||||
| 			data_for_hodnoceni = form.cleaned_data | ||||
| 			data_for_body = data_for_hodnoceni.copy() | ||||
| 			del(data_for_hodnoceni["body_celkem"]) | ||||
| 			del(data_for_hodnoceni["body_neprepocitane"]) | ||||
| 			del(data_for_hodnoceni["body_neprepocitane_celkem"]) | ||||
| 			hodnoceni = m.Hodnoceni( | ||||
| 					reseni=reseni, | ||||
| 					**form.cleaned_data, | ||||
| 					) | ||||
| 			logger.info(f"Creating Hodnoceni: {hodnoceni}") | ||||
| 			zmeny_bodu = [it for it in form.changed_data if it.startswith("body")] | ||||
| 			if len(zmeny_bodu) == 1: | ||||
| 				hodnoceni.__setattr__(zmeny_bodu[0], data_for_body[zmeny_bodu[0]]) | ||||
| 			# > jedna změna je špatně, ale 4 "změny" znamenají že nebylo nic zadáno | ||||
| 			if len(zmeny_bodu) > 1 and len(zmeny_bodu) != 4: | ||||
| 				logger.warning(f"Hodnocení {hodnoceni} mělo mít nastavené víc různých bodů: {zmeny_bodu}. Nastavuji -0.1.") | ||||
| 				hodnoceni.body = -0.1 | ||||
| 			hodnoceni.save() | ||||
| 
 | ||||
| 	return redirect(success_url) | ||||
|  |  | |||
|  | @ -14,6 +14,8 @@ from seminar.models import personalni as pm | |||
| from seminar.models import treenode as tm | ||||
| from seminar.models import base as bm | ||||
| 
 | ||||
| from seminar.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet | ||||
| 
 | ||||
| 
 | ||||
| @reversion.register(ignore_duplicates=True) | ||||
| class Reseni(bm.SeminarModelBase): | ||||
|  | @ -115,6 +117,61 @@ class Hodnoceni(bm.SeminarModelBase): | |||
| 
 | ||||
| 	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(), am.Uloha): | ||||
| 			return None | ||||
| 		return self.problem.uloha.max_body | ||||
| 
 | ||||
| 	def __str__(self): | ||||
| 		return "{}, {}, {}".format(self.problem, self.reseni, self.body) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| # -*- coding: utf-8 -*- | ||||
| 
 | ||||
| import datetime | ||||
| import decimal | ||||
| 
 | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.contrib.auth.decorators import permission_required, \ | ||||
|  | @ -44,6 +45,16 @@ AnonymousUser.je_org = False | |||
| AnonymousUser.je_resitel = False | ||||
| 
 | ||||
| 
 | ||||
| def vzorecek_na_prepocet(body, resitelu): | ||||
| 	""" Vzoreček na přepočet plných bodů na parciálni, když má řešení více řešitelů. """ | ||||
| 	return body * 3 / (resitelu + 2) | ||||
| 
 | ||||
| 
 | ||||
| def inverze_vzorecku_na_prepocet(body: decimal.Decimal, resitelu) -> decimal.Decimal: | ||||
| 	""" Vzoreček na přepočet parciálních bodů na plné, když má řešení více řešitelů. """ | ||||
| 	return round(body * (resitelu + 2) / 3, 1) | ||||
| 
 | ||||
| 
 | ||||
| class FirstTagParser(HTMLParser): | ||||
| 	def __init__(self, *args, **kwargs): | ||||
| 		self.firstTag = None | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue