From 793a42ff13081dd038b16ccfbc269f0648590306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 23 Feb 2024 14:52:50 +0100 Subject: [PATCH] [IMP] event: allow to send registrations emails asynchronously This commit introduces a new configuration parameter 'event.event_mail_async' forcing registrations-based communication to be asynchronous. Instead of directly sending communication it triggers the cron to be run as soon as possible. When having large volume of registrations, and especially concurrent registrations it saves a DB to avoid generating tickets and preparing emails synchronously to the registration creation. Task-3764894: Event: Allow using cron triggers for communication Part of Task-3084943: Event: Improve communication scheduler scalability Part-of: odoo/odoo#155777 --- addons/event/models/event_registration.py | 13 ++- addons/event/tests/common.py | 53 +++++++++++ .../event/tests/test_event_mail_schedule.py | 91 ++++++++++++++----- addons/event_sms/tests/test_sms_schedule.py | 3 + 4 files changed, 132 insertions(+), 28 deletions(-) diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index c7537e476da..df073e59c02 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -308,10 +308,15 @@ class EventRegistration(models.Model): return onsubscribe_schedulers.update({'mail_done': False}) - # we could simply call _create_missing_mail_registrations and let cron do their job - # but it currently leads to several delays. We therefore call execute until - # cron triggers are correctly used - onsubscribe_schedulers.with_user(SUPERUSER_ID).execute() + # either trigger the cron, either run schedulers immediately (scaling choice) + async_scheduler = self.env['ir.config_parameter'].sudo().get_param('event.event_mail_async') + if async_scheduler: + self.env.ref('event.event_mail_scheduler')._trigger() + else: + # we could simply call _create_missing_mail_registrations and let cron do their job + # but it currently leads to several delays. We therefore call execute until + # cron triggers are correctly used + onsubscribe_schedulers.with_user(SUPERUSER_ID).execute() # ------------------------------------------------------------ # MAILING / GATEWAY diff --git a/addons/event/tests/common.py b/addons/event/tests/common.py index 1122af2381f..a7fbac4fef9 100644 --- a/addons/event/tests/common.py +++ b/addons/event/tests/common.py @@ -104,3 +104,56 @@ class EventCase(common.TransactionCase): 'phone': f'04560000{idx}{idx}', } for idx in range(0, reg_count)]) return registrations + + @classmethod + def _setup_test_reports(cls): + cls.test_report_view = cls.env["ir.ui.view"].create({ + "arch_db": """ + + + +
+

This is a sample of an external report.

+
+
+
+
""", + "key": "event_registration_test_report", + "name": "event_registration_test_report", + "type": "qweb", + }) + cls.env["ir.model.data"].create({ + "model": "ir.ui.view", + "module": "event", + "name": "event_registration_test_report", + "res_id": cls.test_report_view.id, + }) + + cls.test_report_action = cls.env['ir.actions.report'].create({ + 'name': 'Test Report on event.registration', + 'model': 'event.registration', + 'print_report_name': "f'TestReport for {object.name}'", + 'report_type': 'qweb-pdf', + 'report_name': 'event.event_registration_test_report', + }) + + cls.template_subscription = cls.env['mail.template'].create({ + "body_html": """
Hello your registration to is confirmed.
""", + "email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}", + "email_to": """{{ (object.email and '"%s" <%s>' % (object.name, object.email)) or object.partner_id.email_formatted or '' }}""", + "lang": "{{ object.event_id.lang or object.partner_id.lang }}", + "model_id": cls.env['ir.model']._get_id("event.registration"), + "name": "Event: Registration Confirmation TEST", + "subject": "Confirmation for {{ object.event_id.name }}", + "report_template_ids": [(4, cls.test_report_action.id)], + }) + cls.template_reminder = cls.env['mail.template'].create({ + "body_html": """
Hello this is a reminder for your registration to .
""", + "email_from": "{{ (object.event_id.organizer_id.email_formatted or object.event_id.user_id.email_formatted or '') }}", + "email_to": """{{ (object.email and '"%s" <%s>' % (object.name, object.email)) or object.partner_id.email_formatted or '' }}""", + "lang": "{{ object.event_id.lang or object.partner_id.lang }}", + "model_id": cls.env['ir.model']._get_id("event.registration"), + "name": "Event: Registration Reminder TEST", + "subject": "Reminder for {{ object.event_id.name }}: {{ object.get_date_range_str() }}", + "report_template_ids": [(4, cls.test_report_action.id)], + }) diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 511bfcc477b..f448e9b6003 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta from freezegun import freeze_time from odoo import Command +from odoo.addons.base.tests.test_ir_cron import CronMixinCase from odoo.addons.event.tests.common import EventCase from odoo.addons.mail.tests.common import MockEmail from odoo.tests import tagged, users @@ -13,7 +14,7 @@ from odoo.tools import formataddr, mute_logger @tagged('event_mail', 'post_install', '-at_install') -class TestMailSchedule(EventCase, MockEmail): +class TestMailSchedule(EventCase, MockEmail, CronMixinCase): @classmethod def setUpClass(cls): @@ -27,6 +28,8 @@ class TestMailSchedule(EventCase, MockEmail): # deactivate other schedulers to avoid messing with crons cls.env['event.mail'].search([]).unlink() + # consider asynchronous sending as default sending + cls.env["ir.config_parameter"].set_param("event.event_mail_async", False) # freeze some datetimes, and ensure more than 1D+1H before event starts # to ease time-based scheduler check @@ -37,9 +40,7 @@ class TestMailSchedule(EventCase, MockEmail): cls.event_date_begin = datetime(2021, 3, 22, 8, 0, 0) cls.event_date_end = datetime(2021, 3, 24, 18, 0, 0) - cls.template_subscription_id = cls.env['ir.model.data']._xmlid_to_res_id('event.event_subscription') - cls.template_reminder_id = cls.env['ir.model.data']._xmlid_to_res_id('event.event_reminder') - + cls._setup_test_reports() with cls.mock_datetime_and_now(cls, cls.reference_now): # create with admin to force create_date cls.test_event = cls.env['event.event'].create({ @@ -52,28 +53,28 @@ class TestMailSchedule(EventCase, MockEmail): 'interval_unit': 'now', 'interval_type': 'after_sub', 'notification_type': 'mail', - 'template_ref': f'mail.template,{cls.template_subscription_id}', + 'template_ref': f'mail.template,{cls.template_subscription.id}', }), (0, 0, { # one hour after subscription 'interval_nbr': 1, 'interval_unit': 'hours', 'interval_type': 'after_sub', 'notification_type': 'mail', - 'template_ref': f'mail.template,{cls.template_subscription_id}', + 'template_ref': f'mail.template,{cls.template_subscription.id}', }), (0, 0, { # 1 days before event 'interval_nbr': 1, 'interval_unit': 'days', 'interval_type': 'before_event', 'notification_type': 'mail', - 'template_ref': f'mail.template,{cls.template_reminder_id}', + 'template_ref': f'mail.template,{cls.template_reminder.id}', }), (0, 0, { # immediately after event 'interval_nbr': 1, 'interval_unit': 'hours', 'interval_type': 'after_event', 'notification_type': 'mail', - 'template_ref': f'mail.template,{cls.template_reminder_id}', + 'template_ref': f'mail.template,{cls.template_reminder.id}', }), ] }) @@ -160,9 +161,10 @@ class TestMailSchedule(EventCase, MockEmail): [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], 'outgoing', content=None, - fields_values={'subject': 'Your registration at %s' % test_event.name, - 'email_from': self.user_eventmanager.company_id.email_formatted, - }) + fields_values={ + 'email_from': self.user_eventmanager.company_id.email_formatted, + 'subject': f'Confirmation for {test_event.name}', + }) # same for second scheduler: scheduled but not sent self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)') @@ -198,9 +200,10 @@ class TestMailSchedule(EventCase, MockEmail): [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], 'outgoing', content=None, - fields_values={'subject': 'Your registration at %s' % test_event.name, - 'email_from': self.user_eventmanager.company_id.email_formatted, - }) + fields_values={ + 'email_from': self.user_eventmanager.company_id.email_formatted, + 'subject': f'Confirmation for {test_event.name}', + }) # PRE SCHEDULERS (MOVE FORWARD IN TIME) # -------------------------------------------------- @@ -233,9 +236,10 @@ class TestMailSchedule(EventCase, MockEmail): [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], 'outgoing', content=None, - fields_values={'subject': '%s: tomorrow' % test_event.name, - 'email_from': self.user_eventmanager.company_id.email_formatted, - }) + fields_values={ + 'email_from': self.user_eventmanager.company_id.email_formatted, + 'subject': f'Reminder for {test_event.name}: tomorrow', + }) # NEW REGISTRATION EFFECT ON SCHEDULERS # -------------------------------------------------- @@ -283,7 +287,7 @@ class TestMailSchedule(EventCase, MockEmail): # manual check because 2 identical mails are sent and mail tools do not support it easily for mail in self._new_mails: self.assertEqual(mail.email_from, self.user_eventmanager.company_id.email_formatted) - self.assertEqual(mail.subject, 'Your registration at %s' % test_event.name) + self.assertEqual(mail.subject, f'Confirmation for {test_event.name}') self.assertEqual(mail.state, 'outgoing') self.assertEqual(mail.email_to, formataddr((reg3.name, reg3.email))) @@ -308,9 +312,10 @@ class TestMailSchedule(EventCase, MockEmail): [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))], 'outgoing', content=None, - fields_values={'subject': '%s: today' % test_event.name, - 'email_from': self.user_eventmanager.company_id.email_formatted, - }) + fields_values={ + 'email_from': self.user_eventmanager.company_id.email_formatted, + 'subject': f"Reminder for {test_event.name}: today", + }) @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') @users('user_eventmanager') @@ -327,8 +332,8 @@ class TestMailSchedule(EventCase, MockEmail): # consider having hanging registrations, still not processed (e.g. adding # a new scheduler after) self.env.invalidate_all() - # com 58, event 36 - with self.assertQueryCount(61), self.mock_datetime_and_now(reference_now), \ + # com 59, event 37 + with self.assertQueryCount(62), self.mock_datetime_and_now(reference_now), \ self.mock_mail_gateway(): _existing = self.env['event.registration'].create([ { @@ -347,7 +352,7 @@ class TestMailSchedule(EventCase, MockEmail): 'interval_unit': 'now', 'interval_type': 'after_sub', 'notification_type': 'mail', - 'template_ref': f'mail.template,{self.template_subscription_id}', + 'template_ref': f'mail.template,{self.template_subscription.id}', }), ]}) self.env.invalidate_all() @@ -369,6 +374,44 @@ class TestMailSchedule(EventCase, MockEmail): self.assertEqual(self.mail_mail_create_mocked.call_count, 7, 'EventMail: TODO: should create one mail / new registration') + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') + @users('user_eventmanager') + def test_event_mail_schedule_on_subscription_async(self): + """ Async mode for schedulers activated, should not send communication + in the same transaction. """ + test_event = self.test_event.with_env(self.env) + cron = self.env.ref('event.event_mail_scheduler') + reference_now = self.reference_now + + self.env['ir.config_parameter'].sudo().set_param('event.event_mail_async', True) + with self.capture_triggers(cron.id) as capt, \ + self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \ + self.mock_mail_gateway(): + existing = self.env['event.registration'].create([ + { + 'email': f'new.async.attendee.{idx}@test.example.com', + 'event_id': test_event.id, + 'name': f'New Async Attendee {idx}', + } for idx in range(5) + ]) + self.assertEqual(len(self._new_mails), 0) + self.assertEqual(self.mail_mail_create_mocked.call_count, 0) + capt.records.ensure_one() + self.assertEqual(capt.records.call_at, reference_now.replace(microsecond=0) + relativedelta(minutes=10)) + + # run cron: emails should be send for registrations + with self.mock_datetime_and_now(reference_now + relativedelta(minutes=10)), \ + self.mock_mail_gateway(): + cron.sudo().method_direct_trigger() + self.assertMailMailWEmails( + [formataddr((reg.name, reg.email)) for reg in existing], + "outgoing", + content=f"Hello your registration to {test_event.name} is confirmed", + fields_values={ + 'email_from': self.user_eventmanager.company_id.email_formatted, + 'subject': f'Confirmation for {test_event.name}', + }) + @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_unique_event_mail_ids(self): # create event with default event_mail_ids lines diff --git a/addons/event_sms/tests/test_sms_schedule.py b/addons/event_sms/tests/test_sms_schedule.py index 4b68c01e118..f27a880a63c 100644 --- a/addons/event_sms/tests/test_sms_schedule.py +++ b/addons/event_sms/tests/test_sms_schedule.py @@ -17,6 +17,9 @@ class TestSMSSchedule(EventCase, SMSCase): def setUpClass(cls): super(TestSMSSchedule, cls).setUpClass() + # consider asynchronous sending as default sending + cls.env["ir.config_parameter"].set_param("event.event_mail_async", False) + cls.sms_template_sub = cls.env['sms.template'].create({ 'name': 'Test subscription', 'model_id': cls.env.ref('event.model_event_registration').id,