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.

187 lines
6.0 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
  1. # Copyright 2018 Tecnativa - Jairo Llopis
  2. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
  3. import hashlib
  4. import hmac
  5. from odoo import api, fields, models
  6. class PrivacyConsent(models.Model):
  7. _name = 'privacy.consent'
  8. _description = "Consent of data processing"
  9. _inherit = "mail.thread"
  10. _rec_name = "partner_id"
  11. _sql_constraints = [
  12. ("unique_partner_activity", "UNIQUE(partner_id, activity_id)",
  13. "Duplicated partner in this data processing activity"),
  14. ]
  15. active = fields.Boolean(
  16. default=True,
  17. index=True,
  18. )
  19. accepted = fields.Boolean(
  20. track_visibility="onchange",
  21. help="Indicates current acceptance status, which can come from "
  22. "subject's last answer, or from the default specified in the "
  23. "related data processing activity.",
  24. )
  25. last_metadata = fields.Text(
  26. readonly=True,
  27. track_visibility="onchange",
  28. help="Metadata from the last acceptance or rejection by the subject",
  29. )
  30. partner_id = fields.Many2one(
  31. "res.partner",
  32. "Subject",
  33. required=True,
  34. readonly=True,
  35. track_visibility="onchange",
  36. help="Subject asked for consent.",
  37. )
  38. activity_id = fields.Many2one(
  39. "privacy.activity",
  40. "Activity",
  41. readonly=True,
  42. required=True,
  43. track_visibility="onchange",
  44. )
  45. state = fields.Selection(
  46. selection=[
  47. ("draft", "Draft"),
  48. ("sent", "Awaiting response"),
  49. ("answered", "Answered"),
  50. ],
  51. default="draft",
  52. readonly=True,
  53. required=True,
  54. track_visibility="onchange",
  55. )
  56. def _track_subtype(self, init_values):
  57. """Return specific subtypes."""
  58. if self.env.context.get("subject_answering"):
  59. return "privacy_consent.mt_consent_acceptance_changed"
  60. if "activity_id" in init_values or "partner_id" in init_values:
  61. return "privacy_consent.mt_consent_consent_new"
  62. if "state" in init_values:
  63. return "privacy_consent.mt_consent_state_changed"
  64. return super(PrivacyConsent, self)._track_subtype(init_values)
  65. def _token(self):
  66. """Secret token to publicly authenticate this record."""
  67. secret = self.env["ir.config_parameter"].sudo().get_param(
  68. "database.secret")
  69. params = "{}-{}-{}-{}".format(
  70. self.env.cr.dbname,
  71. self.id,
  72. self.partner_id.id,
  73. self.activity_id.id,
  74. )
  75. return hmac.new(
  76. secret.encode('utf-8'),
  77. params.encode('utf-8'),
  78. hashlib.sha512,
  79. ).hexdigest()
  80. def _url(self, accept):
  81. """Tokenized URL to let subject decide consent.
  82. :param bool accept:
  83. Indicates if you want the acceptance URL, or the rejection one.
  84. """
  85. return "/privacy/consent/{}/{}/{}?db={}".format(
  86. "accept" if accept else "reject",
  87. self.id,
  88. self._token(),
  89. self.env.cr.dbname,
  90. )
  91. def _send_consent_notification(self):
  92. """Send email notification to subject."""
  93. for one in self.with_context(tpl_force_default_to=True,
  94. mail_notify_user_signature=False,
  95. mail_auto_subscribe_no_notify=True):
  96. one.activity_id.consent_template_id.send_mail(one.id)
  97. def _run_action(self):
  98. """Execute server action defined in data processing activity."""
  99. for one in self:
  100. # Always skip draft consents
  101. if one.state == "draft":
  102. continue
  103. action = one.activity_id.server_action_id.with_context(
  104. active_id=one.id,
  105. active_ids=one.ids,
  106. active_model=one._name,
  107. )
  108. action.run()
  109. @api.model_create_multi
  110. def create(self, vals_list):
  111. """Run server action on create."""
  112. super_ = super(PrivacyConsent,
  113. self.with_context(mail_create_nolog=True))
  114. results = super_.create(vals_list)
  115. # Sync the default acceptance status
  116. results.sudo()._run_action()
  117. return results
  118. def write(self, vals):
  119. """Run server action on update."""
  120. result = super().write(vals)
  121. self._run_action()
  122. return result
  123. def message_get_suggested_recipients(self):
  124. result = super() \
  125. .message_get_suggested_recipients()
  126. reason = self._fields["partner_id"].string
  127. for one in self:
  128. one._message_add_suggested_recipient(
  129. result,
  130. partner=one.partner_id,
  131. reason=reason,
  132. )
  133. return result
  134. def action_manual_ask(self):
  135. """Let user manually ask for consent."""
  136. return {
  137. "context": {
  138. "default_composition_mode": "comment",
  139. "default_model": self._name,
  140. "default_res_id": self.id,
  141. "default_template_id": self.activity_id.consent_template_id.id,
  142. "default_use_template": True,
  143. "tpl_force_default_to": True,
  144. },
  145. "force_email": True,
  146. "res_model": "mail.compose.message",
  147. "target": "new",
  148. "type": "ir.actions.act_window",
  149. "view_mode": "form",
  150. }
  151. def action_auto_ask(self):
  152. """Automatically ask for consent."""
  153. templated = self.filtered("activity_id.consent_template_id")
  154. automated = templated.filtered(
  155. lambda one: one.activity_id.consent_required == "auto")
  156. automated._send_consent_notification()
  157. def action_answer(self, answer, metadata=False):
  158. """Process answer.
  159. :param bool answer:
  160. Did the subject accept?
  161. :param str metadata:
  162. Metadata from last user acceptance or rejection request.
  163. """
  164. self.write({
  165. "state": "answered",
  166. "accepted": answer,
  167. "last_metadata": metadata,
  168. })