From f04f56430dd0e1314f000094e4d7a6e20ef640ec Mon Sep 17 00:00:00 2001
From: Martin Mares <mj@ucw.cz>
Date: Sat, 15 Jan 2022 19:42:57 +0100
Subject: [PATCH] =?UTF-8?q?Job=20na=20odes=C3=ADl=C3=A1n=C3=AD=20e-mail?=
 =?UTF-8?q?=C5=AF=20o=20opraven=C3=BDch=20=C3=BAloh=C3=A1ch?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 mo/email.py           | 12 ++++++
 mo/jobs/__init__.py   |  1 +
 mo/jobs/notify.py     | 94 +++++++++++++++++++++++++++++++++++++++++++
 mo/web/org_contest.py |  8 +++-
 mo/web/org_round.py   |  9 ++++-
 5 files changed, 120 insertions(+), 4 deletions(-)
 create mode 100644 mo/jobs/notify.py

diff --git a/mo/email.py b/mo/email.py
index 81e26dca..7b9a99e9 100644
--- a/mo/email.py
+++ b/mo/email.py
@@ -163,3 +163,15 @@ def send_join_notify_email(dest: db.User, who: db.User, contest: db.Contest) ->
 
         Váš OSMO
     '''), add_footer=True)
+
+
+def send_grading_info_email(dest: db.User, round: db.Round) -> bool:
+    return send_user_email(dest, f'{round.name} kategorie {round.category} opraveno', textwrap.dedent(f'''\
+        {round.name} Matematické olympiády kategorie {round.category} bylo opraveno.
+
+        Svá opravená a obodovaná řešení najdete v odevzdávacím systému:
+
+                {config.WEB_ROOT}
+
+        Váš OSMO
+    '''), add_footer=True)
diff --git a/mo/jobs/__init__.py b/mo/jobs/__init__.py
index d0715518..8a59ce62 100644
--- a/mo/jobs/__init__.py
+++ b/mo/jobs/__init__.py
@@ -217,5 +217,6 @@ def job_handler(type: db.JobType):
 
 
 # Moduly implementující jednotlivé typy jobů
+import mo.jobs.notify
 import mo.jobs.protocols
 import mo.jobs.submit
diff --git a/mo/jobs/notify.py b/mo/jobs/notify.py
new file mode 100644
index 00000000..6299a31e
--- /dev/null
+++ b/mo/jobs/notify.py
@@ -0,0 +1,94 @@
+# Implementace jobů na posílání notifikací
+
+from sqlalchemy import and_
+from typing import Optional
+
+import mo.db as db
+import mo.email
+from mo.jobs import TheJob, job_handler
+from mo.util_format import inflect_number
+
+
+#
+# Job send_grading_info: Pošle upozornění na opravené úlohy
+#
+# Vstupní JSON:
+#        { 'round_id': id_kola,
+#          'contest_id': id_soutěže,    // může být null
+#        }
+#
+# Výstupní JSON:
+#        null
+#
+
+
+def schedule_send_grading_info(round: db.Round, contest: Optional[db.Contest], for_user: db.User) -> None:
+    the_job = TheJob()
+    job = the_job.create(db.JobType.send_grading_info, for_user)
+    job.description = 'E-maily účastníkům kola ' + round.round_code_short()
+    job.in_json = {
+        'round_id': round.round_id,
+        'contest_id': contest.contest_id if contest else None,
+    }
+    the_job.submit()
+
+
+@job_handler(db.JobType.send_grading_info)
+def handle_send_grading_info(the_job: TheJob):
+    job = the_job.job
+    assert job.in_json is not None
+    round_id: int = job.in_json['round_id']  # type: ignore
+    contest_id: int = job.in_json['contest_id']  # type: ignore
+
+    sess = db.get_session()
+    round = sess.query(db.Round).get(round_id)
+    if not round:
+        the_job.error('Kolo nebylo nalezeno')
+        return
+
+    email_key = f'graded:{round_id}'
+    todo = (sess.query(db.User, db.SentEmail)
+            .select_from(db.Contest)
+            .filter(db.Contest.round_id == round.master_round_id))
+    if contest_id is not None:
+        todo = todo.filter(db.Contest.contest_id == contest_id)
+    todo = (todo
+            .join(db.Participation, db.Participation.contest_id == db.Contest.contest_id)
+            .join(db.User, db.User.user_id == db.Participation.user_id)
+            .outerjoin(db.SentEmail, and_(db.SentEmail.user_id == db.User.user_id, db.SentEmail.key == email_key))
+            .all())
+
+    # Podobně jako u importů i zde budeme vytvářet malé transakce
+    sess.commit()
+
+    num_total = 0
+    num_sent = 0
+    num_before = 0
+    num_dont_want = 0
+
+    for user, sent in todo:
+        num_total += 1
+        if not user.email_notify:
+            num_dont_want += 1
+        elif sent:
+            num_before += 1
+        else:
+            # Musíme se zeptat znovu, protože to mezitím mohl poslat jiný proces.
+            sent = sess.query(db.SentEmail).filter_by(user=user, key=email_key).with_for_update().one_or_none()
+            if sent:
+                num_before += 1
+            else:
+                mo.email.send_grading_info_email(user, round)
+                sent = db.SentEmail(user=user, key=email_key)
+                sess.add(sent)
+                num_sent += 1
+        sess.commit()
+
+    stats = []
+    if num_sent > 0:
+        stats.append(inflect_number(num_sent, 'odeslána', 'odeslány', 'odesláno'))
+    if num_before > 0:
+        stats.append(inflect_number(num_before, 'odeslána', 'odeslány', 'odesláno') + ' dříve')
+    if num_dont_want > 0:
+        stats.append(inflect_number(num_dont_want, 'účastník nechce', 'účastníci nechtějí', 'účastníků nechce'))
+    job.result = 'Celkem ' + inflect_number(num_total, 'zpráva', 'zprávy', 'zpráv') + ': ' + ', '.join(stats) + '.'
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index a41ecdf6..13fc7e19 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -1606,9 +1606,13 @@ def org_contest_edit(ct_id: int):
             )
 
             sess.commit()
-            flash('Změny soutěže uloženy', 'success')
+            flash('Změny soutěže uloženy.', 'success')
+
+            if 'state' in changes and contest.state == db.RoundState.graded:
+                mo.jobs.notify.schedule_send_grading_info(round, contest, g.user)
+                flash('Založena dávka na rozeslání e-mailů účastníkům.', 'success')
         else:
-            flash('Žádné změny k uložení', 'info')
+            flash('Žádné změny k uložení.', 'info')
 
         return redirect(ctx.url_for('org_contest'))
 
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index ba0b140e..55d43024 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -18,6 +18,7 @@ from wtforms.widgets.html5 import NumberInput
 import mo.config as config
 import mo.db as db
 import mo.imports
+import mo.jobs.notify
 from mo.rights import Right
 import mo.util
 from mo.util_format import inflect_with_number
@@ -466,9 +467,13 @@ def org_round_edit(round_id: int):
                     )
 
             sess.commit()
-            flash('Změny kola uloženy', 'success')
+            flash('Změny kola uloženy.', 'success')
+
+            if 'state' in changes and round.state == db.RoundState.graded:
+                mo.jobs.notify.schedule_send_grading_info(round, None, g.user)
+                flash('Založena dávka na rozeslání e-mailů účastníkům.', 'success')
         else:
-            flash('Žádné změny k uložení', 'info')
+            flash('Žádné změny k uložení.', 'info')
 
         return redirect(ctx.url_for('org_round'))
 
-- 
GitLab