Browse Source

Merge remote-tracking branch 'origin/master' into upgrade_odevzdavatka

# Conflicts:
#	mamweb/static/css/mamweb.css
pull/30/head
Jonas Havelka 1 year ago
parent
commit
bed107aeac
  1. 6
      api/views/autocomplete.py
  2. 1
      make/init_local
  3. 5
      mamweb/static/css/mamweb.css
  4. 37
      odevzdavatko/forms.py
  5. 10
      odevzdavatko/static/odevzdavatko/dynamic_formsets_for_detail.js
  6. 84
      odevzdavatko/templates/odevzdavatko/detail.html
  7. 34
      odevzdavatko/views.py
  8. 57
      seminar/models/odevzdavatko.py
  9. 22
      seminar/models/tvorba.py
  10. 30478
      seminar/templates/seminar/jakresit/jakresit_1.svg
  11. 30365
      seminar/templates/seminar/jakresit/jakresit_2.svg
  12. 30600
      seminar/templates/seminar/jakresit/jakresit_3.svg
  13. 11
      seminar/utils.py

6
api/views/autocomplete.py

@ -37,9 +37,9 @@ class ResitelAutocomplete(LoginRequiredAjaxMixin,autocomplete.Select2QuerySetVie
query = Q()
for part in parts:
query &= (
Q(osoba__jmeno__istartswith=self.q)|
Q(osoba__prijmeni__istartswith=self.q)|
Q(osoba__prezdivka__istartswith=self.q)
Q(osoba__jmeno__istartswith=part)|
Q(osoba__prijmeni__istartswith=part)|
Q(osoba__prezdivka__istartswith=part)
)
qs = qs.filter(query)
return qs

1
make/init_local

@ -8,3 +8,4 @@ ensure_venv
./manage.py testdata
./manage.py loaddata data/*
make/sync_prod_flatpages
./manage.py load_org_permissions deploy_v2/admin_org_prava.json

5
mamweb/static/css/mamweb.css

@ -1256,6 +1256,11 @@ label[for=id_skola] {
font-weight: bold;
}
/* detail řešení */
.bodovani>input {
width: 4em;
}
/* Select2 používaný hlavně multiple selectem. Přidání checkboxů a změna barvy. */
/* Podle https://stackoverflow.com/a/48290544 */

37
odevzdavatko/forms.py

@ -21,16 +21,25 @@ class DateInput(forms.DateInput):
class PosliReseniForm(forms.Form):
#FIXME jen podproblémy daného problému
problem = forms.ModelChoiceField(label='Problém',queryset=m.Problem.objects.all())
problem = forms.ModelMultipleChoiceField(
queryset=m.Problem.objects.all(),
label="Problémy",
widget=autocomplete.ModelSelect2Multiple(
url='autocomplete_problem',
attrs={
'data-placeholder--id': '-1',
'data-placeholder--text': '---',
'data-allow-clear': 'true'
},
),
)
# to_field_name
#problem = models.ManyToManyField(Problem, verbose_name='problém', help_text='Problém',
# through='Hodnoceni')
# FIXME pridat vice resitelu
resitel = forms.ModelChoiceField(label="Řešitel",
resitel = forms.ModelMultipleChoiceField(label="Řešitelé",
queryset=Resitel.objects.all(),
widget=autocomplete.ModelSelect2(
widget=autocomplete.ModelSelect2Multiple(
url='autocomplete_resitel',
attrs = {'data-placeholder--id': '-1',
'data-placeholder--text' : '---',
@ -117,6 +126,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,
)

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-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);
})
});

84
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 %}
<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 %}

34
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)
@ -360,8 +378,8 @@ class PosliReseniView(LoginRequiredMixin, FormView):
forma=data['forma'],
poznamka=data['poznamka'],
)
nove_reseni.resitele.add(data['resitel'])
nove_reseni.problem.add(data['problem'])
nove_reseni.resitele.add(*data['resitel'])
nove_reseni.problem.add(*data['problem'])
nove_reseni.save()
context = self.get_context_data()

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 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)

22
seminar/models/tvorba.py

@ -270,17 +270,27 @@ class Cislo(SeminarModelBase):
'na adrese {} najdete nejnovější číslo.\n' \
'Vaše M&M\n'.format(odkaz)
predmet_prvni = 'Právě vyšlo 1. číslo M&M, pomoz nám ho poslat dál!'
text_mailu_prvni = 'Milý řešiteli,\n'\
'právě jsme na našem webu zveřejnili první číslo {}. ročníku, najdeš ho na tomto odkazu: {}.\n\n'\
'Doufáme, že tě M&M baví, a byli bychom rádi, kdyby mohlo dělat radost i dalším středoškolákům. Máme na tebe proto jednu prosbu. Sdílej prosím odkaz alespoň s jedním svým kamarádem, který by mohl mít o řešení M&M zájem. Je to pro nás moc důležité a velmi nám tím pomůžeš. Díky!\n\n'\
'Organizátoři M&M\n'.format(self.rocnik.rocnik, odkaz)
predmet_resitel = predmet_prvni if self.poradi == "1" else predmet
text_mailu_resitel = text_mailu_prvni if self.poradi == "1" else text_mailu
# Prijemci e-mailu
resitele_vsichni = aktivniResitele(self).filter(zasilat_cislo_emailem=True)
def posli(text, resitele):
def posli(subject, text, resitele):
emaily = map(lambda resitel: resitel.osoba.email, resitele)
if not settings.POSLI_MAILOVOU_NOTIFIKACI:
print("Poslal bych upozornění na tyto adresy: ", " ".join(emaily))
return
email = EmailMessage(
subject=predmet,
subject=subject,
body=text,
from_email=poslat_z_mailu,
bcc=list(emaily)
@ -291,12 +301,12 @@ class Cislo(SeminarModelBase):
paticka = "---\nK odběru těchto e-mailů jste se přihlásili na stránkách https://mam.matfyz.cz. Z odběru se lze odhlásit na https://mam.matfyz.cz/resitel/osobni-udaje/"
posli(text_mailu + paticka, resitele_vsichni.filter(zasilat=pm.Resitel.zasilat_cislo_papirove))
posli(text_mailu + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka,
resitele_vsichni.exclude(zasilat=pm.Resitel.zasilat_cislo_papirove))
posli(predmet_resitel, text_mailu_resitel + paticka, resitele_vsichni.filter(zasilat_cislo_papirove=False))
posli(predmet_resitel, text_mailu_resitel + 'P. S. Brzy budeme též rozesílat papírovou verzi čísla. Připomínáme, že pokud papírovou verzi čísla nevyužijete, můžete v https://mam.mff.cuni.cz/resitel/osobni-udaje/ zaškrtnout, abychom vám ji neposílali. Čísla vždy můžete nalézt v našem archivu a dál vám budou chodit e-mailem. Děkujeme.\n' + paticka,
resitele_vsichni.filter(zasilat_cislo_papirove=True))
paticka_prijemce = "---\nPokud tyto e-maily nechcete nadále dostávat, prosíme, ozvěte se nám na mam@matfyz.cz."
posli(text_mailu + paticka_prijemce, pm.Prijemce.objects.filter(zasilat_cislo_emailem=True))
posli(predmet, text_mailu + paticka_prijemce, pm.Prijemce.objects.filter(zasilat_cislo_emailem=True))
def save(self, *args, **kwargs):
super().save(*args, **kwargs)

30478
seminar/templates/seminar/jakresit/jakresit_1.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 664 KiB

30365
seminar/templates/seminar/jakresit/jakresit_2.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 689 KiB

30600
seminar/templates/seminar/jakresit/jakresit_3.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 767 KiB

11
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

Loading…
Cancel
Save