[MIG] mail_tracking: Completed migration to 16.0

The following changes were implemented:

1 - Added Failed Message component and related components to reuse the
    Message component when rendering failed messages. This allows us to
    dispose of the messagefailed JS model altogether, since failed messages
    are now just regular messages that were marked as failed.

2 - Added Owl reactivity to failed message actions so that browser does
    not have to be reloaded each time a message is marked as reviewed or
    resent.

3 - Fixed 'Retry' and 'Set as reviewed' flows for failed messages.

4 - Fixed `Failed sent messages` filter on models by overriding `get_view`
    instead of `_fields_view_get`

5 - Refactored folder structure to more closely resemble the `mail`
    module's folder structure.

6 - Refactored module to utilize `Command` as a means to create, write,
    etc. instead of `[0, ...]`, `[4, ...]`.

7 - Fixed and added unit tests.

8 - Removed dead/unused code from `v15`.
pull/1216/head
payen000 2023-09-04 13:28:09 -07:00
parent b3f4068e02
commit ef73e2d7ab
53 changed files with 966 additions and 1214 deletions

View File

@ -25,23 +25,33 @@
"views/res_partner_view.xml",
],
"assets": {
"mail.assets_messaging": [
"mail_tracking/static/src/js/models/*.js",
],
"web.assets_backend": [
"mail_tracking/static/src/xml/mail_tracking.xml",
"mail_tracking/static/src/css/mail_tracking.scss",
"mail_tracking/static/src/css/failed_message.scss",
"mail_tracking/static/src/js/message.esm.js",
"mail_tracking/static/src/js/failed_message/mail_failed_box.esm.js",
"mail_tracking/static/src/js/models/thread.esm.js",
"mail_tracking/static/src/xml/mail_tracking.xml",
"mail_tracking/static/src/xml/failed_message/common.xml",
"mail_tracking/static/src/xml/failed_message/thread.xml",
"mail_tracking/static/src/xml/failed_message/discuss.xml",
],
"web.assets_frontend": [
"mail_tracking/static/src/css/failed_message.scss",
"mail_tracking/static/src/client_actions/failed_message_storage.esm.js",
"mail_tracking/static/src/models/chatter.esm.js",
"mail_tracking/static/src/models/discuss_sidebar_mailbox_view.esm.js",
"mail_tracking/static/src/models/discuss_view.esm.js",
"mail_tracking/static/src/models/mailbox.esm.js",
"mail_tracking/static/src/models/message_list_view_item.esm.js",
"mail_tracking/static/src/models/message_list_view.esm.js",
"mail_tracking/static/src/models/message_view.esm.js",
"mail_tracking/static/src/models/message.esm.js",
"mail_tracking/static/src/models/messaging_initializer.esm.js",
"mail_tracking/static/src/models/messaging.esm.js",
"mail_tracking/static/src/models/thread.esm.js",
"mail_tracking/static/src/components/discuss/discuss.xml",
"mail_tracking/static/src/components/message/message.xml",
"mail_tracking/static/src/components/message/message.esm.js",
"mail_tracking/static/src/components/message/message.scss",
"mail_tracking/static/src/components/message_list/message_list.esm.js",
"mail_tracking/static/src/components/failed_message/failed_message.xml",
"mail_tracking/static/src/components/failed_message/failed_message.esm.js",
"mail_tracking/static/src/components/failed_message/failed_message.scss",
"mail_tracking/static/src/components/failed_message_list/failed_message_list.xml",
"mail_tracking/static/src/components/failed_message_list/failed_message_list.esm.js", # noqa: B950
"mail_tracking/static/src/components/discuss_sidebar_mailbox/discuss_sidebar_mailbox.xml", # noqa: B950
"mail_tracking/static/src/components/discuss_sidebar_mailbox/discuss_sidebar_mailbox.esm.js", # noqa: B950
"mail_tracking/static/src/components/thread_view/thread_view.xml",
"mail_tracking/static/src/components/thread_view/thread_view.scss",
],
},
"demo": ["demo/demo.xml"],

View File

@ -6,9 +6,9 @@ from odoo.addons.mail.controllers.discuss import DiscussController
class MailTrackingDiscussController(DiscussController):
@http.route()
def mail_init_messaging(self):
def mail_init_messaging(self, **kwargs):
"""Route used to initial values of Discuss app"""
values = super().mail_init_messaging()
values = super().mail_init_messaging(**kwargs)
values.update(
{"failed_counter": http.request.env["mail.message"].get_failed_count()}
)
@ -16,9 +16,13 @@ class MailTrackingDiscussController(DiscussController):
@http.route("/mail/failed/messages", methods=["POST"], type="json", auth="user")
def discuss_failed_messages(self, max_id=None, min_id=None, limit=30, **kwargs):
return http.request.env["mail.message"]._message_fetch(
domain=[("is_failed_message", "=", True)],
max_id=max_id,
min_id=min_id,
limit=limit,
return (
http.request.env["mail.message"]
._message_fetch(
domain=[("is_failed_message", "=", True)],
max_id=max_id,
min_id=min_id,
limit=limit,
)
.message_format()
)

View File

@ -40,31 +40,6 @@ class MailTrackingController(MailController):
"ua_family": request.user_agent.browser or False,
}
# TODO Remove useless controller
@http.route(
[
"/mail/tracking/all/<string:db>",
"/mail/tracking/event/<string:db>/<string:event_type>",
],
type="http",
auth="none",
csrf=False,
)
def mail_tracking_event(self, db, event_type=None, **kw):
"""Route used by external mail service"""
metadata = self._request_metadata()
res = None
with db_env(db) as env:
try:
res = env["mail.tracking.email"].event_process(
http.request, kw, metadata, event_type=event_type
)
except Exception as e:
_logger.warning(e)
if not res or res == "NOT FOUND":
return werkzeug.exceptions.NotAcceptable()
return res
@http.route(
[
"/mail/tracking/open/<string:db>" "/<int:tracking_email_id>/blank.gif",

View File

@ -13,10 +13,13 @@
<field name="body"><![CDATA[<p>This is a message with CC</p>]]></field>
<field name="email_from">wood.corner26@example.com</field>
<field name="author_id" ref="base.res_partner_1" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field
name="partner_ids"
eval="[Command.set([ref('base.partner_demo')])]"
/>
<field
name="notification_ids"
eval="[(0, 0, {'res_partner_id': ref('base.partner_demo')})]"
eval="[Command.create({'res_partner_id': ref('base.partner_demo')})]"
/>
<field name="subject">Message with CC</field>
</record>
@ -41,10 +44,13 @@
<field name="body"><![CDATA[<p>This is a failed message</p>]]></field>
<field name="email_from">wood.corner26@example.com</field>
<field name="author_id" ref="base.res_partner_1" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field
name="partner_ids"
eval="[Command.set([ref('base.partner_demo')])]"
/>
<field
name="notification_ids"
eval="[(0, 0, {'res_partner_id': ref('base.partner_demo')})]"
eval="[Command.create({'res_partner_id': ref('base.partner_demo')})]"
/>
<field name="subject">Failed Message</field>
</record>
@ -69,10 +75,13 @@
<field name="body"><![CDATA[<p>This is another failed message</p>]]></field>
<field name="email_from">jackson.group82@example.com</field>
<field name="author_id" ref="base.res_partner_10" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field
name="partner_ids"
eval="[Command.set([ref('base.partner_demo')])]"
/>
<field
name="notification_ids"
eval="[(0, 0, {'res_partner_id': ref('base.partner_demo')})]"
eval="[Command.create({'res_partner_id': ref('base.partner_demo')})]"
/>
<field name="subject">Failed Message</field>
</record>
@ -97,10 +106,13 @@
<field name="body"><![CDATA[<p>This is another failed message</p>]]></field>
<field name="email_from">admin@example.com</field>
<field name="author_id" ref="base.partner_admin" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field
name="partner_ids"
eval="[Command.set([ref('base.partner_demo')])]"
/>
<field
name="notification_ids"
eval="[(0, 0, {'res_partner_id': ref('base.partner_demo')})]"
eval="[Command.create({'res_partner_id': ref('base.partner_demo')})]"
/>
<field name="subject">Failed Message</field>
</record>

View File

@ -262,14 +262,9 @@ class MailMessage(models.Model):
return
failed_partners = failed_trackings.mapped("partner_id")
failed_recipients = failed_partners.name_get()
if self.author_id:
author = self.author_id.name_get()[0]
else:
author = (-1, _("-Unknown Author-"))
return {
"id": self.id,
"date": self.date,
"author": author,
"body": self.body,
"failed_recipients": failed_recipients,
}
@ -292,6 +287,7 @@ class MailMessage(models.Model):
self.env["bus.bus"]._sendone(
self.env.user.partner_id, "toggle_tracking_status", self.ids
)
return self.mail_tracking_needs_action
@api.model
def get_failed_count(self):

View File

@ -88,17 +88,13 @@ class MailThread(models.AbstractModel):
)
@api.model
def _fields_view_get(
self, view_id=None, view_type="form", toolbar=False, submenu=False
):
def get_view(self, view_id=None, view_type="form", **options):
"""Add filters for failed messages.
These filters will show up on any form or search views of any
These filters will show up on any search views of any
model inheriting from ``mail.thread``.
"""
res = super()._fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
)
res = super().get_view(view_id, view_type, **options)
if view_type != "search":
return res
doc = etree.XML(res["arch"])

View File

@ -10,6 +10,7 @@ from datetime import datetime
from odoo import _, api, fields, models, tools
from odoo.exceptions import AccessError
from odoo.fields import Command
from odoo.tools import email_split
_logger = logging.getLogger(__name__)
@ -41,7 +42,7 @@ class MailTrackingEmail(models.Model):
time = fields.Datetime(readonly=True, index=True)
date = fields.Date(readonly=True, compute="_compute_date", store=True)
mail_message_id = fields.Many2one(
string="Message", comodel_name="mail.message", readonly=True, index=True
comodel_name="mail.message", readonly=True, index=True
)
message_id = fields.Char(compute="_compute_message_id")
mail_id = fields.Many2one(string="Email", comodel_name="mail.mail", readonly=True)
@ -395,10 +396,12 @@ class MailTrackingEmail(models.Model):
# add it in order to see his tracking status in chatter
if mail_message.subtype_id:
mail_message.sudo().write(
{"notified_partner_ids": [(4, self.partner_id.id)]}
{"notified_partner_ids": [Command.link(self.partner_id.id)]}
)
else:
mail_message.sudo().write({"partner_ids": [(4, self.partner_id.id)]})
mail_message.sudo().write(
{"partner_ids": [Command.link(self.partner_id.id)]}
)
return True
def _tracking_sent_prepare(self, mail_server, smtp_server, message, message_id):

View File

@ -9,3 +9,6 @@
* `Eezee-IT <https://www.eezee-it.com>`_:
* Asma Elferkhsi
* `Vauxoo <https://www.vauxoo.com>`_:
* Agustín Payen Sandoval

View File

@ -11,7 +11,7 @@
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
<field
name="groups"
eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"
eval="[Command.link(ref('base.group_portal')), Command.link(ref('base.group_public'))]"
/>
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />

View File

@ -0,0 +1,17 @@
/** @odoo-module **/
const {reactive, useState} = owl;
// Set reactive object to observe the current state of failed messages.
// This allows re-rendering only non-reviewed failed messages without
// reloading the window after a failed message has been dealt with.
export const store = reactive({
reviewedMessageIds: new Set(),
addMessage(item) {
this.reviewedMessageIds.add(item);
},
});
export function useStore() {
return useState(store);
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="mail.DiscussSidebar" t-inherit-mode="extension">
<xpath
expr="//div[hasclass('o_DiscussSidebar_categoryMailbox')]"
position="inside"
>
<DiscussSidebarMailbox record="discussView.failedMessageView" />
</xpath>
</t>
<t t-inherit="mail.MessageList" t-inherit-mode="extension">
<t
t-elif="messageListView.threadViewOwner.thread === messaging.history.thread"
position="after"
>
<t
t-elif="messageListView.threadViewOwner.thread === messaging.failedmsg.thread"
>
<div class="o_MessageList_emptyTitle o-neutral-face-icon">
No failed messages
</div>
Failed messages will appear here.
</t>
</t>
</t>
<t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
<xpath
expr="//t[@t-elif='thread.mailbox === messaging.history']"
position="after"
>
<div
t-elif="thread.mailbox === messaging.failedmsg"
class="o_ThreadIcon_mailboxModeration fa fa-fw fa-exclamation"
/>
</xpath>
</t>
<t t-name="mail_tracking.TrackingStatus" owl="1">
<span t-if="tracking['isCc']" class="mail_tracking_cc">
<i class="fa fa-cc" role="img" aria-label="Cc" />
</span>
<span
t-elif="!tracking['isCc'] &amp;&amp; !tracking['partner_id']"
class="mail_anon_recipient"
>
<i class="fa fa-low-vision" role="img" aria-label="Anonymous Recipient" />
</span>
<span t-elif="tracking['status'] === 'unknown'" class="mail_tracking_unknown">
<i class="fa fa-ban" role="img" aria-label="Unknown Status" />
</span>
<span
t-elif="tracking['status'] === 'waiting'"
class="mail_tracking_waiting mail_tracking_pointer"
>
<i class="fa fa-clock-o" role="img" aria-label="Waiting Status" />
</span>
<span
t-elif="tracking['status'] === 'error'"
class="mail_tracking_error mail_tracking_pointer"
>
<i
t-if="tracking['error_type'] === 'no_recipient'"
class="fa fa-user-times"
role="img"
aria-label="Error Status"
/>
<i t-else="" class="fa fa-remove" />
</span>
<span
t-elif="tracking['status'] === 'sent'"
class="mail_tracking_sent mail_tracking_pointer"
>
<i class="fa fa-check" role="img" aria-label="Sent Status" />
</span>
<span
t-elif="tracking['status'] === 'delivered'"
class="fa-stack mail_tracking_delivered mail_tracking_pointer"
>
<i
class="fa fa-check fa-stack-1x"
style="margin-left:1px"
role="img"
aria-label="Delivered Status Left Checkmark"
/>
<i
class="fa fa-check fa-inverse fa-stack-1x"
style="margin-left:-2px;"
role="img"
aria-label="Delivered Status Center Checkmark"
/>
<i
class="fa fa-check fa-stack-1x"
style="margin-left:-3px"
role="img"
aria-label="Delivered Status Right Checkmark"
/>
</span>
<span
t-elif="tracking['status'] === 'opened'"
class="fa-stack mail_tracking_opened mail_tracking_pointer"
>
<i
class="fa fa-check fa-stack-1x"
style="margin-left:1px"
role="img"
aria-label="Opened Status Left Checkmark"
/>
<i
class="fa fa-check fa-inverse fa-stack-1x"
style="margin-left:-2px;"
role="img"
aria-label="Opened Status Center Checkmark"
/>
<i
class="fa fa-check fa-stack-1x"
style="margin-left:-3px"
role="img"
aria-label="Opened Status Right Checkmark"
/>
</span>
</t>
</templates>

View File

@ -0,0 +1,16 @@
/** @odoo-module **/
import {DiscussSidebarMailbox} from "@mail/components/discuss_sidebar_mailbox/discuss_sidebar_mailbox";
import {patch} from "web.utils";
import {useStore} from "../../client_actions/failed_message_storage.esm";
patch(
DiscussSidebarMailbox.prototype,
"mail_tracking/static/src/components/discuss_sidebar_mailbox/discuss_sidebar_mailbox.esm.js",
{
setup() {
this._super(...arguments);
this.store = useStore();
},
}
);

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="mail_tracking.DiscussSidebarMailbox"
t-inherit="mail.DiscussSidebarMailbox"
t-inherit-mode="extension"
>
<xpath
expr="//t[@t-if='discussSidebarMailboxView.mailbox.counter > 0']"
position="before"
>
<t
t-set="messages"
t-value="discussSidebarMailboxView.mailbox.thread.messages"
/>
<t
t-if="discussSidebarMailboxView.isFailedDiscussSidebarMailboxView &amp;&amp; messages.length"
>
<t
t-set="failedMessages"
t-value="discussSidebarMailboxView._getNonReviewedFailedMessages(
messages,
store.reviewedMessageIds,
)"
/>
<div
t-if="failedMessages.length"
t-attf-class="o_DiscussSidebarMailbox_counter o_DiscussSidebarMailbox_item badge rounded-pill {{ discussSidebarMailboxView.mailbox === messaging.starred ? 'bg-400 text-light' : 'text-bg-primary' }} ms-1 me-3"
>
<t t-esc="failedMessages.length" />
</div>
</t>
<t t-else="">
<div
t-if="discussSidebarMailboxView.mailbox.counter > 0"
t-attf-class="o_DiscussSidebarMailbox_counter o_DiscussSidebarMailbox_item badge rounded-pill {{ discussSidebarMailboxView.mailbox === messaging.starred ? 'bg-400 text-light' : 'text-bg-primary' }} ms-1 me-3"
>
<t t-esc="discussSidebarMailboxView.mailbox.counter" />
</div>
</t>
</xpath>
<xpath
expr="//t[@t-if='discussSidebarMailboxView.mailbox.counter > 0']"
position="attributes"
>
<attribute name="t-if">false</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,11 @@
/** @odoo-module **/
import {Message} from "@mail/components/message/message";
import {registerMessagingComponent} from "@mail/utils/messaging_component";
export class FailedMessage extends Message {}
FailedMessage.props = {record: Object, isFailedMessage: true};
FailedMessage.template = "mail_tracking.FailedMessage";
registerMessagingComponent(FailedMessage);

View File

@ -0,0 +1,14 @@
/* Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.o_Activity_icon.fa.fa-exclamation {
color: white;
left: 1.05ch;
position: relative;
top: 0.2ch;
}
.o_Activity_iconContainer.bg-danger.rounded-circle {
width: 3ch;
height: 3ch;
}

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="mail_tracking.FailedMessage"
t-inherit="mail.Message"
t-inherit-mode="primary"
>
<!-- Avoid setting original message as isActive upon hovering or clicking failed message -->
<xpath expr="//div[hasclass('o_Message')]" position="attributes">
<attribute name="t-on-mouseenter">messageView.doNothing</attribute>
<attribute name="t-on-click">messageView.doNothing</attribute>
</xpath>
<!-- Avoid showing Message Action List on failed messages -->
<xpath
expr="//div[hasclass('o_Message_actionListContainer')]"
position="attributes"
>
<attribute name="t-if">false</attribute>
</xpath>
<!-- Replace original Message component's recipients -->
<xpath expr="//p[hasclass('o_mail_tracking')]" position="after">
<div t-if="messageView.message.failedRecipients" class="o_Activity_info">
<strong class="text-danger">Failed Recipients: </strong>
<t
t-foreach="messageView.message.failedRecipients"
t-as="recipient"
t-key="recipient_localId"
>
<a
class="o_mail_action_tracking_partner"
t-att-data-partner="recipient[1]"
t-attf-href="#model=res.partner&amp;id={{recipient[0]}}"
t-out="recipient[1]"
/>
</t>
</div>
</xpath>
<xpath expr="//p[hasclass('o_mail_tracking')]" position="attributes">
<attribute name="t-if">false</attribute>
</xpath>
<!-- Replace original Message component's 'online' icon on partner's picture -->
<t t-if="messageView.personaImStatusIconView" position="after">
<div class="o_Activity_iconContainer bg-danger rounded-circle">
<i
class="o_Activity_icon fa fa-exclamation"
role="img"
aria-label="Failed Message Icon"
/>
</div>
</t>
<t t-if="messageView.personaImStatusIconView" position="attributes">
<attribute name="t-if">false</attribute>
</t>
</t>
</templates>

View File

@ -0,0 +1,14 @@
/** @odoo-module **/
import {MessageList} from "@mail/components/message_list/message_list";
import {registerMessagingComponent} from "@mail/utils/messaging_component";
export class FailedMessageList extends MessageList {
_onClickTitle() {
this.messageListView.toggleMessageFailedBoxVisibility();
}
}
FailedMessageList.template = "mail_tracking.FailedMessageList";
registerMessagingComponent(FailedMessageList);

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="mail_tracking.FailedMessageList" owl="1">
<t
t-set="nonReviewedFailedMessageItems"
t-value="messageListView._getNonReviewedFailedMessageItems(
messageListView.messageFailedListViewItems, store.reviewedMessageIds
)"
/>
<div
t-if="messageListView &amp;&amp; nonReviewedFailedMessageItems.length"
class="o_ActivityBox o_Chatter_activityBox"
>
<a
href="#"
class="o_ActivityBox_title btn d-flex align-items-center p-0 w-100 fw-bold"
role="button"
t-att-aria-expanded="messageListView.isMessageFailedBoxVisible"
t-on-click="_onClickTitle"
>
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 me-3" />
<span class="o_ActivityBox_titleText">
<i
class="fa fa-fw"
t-att-class="messageListView.isMessageFailedBoxVisible ? 'fa-caret-down' : 'fa-caret-right'"
/>
Failed messages
</span>
<span
t-if="!messageListView.isMessageFailedBoxVisible"
class="o_ActivityBox_titleBadges ms-2"
>
<span
class="o_ActivityBox_titleBadge me-1 badge text-bg-danger"
t-out="nonReviewedFailedMessageItems.length"
/>
</span>
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 ms-3" />
</a>
<div
class="o_MessageList bg-view d-flex flex-column overflow-auto"
t-att-class="{
'o-empty align-items-center justify-content-center': messageListView.threadViewOwner.messages.length === 0,
'pb-4': messageListView.threadViewOwner.messages.length !== 0
}"
t-attf-class="{{ className }}"
t-on-scroll="onScroll"
t-ref="root"
t-if="messageListView.isMessageFailedBoxVisible"
>
<t
t-foreach="nonReviewedFailedMessageItems"
t-as="messageListViewItem"
t-key="messageListViewItem.localId"
>
<FailedMessage
t-if="(
!messageListViewItem.message.isEmpty
&amp;&amp; messageListViewItem.messageView
)"
record="messageListViewItem.messageView"
isFailedMessage="true"
/>
</t>
</div>
</div>
</t>
</templates>

View File

@ -0,0 +1,70 @@
/** @odoo-module **/
import {Message} from "@mail/components/message/message";
import {patch} from "web.utils";
import {useStore} from "../../client_actions/failed_message_storage.esm";
patch(Message.prototype, "mail_tracking/static/src/components/message/message.esm.js", {
constructor() {
this._super(...arguments);
},
setup() {
this._super(...arguments);
this.store = useStore();
},
_onTrackingStatusClick(event) {
var tracking_email_id = $(event.currentTarget).data("tracking");
event.preventDefault();
return this.env.services.action.doAction({
type: "ir.actions.act_window",
view_type: "form",
view_mode: "form",
res_model: "mail.tracking.email",
views: [[false, "form"]],
target: "new",
res_id: tracking_email_id,
});
},
_addMessageIdToStore(messageID) {
this.store.addMessage(messageID);
},
async _onMarkFailedMessageReviewed(event) {
event.preventDefault();
const messageID = $(event.currentTarget).data("message-id");
const messageNeedsAction = await this._markFailedMessageReviewed(messageID);
// Add the reviewed message ID to storage so it is excluded from the list of rendered messages
if (!messageNeedsAction) {
this._addMessageIdToStore(messageID);
}
},
_onRetryFailedMessage(event) {
event.preventDefault();
const messageID = $(event.currentTarget).data("message-id");
this.env.services.action.doAction("mail.mail_resend_message_action", {
additionalContext: {
mail_message_to_resend: messageID,
},
onClose: async () => {
// Check if message is still 'failed' after Retry, and if it is not, add its ID to storage so
// it is excluded from the list of rendered messages
const failedMessages = await this.messaging.rpc({
model: "mail.message",
method: "get_failed_messages",
args: [[messageID]],
});
const failedMessageIds = failedMessages.map((message) => {
return (message || {}).id;
});
if (failedMessageIds.length && !failedMessageIds.includes(messageID))
this._addMessageIdToStore(messageID);
},
});
},
_markFailedMessageReviewed(id) {
return this.messaging.rpc({
model: "mail.message",
method: "set_need_action_done",
args: [[id]],
});
},
});

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="mail_tracking.MessageTracking"
t-inherit="mail.Message"
t-inherit-mode="extension"
>
<t t-if="messageView" position="attributes">
<attribute
name="t-if"
add="&amp;&amp; (
!messageView.isInFailedDiscuss || (
messageView.isInFailedDiscuss
&amp;&amp; !store.reviewedMessageIds.has(messageView.message.id)
)
)"
separator=" "
/>
</t>
<xpath expr="//div[hasclass('o_Message_header')]" position="inside">
<!-- Show options only for Discuss failed messages and messages from FailedMessage component -->
<span
t-if="!store.reviewedMessageIds.has(messageView.message.id) &amp;&amp; (
(
messageView.message.isFailed
&amp;&amp; messageView.isInDiscuss
) || (
this.props.isFailedMessage
&amp;&amp; messageView.isFailedChatterMessageView
)
)"
t-attf-class="o_thread_icons"
>
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_reviewed o_activity_link"
role="button"
t-on-click="_onMarkFailedMessageReviewed"
t-att-data-message-id="messageView.message.id"
>
<i class="fa fa-check" role="img" aria-label="Set As Reviewed" />
Set as Reviewed
</a>
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_retry o_activity_link"
role="button"
t-on-click="_onRetryFailedMessage"
t-att-data-message-id="messageView.message.id"
>
<i class="fa fa-retweet" role="img" aria-label="Retry" />
Retry
</a>
</span>
</xpath>
<xpath expr="//div[hasclass('o_Message_header')]" position="after">
<p
t-if="messageView.message.hasPartnerTrackings() || messageView.message.hasEmailCc()"
class="o_mail_tracking"
>
<strong>To:</strong>
<t
t-foreach="messageView.message.getPartnerTrackings()"
t-as="tracking"
t-key="tracking_index"
>
<t t-if="!tracking_first">
-
</t>
<a
t-if="tracking['partner_id']"
t-attf-class="o_mail_action_tracking_partner #{tracking['isCc'] ? 'o_mail_cc' : ''}"
t-att-data-partner="tracking['partner_id']"
t-attf-href="#model=res.partner&amp;id={{tracking['partner_id']}}"
t-out="tracking['recipient']"
/>
<span
t-else=""
t-attf-class="#{tracking['isCc'] ? 'o_mail_cc' : ''}"
t-out="tracking['recipient']"
/>
<t
t-if="tracking['status'] === 'error' &amp;&amp; tracking['error_type'] === 'no_recipient'"
t-set="title_status"
t-value="tracking['error_description']"
/>
<t
t-else=""
t-set="title_status"
t-value="tracking['status_human']"
/>
<span
class="mail_tracking o_mail_action_tracking_status"
t-att-data-tracking="tracking['tracking_id']"
t-att-title="title_status"
type="button"
t-on-click="_onTrackingStatusClick"
>
<t t-call="mail_tracking.TrackingStatus" />
</span>
</t>
</p>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,12 @@
/** @odoo-module **/
import {MessageList} from "@mail/components/message_list/message_list";
import {patch} from "web.utils";
import {useStore} from "../../client_actions/failed_message_storage.esm";
patch(MessageList.prototype, "mail_tracking/static/src/js/message_list.esm.js", {
setup() {
this._super(...arguments);
this.store = useStore();
},
});

View File

@ -0,0 +1,5 @@
.o_ActivityThreadView {
.o_MessageList {
color: #000000;
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-inherit="mail.ThreadView" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_ThreadView')]" position="before">
<div
t-if="this.props.className == 'o_Chatter_thread' &amp;&amp; threadView.messageListView"
class="o_ThreadView_core o_ActivityThreadView d-flex flex-column flex-grow-1"
>
<FailedMessageList
className="'o_ThreadView_messageList flex-grow-1'"
record="threadView.messageListView"
/>
</div>
</xpath>
</t>
</templates>

View File

@ -1,348 +0,0 @@
/* Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
// FIXME: More of these classes are cloned from other scss files.
.o_mail_failed_message {
&.o_field_widget {
display: block;
}
.o_thread_date_separator.o_border_dashed {
border-bottom-style: dashed;
&[data-toggle="collapse"] {
cursor: pointer;
.o_chatter_failed_message_summary {
display: none;
}
&.collapsed {
margin-bottom: 0;
transition: margin 0.8s ease 0s;
.o_chatter_failed_message_summary {
display: inline-block;
span {
padding: 0 5px;
border-radius: 100%;
font-size: 11px;
}
}
i.fa-caret-down:before {
content: "\f0da";
}
}
}
}
.o_thread_show_more {
text-align: center;
}
.o_mail_thread_content {
display: flex;
flex-direction: column;
min-height: 100%;
}
.o_thread_bottom_free_space {
height: 15px;
}
.o_thread_typing_notification_free_space {
flex-grow: 1;
}
.o_thread_typing_notification_bar {
flex: 0, 0, 20px;
background-color: rgba($white, 0.75);
padding: 5px;
text-align: center;
color: gray("600");
&.o_thread_order_asc {
@include o-position-sticky($bottom: 0px);
}
&.o_thread_order_desc {
@include o-position-sticky($top: 0px);
}
}
.o_thread_tooltip_container {
display: inline;
position: relative;
}
.o_thread_date_separator {
margin-top: 15px;
margin-bottom: 30px;
@include media-breakpoint-down(sm) {
margin-top: 0px;
margin-bottom: 15px;
}
border-bottom: 1px solid gray("400");
text-align: center;
.o_thread_date {
position: relative;
top: 10px;
margin: 0 auto;
padding: 0 10px;
font-weight: bold;
background: white;
}
}
.o_thread_new_messages_separator {
margin-bottom: 15px;
border-bottom: solid lighten($o-brand-odoo, 15%) 1px;
text-align: right;
.o_thread_separator_label {
position: relative;
top: 8px;
padding: 0 10px;
background: white;
color: lighten($o-brand-odoo, 15%);
font-size: smaller;
}
}
.o_thread_message {
display: flex;
padding: 4px $o-horizontal-padding;
margin-bottom: 0px;
&.o_mail_not_discussion {
background-color: rgba(map-get($grays, "300"), 0.5);
border-bottom: 1px solid map-get($grays, "400");
}
.o_thread_message_sidebar {
flex: 0 0 $o-mail-thread-avatar-size;
margin-right: 10px;
margin-top: 2px;
text-align: center;
font-size: smaller;
@include media-breakpoint-down(sm) {
margin-top: 4px;
font-size: x-small;
}
.o_thread_message_avatar {
max-width: $o-mail-thread-avatar-size;
}
.o_thread_message_side_date {
margin-left: -5px;
}
.o_thread_message_star {
margin-right: -5px;
}
.o_thread_message_side_date {
opacity: 0;
}
}
.o_thread_icon {
cursor: pointer;
opacity: 0;
&.fa-star {
opacity: $o-mail-thread-icon-opacity;
color: gold;
}
}
&:hover,
&.o_thread_selected_message {
.o_thread_message_side_date {
opacity: $o-mail-thread-side-date-opacity;
}
.o_thread_icon {
opacity: $o-mail-thread-icon-opacity;
&:hover {
opacity: 1;
}
}
}
.o_mail_redirect {
cursor: pointer;
}
.o_thread_message_core {
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
word-wrap: break-word;
> pre {
white-space: pre-wrap;
word-break: break-word;
text-align: justify;
}
.o_mail_subject {
font-style: italic;
}
.o_mail_notification {
font-style: italic;
color: gray;
}
[summary~="o_mail_notification"] {
// name conflicts with channel notifications, but is odoo notification buttons to hide in chatter if present
display: none;
}
p {
margin: 0 0 9px; // Required by the old design to override a general rule on p's
&:last-child {
margin-bottom: 0;
}
}
a {
display: inline-block;
word-break: break-all;
}
:not(.o_image_box) > img {
max-width: 100%;
height: auto;
}
.o_mail_body_long {
display: none;
}
.o_mail_info {
margin-bottom: 2px;
strong {
color: $headings-color;
}
}
.o_thread_message_star,
.o_thread_message_needaction,
.o_thread_message_reply,
.o_thread_message_email {
padding: 4px;
}
i.o_thread_message_email {
&.o_thread_message_email_ready {
color: grey;
}
&.o_thread_message_email_exception,
&.o_thread_message_email_bounce {
color: red;
opacity: 1;
cursor: pointer;
}
}
.o_attachments_list,
.o_attachments_previews {
&:last-child {
margin-bottom: $grid-gutter-width;
}
}
.o_thread_tooltip_container {
display: inline;
position: relative;
}
}
}
.o_thread_title {
margin-top: 20px;
margin-bottom: 20px;
font-weight: bold;
font-size: 125%;
}
.o_mail_no_content {
@include o-position-absolute(30%, 0, 0, 0);
text-align: center;
font-size: 115%;
}
.o_thread_message .o_thread_message_core .o_mail_read_more {
display: block;
}
#o_chatter_failed_message {
.o_thread_message {
.o_thread_message_sidebar {
.o_avatar_stack {
position: relative;
text-align: left;
margin-bottom: 8px;
img {
width: 31px;
height: 31px;
}
.o_avatar_icon {
@include o-position-absolute($right: -5px, $bottom: -5px);
width: 25px;
height: 25px;
padding: 6px 5px;
text-align: center;
line-height: 1.2;
color: white;
border-radius: 100%;
border: 2px solid white;
}
}
}
.o_mail_info {
.o_activity_info {
vertical-align: baseline;
padding: 4px 6px;
background: theme-color("light");
border-radius: 2px 2px 0 0;
@include o-hover-opacity(1, 1);
&.collapsed {
@include o-hover-opacity(0.5, 1);
background: transparent;
}
}
}
.o_thread_message_collapse .dl-horizontal.card {
display: inline-block;
margin-bottom: 0;
dt {
max-width: 80px;
}
dd {
margin-left: 95px;
}
}
.o_thread_message_note {
margin: 2px 0 5px;
padding: 0px;
}
.o_thread_message_warning {
margin: 2px 0 5px;
}
.o_thread_message_tools {
.o_failed_message_link {
padding: 0 $input-btn-padding-x;
}
.o_failed_message_retry {
padding-left: 0;
}
}
}
}
}

View File

@ -1,57 +0,0 @@
/** @odoo-module **/
import {registerMessagingComponent} from "@mail/utils/messaging_component";
const {Component} = owl;
export class MessageFailedBox extends Component {
_onClickTitle() {
this.chatter.toggleMessageFailedBoxVisibility();
}
_markFailedMessageReviewed(id) {
return this.env.services.rpc({
model: "mail.message",
method: "set_need_action_done",
args: [[id]],
});
}
_onRetryFailedMessage(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
const thread = this.chatter.thread;
var self = this;
this.env.bus.trigger("do-action", {
action: "mail.mail_resend_message_action",
options: {
additional_context: {
mail_message_to_resend: messageID,
},
on_close: () => {
self.trigger("reload", {keepChanges: true});
thread.refresh();
},
},
});
}
_onMarkFailedMessageReviewed(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
this._markFailedMessageReviewed(messageID);
this.trigger("reload", {keepChanges: true});
this.chatter.thread.refreshMessagefailed();
this.chatter.thread.refresh();
}
/**
* @returns {Chatter}
*/
get chatter() {
return this.messaging.models["mail.chatter"].get(this.props.chatterLocalId);
}
}
Object.assign(MessageFailedBox, {
props: {record: Object},
template: "mail_tracking.MessageFailedBox",
});
registerMessagingComponent(MessageFailedBox);

View File

@ -1,58 +0,0 @@
/** @odoo-module **/
import {
registerFieldPatchModel,
registerInstancePatchModel,
} from "@mail/model/model_core";
import {one2many} from "@mail/model/model_field";
registerInstancePatchModel(
"mail.thread",
"mail_tracking/static/src/js/failed_message/thread.esm.js",
{
async refreshMessagefailed() {
var id = this.__values.id;
var model = this.__values.model;
const messagefailedData = await this.async(() =>
this.env.services.rpc(
{
model: "mail.message",
method: "get_failed_messsage_info",
args: [id, model],
},
{
shadow: true,
}
)
);
const messagefailed = this.messaging.models["mail.message.failed"].insert(
messagefailedData.map((messageData) =>
this.messaging.models["mail.message.failed"].convertData(
messageData
)
)
);
this.update({
messagefailed: [["replace", messagefailed]],
});
},
_computeFetchMessagesUrl() {
switch (this) {
case this.messaging.failedmsg:
return "/mail/failed/messages";
}
return this._super();
},
}
);
registerFieldPatchModel(
"mail.thread",
"mail_tracking/static/src/js/failed_message/thread.esm.js",
{
messagefailed: one2many("mail.message.failed", {
inverse: "thread",
}),
}
);

View File

@ -1,55 +0,0 @@
/** @odoo-module **/
import {Message} from "@mail/components/message/message";
import {patch} from "web.utils";
patch(Message.prototype, "mail_tracking/static/src/js/message.esm.js", {
constructor() {
this._super(...arguments);
},
_onTrackingStatusClick(event) {
var tracking_email_id = $(event.currentTarget).data("tracking");
event.preventDefault();
return this.env.bus.trigger("do-action", {
action: {
type: "ir.actions.act_window",
view_type: "form",
view_mode: "form",
res_model: "mail.tracking.email",
views: [[false, "form"]],
target: "new",
res_id: tracking_email_id,
},
});
},
// For discuss
_onMarkFailedMessageReviewed(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
this._markFailedMessageReviewed(messageID);
window.location.reload();
},
_onRetryFailedMessage(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
this.env.bus.trigger("do-action", {
action: "mail.mail_resend_message_action",
options: {
additional_context: {
mail_message_to_resend: messageID,
},
on_close: () => {
window.location.reload();
},
},
});
},
_markFailedMessageReviewed(id) {
return this.env.services.rpc({
model: "mail.message",
method: "set_need_action_done",
args: [[id]],
});
},
});

View File

@ -1,28 +0,0 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Chatter",
modelMethods: {
async refresh() {
this._super(...arguments);
this.thread.refreshMessagefailed();
},
toggleMessageFailedBoxVisibility() {
this.update({
isMessageFailedBoxVisible: !this.isMessageFailedBoxVisible,
});
},
_onThreadIdOrThreadModelChanged() {
this._super(...arguments);
this.thread.refreshMessagefailed();
},
},
fields: {
isMessageFailedBoxVisible: attr({
default: true,
}),
},
});

View File

@ -1,22 +0,0 @@
/** @odoo-module **/
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "DiscussSidebarMailboxView",
fields: {
discussViewOwnerAsFailedmsg: one("DiscussView", {
identifying: true,
inverse: "failedmsgView",
}),
mailbox: {
compute() {
if (this.discussViewOwnerAsFailedmsg) {
return this.messaging.failedmsg;
}
return this._super();
},
},
},
});

View File

@ -1,36 +0,0 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Message",
modelMethods: {
convertData(data) {
const data2 = this._super(data);
if ("partner_trackings" in data) {
console.log(data.partner_trackings);
data2.partner_trackings = data.partner_trackings;
}
return data2;
},
},
recordMethods: {
hasPartnerTrackings() {
return _.some(this.__values.get("partner_trackings"));
},
hasEmailCc() {
return _.some(this._emailCc);
},
getPartnerTrackings: function () {
if (!this.hasPartnerTrackings()) {
return [];
}
return this.__values.get("partner_trackings");
},
},
fields: {
partner_trackings: attr(),
},
});

View File

@ -1,22 +0,0 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Message",
modelMethods: {
convertData(data) {
const data2 = this._super(data);
if ("is_failed_message" in data) {
data2.isFailed = data.is_failed_message;
}
return data2;
},
},
fields: {
isFailed: attr({
default: false,
}),
},
});

View File

@ -1,52 +0,0 @@
/** @odoo-module **/
import {attr, many} from "@mail/model/model_field";
import {registerModel} from "@mail/model/model_core";
registerModel({
name: "MessageFailed",
modelMethods: {
/**
* @param {Object} data
* @returns {Object}
*/
convertData(data) {
const data2 = {};
if ("author" in data) {
if (!data.author) {
data2.author = [["unlink-all"]];
} else if (data.author) {
data2.author = data.author[1];
data2.author_id = data.author[0];
}
}
if ("body" in data) {
data2.body = data.body;
}
if ("date" in data) {
data2.date = data.date;
}
if ("failed_recipients" in data) {
data2.failed_recipients = data.failed_recipients;
}
if ("id" in data) {
data2.id = data.id;
}
return data2;
},
},
fields: {
thread: many("Thread", {
inverse: "messagefailed",
}),
body: attr(),
author: attr(),
author_id: attr(),
date: attr(),
failed_recipients: attr(),
id: attr({
readonly: true,
required: true,
}),
},
});

View File

@ -1,50 +0,0 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageView",
recordMethods: {
_onTrackingStatusClick(event) {
var tracking_email_id = $(event.currentTarget).data("tracking");
event.preventDefault();
return this.env.services.action.doAction({
type: "ir.actions.act_window",
view_type: "form",
view_mode: "form",
res_model: "mail.tracking.email",
views: [[false, "form"]],
target: "new",
res_id: tracking_email_id,
});
},
_onMarkFailedMessageReviewed(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
this._markFailedMessageReviewed(messageID);
window.location.reload();
},
_onRetryFailedMessage(event) {
event.preventDefault();
var messageID = $(event.currentTarget).data("message-id");
this.env.services.action.doAction({
action: "mail.mail_resend_message_action",
options: {
additional_context: {
mail_message_to_resend: messageID,
},
on_close: () => {
window.location.reload();
},
},
});
},
_markFailedMessageReviewed(id) {
return this.env.services.rpc({
model: "mail.message",
method: "set_need_action_done",
args: [[id]],
});
},
},
});

View File

@ -1,17 +0,0 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessagingInitializer",
recordMethods: {
async _init({starred_counter = 0}) {
await this._super(...arguments);
this._initMailboxesFailed({starred_counter});
},
_initMailboxesFailed({failedmsg_counter}) {
this.messaging.failedmsg.update({counter: failedmsg_counter});
},
},
});

View File

@ -1,16 +0,0 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "ThreadCache",
recordMethods: {
_extendMessageDomain(domain) {
const thread = this.thread;
if (thread === this.env.messaging.failedmsg) {
return domain.concat([["is_failed_message", "=", true]]);
}
return this._super(...arguments);
},
},
});

View File

@ -0,0 +1,17 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Chatter",
recordMethods: {
async refresh() {
this._super(...arguments);
if (this.thread) this.thread.refreshMessagefailed();
},
_onThreadIdOrThreadModelChanged() {
this._super(...arguments);
if (this.thread) this.thread.refreshMessagefailed();
},
},
});

View File

@ -0,0 +1,33 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "DiscussSidebarMailboxView",
recordMethods: {
_getNonReviewedFailedMessages(messages, reviewedMessageIds) {
if (!messages.length) return [];
return messages.filter((message) => !reviewedMessageIds.has(message.id));
},
},
fields: {
discussViewOwnerAsFailedMessage: one("DiscussView", {
identifying: true,
inverse: "failedMessageView",
}),
mailbox: {
compute() {
if (this.discussViewOwnerAsFailedMessage) {
return this.messaging.failedmsg;
}
return this._super();
},
},
isFailedDiscussSidebarMailboxView: attr({
compute() {
return Boolean(this.discussViewOwnerAsFailedMessage);
},
}),
},
});

View File

@ -6,9 +6,9 @@ import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "DiscussView",
fields: {
failedmsgView: one("DiscussSidebarMailboxView", {
failedMessageView: one("DiscussSidebarMailboxView", {
default: {},
inverse: "discussViewOwnerAsFailedmsg",
inverse: "discussViewOwnerAsFailedMessage",
}),
},
});

View File

@ -1,6 +1,5 @@
/** @odoo-module **/
import {clear} from "@mail/model/model_field_command";
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
@ -11,12 +10,12 @@ registerPatch({
compute() {
switch (this) {
case this.messaging.failedmsg:
return "/mail/failedmsg/messages";
return "/mail/failed/messages";
}
return this._super();
},
},
messagingAsFailedmsg: one("Messaging", {
messagingAsFailed: one("Messaging", {
identifying: true,
inverse: "failedmsg",
}),
@ -24,7 +23,7 @@ registerPatch({
compute() {
switch (this) {
case this.messaging.failedmsg:
return this.env._t("Failedmsg");
return this.env._t("Failed");
}
return this._super();
},
@ -33,7 +32,7 @@ registerPatch({
compute() {
switch (this) {
case this.messaging.failedmsg:
return 4;
return 3;
}
return this._super();
},
@ -45,10 +44,9 @@ registerPatch({
case this.messaging.failedmsg:
return "failedmsg";
}
return this._super();
})();
if (!threadId) {
return clear();
return this._super();
}
return {
id: threadId,

View File

@ -0,0 +1,64 @@
/** @odoo-module **/
import {attr, one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "Message",
modelMethods: {
convertData(data) {
const data2 = this._super(data);
if ("partner_trackings" in data) {
data2.partner_trackings = data.partner_trackings;
}
if ("is_failed_message" in data) {
data2.isFailed = data.is_failed_message;
}
if ("failed_recipients" in data) {
data2.failedRecipients = data.failed_recipients;
}
if ("is_failed_chatter_message" in data) {
data2.isFailedChatterMessage = data.is_failed_chatter_message;
}
return data2;
},
},
recordMethods: {
hasPartnerTrackings() {
return _.some(this.__values.get("partner_trackings"));
},
hasEmailCc() {
return _.some(this._emailCc);
},
getPartnerTrackings: function () {
if (!this.hasPartnerTrackings()) {
return [];
}
return this.__values.get("partner_trackings");
},
},
fields: {
partner_trackings: attr(),
threads: {
compute() {
const threads = this._super();
if (this.isFailed && this.messaging.failedmsg) {
threads.push(this.messaging.failedmsg.thread);
}
return threads;
},
},
messagingFailedmsg: one("Mailbox", {
related: "messaging.failedmsg",
}),
isFailed: attr({
default: false,
}),
failedRecipients: attr(),
isFailedChatterMessage: attr({
default: false,
}),
},
});

View File

@ -0,0 +1,33 @@
/** @odoo-module **/
import {attr, many} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageListView",
recordMethods: {
toggleMessageFailedBoxVisibility() {
this.update({
isMessageFailedBoxVisible: !this.isMessageFailedBoxVisible,
});
},
_getNonReviewedFailedMessageItems(messageItems, reviewedMessageIds) {
if (!messageItems.length) return [];
return messageItems.filter(
(item) => !reviewedMessageIds.has(item.message.id)
);
},
},
fields: {
isMessageFailedBoxVisible: attr({
default: true,
}),
messageFailedListViewItems: many("MessageListViewItem", {
compute() {
return this.messageListViewItems.filter(
(messageListViewItem) => messageListViewItem.isFailedChatterMessage
);
},
}),
},
});

View File

@ -0,0 +1,15 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageListViewItem",
fields: {
isFailedChatterMessage: attr({
compute() {
return this.message.isFailedChatterMessage;
},
}),
},
});

View File

@ -0,0 +1,31 @@
/** @odoo-module **/
import {attr} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessageView",
recordMethods: {
doNothing() {
return true;
},
},
fields: {
isInFailedDiscuss: attr({
compute() {
const discuss =
this.messageListViewItemOwner &&
this.messageListViewItemOwner.messageListViewOwner.threadViewOwner
.threadViewer.discuss;
return Boolean(
discuss && discuss.threadView.thread.mailbox.messagingAsFailed
);
},
}),
isFailedChatterMessageView: attr({
compute() {
return this.message.isFailedChatterMessage;
},
}),
},
});

View File

@ -8,7 +8,7 @@ registerPatch({
fields: {
failedmsg: one("Mailbox", {
default: {},
inverse: "messagingAsFailedmsg",
inverse: "messagingAsFailed",
}),
},
});

View File

@ -0,0 +1,17 @@
/** @odoo-module **/
import {registerPatch} from "@mail/model/model_core";
registerPatch({
name: "MessagingInitializer",
recordMethods: {
async _init({failed_counter = 0}) {
await this._super(...arguments);
this._initFailedMailboxes(failed_counter);
},
_initFailedMailboxes(failed_counter) {
this.messaging.failedmsg.update({counter: failed_counter});
},
},
});

View File

@ -1,6 +1,5 @@
/** @odoo-module **/
import {one} from "@mail/model/model_field";
import {registerPatch} from "@mail/model/model_core";
registerPatch({
@ -19,19 +18,15 @@ registerPatch({
shadow: true,
}
);
const messagefailed = this.messaging.models.MessageFailed.insert(
/* Create failed Message records; these will be updated when fetching
their usual Message data and assigned to their respective threads. */
this.messaging.models.Message.insert(
messagefailedData.map((messageData) =>
this.messaging.models.MessageFailed.convertData(messageData)
this.messaging.models.Message.convertData(
Object.assign(messageData, {is_failed_chatter_message: true})
)
)
);
this.update({
messagefailed: [["replace", messagefailed]],
});
},
},
fields: {
messagefailed: one("MessageFailed", {
inverse: "thread",
}),
},
});

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-extend="mail.widget.Thread.Message">
<t t-jquery="span[t-attf-class=o_thread_icons]" t-operation="append">
<a
t-if="message.isFailed() &amp;&amp; options.displayRetryButton"
class="btn btn-link o_thread_icon btn-default text-muted btn-sm o_failed_message_reviewed o_activity_link mr8"
t-att-data-message-id="message.getID()"
>
<i class="fa fa-check" /> Set as Reviewed
</a>
<a
t-if="message.isFailed() &amp;&amp; options.displayReviewedButton"
class="btn btn-link o_thread_icon btn-default text-muted btn-sm o_failed_message_retry"
t-att-data-message-id="message.getID()"
>
<i class="fa fa-retweet" /> Retry
</a>
</t>
</t>
</templates>

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
<xpath
expr="//t[@t-elif='thread.mailbox === messaging.history']"
position="after"
>
<t t-elif="thread === messaging.failedmsg">
<div class="o_ThreadIcon_mailboxModeration fa fa-exclamation" />
</t>
</xpath>
</t>
<t t-inherit="mail.DiscussSidebar" t-inherit-mode="extension">
<xpath
expr="//div[hasclass('o_DiscussSidebar_categoryMailbox')]"
position="inside"
>
<DiscussSidebarMailbox record="discussView.failedmsgView" />
</xpath>
</t>
<t t-inherit="mail.MessageList" t-inherit-mode="extension">
<t
t-elif="messageListView.threadViewOwner.thread === messaging.history.thread"
position="after"
>
<t
t-if="messageListView.threadViewOwner.thread === messaging.failedmsg.thread"
>
<div class="o_MessageList_emptyTitle o-neutral-face-icon">
No failed messages
</div>
Failed messages will be appeared here.
</t>
</t>
</t>
<t t-inherit="mail.Message" t-inherit-mode="extension">
<xpath expr="//small[hasclass('o_Message_originThread')]" position="inside">
<t t-if="messageView.message.isFailed">
<span t-attf-class="o_thread_icons">
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_reviewed o_activity_link"
t-on-click="_onMarkFailedMessageReviewed"
t-att-data-message-id="messageView.message.id"
>
<i class="fa fa-check" />
Set as Reviewed
</a>
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_retry o_activity_link"
t-on-click="_onRetryFailedMessage"
t-att-data-message-id="messageView.message.id"
>
<i class="fa fa-retweet" />
Retry
</a>
</span>
</t>
</xpath>
</t>
</templates>

View File

@ -1,128 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="mail_tracking.MessageFailedBox" owl="1">
<div class="o_ActivityBox">
<t
t-if="chatter and chatter.thread and chatter.thread.messagefailed.length > 0"
>
<a role="button" class="o_ActivityBox_title btn" t-on-click="_onClickTitle">
<hr class="o_ActivityBox_titleLine" />
<span class="o_ActivityBox_titleText">
<i
class="fa fa-fw"
t-att-class="chatter.isMessageFailedBoxVisible ? 'fa-caret-down' : 'fa-caret-right'"
/>
Failed messages
</span>
<t t-if="!chatter.isMessageFailedBoxVisible">
<span class="o_ActivityBox_titleBadges">
<t t-if="chatter.thread.messagefailed.length > 0">
<span
class="o_ActivityBox_titleBadge badge rounded-circle badge-danger"
>
<t t-esc="chatter.thread.messagefailed.length" />
</span>
</t>
</span>
</t>
<hr class="o_ActivityBox_titleLine" />
</a>
<t
t-if="chatter.isMessageFailedBoxVisible and chatter.thread.messagefailed"
>
<div class="o_ActivityList">
<t
t-foreach="chatter.thread.messagefailed"
t-as="messagefailed"
t-key="messagefailed.localId"
>
<div class="o_Activity">
<t t-if="messagefailed">
<div class="o_Activity_sidebar">
<div class="o_Activity_user">
<t t-if="messagefailed.author">
<img
class="o_Activity_userAvatar"
t-attf-src="/web/image/res.partner/{{ messagefailed.author_id }}/avatar_128"
t-att-alt="messagefailed.author"
/>
</t>
<div class="o_Activity_iconContainer bg-danger">
<i class="o_Activity_icon fa fa-exclamation" />
</div>
</div>
</div>
<div class="o_Activity_core">
<div class="o_Activity_info">
<div class="o_Activity_dueDateText" t-att-class="{}">
<t t-esc="messagefailed.author" />
</div>
<t t-if="messagefailed.date">
<div class="o_Activity_summary text-muted">
<t t-esc="messagefailed.date" />
</div>
</t>
<span t-attf-class="o_thread_icons">
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_reviewed o_activity_link"
t-on-click="_onMarkFailedMessageReviewed"
t-att-data-message-id="messagefailed.id"
>
<i class="fa fa-check" /> Set as Reviewed
</a>
<a
href="#"
class="btn btn-link btn-success o_thread_icon text-muted btn-sm o_failed_message_retry o_activity_link"
t-on-click="_onRetryFailedMessage"
t-att-data-message-id="messagefailed.id"
>
<i class="fa fa-retweet" /> Retry
</a>
</span>
</div>
<div class="o_Activity_info">
<strong class="text-danger">Failed Recipients:</strong>
<t
t-foreach="messagefailed.failed_recipients"
t-as="recipient"
t-key="recipient[0]"
>
<t t-if="!recipient_first">
-
</t>
<a
class="o_mail_action_tracking_partner"
t-att-data-partner="recipient[1]"
t-attf-href="#model=res.partner&amp;id={{recipient[0]}}"
>
<t t-esc="recipient[1]" />
</a>
</t>
</div>
<div class="o_thread_message_note small">
<t t-out="messagefailed.body" />
</div>
</div>
</t>
</div>
</t>
</div>
</t>
</t>
</div>
</t>
<t t-inherit="mail.Chatter" t-inherit-mode="extension">
<xpath
expr="//div[hasclass('o_Chatter_scrollPanel')]/t[@t-if='chatter.attachmentBoxView']"
position="before"
>
<t t-if="chatter.activityBoxView">
<MessageFailedBox
className="'o_Chatter_activityBox'"
record="chatter.activityBoxView"
/>
</t>
</xpath>
</t>
</templates>

View File

@ -1,116 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<template>
<t t-name="mail.tracking.status" owl="1">
<t t-if="tracking['isCc']">
<span class="mail_tracking_cc">
<i class="fa fa-cc" />
</span>
</t>
<t t-elif="!tracking['isCc'] &amp;&amp; !tracking['partner_id']">
<span class="mail_anon_recipient">
<i class="fa fa-low-vision" />
</span>
</t>
<t t-elif="tracking['status'] === 'unknown'">
<span class="mail_tracking_unknown">
<i class="fa fa-ban" />
</span>
</t>
<t t-elif="tracking['status'] === 'waiting'">
<span class="mail_tracking_waiting mail_tracking_pointer">
<i class="fa fa-clock-o" />
</span>
</t>
<t t-elif="tracking['status'] === 'error'">
<span class="mail_tracking_error mail_tracking_pointer">
<i
t-if="tracking['error_type'] === 'no_recipient'"
class="fa fa-user-times"
/>
<i t-else="" class="fa fa-remove" />
</span>
</t>
<t t-elif="tracking['status'] === 'sent'">
<span class="mail_tracking_sent mail_tracking_pointer">
<i class="fa fa-check" />
</span>
</t>
<t t-elif="tracking['status'] === 'delivered'">
<span class="fa-stack mail_tracking_delivered mail_tracking_pointer">
<i class="fa fa-check fa-stack-1x" style="margin-left:1px" />
<i class="fa fa-check fa-inverse fa-stack-1x" style="margin-left:-2px;" />
<i class="fa fa-check fa-stack-1x" style="margin-left:-3px" />
</span>
</t>
<t t-elif="tracking['status'] === 'opened'">
<span class="fa-stack mail_tracking_opened mail_tracking_pointer">
<i class="fa fa-check fa-stack-1x" style="margin-left:1px" />
<i class="fa fa-check fa-inverse fa-stack-1x" style="margin-left:-2px;" />
<i class="fa fa-check fa-stack-1x" style="margin-left:-3px" />
</span>
</t>
</t>
<t
t-name="mail.MessageTracking"
t-inherit="mail.Message"
t-inherit-mode="extension"
owl="1"
>>
<xpath expr="//div[hasclass('o_Message_header')]" position="after">
<t
t-if="messageView.message.hasPartnerTrackings() || messageView.message.hasEmailCc()"
>
<p class="o_mail_tracking">
<strong>To:</strong>
<t
t-foreach="messageView.message.getPartnerTrackings()"
t-as="tracking"
t-key="tracking.tracking_delta"
>
<t t-if="!tracking_first">
-
</t>
<t t-if="tracking['partner_id']">
<a
t-attf-class="o_mail_action_tracking_partner #{tracking['isCc'] ? 'o_mail_cc' : ''}"
t-att-data-partner="tracking['partner_id']"
t-attf-href="#model=res.partner&amp;id={{tracking['partner_id']}}"
>
<t t-esc="tracking['recipient']" />
</a>
</t>
<t t-else="">
<span t-attf-class="#{tracking['isCc'] ? 'o_mail_cc' : ''}"><t
t-esc="tracking['recipient']"
/></span>
</t>
<t
t-if="tracking['status'] === 'error' &amp;&amp; tracking['error_type'] === 'no_recipient'"
t-set="title_status"
t-value="tracking['error_description']"
/>
<t
t-else=""
t-set="title_status"
t-value="tracking['status_human']"
/>
<span
class="mail_tracking o_mail_action_tracking_status"
t-att-data-tracking="tracking['tracking_id']"
t-att-title="title_status"
type="button"
t-on-click="messageView._onTrackingStatusClick"
>
<t t-call="mail.tracking.status" />
</span>
</t>
</p>
</t>
</xpath>
</t>
</template>

View File

@ -3,15 +3,16 @@
import base64
import time
from unittest import mock
from unittest.mock import patch
import psycopg2
import psycopg2.errorcodes
from werkzeug.exceptions import BadRequest
from odoo import http
from odoo.fields import Command
from odoo.tests.common import TransactionCase
from odoo.tools.misc import mute_logger
from odoo.tools import mute_logger
from ..controllers.discuss import MailTrackingDiscussController
from ..controllers.main import BLANK, MailTrackingController
mock_send_email = "odoo.addons.base.models.ir_mail_server." "IrMailServer.send_email"
@ -51,6 +52,10 @@ class TestMailTracking(TransactionCase):
),
},
)
for _ in http._generate_routing_rules(
["mail", "mail_tracking"], nodb_only=False
):
pass
def tearDown(self, *args, **kwargs):
http.request = self.last_request
@ -84,7 +89,7 @@ class TestMailTracking(TransactionCase):
"message_type": "comment",
"model": "res.partner",
"res_id": self.recipient.id,
"partner_ids": [(4, self.recipient.id)],
"partner_ids": [Command.link(self.recipient.id)],
"body": "<p>This is a test message</p>",
}
)
@ -102,7 +107,7 @@ class TestMailTracking(TransactionCase):
self.assertEqual(tracking_email.state, "sent")
# message_dict read by web interface
message_dict = message.message_format()[0]
self.assertTrue(len(message_dict["history_partner_ids"]) > 0)
self.assertTrue(message_dict["history_partner_ids"])
# First partner is recipient
partner_id = message_dict["history_partner_ids"][0]
self.assertEqual(partner_id, self.recipient.id)
@ -135,7 +140,7 @@ class TestMailTracking(TransactionCase):
"message_type": "comment",
"model": "res.partner",
"res_id": self.recipient.id,
"partner_ids": [(4, self.recipient.id)],
"partner_ids": [Command.link(self.recipient.id)],
"body": "<p>This is a test message</p>",
}
)
@ -201,7 +206,7 @@ class TestMailTracking(TransactionCase):
"message_type": "comment",
"model": "res.partner",
"res_id": self.recipient.id,
"partner_ids": [(4, self.recipient.id)],
"partner_ids": [Command.link(self.recipient.id)],
"email_cc": "Dominique Pinon <unnamed@test.com>, sender@example.com"
", recipient@example.com",
"body": "<p>This is another test message</p>",
@ -255,7 +260,7 @@ class TestMailTracking(TransactionCase):
"message_type": "comment",
"model": "res.partner",
"res_id": self.recipient.id,
"partner_ids": [(4, self.recipient.id)],
"partner_ids": [Command.link(self.recipient.id)],
"email_to": "Dominique Pinon <support+unnamed@test.com>"
", sender@example.com, recipient@example.com"
", TheCatchall@test.com",
@ -318,7 +323,7 @@ class TestMailTracking(TransactionCase):
"message_type": "comment",
"model": "res.partner",
"res_id": self.recipient.id,
"partner_ids": [(4, self.recipient.id)],
"partner_ids": [Command.link(self.recipient.id)],
"body": "<p>This is a test message</p>",
}
)
@ -364,6 +369,7 @@ class TestMailTracking(TransactionCase):
)
return mail, tracking_email
@mute_logger("odoo.addons.mail_tracking.controllers.main")
def test_mail_send(self):
controller = MailTrackingController()
db = self.env.cr.dbname
@ -371,7 +377,7 @@ class TestMailTracking(TransactionCase):
mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender)
with mock.patch("odoo.http.db_filter") as mock_client:
with patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True
res = controller.mail_tracking_open(db, tracking.id, tracking.token)
self.assertEqual(image, res.response[0])
@ -383,10 +389,14 @@ class TestMailTracking(TransactionCase):
# Two events again because no tracking_email_id found for False
self.assertEqual(2, len(tracking.tracking_event_ids))
@mute_logger("odoo.addons.mail_tracking.controllers.main")
def test_mail_tracking_open(self):
def mock_error_function(*args, **kwargs):
raise Exception()
controller = MailTrackingController()
db = self.env.cr.dbname
with mock.patch("odoo.http.db_filter") as mock_client:
with patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True
mail, tracking = self.mail_send(self.recipient.email)
# Tracking is in sent or delivered state. But no token give.
@ -416,6 +426,30 @@ class TestMailTracking(TransactionCase):
# Generates tracking event
controller.mail_tracking_open(db, tracking.id, False)
self.assertEqual(2, len(tracking.tracking_event_ids))
# Purposely trigger an error during mail_tracking_open
# flow (to increase coverage)
with patch(
"odoo.addons.mail_tracking.models.mail_tracking_email.MailTrackingEmail.search",
wraps=mock_error_function,
):
controller.mail_tracking_open(db, tracking.id, False)
# Purposely trigger an error during db_env (to increase coverage)
with patch("odoo.http.db_filter") as mock_client, self.assertRaises(BadRequest):
mock_client.return_value = False
controller.mail_tracking_open(db, tracking.id, False)
def test_db_env_no_cr(self):
http.request.cr = None
db = self.env.cr.dbname
controller = MailTrackingController()
# Cast Cursor to Mock object to avoid raising 'Cursor not closed explicitly' log
with patch("odoo.sql_db.db_connect"), patch(
"odoo.http.db_filter"
) as mock_client:
mock_client.return_value = True
mail, tracking = self.mail_send(self.recipient.email)
response = controller.mail_tracking_open(db, tracking.id, False)
self.assertEqual(response.status_code, 200)
def test_concurrent_open(self):
mail, tracking = self.mail_send(self.recipient.email)
@ -476,7 +510,7 @@ class TestMailTracking(TransactionCase):
@mute_logger("odoo.addons.mail.models.mail_mail")
def test_smtp_error(self):
with mock.patch(mock_send_email) as mock_func:
with patch(mock_send_email) as mock_func:
mock_func.side_effect = Warning("Test error")
mail, tracking = self.mail_send(self.recipient.email)
self.assertEqual("error", tracking.state)
@ -573,22 +607,10 @@ class TestMailTracking(TransactionCase):
trackings |= tracking
self.assertEqual(100.0, trackings.email_score())
def test_db(self):
db = self.env.cr.dbname
controller = MailTrackingController()
with mock.patch("odoo.http.db_filter") as mock_client:
mock_client.return_value = True
with self.assertRaises(psycopg2.OperationalError):
controller.mail_tracking_event("not_found_db")
none = controller.mail_tracking_event(db)
self.assertEqual(b"NONE", none.response[0])
none = controller.mail_tracking_event(db, "open")
self.assertEqual(b"NONE", none.response[0])
def test_bounce_tracking_event_created(self):
mail, tracking = self.mail_send(self.recipient.email)
message = self.env.ref("mail.mail_message_channel_1_1")
message.mail_tracking_ids = [(4, tracking.id, False)]
message.mail_tracking_ids = [Command.link(tracking.id)]
mail.mail_message_id = message
message_dict = {
"bounced_email": "test@test.net",
@ -628,7 +650,7 @@ class TestMailTracking(TransactionCase):
msg = "data-odoo-tracking-email found"
raise AssertionError(msg)
with mock.patch(mock_send_email) as mock_func:
with patch(mock_send_email) as mock_func:
mock_func.side_effect = assert_tracking_tag_side_effect
self.env["ir.config_parameter"].set_param(
"mail_tracking.tracking_img_disabled", False
@ -647,3 +669,38 @@ class TestMailTracking(TransactionCase):
self.assertEqual(
"data-odoo-tracking-email not found", tracking.error_description
)
def test_mail_init_messaging(self):
def mock_json_response(*args, **kwargs):
return {"expected_result": True}
controller = MailTrackingDiscussController()
# This is non-functional test to increase coverage
with patch(
"odoo.addons.mail.controllers.discuss.DiscussController.mail_init_messaging",
wraps=mock_json_response,
):
res = controller.mail_init_messaging()
self.assertTrue(res["expected_result"])
def test_discuss_failed_messages(self):
def mock_json_response(*args, **kwargs):
return {"expected_result": True}
def mock_message_fetch(*args, **kwargs):
return self.env["mail.message"]
controller = MailTrackingDiscussController()
# This is non-functional test to increase coverage
with patch(
"odoo.addons.mail_tracking.models.mail_message.MailMessage.message_format",
wraps=mock_json_response,
), patch(
"odoo.addons.mail.models.mail_message.Message._message_fetch",
wraps=mock_message_fetch,
):
res = controller.discuss_failed_messages()
self.assertTrue(res["expected_result"])
def test_unlink_mail_alias(self):
self.env["ir.config_parameter"].search([], limit=1).unlink()

View File

@ -22,7 +22,7 @@
</group>
<group>
<group>
<field name="mail_message_id" />
<field name="mail_message_id" string="Message" />
<field name="mail_id" />
<field name="partner_id" />
<field name="recipient" />