odevzdavatko: odesílání emailu řešiteli při změně zpětné vazby #83

Merged
zelvuska merged 4 commits from notifikace-zpetne-vazby into master 2025-01-21 18:10:49 +01:00
9 changed files with 83 additions and 16 deletions

View file

@ -65,6 +65,9 @@ class Reseni(SeminarModelBase):
def absolute_url(self):
return "https://" + str(get_current_site(None)) + self.verejne_url()
def resitel_url(self):
return f'https://{get_current_site(None)}{reverse_lazy("odevzdavatko_resitel_reseni", args=[self.id])}'
# má OneToOneField s:
# Konfera

View file

@ -191,7 +191,7 @@ Sloupce:
</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>
<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é. 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 <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

View file

@ -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"])
@ -330,6 +332,22 @@ def hodnoceniReseniView(request, pk, *args, **kwargs):
hodnoceni.body = -0.1
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)

View file

@ -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 == '':

View file

@ -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(
karelb marked this conversation as resolved
Review

Za mě spíš default=False, je to změna chování webu a těch mailů potenciálně může být docela hodně… Ale je to asi otázka spíš mého osobního názoru (věci nemají random měnit chování a vývojáři nemají diktovat uživatelům, co chtějí) než něčeho striktního, klidně mě přehlasujte…

Za mě spíš `default=False`, je to změna chování webu a těch mailů potenciálně může být docela hodně… Ale je to asi otázka spíš mého osobního názoru (věci nemají random měnit chování a vývojáři nemají diktovat uživatelům, co chtějí) než něčeho striktního, klidně mě přehlasujte…
Review

Za mě default=True.

Klidně tím způsobem, že v migraci je všechny nastavíme na False a s příštím číslem (a na discordu a v novince) pošleme informaci.

Za mě `default=True`. Klidně tím způsobem, že v migraci je všechny nastavíme na False a s příštím číslem (a na discordu a v novince) pošleme informaci.
Review

Však taky komentuji jen migraci a ne formulář – myslím, že máme na mysli to samé :-)

Však taky komentuji jen migraci a ne formulář – myslím, že máme na mysli to samé :-)
Review

Klidně tím způsobem, že v migraci je všechny nastavíme na False a s příštím číslem (a na discordu a v novince) pošleme informaci.

Jakože by to bylo True pro nové řešitele ale pro již existující False?

> Klidně tím způsobem, že v migraci je všechny nastavíme na False a s příštím číslem (a na discordu a v novince) pošleme informaci. Jakože by to bylo `True` pro nové řešitele ale pro již existující `False`?
Review

Přesně tak :)

Ale podle mě to nejde udělat tím, že v migraci napíš default=False (to dle mého změní default tabulky, tedy django pořád bude chtít vyrobit další migraci), nebo ne?

Je pravda, že pak můžu vyrobit druhou migraci, kterou se to vyřeší…

Přesně tak :) Ale podle mě to nejde udělat tím, že v migraci napíš `default=False` (to dle mého změní default tabulky, tedy django pořád bude chtít vyrobit další migraci), nebo ne? Je pravda, že pak můžu vyrobit druhou migraci, kterou se to vyřeší…
Review

Tak do tabulky se tak jak tak uloží ten boolean, který přijde ve formuláři, tedy pokud má formulář initial=True (což iirc má), tak se tady default použije jen pro stávající řádky tabulky v rámci migrace (a ano, možná tam někde bude poznamenaný, ale nikdy se nepoužije…).

Tak do tabulky se tak jak tak uloží ten boolean, který přijde ve formuláři, tedy pokud má formulář `initial=True` (což iirc má), tak se tady `default` použije jen pro stávající řádky tabulky v rámci migrace (a ano, možná tam někde bude poznamenaný, ale nikdy se nepoužije…).
Review

Pokud chceme být hodně explicitní, tak můžeme nastavit default=True a pak napsat něco typu Resitel.objects.update(notifikace_o_zpetne_vazbe=False), ale to mi přijde zbytečně překomplikované…

Pokud chceme být hodně explicitní, tak můžeme nastavit `default=True` a pak napsat něco typu `Resitel.objects.update(notifikace_o_zpetne_vazbe=False)`, ale to mi přijde zbytečně překomplikované…
Review

A můj přístup by byl z models.py default zrušit úplně a naopak nastavit null=False. To vynutí, že ta tabulka bude vždycky chtít explicitní nastavení při přidání řádku a default v migraci se použije jen pro tu migraci… (Speciálně: teď nevím, jestli staré řádky náhodou v DB nebudou mít NULL místo False, ale čekám, že se ten default spíš projeví.)

A můj přístup by byl z `models.py` `default` zrušit úplně a naopak nastavit `null=False`. To vynutí, že ta tabulka bude vždycky chtít explicitní nastavení při přidání řádku a `default` v migraci se použije jen pro tu migraci… (Speciálně: teď nevím, jestli staré řádky náhodou v DB nebudou mít NULL místo False, ale čekám, že se ten default spíš projeví.)
Review

Zvolil jsem variantu s default=True a změnou existujících řádků v rámci migrace právě proto, že mi přijde nejvíce explicitní.

Zvolil jsem variantu s `default=True` a změnou existujících řádků v rámci migrace právě proto, že mi přijde nejvíce explicitní.
Review

Mně se to líbí. LEdo?

Mně se to líbí. LEdo?
Review

LEdo lgtm…

LEdo lgtm…
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),
]

View file

@ -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',
),
]

View file

@ -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"

View file

@ -51,6 +51,7 @@
</h4>
<table class="form">
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_emailem %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.upozornovat_na_opravy_reseni %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat_cislo_papirove %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.spam %}
{% include "personalni/udaje/prihlaska_field.html" with field=form.zasilat %}

View file

@ -230,6 +230,7 @@ def resitelEditView(request):
resitel_edit.zasilat = fcd['zasilat']
resitel_edit.zasilat_cislo_emailem = fcd['zasilat_cislo_emailem']
resitel_edit.zasilat_cislo_papirove = fcd['zasilat_cislo_papirove']
resitel_edit.upozornovat_na_opravy_reseni = fcd['upozornovat_na_opravy_reseni']
if fcd.get('skola'):
resitel_edit.skola = fcd['skola']
else: