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()`.
6 years ago |
|
# Copyright 2018 Tecnativa - Jairo Llopis # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import hashlib import hmac
from odoo import api, fields, models
class PrivacyConsent(models.Model): _name = "privacy.consent" _description = "Consent of data processing" _inherit = "mail.thread" _rec_name = "partner_id" _sql_constraints = [ ( "unique_partner_activity", "UNIQUE(partner_id, activity_id)", "Duplicated partner in this data processing activity", ), ]
active = fields.Boolean( default=True, index=True, ) accepted = fields.Boolean( track_visibility="onchange", help="Indicates current acceptance status, which can come from " "subject's last answer, or from the default specified in the " "related data processing activity.", ) last_metadata = fields.Text( readonly=True, track_visibility="onchange", help="Metadata from the last acceptance or rejection by the subject", ) partner_id = fields.Many2one( "res.partner", "Subject", required=True, readonly=True, track_visibility="onchange", help="Subject asked for consent.", ) activity_id = fields.Many2one( "privacy.activity", "Activity", readonly=True, required=True, track_visibility="onchange", ) state = fields.Selection( selection=[ ("draft", "Draft"), ("sent", "Awaiting response"), ("answered", "Answered"), ], default="draft", readonly=True, required=True, track_visibility="onchange", )
def _creation_subtype(self): return self.env.ref("privacy_consent.mt_consent_consent_new")
def _track_subtype(self, init_values): """Return specific subtypes.""" if self.env.context.get("subject_answering"): return self.env.ref("privacy_consent.mt_consent_acceptance_changed") if "state" in init_values: return self.env.ref("privacy_consent.mt_consent_state_changed") return super(PrivacyConsent, self)._track_subtype(init_values)
def _token(self): """Secret token to publicly authenticate this record.""" secret = self.env["ir.config_parameter"].sudo().get_param("database.secret") params = "{}-{}-{}-{}".format( self.env.cr.dbname, self.id, self.partner_id.id, self.activity_id.id, ) return hmac.new( secret.encode("utf-8"), params.encode("utf-8"), hashlib.sha512, ).hexdigest()
def _url(self, accept): """Tokenized URL to let subject decide consent.
:param bool accept: Indicates if you want the acceptance URL, or the rejection one. """
return "/privacy/consent/{}/{}/{}?db={}".format( "accept" if accept else "reject", self.id, self._token(), self.env.cr.dbname, )
def _send_consent_notification(self): """Send email notification to subject.""" for one in self.with_context( tpl_force_default_to=True, mail_notify_user_signature=False, mail_auto_subscribe_no_notify=True, ): one.activity_id.consent_template_id.send_mail(one.id)
def _run_action(self): """Execute server action defined in data processing activity.""" for one in self: # Always skip draft consents if one.state == "draft": continue action = one.activity_id.server_action_id.with_context( active_id=one.id, active_ids=one.ids, active_model=one._name, ) action.run()
@api.model_create_multi def create(self, vals_list): """Run server action on create.""" results = super().create(vals_list) # Sync the default acceptance status results._run_action() return results
def write(self, vals): """Run server action on update.""" result = super().write(vals) self._run_action() return result
def message_get_suggested_recipients(self): result = super().message_get_suggested_recipients() reason = self._fields["partner_id"].string for one in self: one._message_add_suggested_recipient( result, partner=one.partner_id, reason=reason, ) return result
def action_manual_ask(self): """Let user manually ask for consent.""" return { "context": { "default_composition_mode": "comment", "default_model": self._name, "default_res_id": self.id, "default_template_id": self.activity_id.consent_template_id.id, "default_use_template": True, "tpl_force_default_to": True, }, "force_email": True, "res_model": "mail.compose.message", "target": "new", "type": "ir.actions.act_window", "view_mode": "form", }
def action_auto_ask(self): """Automatically ask for consent.""" templated = self.filtered("activity_id.consent_template_id") automated = templated.filtered( lambda one: one.activity_id.consent_required == "auto" ) automated._send_consent_notification()
def action_answer(self, answer, metadata=False): """Process answer.
:param bool answer: Did the subject accept?
:param str metadata: Metadata from last user acceptance or rejection request. """
self.write({"state": "answered", "accepted": answer, "last_metadata": metadata})
|