[REF] {website_}mail: move and replace published field of mail.message to is_internal

PURPOSE

Clean posting process and improve mail.message definition and comprehension.

SPECIFICATIONS

Website mail defines a website_published field allowing to publish / unpublish
comments on the frontend of some modules. This field has several drawbacks :

  * it is used only for front-end people (portal, public) and has no real
    effect in chatter / classic discussions;
  * it is used only in some advanced front-end module and is not available
    in portal by default;
  * its naming is not really correct as it is not linked to fields coming
    from the website_published mixin and its behavior is not really
    the same;
  * its use is a bit duplicated with internal flag coming from subtype
    allowing to hide messages related to an internal subtype;
  * there are overrides of standard mail.message methods just to handle
    this flag;

In this commit we change that field by an is_internal flag directly on
mail.message model itself. It tells if share people (customers, share users)
are allowed to read the message. This field can be given through posting
API or set manually using widgets. It is also used in access rights custom
methods and managed like the internal flag of subtypes.

Mailgateway was already using an internal flag for internal note replies. It
is renamed to is_internal and propagated as it is now a standard field. It
also eases code understanding.

Portal is updated to allow managing the flag directly. It means customer portal
now natively allows to moderate customer comments without any need of website
modules.

Rating is updated accordingly. An is_internal field is added, replacing the
related on website published.

LINKS

Task ID 2071556
PR #38692
This commit is contained in:
Thibault Delavallée
2019-12-02 15:18:44 +00:00
parent 46b9a97b56
commit 5649fc88e8
23 changed files with 92 additions and 139 deletions
+1
View File
@@ -112,6 +112,7 @@ class LivechatController(http.Controller):
'rating': rate,
'consumed': True,
'feedback': reason,
'is_internal': False,
}
if not channel.rating_ids:
res_model_id = request.env['ir.model'].sudo().search([('model', '=', channel._name)], limit=1).id
+9 -3
View File
@@ -73,6 +73,7 @@ class Message(models.Model):
mail_activity_type_id = fields.Many2one(
'mail.activity.type', 'Mail Activity Type',
index=True, ondelete='set null')
is_internal = fields.Boolean('Employee Only', help='Hide to public / portal users, independently from subtype configuration.')
# origin
email_from = fields.Char('From', help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.")
author_id = fields.Many2one(
@@ -234,9 +235,9 @@ class Message(models.Model):
return super(Message, self)._search(
args, offset=offset, limit=limit, order=order,
count=count, access_rights_uid=access_rights_uid)
# Non-employee see only messages with a subtype (aka, no internal logs)
# Non-employee see only messages with a subtype and not internal
if not self.env['res.users'].has_group('base.group_user'):
args = ['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)] + list(args)
args = expression.AND([self._get_search_domain_share(), args])
# Perform a super with count as False, to have the ids, not a counter
ids = super(Message, self)._search(
args, offset=offset, limit=limit, order=order,
@@ -359,7 +360,9 @@ class Message(models.Model):
FROM "%s" AS message
LEFT JOIN "mail_message_subtype" as subtype
ON message.subtype_id = subtype.id
WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
WHERE message.message_type = %%s AND
(message.is_internal IS TRUE OR message.subtype_id IS NULL OR subtype.internal IS TRUE) AND
message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
if self._cr.fetchall():
raise AccessError(
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
@@ -1279,3 +1282,6 @@ class Message(models.Model):
'message_needaction',
'message_needaction_counter',
], ids=[res_id])
def _get_search_domain_share(self):
return ['&', '&', ('is_internal', '=', False), ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)]
+7 -4
View File
@@ -1056,7 +1056,7 @@ class MailThread(models.AbstractModel):
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
partner_ids = []
if not subtype_id:
if message_dict.pop('internal', False):
if message_dict.get('is_internal'):
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
if parent_message and parent_message.author_id:
partner_ids = [parent_message.author_id.id]
@@ -1367,7 +1367,7 @@ class MailThread(models.AbstractModel):
'references': references,
'in_reply_to': in-reply-to,
'parent_id': parent mail.message based on in_reply_to or references,
'internal': answer to an internal message (note),
'is_internal': answer to an internal message (note),
'date': date,
'attachments': [('file1', 'bytes'),
('file2', 'bytes')}
@@ -1441,14 +1441,14 @@ class MailThread(models.AbstractModel):
parent_ids = self.env['mail.message'].search([('message_id', '=', msg_dict['in_reply_to'])], limit=1)
if parent_ids:
msg_dict['parent_id'] = parent_ids.id
msg_dict['internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
msg_dict['is_internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
if msg_dict['references'] and 'parent_id' not in msg_dict:
references_msg_id_list = tools.mail_header_msgid_re.findall(msg_dict['references'])
parent_ids = self.env['mail.message'].search([('message_id', 'in', [x.strip() for x in references_msg_id_list])], limit=1)
if parent_ids:
msg_dict['parent_id'] = parent_ids.id
msg_dict['internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
msg_dict['is_internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
msg_dict.update(self._message_parse_extract_payload(message, save_original=save_original))
msg_dict.update(self._message_parse_extract_bounce(message, msg_dict))
@@ -1996,6 +1996,7 @@ class MailThread(models.AbstractModel):
'email_from': email_from,
'partner_ids': partner_ids,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
'reply_to': MailThread._notify_get_reply_to(default=email_from, records=None)[False],
'message_id': tools.generate_tracking_message_id('message-notify'),
@@ -2024,6 +2025,7 @@ class MailThread(models.AbstractModel):
'model': kwargs.get('model', self._name),
'res_id': self.ids[0] if self.ids else False,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
@@ -2046,6 +2048,7 @@ class MailThread(models.AbstractModel):
'message_type': message_type,
'model': self._name,
'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
+8 -1
View File
@@ -158,6 +158,7 @@ class PortalChatter(http.Controller):
'options': {
'message_count': message_data['message_count'],
'is_user_public': is_user_public,
'is_user_employee': request.env.user.has_group('base.group_user'),
'is_user_publisher': request.env.user.has_group('website.group_website_publisher'),
'display_composer': display_composer,
'partner_id': request.env.user.partner_id.id
@@ -183,13 +184,19 @@ class PortalChatter(http.Controller):
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
domain = expression.AND([Message._non_employee_message_domain(), domain])
domain = expression.AND([Message._get_search_domain_share(), domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
'message_count': Message.search_count(domain)
}
@http.route(['/mail/update_is_internal'], type='json', auth="user", website=True)
def portal_message_update_is_internal(self, message_id, is_internal):
message = request.env['mail.message'].browse(int(message_id))
message.write({'is_internal': is_internal})
return message.is_internal
class MailController(MailController):
+1 -5
View File
@@ -10,7 +10,7 @@ class MailMessage(models.Model):
def portal_message_format(self):
return self._portal_message_format([
'id', 'body', 'date', 'author_id', 'email_from', # base message fields
'message_type', 'subtype_id', 'subject', # message specific
'message_type', 'subtype_id', 'is_internal', 'subject', # message specific
'model', 'res_id', 'record_name', # document related
])
@@ -24,7 +24,3 @@ class MailMessage(models.Model):
if not attachment.get('access_token'):
attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0]
return message_values
@api.model
def _non_employee_message_domain(self):
return ['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)]
+32 -1
View File
@@ -20,7 +20,8 @@ var PortalChatter = publicWidget.Widget.extend({
template: 'portal.Chatter',
xmlDependencies: ['/portal/static/src/xml/portal_chatter.xml'],
events: {
"click .o_portal_chatter_pager_btn": '_onClickPager',
'click .o_portal_chatter_pager_btn': '_onClickPager',
'click .o_portal_chatter_js_is_internal': 'async _onClickUpdateIsInternal',
},
/**
@@ -45,6 +46,7 @@ var PortalChatter = publicWidget.Widget.extend({
'pager_scope': 5,
'pager_start': 1,
'is_user_public': true,
'is_user_employee': false,
'is_user_publisher': false,
'hash': false,
'pid': false,
@@ -254,6 +256,35 @@ var PortalChatter = publicWidget.Widget.extend({
var page = $(ev.currentTarget).data('page');
this._changeCurrentPage(page);
},
/**
* Toggle is_internal state of message. Update both node data and
* classes to ensure DOM is updated accordingly to RPC call result.
* @private
* @returns {Promise}
*/
_onClickUpdateIsInternal: function (ev) {
ev.preventDefault();
var $elem = $(ev.currentTarget);
return this._rpc({
route: '/mail/update_is_internal',
params: {
message_id: $elem.data('message-id'),
is_internal: ! $elem.data('is-internal'),
},
}).then(function (result) {
$elem.data('is-internal', result);
if (result === true) {
$elem.addClass('o_portal_message_internal_on');
$elem.removeClass('o_portal_message_internal_off');
}
else {
$elem.addClass('o_portal_message_internal_off');
$elem.removeClass('o_portal_message_internal_on');
}
});
},
});
publicWidget.registry.portalChatter = publicWidget.Widget.extend({
+12
View File
@@ -524,4 +524,16 @@ form label {
opacity: 1;
}
}
.o_portal_message_internal_off {
.btn-danger {
display: none;
}
}
.o_portal_message_internal_on {
.btn-success {
display: none;
}
}
}
@@ -92,6 +92,7 @@
<div class="media o_portal_chatter_message" t-att-id="'message-' + message.id">
<img class="o_portal_chatter_avatar" t-att-src="message.author_avatar_url" alt="avatar"/>
<div class="media-body">
<t t-call="portal.chatter_internal_toggle" t-if="widget.options['is_user_employee']"/>
<div class="o_portal_chatter_message_title">
<h5><t t-esc="message.author_id[1]"/></h5>
@@ -110,6 +111,18 @@
</div>
</t>
<!-- Chatter: internal toggle widget -->
<t t-name="portal.chatter_internal_toggle">
<div t-attf-class="float-right o_portal_chatter_js_is_internal #{message.is_internal and 'o_portal_message_internal_on' or 'o_portal_message_internal_off'}"
t-att-data-message-id="message.id"
t-att-data-is-internal="message.is_internal">
<button class="btn btn-danger"
title="Currently restricted to internal employees, click to make it available to everyone viewing this document.">Employees Only</button>
<button class="btn btn-success"
title="Current available to everyone viewing this document, click to restrict to internal employees.">Visible</button>
</div>
</t>
<t t-name="portal.pager">
<div class="o_portal_chatter_pager">
<t t-if="!_.isEmpty(widget.get('pager'))">
+1
View File
@@ -53,6 +53,7 @@ class Rating(models.Model):
'mail.message', string="Linked message",
index=True, ondelete='cascade',
help="Associated message when posting a review. Mainly used in website addons.")
is_internal = fields.Boolean('Employee Only', readonly=False, related='message_id.is_internal', store=True)
access_token = fields.Char('Security Token', default=_default_access_token, help="Access token to set the rating of the value")
consumed = fields.Boolean(string="Filled Rating", help="Enabled if the rating has been filled.")
+2 -1
View File
@@ -134,7 +134,8 @@ class RatingMixin(models.AbstractModel):
'partner_id': partner.id,
'rated_partner_id': rated_partner.id,
'res_model_id': record_model_id,
'res_id': self.id
'res_id': self.id,
'is_internal': False,
})
else:
rating = ratings[0]
+1 -3
View File
@@ -7,14 +7,12 @@
'summary': 'Website Module for Mail',
'version': '0.1',
'description': """
Module holding mail improvements for website.
It is responsible of comments moderation for published documents (forum, slides, blog, ...)
Module holding mail improvements for website. It holds the follow widget.
""",
'depends': ['website', 'mail'],
'data': [
'views/assets.xml',
'views/website_mail_templates.xml',
'security/website_mail_security.xml',
],
'installable': True,
'auto_install': True,
-1
View File
@@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_message
from . import update
@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.exceptions import AccessError
class MailMessage(models.Model):
_inherit = 'mail.message'
@api.model
def default_get(self, fields_list):
defaults = super(MailMessage, self).default_get(fields_list)
# Note: explicitly implemented in default_get() instead of field default,
# to avoid setting to True for all existing messages during upgrades.
# TODO: this default should probably be dynamic according to the model
# on which the messages are attached, thus moved to create().
if 'website_published' in fields_list:
defaults.setdefault('website_published', True)
return defaults
website_published = fields.Boolean(string='Published', help="Visible on the website as a comment", copy=False)
@api.model
def _non_employee_message_domain(self):
domain = super(MailMessage, self)._non_employee_message_domain()
return expression.AND([domain, [('website_published', '=', True)]])
@api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
""" Override that adds specific access rights of mail.message, to restrict
messages to published messages for public users. """
if self.user_has_groups('base.group_public'):
args = expression.AND([[('website_published', '=', True)], list(args)])
return super(MailMessage, self)._search(args, offset=offset, limit=limit, order=order,
count=count, access_rights_uid=access_rights_uid)
def check_access_rule(self, operation):
""" Add Access rules of mail.message for non-employee user:
- read:
- raise if the type is comment and subtype NULL (internal note)
"""
if self.user_has_groups('base.group_public'):
self.env.cr.execute('SELECT id FROM "%s" WHERE website_published IS FALSE AND id = ANY (%%s)' % (self._table), (self.ids,))
if self.env.cr.fetchall():
raise AccessError(
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
+ ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
)
return super(MailMessage, self).check_access_rule(operation=operation)
def _portal_message_format(self, fields_list):
fields_list += ['website_published']
return super(MailMessage, self)._portal_message_format(fields_list)
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_message_rule_public" model="ir.rule">
<field name="name">mail.message: portal/public: read published messages</field>
<field name="model_id" ref="mail.model_mail_message"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
</data>
</odoo>
@@ -1,13 +0,0 @@
odoo.define('website_mail.thread', function (require) {
'use strict';
var portalChatter = require('portal.chatter');
/**
* Extends Frontend Chatter to handle rating
*/
portalChatter.PortalChatter.include({
xmlDependencies: (portalChatter.PortalChatter.prototype.xmlDependencies || [])
.concat(['/website_mail/static/src/xml/portal_chatter.xml']),
});
});
@@ -1,23 +0,0 @@
<templates id="template" xml:space="preserve">
<t t-name="website_mail.publish_short">
<t t-if="is_publisher" t-ignore="true">
<div t-attf-class="float-right js_publish_management #{object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-id="res_id" t-att-data-object="res_model" t-att-data-controller="publish_controller">
<button class="btn btn-danger js_publish_btn">Unpublished</button>
<button class="btn btn-success js_publish_btn">Published</button>
</div>
</t>
</t>
<t t-extend="portal.chatter_messages">
<t t-jquery=".o_portal_chatter_message_title" t-operation="before">
<t t-call="website_mail.publish_short">
<t t-set="res_model" t-value="'mail.message'"/>
<t t-set="res_id" t-value="message.id"/>
<t t-set="object" t-value="message"/>
<t t-set="is_publisher" t-value="widget.options['is_user_publisher']"/>
</t>
</t>
</t>
</templates>
-1
View File
@@ -3,7 +3,6 @@
<template id="head" inherit_id="website.assets_frontend" name="Mail customization">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_mail/static/src/js/follow.js"></script>
<script type="text/javascript" src="/website_mail/static/src/js/portal_chatter.js"></script>
<link rel="stylesheet" type="text/scss" href="/website_mail/static/src/css/website_mail.scss"/>
</xpath>
</template>
-2
View File
@@ -6,8 +6,6 @@ from odoo import fields, models, exceptions, _
class Rating(models.Model):
_inherit = 'rating.rating'
# Add this related field to mail.message for performance reason
website_published = fields.Boolean(related='message_id.website_published', store=True, readonly=False)
# Adding information for comment a rating message
publisher_comment = fields.Text("Publisher Comment")
publisher_id = fields.Many2one('res.partner', 'Commented by',
+1 -1
View File
@@ -372,7 +372,7 @@ class ProductTemplate(models.Model):
def _rating_domain(self):
""" Only take the published rating into account to compute avg and count """
domain = super(ProductTemplate, self)._rating_domain()
return expression.AND([domain, [('website_published', '=', True)]])
return expression.AND([domain, [('is_internal', '=', False)]])
def _get_images(self):
"""Return a list of records implementing `image.mixin` to
+2 -2
View File
@@ -49,7 +49,7 @@ class SlidesPortalChatter(PortalChatter):
domain = [
('model', '=', res_model),
('res_id', '=', res_id),
('website_published', '=', True),
('is_internal', '=', False),
('author_id', '=', request.env.user.partner_id.id),
('message_type', '=', 'comment'),
('id', '=', message_id)
@@ -64,7 +64,7 @@ class SlidesPortalChatter(PortalChatter):
# update rating
if post.get('rating_value'):
domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('website_published', '=', True), ('message_id', '=', message.id)]
domain = [('res_model', '=', res_model), ('res_id', '=', res_id), ('is_internal', '=', False), ('message_id', '=', message.id)]
rating = request.env['rating.rating'].sudo().search(domain, order='write_date DESC', limit=1)
rating.write({
'rating': float(post['rating_value'])
+1 -1
View File
@@ -433,7 +433,7 @@ class WebsiteSlides(WebsiteProfile):
('res_id', '=', channel.id),
('author_id', '=', request.env.user.partner_id.id),
('message_type', '=', 'comment'),
('website_published', '=', True)
('is_internal', '=', False)
], order='write_date DESC', limit=1)
if last_message:
last_message_values = last_message.read(['body', 'rating_value', 'attachment_ids'])[0]
@@ -42,7 +42,6 @@
<field name="model">slide.channel</field>
<field name="res_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="website_published" eval="True"/>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_admin"/>
<field name="body" type="html"><div>I fear beginners could be lost... Isn't it a bit harsh for a "basics" course ?</div></field>
@@ -60,7 +59,6 @@
<field name="model">slide.channel</field>
<field name="res_id" ref="website_slides.slide_channel_demo_0_gard_0"/>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="website_published" eval="True"/>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_demo"/>
<field name="body" type="html"><div>Back to basics and interesting. Just WOW !</div></field>
@@ -132,7 +130,6 @@
<field name="model">slide.channel</field>
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="website_published" eval="True"/>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_admin"/>
<field name="body" type="html"><div>Very good course.</div></field>
@@ -150,7 +147,6 @@
<field name="model">slide.channel</field>
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="website_published" eval="True"/>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_demo"/>
<field name="body" type="html"><div>Interesting !</div></field>
@@ -168,7 +164,6 @@
<field name="model">slide.channel</field>
<field name="res_id" ref="website_slides.slide_channel_demo_1_gard1"/>
<field name="subtype_id" ref="mail.mt_comment"/>
<field name="website_published" eval="True"/>
<field name="message_type">comment</field>
<field name="author_id" ref="base.partner_demo_portal"/>
<field name="body" type="html"><div>Interesting. Could be great to include more examples.</div></field>
@@ -539,7 +539,7 @@ class Channel(models.Model):
def _rating_domain(self):
""" Only take the published rating into account to compute avg and count """
domain = super(Channel, self)._rating_domain()
return expression.AND([domain, [('website_published', '=', True)]])
return expression.AND([domain, [('is_internal', '=', False)]])
# ---------------------------------------------------------
# Data / Misc