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