diff --git a/mo/email.py b/mo/email.py index 81e26dca0adc9e124311d2d65791dbb7d66d6bc7..7b9a99e999698caa4ec700ccb71fa79c60039f68 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 d071551859f1e49dccf48dd10ca082a41ee82a23..8a59ce62087501c0bd20ac4a0e3fbafde6bba93b 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 0000000000000000000000000000000000000000..6299a31e5d0fb7e2d78e3523f3a756716ec9d88a --- /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 a41ecdf6f2c1be4660daed2d3d861621286d7ad9..13fc7e1909d37ff9c9c30ed812f31f8e5b5a97ba 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 ba0b140e9f87cb2afad6cf47f05a4b1e95eb60d6..55d430241bbe3119dd0588ce2901719a08ace8c2 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'))