You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

252 lines
11 KiB

privacy_consent: Separate automated emails send process Before https://github.com/OCA/data-protection/pull/29 there was a race condition where an email could be sent while the same transaction that created the `privacy.consent` record still wasn't committed, producing a 404 error if the user clicked on "Accept" or "Reject" before all mails were sent. To avoid that, a raw `cr.commit()` was issued, but this produced another situation where the user had to wait until the full email queue is cleared to get his page loaded. It wasn't an error, but a long queue meant several minutes waiting, and it's ulikely that an average human is so patient. So, here's the final fix (I hope!). The main problem was that I was looking in the wrong place to send the email. It turns out that the `self.post_message_with_template()` method is absolutely helpless in the case at hand, where these criteria must be met: * E-mail must be enqueued, no matter if there are less or more than 50 consents to send. * The template must be processed per record. * In an ideal world, a `cr.commit()` must be issued after each sent mail. The metod that was being used: * Didn't allow to use `auto_commit` mode. * Only allowed to render the template per record if called with `composition_mode="mass_mail"`. * Only allowed to enqueue emails if called with `composition_mode="mass_post"`. Obviously, I cannot set 2 different values for `composition_mode`, so a different strategy had to be used. I discovered that the `mail.template` model has a helpful method called `send_mail()` that, by default: * Renders the template per record * Enqueues the email * The email queue is cleared in `auto_commit=True` mode. So, from now on, problems are gone: * The user click, or the cron run, will just generate the missing `privacy.consent` records and enqueue mails for them. * The mail queue manager will send them later, in `auto_commit` mode. * After sending the e-mail, this module will set the `privacy.consent` record as `sent`. * Thanks to *not* sending the email, the process the user faces when he hits the "generate" button is faster. * Instructions in the README and text in the "generate" button are updated to reflect this new behavior. * Thanks to the `auto_commit` feature, if Odoo is rebooted in the middle of a mail queue clearance, the records that were sent remain properly marked as sent, and the missing mails will be sent after the next boot. * No hardcoded commits. * No locked transactions. * BTW I discovered that 2 different emails were created when creating a new consent. I started using `mail_create_nolog=True` to avoid that problem and only log a single creation message. Note to self: never use again `post_message_with_template()`.
5 years ago
privacy_consent: Separate automated emails send process Before https://github.com/OCA/data-protection/pull/29 there was a race condition where an email could be sent while the same transaction that created the `privacy.consent` record still wasn't committed, producing a 404 error if the user clicked on "Accept" or "Reject" before all mails were sent. To avoid that, a raw `cr.commit()` was issued, but this produced another situation where the user had to wait until the full email queue is cleared to get his page loaded. It wasn't an error, but a long queue meant several minutes waiting, and it's ulikely that an average human is so patient. So, here's the final fix (I hope!). The main problem was that I was looking in the wrong place to send the email. It turns out that the `self.post_message_with_template()` method is absolutely helpless in the case at hand, where these criteria must be met: * E-mail must be enqueued, no matter if there are less or more than 50 consents to send. * The template must be processed per record. * In an ideal world, a `cr.commit()` must be issued after each sent mail. The metod that was being used: * Didn't allow to use `auto_commit` mode. * Only allowed to render the template per record if called with `composition_mode="mass_mail"`. * Only allowed to enqueue emails if called with `composition_mode="mass_post"`. Obviously, I cannot set 2 different values for `composition_mode`, so a different strategy had to be used. I discovered that the `mail.template` model has a helpful method called `send_mail()` that, by default: * Renders the template per record * Enqueues the email * The email queue is cleared in `auto_commit=True` mode. So, from now on, problems are gone: * The user click, or the cron run, will just generate the missing `privacy.consent` records and enqueue mails for them. * The mail queue manager will send them later, in `auto_commit` mode. * After sending the e-mail, this module will set the `privacy.consent` record as `sent`. * Thanks to *not* sending the email, the process the user faces when he hits the "generate" button is faster. * Instructions in the README and text in the "generate" button are updated to reflect this new behavior. * Thanks to the `auto_commit` feature, if Odoo is rebooted in the middle of a mail queue clearance, the records that were sent remain properly marked as sent, and the missing mails will be sent after the next boot. * No hardcoded commits. * No locked transactions. * BTW I discovered that 2 different emails were created when creating a new consent. I started using `mail_create_nolog=True` to avoid that problem and only log a single creation message. Note to self: never use again `post_message_with_template()`.
5 years ago
privacy_consent: Separate automated emails send process Before https://github.com/OCA/data-protection/pull/29 there was a race condition where an email could be sent while the same transaction that created the `privacy.consent` record still wasn't committed, producing a 404 error if the user clicked on "Accept" or "Reject" before all mails were sent. To avoid that, a raw `cr.commit()` was issued, but this produced another situation where the user had to wait until the full email queue is cleared to get his page loaded. It wasn't an error, but a long queue meant several minutes waiting, and it's ulikely that an average human is so patient. So, here's the final fix (I hope!). The main problem was that I was looking in the wrong place to send the email. It turns out that the `self.post_message_with_template()` method is absolutely helpless in the case at hand, where these criteria must be met: * E-mail must be enqueued, no matter if there are less or more than 50 consents to send. * The template must be processed per record. * In an ideal world, a `cr.commit()` must be issued after each sent mail. The metod that was being used: * Didn't allow to use `auto_commit` mode. * Only allowed to render the template per record if called with `composition_mode="mass_mail"`. * Only allowed to enqueue emails if called with `composition_mode="mass_post"`. Obviously, I cannot set 2 different values for `composition_mode`, so a different strategy had to be used. I discovered that the `mail.template` model has a helpful method called `send_mail()` that, by default: * Renders the template per record * Enqueues the email * The email queue is cleared in `auto_commit=True` mode. So, from now on, problems are gone: * The user click, or the cron run, will just generate the missing `privacy.consent` records and enqueue mails for them. * The mail queue manager will send them later, in `auto_commit` mode. * After sending the e-mail, this module will set the `privacy.consent` record as `sent`. * Thanks to *not* sending the email, the process the user faces when he hits the "generate" button is faster. * Instructions in the README and text in the "generate" button are updated to reflect this new behavior. * Thanks to the `auto_commit` feature, if Odoo is rebooted in the middle of a mail queue clearance, the records that were sent remain properly marked as sent, and the missing mails will be sent after the next boot. * No hardcoded commits. * No locked transactions. * BTW I discovered that 2 different emails were created when creating a new consent. I started using `mail_create_nolog=True` to avoid that problem and only log a single creation message. Note to self: never use again `post_message_with_template()`.
5 years ago
privacy_consent: Separate automated emails send process Before https://github.com/OCA/data-protection/pull/29 there was a race condition where an email could be sent while the same transaction that created the `privacy.consent` record still wasn't committed, producing a 404 error if the user clicked on "Accept" or "Reject" before all mails were sent. To avoid that, a raw `cr.commit()` was issued, but this produced another situation where the user had to wait until the full email queue is cleared to get his page loaded. It wasn't an error, but a long queue meant several minutes waiting, and it's ulikely that an average human is so patient. So, here's the final fix (I hope!). The main problem was that I was looking in the wrong place to send the email. It turns out that the `self.post_message_with_template()` method is absolutely helpless in the case at hand, where these criteria must be met: * E-mail must be enqueued, no matter if there are less or more than 50 consents to send. * The template must be processed per record. * In an ideal world, a `cr.commit()` must be issued after each sent mail. The metod that was being used: * Didn't allow to use `auto_commit` mode. * Only allowed to render the template per record if called with `composition_mode="mass_mail"`. * Only allowed to enqueue emails if called with `composition_mode="mass_post"`. Obviously, I cannot set 2 different values for `composition_mode`, so a different strategy had to be used. I discovered that the `mail.template` model has a helpful method called `send_mail()` that, by default: * Renders the template per record * Enqueues the email * The email queue is cleared in `auto_commit=True` mode. So, from now on, problems are gone: * The user click, or the cron run, will just generate the missing `privacy.consent` records and enqueue mails for them. * The mail queue manager will send them later, in `auto_commit` mode. * After sending the e-mail, this module will set the `privacy.consent` record as `sent`. * Thanks to *not* sending the email, the process the user faces when he hits the "generate" button is faster. * Instructions in the README and text in the "generate" button are updated to reflect this new behavior. * Thanks to the `auto_commit` feature, if Odoo is rebooted in the middle of a mail queue clearance, the records that were sent remain properly marked as sent, and the missing mails will be sent after the next boot. * No hardcoded commits. * No locked transactions. * BTW I discovered that 2 different emails were created when creating a new consent. I started using `mail_create_nolog=True` to avoid that problem and only log a single creation message. Note to self: never use again `post_message_with_template()`.
5 years ago
privacy_consent: Separate automated emails send process Before https://github.com/OCA/data-protection/pull/29 there was a race condition where an email could be sent while the same transaction that created the `privacy.consent` record still wasn't committed, producing a 404 error if the user clicked on "Accept" or "Reject" before all mails were sent. To avoid that, a raw `cr.commit()` was issued, but this produced another situation where the user had to wait until the full email queue is cleared to get his page loaded. It wasn't an error, but a long queue meant several minutes waiting, and it's ulikely that an average human is so patient. So, here's the final fix (I hope!). The main problem was that I was looking in the wrong place to send the email. It turns out that the `self.post_message_with_template()` method is absolutely helpless in the case at hand, where these criteria must be met: * E-mail must be enqueued, no matter if there are less or more than 50 consents to send. * The template must be processed per record. * In an ideal world, a `cr.commit()` must be issued after each sent mail. The metod that was being used: * Didn't allow to use `auto_commit` mode. * Only allowed to render the template per record if called with `composition_mode="mass_mail"`. * Only allowed to enqueue emails if called with `composition_mode="mass_post"`. Obviously, I cannot set 2 different values for `composition_mode`, so a different strategy had to be used. I discovered that the `mail.template` model has a helpful method called `send_mail()` that, by default: * Renders the template per record * Enqueues the email * The email queue is cleared in `auto_commit=True` mode. So, from now on, problems are gone: * The user click, or the cron run, will just generate the missing `privacy.consent` records and enqueue mails for them. * The mail queue manager will send them later, in `auto_commit` mode. * After sending the e-mail, this module will set the `privacy.consent` record as `sent`. * Thanks to *not* sending the email, the process the user faces when he hits the "generate" button is faster. * Instructions in the README and text in the "generate" button are updated to reflect this new behavior. * Thanks to the `auto_commit` feature, if Odoo is rebooted in the middle of a mail queue clearance, the records that were sent remain properly marked as sent, and the missing mails will be sent after the next boot. * No hardcoded commits. * No locked transactions. * BTW I discovered that 2 different emails were created when creating a new consent. I started using `mail_create_nolog=True` to avoid that problem and only log a single creation message. Note to self: never use again `post_message_with_template()`.
5 years ago
  1. # Copyright 2018 Tecnativa - Jairo Llopis
  2. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  3. from contextlib import contextmanager
  4. from odoo.exceptions import ValidationError
  5. from odoo.tests.common import HttpCase
  6. class ActivityCase(HttpCase):
  7. def setUp(self):
  8. super(ActivityCase, self).setUp()
  9. # HACK https://github.com/odoo/odoo/issues/12237
  10. # TODO Remove hack in v12
  11. self._oldenv = self.env
  12. self.env = self._oldenv(self.cursor())
  13. # HACK end
  14. self.cron = self.env.ref("privacy_consent.cron_auto_consent")
  15. self.cron_mail_queue = self.env.ref(
  16. "mail.ir_cron_mail_scheduler_action")
  17. self.update_opt_out = self.env.ref("privacy_consent.update_opt_out")
  18. self.mt_consent_consent_new = self.env.ref(
  19. "privacy_consent.mt_consent_consent_new")
  20. self.mt_consent_acceptance_changed = self.env.ref(
  21. "privacy_consent.mt_consent_acceptance_changed")
  22. self.mt_consent_state_changed = self.env.ref(
  23. "privacy_consent.mt_consent_state_changed")
  24. # Some partners to ask for consent
  25. self.partners = self.env["res.partner"]
  26. self.partners += self.partners.create({
  27. "name": "consent-partner-0",
  28. "email": "partner0@example.com",
  29. "notify_email": "none",
  30. "opt_out": False,
  31. })
  32. self.partners += self.partners.create({
  33. "name": "consent-partner-1",
  34. "email": "partner1@example.com",
  35. "notify_email": "always",
  36. "opt_out": True,
  37. })
  38. self.partners += self.partners.create({
  39. "name": "consent-partner-2",
  40. "email": "partner2@example.com",
  41. "opt_out": False,
  42. })
  43. # Partner without email, on purpose
  44. self.partners += self.partners.create({
  45. "name": "consent-partner-3",
  46. "opt_out": True,
  47. })
  48. # Activity without consent
  49. self.activity_noconsent = self.env["privacy.activity"].create({
  50. "name": "activity_noconsent",
  51. "description": "I'm activity 1",
  52. })
  53. # Activity with auto consent, for all partners
  54. self.activity_auto = self.env["privacy.activity"].create({
  55. "name": "activity_auto",
  56. "description": "I'm activity auto",
  57. "subject_find": True,
  58. "subject_domain": repr([("id", "in", self.partners.ids)]),
  59. "consent_required": "auto",
  60. "default_consent": True,
  61. "server_action_id": self.update_opt_out.id,
  62. })
  63. # Activity with manual consent, skipping partner 0
  64. self.activity_manual = self.env["privacy.activity"].create({
  65. "name": "activity_manual",
  66. "description": "I'm activity 3",
  67. "subject_find": True,
  68. "subject_domain": repr([("id", "in", self.partners[1:].ids)]),
  69. "consent_required": "manual",
  70. "default_consent": False,
  71. "server_action_id": self.update_opt_out.id,
  72. })
  73. # HACK https://github.com/odoo/odoo/issues/12237
  74. # TODO Remove hack in v12
  75. def tearDown(self):
  76. self.env = self._oldenv
  77. super(ActivityCase, self).tearDown()
  78. # HACK https://github.com/odoo/odoo/issues/12237
  79. # TODO Remove hack in v12
  80. @contextmanager
  81. def release_cr(self):
  82. self.env.cr.release()
  83. yield
  84. self.env.cr.acquire()
  85. def check_activity_auto_properly_sent(self):
  86. """Check emails sent by ``self.activity_auto``."""
  87. consents = self.env["privacy.consent"].search([
  88. ("activity_id", "=", self.activity_auto.id),
  89. ])
  90. # Check pending mails
  91. for consent in consents:
  92. self.assertEqual(consent.state, "draft")
  93. messages = consent.message_ids
  94. self.assertEqual(len(messages), 2)
  95. # Check sent mails
  96. self.cron_mail_queue.method_direct_trigger()
  97. for consent in consents:
  98. self.assertEqual(consent.state, "sent")
  99. messages = consent.message_ids
  100. self.assertEqual(len(messages), 3)
  101. # 2nd message notifies creation
  102. self.assertEqual(
  103. messages[2].subtype_id,
  104. self.mt_consent_consent_new,
  105. )
  106. # 3rd message notifies subject
  107. # Placeholder links should be logged
  108. self.assertTrue("/privacy/consent/accept/" in messages[1].body)
  109. self.assertTrue("/privacy/consent/reject/" in messages[1].body)
  110. # Tokenized links shouldn't be logged
  111. self.assertFalse(consent._url(True) in messages[1].body)
  112. self.assertFalse(consent._url(False) in messages[1].body)
  113. # 4th message contains the state change
  114. self.assertEqual(
  115. messages[0].subtype_id,
  116. self.mt_consent_state_changed,
  117. )
  118. # Partner's opt_out should be synced with default consent
  119. self.assertFalse(consent.partner_id.opt_out)
  120. def test_default_template(self):
  121. """We have a good mail template by default."""
  122. good = self.env.ref("privacy_consent.template_consent")
  123. self.assertEqual(
  124. self.activity_noconsent.consent_template_id,
  125. good,
  126. )
  127. self.assertEqual(
  128. self.activity_noconsent.consent_template_default_body_html,
  129. good.body_html,
  130. )
  131. self.assertEqual(
  132. self.activity_noconsent.consent_template_default_subject,
  133. good.subject,
  134. )
  135. def test_find_subject_if_consent_required(self):
  136. """If user wants to require consent, it needs subjects."""
  137. # Test the onchange helper
  138. onchange_activity1 = self.env["privacy.activity"].new(
  139. self.activity_noconsent.copy_data()[0])
  140. self.assertFalse(onchange_activity1.subject_find)
  141. onchange_activity1.consent_required = "auto"
  142. onchange_activity1._onchange_consent_required_subject_find()
  143. self.assertTrue(onchange_activity1.subject_find)
  144. # Test very dumb user that forces an error
  145. with self.assertRaises(ValidationError):
  146. self.activity_noconsent.consent_required = "manual"
  147. def test_template_required_auto(self):
  148. """Automatic consent activities need a template."""
  149. self.activity_noconsent.subject_find = True
  150. self.activity_noconsent.consent_template_id = False
  151. self.activity_noconsent.consent_required = "manual"
  152. with self.assertRaises(ValidationError):
  153. self.activity_noconsent.consent_required = "auto"
  154. def test_generate_manually(self):
  155. """Manually-generated consents work as expected."""
  156. self.partners.write({"opt_out": False})
  157. result = self.activity_manual.action_new_consents()
  158. self.assertEqual(result["res_model"], "privacy.consent")
  159. consents = self.env[result["res_model"]].search(result["domain"])
  160. self.assertEqual(consents.mapped("state"), ["draft"] * 2)
  161. self.assertEqual(consents.mapped("partner_id.opt_out"), [False] * 2)
  162. self.assertEqual(consents.mapped("accepted"), [False] * 2)
  163. self.assertEqual(consents.mapped("last_metadata"), [False] * 2)
  164. # Check sent mails
  165. messages = consents.mapped("message_ids")
  166. self.assertEqual(len(messages), 2)
  167. subtypes = messages.mapped("subtype_id")
  168. self.assertTrue(subtypes & self.mt_consent_consent_new)
  169. self.assertFalse(subtypes & self.mt_consent_acceptance_changed)
  170. self.assertFalse(subtypes & self.mt_consent_state_changed)
  171. # Send one manual request
  172. action = consents[0].action_manual_ask()
  173. self.assertEqual(action["res_model"], "mail.compose.message")
  174. composer = self.env[action["res_model"]] \
  175. .with_context(active_ids=consents[0].ids,
  176. active_model=consents._name,
  177. **action["context"]).create({})
  178. composer.onchange_template_id_wrapper()
  179. composer.send_mail()
  180. messages = consents.mapped("message_ids") - messages
  181. self.assertEqual(len(messages), 2)
  182. self.assertEqual(messages[0].subtype_id, self.mt_consent_state_changed)
  183. self.assertEqual(consents.mapped("state"), ["sent", "draft"])
  184. self.assertEqual(consents.mapped("partner_id.opt_out"), [True, False])
  185. # Placeholder links should be logged
  186. self.assertTrue("/privacy/consent/accept/" in messages[1].body)
  187. self.assertTrue("/privacy/consent/reject/" in messages[1].body)
  188. # Tokenized links shouldn't be logged
  189. accept_url = consents[0]._url(True)
  190. reject_url = consents[0]._url(False)
  191. self.assertNotIn(accept_url, messages[1].body)
  192. self.assertNotIn(reject_url, messages[1].body)
  193. # Visit tokenized accept URL
  194. with self.release_cr():
  195. result = self.url_open(accept_url).text
  196. self.assertIn("accepted", result)
  197. self.assertIn(reject_url, result)
  198. self.assertIn(self.activity_manual.name, result)
  199. self.assertIn(self.activity_manual.description, result)
  200. consents.invalidate_cache()
  201. self.assertEqual(consents.mapped("accepted"), [True, False])
  202. self.assertTrue(consents[0].last_metadata)
  203. self.assertFalse(consents[0].partner_id.opt_out)
  204. self.assertEqual(consents.mapped("state"), ["answered", "draft"])
  205. self.assertEqual(
  206. consents[0].message_ids[0].subtype_id,
  207. self.mt_consent_acceptance_changed,
  208. )
  209. # Visit tokenized reject URL
  210. with self.release_cr():
  211. result = self.url_open(reject_url).text
  212. self.assertIn("rejected", result)
  213. self.assertIn(accept_url, result)
  214. self.assertIn(self.activity_manual.name, result)
  215. self.assertIn(self.activity_manual.description, result)
  216. consents.invalidate_cache()
  217. self.assertEqual(consents.mapped("accepted"), [False, False])
  218. self.assertTrue(consents[0].last_metadata)
  219. self.assertTrue(consents[0].partner_id.opt_out)
  220. self.assertEqual(consents.mapped("state"), ["answered", "draft"])
  221. self.assertEqual(
  222. consents[0].message_ids[0].subtype_id,
  223. self.mt_consent_acceptance_changed,
  224. )
  225. self.assertFalse(consents[1].last_metadata)
  226. def test_generate_automatically(self):
  227. """Automatically-generated consents work as expected."""
  228. result = self.activity_auto.action_new_consents()
  229. self.assertEqual(result["res_model"], "privacy.consent")
  230. self.check_activity_auto_properly_sent()
  231. def test_generate_cron(self):
  232. """Cron-generated consents work as expected."""
  233. self.cron.method_direct_trigger()
  234. self.check_activity_auto_properly_sent()
  235. def test_mail_template_without_links(self):
  236. """Cannot create mail template without needed links."""
  237. with self.assertRaises(ValidationError):
  238. self.activity_manual.consent_template_id.body_html = "No links :("