diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 1b5ee06f84c..e429f98c5e5 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -178,6 +178,16 @@ class IrHttp(models.AbstractModel):
rerouting_limit = 10
+ @classmethod
+ def _get_converters(cls):
+ """ Get the converters list for custom url pattern werkzeug need to
+ match Rule. This override adds the website ones.
+ """
+ return dict(
+ super(IrHttp, cls)._get_converters(),
+ model=ModelConverter,
+ )
+
@classmethod
def _get_languages(cls):
return request.env['res.lang'].search([])
diff --git a/addons/portal/controllers/__init__.py b/addons/portal/controllers/__init__.py
index ac871b40c75..c1b076c5a27 100644
--- a/addons/portal/controllers/__init__.py
+++ b/addons/portal/controllers/__init__.py
@@ -3,3 +3,4 @@
from . import web
from . import portal
+from . import mail
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
new file mode 100644
index 00000000000..4ee2414548a
--- /dev/null
+++ b/addons/portal/controllers/mail.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from werkzeug.exceptions import NotFound, Forbidden
+
+from odoo import http
+from odoo.http import request
+from odoo.tools import consteq
+
+
+def _special_access_object(res_model, res_id, token='', token_field=''):
+ record = request.env[res_model].browse(res_id).sudo()
+ if token and record and getattr(record, token_field, None) and consteq(getattr(record, token_field), token):
+ return True
+ return False
+
+def _message_post_helper(res_model='', res_id=None, message='', token='', token_field='token', nosubscribe=True, **kw):
+ """ Generic chatter function, allowing to write on *any* object that inherits mail.thread.
+ If a token is specified, all logged in users will be able to write a message regardless
+ of access rights; if the user is the public user, the message will be posted under the name
+ of the partner_id of the object (or the public user if there is no partner_id on the object).
+
+ :param string res_model: model name of the object
+ :param int res_id: id of the object
+ :param string message: content of the message
+
+ optional keywords arguments:
+ :param string token: access token if the object's model uses some kind of public access
+ using tokens (usually a uuid4) to bypass access rules
+ :param string token_field: name of the field that contains the token on the object (defaults to 'token')
+ :param bool nosubscribe: set False if you want the partner to be set as follower of the object when posting (default to True)
+
+ The rest of the kwargs are passed on to message_post()
+ """
+ record = request.env[res_model].browse(res_id)
+ author_id = request.env.user.partner_id.id if request.env.user.partner_id else False
+ if token_field and token:
+ access_as_sudo = _special_access_object(res_model, res_id, token=token, token_field=token_field)
+ if access_as_sudo:
+ record = record.sudo()
+ if request.env.user == request.env.ref('base.public_user'):
+ author_id = record.partner_id.id if hasattr(record, 'partner_id') else author_id
+ else:
+ if not author_id:
+ raise NotFound()
+ else:
+ raise Forbidden()
+ kw.pop('csrf_token', None)
+ return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message,
+ message_type=kw.pop('message_type', "comment"),
+ subtype=kw.pop('subtype', "mt_comment"),
+ author_id=author_id,
+ **kw)
+
+
+class PortalChatter(http.Controller):
+
+ @http.route(['/mail/chatter_post'], type='http', methods=['POST'], auth='public', website=True)
+ def portal_chatter_post(self, res_model, res_id, message, **kw):
+ url = request.httprequest.referrer
+ if message:
+ _message_post_helper(res_model, int(res_id), message, **kw)
+ url = url + "#discussion"
+ return request.redirect(url)
+
+ @http.route('/mail/chatter_init', type='json', auth='public', website=True)
+ def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
+ is_user_public = request.env.user.has_group('base.group_public')
+ message_data = self.portal_message_fetch(res_model, res_id, domain=domain, limit=limit, **kwargs)
+ display_composer = False
+ if kwargs.get('allow_composer'):
+ display_composer = kwargs.get('token') or not is_user_public
+ return {
+ 'messages': message_data['messages'],
+ 'options': {
+ 'message_count': message_data['message_count'],
+ 'is_user_public': is_user_public,
+ 'is_user_publisher': request.env.user.has_group('website.group_website_publisher'),
+ 'display_composer': display_composer,
+ 'partner_id': request.env.user.partner_id.id
+ }
+ }
+
+ @http.route('/mail/chatter_fetch', type='json', auth='public', website=True)
+ def portal_message_fetch(self, res_model, res_id, domain=False, limit=10, offset=0, **kw):
+ if not domain:
+ domain = []
+ # Only search into website_message_ids, so apply the same domain to perform only one search
+ # extract domain from the 'website_message_ids' field
+ field_domain = request.env[res_model]._fields['website_message_ids'].domain
+ domain += field_domain(request.env[res_model]) if callable(field_domain) else field_domain
+ domain += [('res_id', '=', res_id)]
+ # Check access
+ Message = request.env['mail.message']
+ if kw.get('token'):
+ access_as_sudo = _special_access_object(res_model, res_id, token=kw.get('token'), token_field=kw.get('token_field'))
+ if not access_as_sudo: # if token is not correct, raise Forbidden
+ raise Forbidden()
+ Message = request.env['mail.message'].sudo()
+ return {
+ 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
+ 'message_count': Message.search_count(domain)
+ }
diff --git a/addons/portal/models/__init__.py b/addons/portal/models/__init__.py
index bccc8440508..320a9e84f38 100644
--- a/addons/portal/models/__init__.py
+++ b/addons/portal/models/__init__.py
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import portal_mixin
+from . import mail_thread
+from . import mail_message
+from . import portal_mixin
\ No newline at end of file
diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py
new file mode 100644
index 00000000000..4a5d1f804d9
--- /dev/null
+++ b/addons/portal/models/mail_message.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class MailMessage(models.Model):
+ _inherit = 'mail.message'
+
+ @api.multi
+ 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
+ 'model', 'res_id', 'record_name', # document related
+ ])
+
+ @api.multi
+ def _portal_message_format(self, fields_list):
+ message_values = self.read(fields_list)
+ message_tree = dict((m.id, m) for m in self.sudo())
+ self._message_read_dict_postprocess(message_values, message_tree)
+ return message_values
diff --git a/addons/website_mail/models/mail_thread.py b/addons/portal/models/mail_thread.py
similarity index 100%
rename from addons/website_mail/models/mail_thread.py
rename to addons/portal/models/mail_thread.py
diff --git a/addons/portal/static/src/js/portal_chatter.js b/addons/portal/static/src/js/portal_chatter.js
new file mode 100644
index 00000000000..bfb44837bf4
--- /dev/null
+++ b/addons/portal/static/src/js/portal_chatter.js
@@ -0,0 +1,238 @@
+odoo.define('portal.chatter', function(require) {
+'use strict';
+
+var base = require('web_editor.base');
+var ajax = require('web.ajax');
+var core = require('web.core');
+var Widget = require('web.Widget');
+var rpc = require('web.rpc');
+
+var qweb = core.qweb;
+var _t = core._t;
+
+/**
+ * Widget PortalChatter
+ *
+ * - Fetch message fron controller
+ * - Display chatter: pager, total message, composer (according to access right)
+ * - Provider API to filter displayed messages
+ */
+var PortalChatter = Widget.extend({
+ template: 'portal.chatter',
+ events: {
+ "click .o_portal_chatter_pager_btn": '_onClickPager'
+ },
+
+ init: function(parent, options){
+ this._super.apply(this, arguments);
+ this.options = _.defaults(options || {}, {
+ 'allow_composer': true,
+ 'display_composer': false,
+ 'csrf_token': odoo.csrf_token,
+ 'message_count': 0,
+ 'pager_step': 10,
+ 'pager_scope': 5,
+ 'pager_start': 1,
+ 'is_user_public': true,
+ 'is_user_publisher': false,
+ 'domain': [],
+ });
+ this.set('messages', []);
+ this.set('message_count', this.options['message_count']);
+ this.set('pager', {});
+ this.set('domain', this.options['domain']);
+ this._current_page = this.options['pager_start'];
+ },
+ willStart: function(){
+ var self = this;
+ // load qweb template and init data
+ return $.when(
+ rpc.query({
+ route: '/mail/chatter_init',
+ params: this._messageFetchPrepareParams()
+ }), this._loadTemplates()
+ ).then(function(result){
+ // bind events
+ self.on("change:messages", self, self._renderMessages);
+ self.on("change:message_count", self, function(){
+ self._renderMessageCount();
+ self.set('pager', self._pager(self._current_page));
+ });
+ self.on("change:pager", self, self._renderPager);
+ self.on("change:domain", self, self._onChangeDomain);
+ // set options and parameters
+ self.options = _.extend(self.options, result['options'] || {});
+ self.set('message_count', self.options['message_count']);
+ self.set('messages', self.preprocessMessages(result['messages']));
+ return result;
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Fetch the messages and the message count from the server for the
+ * current page and current domain.
+ *
+ * @param {Array}
+ * @returns {Deferred}
+ */
+ messageFetch: function(domain){
+ var self = this;
+ return rpc.query({
+ route: '/mail/chatter_fetch',
+ params: self._messageFetchPrepareParams()
+ }).then(function(result){
+ self.set('messages', self.preprocessMessages(result['messages']));
+ self.set('message_count', result['message_count']);
+ });
+ },
+ /**
+ * Update the messages format
+ *
+ * @param {Array