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í).
-
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“.
+
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é. Změníte-li u nějakého hodnocení toto políčko, řešitel bude upozorněn emailem, pokud si tuto možnost nevypl ve svém profilu. 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“.
Další poznámky
diff --git a/odevzdavatko/views.py b/odevzdavatko/views.py
index 2a213a2c..1b626b3d 100644
--- a/odevzdavatko/views.py
+++ b/odevzdavatko/views.py
@@ -222,6 +222,17 @@ class ReseniProblemuView(MultipleObjectTemplateResponseMixin, MultipleObjectMixi
ctx["problem_id"] = self.kwargs['problem']
return ctx
+HODNOCENI_INITIAL_DATA = [
+ "problem",
+ "body",
+ "body_celkem",
+ "body_neprepocitane",
+ "body_neprepocitane_celkem",
+ "body_max",
+ "body_neprepocitane_max",
+ "deadline_body",
+ "feedback",
+]
## XXX: https://docs.djangoproject.com/en/3.1/topics/class-based-views/mixins/#avoid-anything-more-complex
class DetailReseniView(DetailView):
""" Náhled na řešení. Editace je v :py:class:`EditReseniView`. """
@@ -232,18 +243,7 @@ class DetailReseniView(DetailView):
self.reseni = get_object_or_404(Reseni, id=self.kwargs['pk'])
result = [] # Slovníky s klíči problem, body, deadline_body -- initial data pro f.OhodnoceniReseniFormSet
for hodn in Hodnoceni.objects.filter(reseni=self.reseni):
- 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})
+ result.append({attr: getattr(hodn, attr) for attr in HODNOCENI_INITIAL_DATA})
return result
def get_context_data(self, **kw):
@@ -291,9 +291,9 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
reseni = get_object_or_404(Reseni, pk=pk)
success_url = reverse('odevzdavatko_detail_reseni', kwargs={'pk': pk})
- # FIXME: Použit initial i tady a nebastlit hodnocení tak nízkoúrovňově
- # Also: https://docs.djangoproject.com/en/2.2/topics/forms/modelforms/#django.forms.ModelForm
- formset = f.OhodnoceniReseniFormSet(request.POST)
+ formset = f.OhodnoceniReseniFormSet(request.POST, initial=[
+ {k: getattr(h, k) for k in HODNOCENI_INITIAL_DATA} for h in Hodnoceni.objects.filter(reseni=reseni)
+ ])
poznamka_form = f.PoznamkaReseniForm(request.POST, instance=reseni)
# TODO: Napsat validaci formuláře a formsetu
if not (formset.is_valid() and poznamka_form.is_valid()):
@@ -309,7 +309,9 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
qs.delete()
# Vyrobíme nová podle formsetu
+ notifikace = False
for form in formset:
+ notifikace |= 'feedback' in form.changed_data
data_for_hodnoceni = form.cleaned_data
data_for_body = data_for_hodnoceni.copy()
del(data_for_hodnoceni["body_celkem"])
@@ -320,16 +322,44 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
**form.cleaned_data,
)
logger.info(f"Creating Hodnoceni: {hodnoceni}")
+ # FIXME následující kód má velmi vysokou šanci se rozbít, vymyslet, jak to udělat jinak
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 and len(zmeny_bodu) != 2:
- # 4 znamená vše už vyplněno a nic nezměněno, 2 znamená předvyplnili se součty a nic se nezměnilo
- 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
+ if len(zmeny_bodu) != 0:
+ body_nastaveny: None | tuple[str, object] = None
+ def nastav_body(jake, na_kolik):
+ nonlocal body_nastaveny
+ if body_nastaveny is not None:
+ logger.warning(f"Hodnocení {hodnoceni} s id {hodnoceni.id} k řešení {reseni.id} mělo mít nastavené kromě {body_nastaveny[0]} na {body_nastaveny[1]} ještě další body: {jake} na {na_kolik}. Nastavuji -0.1.")
+ hodnoceni.body = -0.1
+ else:
+ body_nastaveny = (jake, na_kolik)
+ hodnoceni.__setattr__(jake, na_kolik)
+
+ for key, value in data_for_body.items():
+ if key.startswith("body") and value is not None:
+ nastav_body(key, value)
+
+ # Něco se změnilo, ale nic není nastavené = něco bylo smazáno
+ if body_nastaveny is None:
+ hodnoceni.body = None
hodnoceni.save()
+ adresati = reseni.resitele.filter(upozornovat_na_opravy_reseni=True).values_list('osoba__email', flat=True)
+ if notifikace and adresati:
+ email = EmailMessage(
+ subject='Změna hodnocení odevzdaného řešení',
+ body=f"""Milá řešitelko, milý řešiteli,
+
+došlo ke změně zpětné vazby k Tebou odevzdanému řešení. Zobrazit si ji můžeš na {reseni.resitel_url()}.
+
+Tvoji organizátoři M&M
+---
+Nechceš-li tato upozornění dostávat, můžeš si to nastavit ve svém profilu.""",
+ from_email='odevzdavatko@mam.mff.cuni.cz',
+ bcc=adresati,
+ )
+ email.send()
+
return redirect(success_url)
diff --git a/personalni/forms.py b/personalni/forms.py
index ae08a8c9..39e1b6ab 100644
--- a/personalni/forms.py
+++ b/personalni/forms.py
@@ -71,6 +71,8 @@ class UdajeForm(forms.Form):
zasilat_cislo_emailem = forms.BooleanField(label='Chci dostávat e-mailem upozornění na vydání nového čísla', required=False, initial=True)
spam = forms.BooleanField(label='Souhlasím se zasíláním propagačních materiálů od MFF UK', required=False)
+ upozornovat_na_opravy_reseni = forms.BooleanField(label='Chci dostávat emailová upozornění na změnu zpětné vazby k mým řešením', required=False, initial=True)
+
def clean_prezdivka_resitele(self):
prezdivka_resitele = self.cleaned_data.get('prezdivka_resitele')
if prezdivka_resitele == '':
diff --git a/personalni/migrations/0018_resitel_upozorneni.py b/personalni/migrations/0018_resitel_upozorneni.py
new file mode 100644
index 00000000..1b5b7280
--- /dev/null
+++ b/personalni/migrations/0018_resitel_upozorneni.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.16 on 2024-12-03 19:08
+
+from django.db import migrations, models
+
+def vypnuti_upozorneni_na_opravy_reseni(apps, schema_editor):
+ Resitel = apps.get_model('personalni', 'Resitel')
+ Resitel.objects.update(upozorneni=False)
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('personalni', '0017_odstrel_treenode_post'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='resitel',
+ name='upozorneni',
+ field=models.BooleanField(default=True, verbose_name='zasílat upozornění na změnu zpětné vazby k řešení emailem'),
+ ),
+ migrations.RunPython(vypnuti_upozorneni_na_opravy_reseni),
+ ]
diff --git a/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py b/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py
new file mode 100644
index 00000000..e5a4caaf
--- /dev/null
+++ b/personalni/migrations/0019_rename_upozorneni_resitel_upozornovat_na_opravy_reseni.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.18 on 2025-01-14 19:48
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('personalni', '0018_resitel_upozorneni'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='resitel',
+ old_name='upozorneni',
+ new_name='upozornovat_na_opravy_reseni',
+ ),
+ ]
diff --git a/personalni/models.py b/personalni/models.py
index 636b132e..e04aca0b 100644
--- a/personalni/models.py
+++ b/personalni/models.py
@@ -250,6 +250,8 @@ class Resitel(SeminarModelBase):
poznamka = models.TextField('neveřejná poznámka', blank=True,
help_text='Neveřejná poznámka k řešiteli (plain text)')
+ upozornovat_na_opravy_reseni = models.BooleanField('zasílat upozornění na změnu zpětné vazby k řešení emailem', default=True)
+
def export_row(self):
"Slovnik pro pouziti v AESOP exportu"
diff --git a/personalni/static/personalni/jak_se_dozvedeli.css b/personalni/static/personalni/jak_se_dozvedeli.css
new file mode 100644
index 00000000..15a47b80
--- /dev/null
+++ b/personalni/static/personalni/jak_se_dozvedeli.css
@@ -0,0 +1,34 @@
+.seznam {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3em;
+}
+
+.hint {
+ border: 1px solid #ccc;
+ padding: 0.3em 1em;
+ border-radius: 5px;
+ margin-bottom: 1em;
+}
+
+.osoba {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.5em;
+
+ .uno {
+ flex: 2;
+ }
+
+ .dos {
+ flex: 2;
+ }
+
+ .tres {
+ flex: 1;
+ }
+
+ .grey {
+ opacity: 0.5;
+ }
+}
diff --git a/personalni/templates/personalni/jak_se_dozvedeli.html b/personalni/templates/personalni/jak_se_dozvedeli.html
new file mode 100644
index 00000000..6695f94c
--- /dev/null
+++ b/personalni/templates/personalni/jak_se_dozvedeli.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+
+{% block custom_css %}
+{% load static %}
+
+{% endblock %}
+
+
+
+{% block content %}
+