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.

291 lines
12 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
  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 Form, HttpCase
  6. class ActivityCase(HttpCase):
  7. def setUp(self):
  8. super(ActivityCase, self).setUp()
  9. self.cron = self.env.ref("privacy_consent.cron_auto_consent")
  10. self.cron_mail_queue = self.env.ref("mail.ir_cron_mail_scheduler_action")
  11. self.sync_blacklist = self.env.ref("privacy_consent.sync_blacklist")
  12. self.mt_consent_consent_new = self.env.ref(
  13. "privacy_consent.mt_consent_consent_new"
  14. )
  15. self.mt_consent_acceptance_changed = self.env.ref(
  16. "privacy_consent.mt_consent_acceptance_changed"
  17. )
  18. self.mt_consent_state_changed = self.env.ref(
  19. "privacy_consent.mt_consent_state_changed"
  20. )
  21. # Some partners to ask for consent
  22. self.partners = self.env["res.partner"]
  23. self.partners += self.partners.create(
  24. {"name": "consent-partner-0", "email": "partner0@example.com"}
  25. )
  26. self.partners += self.partners.create(
  27. {"name": "consent-partner-1", "email": "partner1@example.com"}
  28. )
  29. self.partners += self.partners.create(
  30. {"name": "consent-partner-2", "email": "partner2@example.com"}
  31. )
  32. # Partner without email, on purpose
  33. self.partners += self.partners.create({"name": "consent-partner-3"})
  34. # Partner with wrong email, on purpose
  35. self.partners += self.partners.create(
  36. {"name": "consent-partner-4", "email": "wrong-mail"}
  37. )
  38. # Blacklist some partners
  39. self.blacklists = self.env["mail.blacklist"]
  40. self.blacklists += self.blacklists._add("partner1@example.com")
  41. # Activity without consent
  42. self.activity_noconsent = self.env["privacy.activity"].create(
  43. {"name": "activity_noconsent", "description": "I'm activity 1"}
  44. )
  45. # Activity with auto consent, for all partners
  46. self.activity_auto = self.env["privacy.activity"].create(
  47. {
  48. "name": "activity_auto",
  49. "description": "I'm activity auto",
  50. "subject_find": True,
  51. "subject_domain": repr([("id", "in", self.partners.ids)]),
  52. "consent_required": "auto",
  53. "default_consent": True,
  54. "server_action_id": self.sync_blacklist.id,
  55. }
  56. )
  57. # Activity with manual consent, skipping partner 0
  58. self.activity_manual = self.env["privacy.activity"].create(
  59. {
  60. "name": "activity_manual",
  61. "description": "I'm activity 3",
  62. "subject_find": True,
  63. "subject_domain": repr([("id", "in", self.partners[1:].ids)]),
  64. "consent_required": "manual",
  65. "default_consent": False,
  66. "server_action_id": self.sync_blacklist.id,
  67. }
  68. )
  69. @contextmanager
  70. def _patch_build(self):
  71. self._built_messages = []
  72. IMS = self.env["ir.mail_server"]
  73. def _build_email(_self, email_from, email_to, subject, body, *args, **kwargs):
  74. self._built_messages.append(body)
  75. return _build_email.origin(
  76. _self,
  77. email_from,
  78. email_to,
  79. subject,
  80. body,
  81. *args,
  82. **kwargs,
  83. )
  84. try:
  85. IMS._patch_method("build_email", _build_email)
  86. yield
  87. finally:
  88. IMS._revert_method("build_email")
  89. def check_activity_auto_properly_sent(self):
  90. """Check emails sent by ``self.activity_auto``."""
  91. consents = self.env["privacy.consent"].search(
  92. [("activity_id", "=", self.activity_auto.id)]
  93. )
  94. # Check pending mails
  95. for consent in consents:
  96. self.assertEqual(consent.state, "draft")
  97. messages = consent.message_ids
  98. self.assertEqual(len(messages), 2)
  99. # Check sent mails
  100. with self._patch_build():
  101. self.cron_mail_queue.method_direct_trigger()
  102. for consent in consents:
  103. good_email = "@" in (consent.partner_id.email or "")
  104. expected_messages = 3 if good_email else 2
  105. self.assertEqual(
  106. consent.state,
  107. "sent" if good_email else "draft",
  108. )
  109. messages = consent.message_ids
  110. self.assertEqual(len(messages), expected_messages)
  111. # 2nd message notifies creation
  112. self.assertEqual(
  113. messages[expected_messages - 1].subtype_id,
  114. self.mt_consent_consent_new,
  115. )
  116. # 3rd message notifies subject
  117. # Placeholder links should be logged
  118. self.assertIn(
  119. "/privacy/consent/accept/", messages[expected_messages - 2].body
  120. )
  121. self.assertIn(
  122. "/privacy/consent/reject/", messages[expected_messages - 2].body
  123. )
  124. # Tokenized links shouldn't be logged
  125. self.assertNotIn(consent._url(True), messages[expected_messages - 2].body)
  126. self.assertNotIn(consent._url(False), messages[expected_messages - 2].body)
  127. # 4th message contains the state change
  128. if good_email:
  129. self.assertEqual(
  130. messages[0].subtype_id,
  131. self.mt_consent_state_changed,
  132. )
  133. # Partner's is_blacklisted should be synced with default consent
  134. self.assertFalse(consent.partner_id.is_blacklisted)
  135. # Check the sent message was built properly tokenized
  136. accept_url, reject_url = map(consent._url, (True, False))
  137. for body in self._built_messages:
  138. if accept_url in body and reject_url in body:
  139. self._built_messages.remove(body)
  140. break
  141. else:
  142. raise AssertionError("Some message body should have these urls")
  143. def test_default_template(self):
  144. """We have a good mail template by default."""
  145. good = self.env.ref("privacy_consent.template_consent")
  146. self.assertEqual(
  147. self.activity_noconsent.consent_template_id,
  148. good,
  149. )
  150. self.assertEqual(
  151. self.activity_noconsent.consent_template_default_body_html,
  152. good.body_html,
  153. )
  154. self.assertEqual(
  155. self.activity_noconsent.consent_template_default_subject,
  156. good.subject,
  157. )
  158. def test_find_subject_if_consent_required(self):
  159. """If user wants to require consent, it needs subjects."""
  160. # Test the onchange helper
  161. onchange_activity1 = self.env["privacy.activity"].new(
  162. self.activity_noconsent.copy_data()[0]
  163. )
  164. self.assertFalse(onchange_activity1.subject_find)
  165. onchange_activity1.consent_required = "auto"
  166. onchange_activity1._onchange_consent_required_subject_find()
  167. self.assertTrue(onchange_activity1.subject_find)
  168. # Test very dumb user that forces an error
  169. with self.assertRaises(ValidationError):
  170. self.activity_noconsent.consent_required = "manual"
  171. def test_template_required_auto(self):
  172. """Automatic consent activities need a template."""
  173. self.activity_noconsent.subject_find = True
  174. self.activity_noconsent.consent_template_id = False
  175. self.activity_noconsent.consent_required = "manual"
  176. with self.assertRaises(ValidationError):
  177. self.activity_noconsent.consent_required = "auto"
  178. def test_generate_manually(self):
  179. """Manually-generated consents work as expected."""
  180. for partner in self.partners:
  181. if "@" in (partner.email or ""):
  182. self.blacklists._remove(partner.email)
  183. result = self.activity_manual.action_new_consents()
  184. self.assertEqual(result["res_model"], "privacy.consent")
  185. consents = self.env[result["res_model"]].search(result["domain"])
  186. self.assertEqual(consents.mapped("state"), ["draft"] * 3)
  187. self.assertEqual(
  188. consents.mapped("partner_id.is_blacklisted"),
  189. [False] * 3,
  190. )
  191. self.assertEqual(consents.mapped("accepted"), [False] * 3)
  192. self.assertEqual(consents.mapped("last_metadata"), [False] * 3)
  193. # Check sent mails
  194. messages = consents.mapped("message_ids")
  195. self.assertEqual(len(messages), 3)
  196. subtypes = messages.mapped("subtype_id")
  197. self.assertTrue(subtypes & self.mt_consent_consent_new)
  198. self.assertFalse(subtypes & self.mt_consent_acceptance_changed)
  199. self.assertFalse(subtypes & self.mt_consent_state_changed)
  200. # Send one manual request
  201. action = consents[0].action_manual_ask()
  202. self.assertEqual(action["res_model"], "mail.compose.message")
  203. Composer = self.env[action["res_model"]].with_context(
  204. active_ids=consents[0].ids,
  205. active_model=consents._name,
  206. **action["context"],
  207. )
  208. composer_wizard = Form(Composer)
  209. self.assertIn(consents[0].partner_id.name, composer_wizard.body)
  210. composer_record = composer_wizard.save()
  211. with self._patch_build():
  212. composer_record.send_mail()
  213. # Check the sent message was built properly tokenized
  214. body = self._built_messages[0]
  215. self.assertIn(consents[0]._url(True), body)
  216. self.assertIn(consents[0]._url(False), body)
  217. messages = consents.mapped("message_ids") - messages
  218. self.assertEqual(len(messages), 2)
  219. self.assertEqual(messages[0].subtype_id, self.mt_consent_state_changed)
  220. self.assertEqual(consents.mapped("state"), ["sent", "draft", "draft"])
  221. self.assertEqual(
  222. consents.mapped("partner_id.is_blacklisted"),
  223. [True, False, False],
  224. )
  225. # Placeholder links should be logged
  226. self.assertTrue("/privacy/consent/accept/" in messages[1].body)
  227. self.assertTrue("/privacy/consent/reject/" in messages[1].body)
  228. # Tokenized links shouldn't be logged
  229. accept_url = consents[0]._url(True)
  230. reject_url = consents[0]._url(False)
  231. self.assertNotIn(accept_url, messages[1].body)
  232. self.assertNotIn(reject_url, messages[1].body)
  233. # Visit tokenized accept URL
  234. result = self.url_open(accept_url).text
  235. self.assertIn("accepted", result)
  236. self.assertIn(reject_url, result)
  237. self.assertIn(self.activity_manual.name, result)
  238. self.assertIn(self.activity_manual.description, result)
  239. consents.invalidate_cache()
  240. self.assertEqual(consents.mapped("accepted"), [True, False, False])
  241. self.assertTrue(consents[0].last_metadata)
  242. self.assertFalse(consents[0].partner_id.is_blacklisted)
  243. self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
  244. self.assertEqual(
  245. consents[0].message_ids[0].subtype_id,
  246. self.mt_consent_acceptance_changed,
  247. )
  248. # Visit tokenized reject URL
  249. result = self.url_open(reject_url).text
  250. self.assertIn("rejected", result)
  251. self.assertIn(accept_url, result)
  252. self.assertIn(self.activity_manual.name, result)
  253. self.assertIn(self.activity_manual.description, result)
  254. consents.invalidate_cache()
  255. self.assertEqual(consents.mapped("accepted"), [False, False, False])
  256. self.assertTrue(consents[0].last_metadata)
  257. self.assertTrue(consents[0].partner_id.is_blacklisted)
  258. self.assertEqual(consents.mapped("state"), ["answered", "draft", "draft"])
  259. self.assertEqual(
  260. consents[0].message_ids[0].subtype_id,
  261. self.mt_consent_acceptance_changed,
  262. )
  263. self.assertFalse(consents[1].last_metadata)
  264. def test_generate_automatically(self):
  265. """Automatically-generated consents work as expected."""
  266. result = self.activity_auto.action_new_consents()
  267. self.assertEqual(result["res_model"], "privacy.consent")
  268. self.check_activity_auto_properly_sent()
  269. def test_generate_cron(self):
  270. """Cron-generated consents work as expected."""
  271. self.cron.method_direct_trigger()
  272. self.check_activity_auto_properly_sent()
  273. def test_mail_template_without_links(self):
  274. """Cannot create mail template without needed links."""
  275. with self.assertRaises(ValidationError):
  276. self.activity_manual.consent_template_id.body_html = "No links :("