Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 78 additions & 8 deletions mail_tracking/models/mail_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from email.utils import getaddresses

from odoo import api, fields, models
from odoo.exceptions import AccessError
from odoo.osv import expression
from odoo.tools import email_split

Expand Down Expand Up @@ -305,15 +306,84 @@ def get_failed_messsage_info(self, ids, model):
]
return res

def _tracking_status_vals(self):
"""Tracking data consumed by the chatter and the failed messages widget.

Beware that ``is_failed_message`` is computed for ``self.env.user``, so
these values must be built once per recipient.
"""
self.ensure_one()
return {
"partner_trackings": self.tracking_status(),
"mail_tracking_needs_action": self.mail_tracking_needs_action,
"is_failed_message": self.is_failed_message,
}

def _tracking_status_recipients(self):
"""Users whose web client is showing the tracking status of a message."""
self.ensure_one()
partners = self.author_id | self.notification_ids.res_partner_id
users = partners.with_context(active_test=False).user_ids
return users.filtered(lambda one: not one._is_public())

def _notify_tracking_status_update(self):
"""Queue a refresh of the tracking status in the involved web clients.

Tracking states change outside of any user session (Mailgun webhooks,
the open pixel, the SMTP layer) and they don't touch ``mail.notification``,
so core's ``_notify_message_notification_update()`` is never triggered for
them and the chatter would keep the stale status until a page reload.

Emails are sent one at a time, so a message with several recipients would
emit one notification per recipient. The refresh is aggregated here and
sent once per message at the end of the transaction instead.
"""
messages = self.sudo().exists().filtered("mail_tracking_ids")
if not messages:
return
precommit = self.env.cr.precommit
if "mail_tracking.status_update" not in precommit.data:
precommit.data["mail_tracking.status_update"] = set()
env = self.env

@precommit.add
def send_tracking_status_update():
message_ids = env.cr.precommit.data.pop(
"mail_tracking.status_update", ()
)
env["mail.message"].browse(message_ids)._send_tracking_status_update()

precommit.data["mail_tracking.status_update"].update(messages.ids)

def _send_tracking_status_update(self):
"""Push the current tracking status to the web clients showing the messages."""
for message in self.sudo().exists():
for user in message._tracking_status_recipients():
# 'is_failed_message' depends on the reader, so the store has to
# be built within each recipient's own environment.
message_as_user = message.with_user(user)
try:
vals = message_as_user._tracking_status_vals()
except AccessError: # pragma: no cover
continue
store = Store()
store.add(message_as_user, vals)
user.partner_id._bus_send_store(store)

def _message_notifications_to_store(self, store: Store):
"""Refresh the tracking data along with the notification statuses.

Core pushes this to the web client whenever the notifications of a
message change (a delivery failure, a resend...), so it's the right
place to keep the failed messages widget up to date.
"""
res = super()._message_notifications_to_store(store)
for message in self.filtered("mail_tracking_ids"):
store.add(message, message._tracking_status_vals())
return res

def _extras_to_store(self, store: Store, format_reply):
res = super()._extras_to_store(store, format_reply=format_reply)
for message in self:
store.add(
message,
{
"partner_trackings": message.tracking_status(),
"mail_tracking_needs_action": message.mail_tracking_needs_action,
"is_failed_message": message.is_failed_message,
},
)
store.add(message, message._tracking_status_vals())
return res
10 changes: 8 additions & 2 deletions mail_tracking/models/mail_tracking_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,14 @@ def create(self, vals_list):
def write(self, vals):
res = super().write(vals)
state = vals.get("state")
if state and state in self.env["mail.message"].get_failed_states():
self.mapped("mail_message_id").write({"mail_tracking_needs_action": True})
if state:
messages = self.mapped("mail_message_id")
if state in self.env["mail.message"].get_failed_states():
messages.write({"mail_tracking_needs_action": True})
# The state is usually written from a webhook, the open pixel or the
# SMTP layer, none of which triggers the core notification update, so
# the chatter has to be refreshed explicitly.
messages._notify_tracking_status_update()
return res

@api.model
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@ export class MessageTracking extends Component {
static props = ["message", "partner_trackings", "skip_track_links?"];
setup() {
this.message = useState(this.props.message);
this.partner_trackings = useState(this.props.partner_trackings);
}
/**
* Read the trackings from the props on every render.
*
* `tracking_status()` builds a brand new list every time it's computed, so
* the message record gets a different array each time the server pushes an
* update. A reference stored in `setup()` (which isn't re-run when the props
* change) would keep rendering the trackings the message had when the
* component was created.
*/
get partner_trackings() {
return this.props.partner_trackings;
}
_onTrackingStatusClick(event) {
const tracking_email_id = event.currentTarget.dataset.tracking;
Expand Down
Loading