diff --git a/mamweb/static/css/mamweb.css b/mamweb/static/css/mamweb.css index 63c5f527..3833ff92 100644 --- a/mamweb/static/css/mamweb.css +++ b/mamweb/static/css/mamweb.css @@ -1255,3 +1255,8 @@ div.gdpr { label[for=id_skola] { font-weight: bold; } + +/* detail řešení */ +.bodovani>input { + width: 4em; +} diff --git a/odevzdavatko/forms.py b/odevzdavatko/forms.py index 0bc99927..0b93d555 100644 --- a/odevzdavatko/forms.py +++ b/odevzdavatko/forms.py @@ -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, ) diff --git a/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js b/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js index a14c9f8f..1c9bf2f9 100644 --- a/odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js +++ b/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-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); + }) }); diff --git a/odevzdavatko/templates/odevzdavatko/detail.html b/odevzdavatko/templates/odevzdavatko/detail.html index 7cb79c21..5a43c4b5 100644 --- a/odevzdavatko/templates/odevzdavatko/detail.html +++ b/odevzdavatko/templates/odevzdavatko/detail.html @@ -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 %} + + {% endif %} +{% endblock %} + {% block content %} {% if edit %} @@ -76,6 +83,22 @@

Neveřejná poznámka:

{{ poznamka_form.poznamka }}

+ + +{% for h in hodnoceni %}{% if h.body < 0.0 %} + +{% endif %}{% endfor %} + + {# Hodnocení: #}

Hodnocení:

@@ -83,12 +106,15 @@ {{ form.management_form }}
- + {% for subform in form %} - + + + + @@ -104,7 +130,10 @@
ProblémBodyDeadline pro bodyZpětná vazba pro řešitele
Problém{# 📖 #}🧍{# 🔵 #}🧍∑{# 💪 #}🧑‍🤝‍🧑{# ❤ #}🧑‍🤝‍🧑∑Deadline pro bodyZpětná vazba pro řešitele
{{ subform.problem }}{{ subform.body }}{{ subform.body }}{{ subform.body_celkem }}{{ subform.body_neprepocitane }}{{ subform.body_neprepocitane_celkem }} {{ subform.deadline_body }} {{ subform.feedback }} Smazat
- + + + + @@ -114,16 +143,61 @@ {% else %}

Hodnocení:

- + {% for h in hodnoceni %} - + + + + {% endfor %}
ProblémBodyZpětná vazba od opravovatele
Problém{# 📖 #}🧍{# 🔵 #}🧍∑{# 💪 #}🧑‍🤝‍🧑{# ❤ #}🧑‍🤝‍🧑∑Zpětná vazba od opravovatele
{{ h.problem }}{{ h.body }}{{ h.body }}{{ h.body_celkem }}{{ h.body_neprepocitane }}{{ h.body_neprepocitane_celkem }} {{ h.feedback | linebreaks }}
{% endif %} +

Vysvětlivky:

+
+
{# 📖 #}🧍
+
Body za toto řešení.
+ +
{# 🔵 #}🧍∑
+
Body za tento problém/úlohu (součet za všechna řešení).
+ +
{# 💪 #}🧑‍🤝‍🧑
+
Body, které by dostal tým, kdyby to řešil jako jeden řešitel, za toto řešení.
+ +
{# ❤ #}🧑‍🤝‍🧑∑
+
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í).
+
+ + +{% if edit %} +

Návod pro hodnocení:

+Sloupce: +
    +
  1. 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).
  2. +
  3. Pak je třeba do jednoho ze 2 nebo 4 sloupců vyplnit body (lze udělovat desetiny, setiny už udělovat nejde): + +
  4. +
  5. 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í).
  6. +
  7. 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 zde. Pokud chcete z nějakého důvodu napsat řešitelům e-mail, klikněte na „Poslat mail všem řešitelům“.
  8. +
+ +Další poznámky + +{% endif %} {% endblock %} diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py index 5660af71..e87e19ea 100644 --- a/odevzdavatko/views.py +++ b/odevzdavatko/views.py @@ -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) diff --git a/seminar/models/odevzdavatko.py b/seminar/models/odevzdavatko.py index c286558c..744fe38c 100644 --- a/seminar/models/odevzdavatko.py +++ b/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 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) diff --git a/seminar/utils.py b/seminar/utils.py index e10920b8..891f8c15 100644 --- a/seminar/utils.py +++ b/seminar/utils.py @@ -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