Vylepšení hodnotítka fix #1354 fix #1237 #20

Merged
zelvuska merged 25 commits from vylepseni_odevzdavatka into master 1 year ago
  1. 5
      mamweb/static/css/mamweb.css
  2. 18
      odevzdavatko/forms.py
  3. 10
      odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js
  4. 84
      odevzdavatko/templates/odevzdavatko/detail.html
  5. 30
      odevzdavatko/views.py
  6. 57
      seminar/models/odevzdavatko.py
  7. 11
      seminar/utils.py

5
mamweb/static/css/mamweb.css

@ -1255,3 +1255,8 @@ div.gdpr {
label[for=id_skola] { label[for=id_skola] {
font-weight: bold; font-weight: bold;
} }
/* detail řešení */
.bodovani>input {
width: 4em;
}

18
odevzdavatko/forms.py

@ -119,6 +119,24 @@ class JednoHodnoceniForm(forms.ModelForm):
'feedback': forms.Textarea(attrs={'rows': 1, 'cols': 30, 'class': 'feedback'}), '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, OhodnoceniReseniFormSet = formset_factory(JednoHodnoceniForm,
extra = 0, extra = 0,
) )

10
odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js

@ -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-' + form_idx + '-deadline_body')[0].value = $('#id_form-' + (form_idx - 1) + '-deadline_body')[0].value
} }
$('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1); $('#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(){ $('.smazat_hodnoceni').click(function(){
deleteForm("form",this); deleteForm("form",this);
}); });
$('.bodovani').children().change(function(){
$(this).parent().parent().children(".bodovani").children().attr("disabled", true);
$(this).attr("disabled", false);
})
}); });

84
odevzdavatko/templates/odevzdavatko/detail.html

@ -4,6 +4,13 @@
{% load mail %} {% load mail %}
{% load jmena %} {% 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 %} {% block content %}
{% if edit %} {% if edit %}
@ -76,6 +83,22 @@
<h3>Neveřejná poznámka:</h3> <h3>Neveřejná poznámka:</h3>
<p>{{ poznamka_form.poznamka }}</p> <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í: #} {# Hodnocení: #}
<h3>Hodnocení:</h3> <h3>Hodnocení:</h3>
<table> <table>
@ -83,12 +106,15 @@
{{ form.management_form }} {{ form.management_form }}
</table> </table>
<table id="form_set"> <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 %} {% for subform in form %}
<tbody> <tbody>
<tr class="hodnoceni"> <tr class="hodnoceni">
<td>{{ subform.problem }}</td> <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.deadline_body }}</td>
<td>{{ subform.feedback }}</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> <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;"> <table id="empty_form" style="display: none;">
<tr class="hodnoceni"> <tr class="hodnoceni">
<td>{{ form.empty_form.problem }}</td> <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.deadline_body }}</td>
<td>{{ form.empty_form.feedback }}</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> <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 %} {% else %}
<h3>Hodnocení:</h3> <h3>Hodnocení:</h3>
<table class="dosla_reseni"> <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 %} {% for h in hodnoceni %}
<tr class="hodnoceni"> <tr class="hodnoceni">
<td>{{ h.problem }}</td> <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> <td>{{ h.feedback | linebreaks }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
{% endif %} {% 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 %} {% endblock %}

30
odevzdavatko/views.py

@ -224,12 +224,18 @@ class DetailReseniView(DetailView):
self.reseni = get_object_or_404(m.Reseni, id=self.kwargs['pk']) 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 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): for hodn in m.Hodnoceni.objects.filter(reseni=self.reseni):
result.append({ seznam_atributu = [
"problem": hodn.problem, "problem",
"body": hodn.body, "body",
"deadline_body": hodn.deadline_body, "body_celkem",
"feedback": hodn.feedback, "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 return result
def get_context_data(self, **kw): def get_context_data(self, **kw):
@ -296,11 +302,23 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
# Vyrobíme nová podle formsetu # Vyrobíme nová podle formsetu
for form in formset: 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( hodnoceni = m.Hodnoceni(
reseni=reseni, reseni=reseni,
**form.cleaned_data, **form.cleaned_data,
) )
logger.info(f"Creating Hodnoceni: {hodnoceni}") 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() hodnoceni.save()
return redirect(success_url) return redirect(success_url)

57
seminar/models/odevzdavatko.py

@ -14,6 +14,8 @@ from seminar.models import personalni as pm
from seminar.models import treenode as tm from seminar.models import treenode as tm
from seminar.models import base as bm from seminar.models import base as bm
from seminar.utils import vzorecek_na_prepocet, inverze_vzorecku_na_prepocet
@reversion.register(ignore_duplicates=True) @reversion.register(ignore_duplicates=True)
class Reseni(bm.SeminarModelBase): 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)') 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
zelvuska marked this conversation as resolved
Review

Nechceš k tomu napsat assert nebo rovnou celý test, který by hlídal, že po nastavení bodů tímhle způsobem se správně aktualizuje počet bodů a celkový součet?

Nechceš k tomu napsat assert nebo rovnou celý test, který by hlídal, že po nastavení bodů tímhle způsobem se správně aktualizuje počet bodů a celkový součet?
Review

ditto níž

ditto níž
@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):
zelvuska marked this conversation as resolved
Review

Tohle potenciálně generuje záporné body…

Tohle potenciálně generuje záporné body…
Review

A to je špatně?

A to je špatně?
Review

Považuji to za dost neočekávatelné chování, zvlášť když jako org píšu body kladné. Obecně si myslím, že spíš strhávat body od již udělených nechceme (možná vyjma „ujelo mi bodování, zpětně měním počet bodů)…

Považuji to za dost neočekávatelné chování, zvlášť když jako org píšu body kladné. Obecně si myslím, že spíš strhávat body od již udělených nechceme (možná vyjma „ujelo mi bodování, zpětně _měním_ počet bodů)…
Review

A jaké je tedy očekávané chování?

A jaké je tedy očekávané chování?
Review

Nedovolit to vůbec (a že má kdyžtak org napsat webařům, pokud nechápe proč)? Hodit aspoň alert(), že se o to org pokouší a že to bude vypadat blbě ve výsledkovce?

Nedovolit to vůbec (a že má kdyžtak org napsat webařům, pokud nechápe proč)? Hodit aspoň `alert()`, že se o to org pokouší a že to bude vypadat blbě ve výsledkovce?
Review

Ale doteď tam žádná taková šaráda nebyla. (Otestováno, záporné body mohu zadat vesele.) A já na frontendu nepoznám, kolik za to už řešitel dostal…

Ale doteď tam žádná taková šaráda nebyla. (Otestováno, záporné body mohu zadat vesele.) A já na frontendu nepoznám, kolik za to už řešitel dostal…
Review

Ale doteď se ti nemohlo stát, že bys ty záporné body napsal omylem – musel bys explicitně zmáčknout mínus. Není potřeba orgy ochránit proti zlým úmyslům, ale proti dobrým chybám…

Ale doteď se ti nemohlo stát, že bys ty záporné body napsal omylem – musel bys explicitně zmáčknout mínus. Není potřeba orgy ochránit proti zlým úmyslům, ale proti dobrým chybám…
Review

Přijde mi, že to v podstatě odchytává jen miniaturní část toho problému. To, když to zrovna vyjde záporné. Ale ve chvíli, kdy tam org zadá libovolný jiný nesprávný počet, tak to stejně nezjistíme. Tohle políčko prostě vyžaduje, aby tam org zadal správný počet.

Přijde mi, že to v podstatě odchytává jen miniaturní část toho problému. To, když to zrovna vyjde záporné. Ale ve chvíli, kdy tam org zadá libovolný jiný nesprávný počet, tak to stejně nezjistíme. Tohle políčko prostě vyžaduje, aby tam org zadal správný počet.
Review

Jakoby souhlasím s tím, že tam ta obrana může být (přídám tam asi javascript, který vykřikne, pokud při načtení libovolná z těch hodnot je záporná).

Ale nepřijde mi, že by to nějak signifikantně zvyšovalo ochranu proti špatnému zadání.

Jakoby souhlasím s tím, že tam ta obrana může být (přídám tam asi javascript, který vykřikne, pokud při načtení libovolná z těch hodnot je záporná). Ale nepřijde mi, že by to nějak signifikantně zvyšovalo ochranu proti špatnému zadání.
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): def __str__(self):
return "{}, {}, {}".format(self.problem, self.reseni, self.body) return "{}, {}, {}".format(self.problem, self.reseni, self.body)

11
seminar/utils.py

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import decimal
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import permission_required, \ from django.contrib.auth.decorators import permission_required, \
@ -44,6 +45,16 @@ AnonymousUser.je_org = False
AnonymousUser.je_resitel = 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): class FirstTagParser(HTMLParser):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.firstTag = None self.firstTag = None

Loading…
Cancel
Save