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..6a242609d14 --- /dev/null +++ b/addons/portal/models/mail_message.py @@ -0,0 +1,24 @@ +# -*- 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 + 'website_published', + ]) + + @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} + * @returns {Array} + */ + preprocessMessages: function(messages){ + _.each(messages, function(m){ + m['author_avatar_url'] = _.str.sprintf('/web/image/%s/%s/author_avatar/50x50', 'mail.message', m.id); + m['published_date_str'] = _.str.sprintf(_t('Published on %s'), moment(m.date).format('MMMM Do YYYY, h:mm:ss a')); + }); + return messages; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Change the current page by refreshing current domain + * + * @private + * @param {Number} page + * @param {Array} domain + */ + _changeCurrentPage: function(page, domain){ + this._current_page = page; + var d = domain ? domain : _.clone(this.get('domain')); + this.set('domain', d); // trigger fetch message + }, + /** + * @private + * @returns {Deferred} + */ + _loadTemplates: function(){ + return ajax.loadXML('/portal/static/src/xml/portal_chatter.xml', qweb); + }, + _messageFetchPrepareParams: function(){ + var self = this; + var data = { + 'res_model': this.options['res_model'], + 'res_id': this.options['res_id'], + 'limit': this.options['pager_step'], + 'offset': (this._current_page-1) * this.options['pager_step'], + 'allow_composer': this.options['allow_composer'], + }; + // add fields to allow to post comment without being logged + _.each(['token', 'token_field'], function(field){ + if(self.options[field]){ + data[field] = self.options[field]; + } + }); + // add domain + if(this.get('domain')){ + data['domain'] = this.get('domain'); + } + return data; + }, + /** + * Generate the pager data for the given page number + * + * @private + * @param {Number} page + * @returns {Object} + */ + _pager: function(page){ + var page = page || 1; + var total = this.get('message_count'); + var scope = this.options['pager_scope']; + var step = this.options['pager_step']; + + // Compute Pager + var page_count = Math.ceil(parseFloat(total) / step); + + var page = Math.max(1, Math.min(parseInt(page), page_count)); + scope -= 1; + + var pmin = Math.max(page - parseInt(Math.floor(scope/2)), 1); + var pmax = Math.min(pmin + scope, page_count); + + if(pmax - scope > 0){ + pmin = pmax - scope; + }else{ + pmin = 1; + } + + var pages = []; + _.each(_.range(pmin, pmax+1), function(index){ + pages.push(index); + }); + + return { + "page_count": page_count, + "offset": (page - 1) * step, + "page": page, + "page_start": pmin, + "page_previous": Math.max(pmin, page - 1), + "page_next": Math.min(pmax, page + 1), + "page_end": pmax, + "pages": pages + }; + }, + _renderMessages: function(){ + this.$('.o_portal_chatter_messages').html(qweb.render("portal.chatter_messages", {widget: this})); + }, + _renderMessageCount: function(){ + this.$('.o_message_counter').replaceWith(qweb.render("portal.chatter_message_count", {widget: this})); + }, + _renderPager: function(){ + this.$('.o_portal_chatter_pager').replaceWith(qweb.render("portal.pager", {widget: this})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onChangeDomain: function(){ + var self = this; + this.messageFetch().then(function(){ + var p = self._current_page; + self.set('pager', self._pager(p)); + }); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onClickPager: function(ev){ + ev.preventDefault(); + var page = $(ev.currentTarget).data('page'); + this._changeCurrentPage(page); + }, +}); + +base.ready().then(function () { + $('.o_website_mail_thread').each(function (index) { + var $elem = $(this); + var mail_thread = new WebsiteMailThread(null, $elem.data()); + mail_thread.appendTo($elem); + }); +}); + +return { + PortalChatter: PortalChatter, +}; + +}); diff --git a/addons/portal/static/src/less/portal_chatter.less b/addons/portal/static/src/less/portal_chatter.less new file mode 100644 index 00000000000..00cda0afe7e --- /dev/null +++ b/addons/portal/static/src/less/portal_chatter.less @@ -0,0 +1,33 @@ + +.o_portal_chatter { + padding: 10px; + + .o_portal_chatter_avatar{ + width: 45px; + height: 45px; + } + + .o_portal_chatter_header { + margin-bottom: 15px; + } + + .o_portal_chatter_composer { + margin-bottom: 15px; + } + + .o_portal_chatter_messages { + margin-bottom: 15px; + + .o_portal_chatter_message_title { + p { + font-size:85%; + color:@odoo-main-color-muted; + margin: 0px; + } + } + } + + .o_portal_chatter_pager { + text-align: center; + } +} diff --git a/addons/portal/static/src/xml/portal_chatter.xml b/addons/portal/static/src/xml/portal_chatter.xml new file mode 100644 index 00000000000..c4cfca119e0 --- /dev/null +++ b/addons/portal/static/src/xml/portal_chatter.xml @@ -0,0 +1,117 @@ + + + + +
+ + + comment + comments + + + There are no comments for now. + +
+
+ + +
+ +

Leave a comment

+

You must be logged in to post a comment.

+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + +
+
+
+
+
+
+
+
+ + +
+ +
+
+ avatar +
+
+ +
+
+ +
+

+
+ + +
+ +
+
+ +
+
+ + +
+ +
    +
  • + +
  • + +
  • + +
  • +
    +
  • + +
  • +
+
+
+
+ + +
+
+ +
+
+ + + +
+
+ + diff --git a/addons/portal/views/assets.xml b/addons/portal/views/assets.xml index af6569e150f..f02205681c6 100644 --- a/addons/portal/views/assets.xml +++ b/addons/portal/views/assets.xml @@ -5,10 +5,12 @@ + + diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 67e027aea48..87e566b874e 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -336,4 +336,22 @@
  • My Account
  • + + + + diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml index 2bd30f00c2a..534dea06da3 100644 --- a/addons/website_blog/views/website_blog_templates.xml +++ b/addons/website_blog/views/website_blog_templates.xml @@ -302,7 +302,7 @@ - - - diff --git a/addons/website_project/views/project_templates.xml b/addons/website_project/views/project_templates.xml index c9e4b047d53..dca48749e88 100644 --- a/addons/website_project/views/project_templates.xml +++ b/addons/website_project/views/project_templates.xml @@ -117,7 +117,7 @@

    Message and communication history

    - +
    @@ -235,7 +235,7 @@

    Message and communication history

    - +
    diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index 522d3561b7f..89d93a1854e 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -6,7 +6,7 @@ import werkzeug from odoo import fields, http, _ from odoo.http import request from odoo.addons.portal.controllers.portal import get_records_pager -from odoo.addons.website_mail.controllers.main import _message_post_helper +from odoo.addons.portal.controllers.mail import _message_post_helper class sale_quote(http.Controller): diff --git a/addons/website_quote/views/website_quote_templates.xml b/addons/website_quote/views/website_quote_templates.xml index 9567e0241e2..92e9dbbc846 100644 --- a/addons/website_quote/views/website_quote_templates.xml +++ b/addons/website_quote/views/website_quote_templates.xml @@ -430,7 +430,7 @@

    History

    - + diff --git a/addons/website_rating/controllers/website_mail.py b/addons/website_rating/controllers/website_mail.py index 4487307effd..26c0f4dedfa 100644 --- a/addons/website_rating/controllers/website_mail.py +++ b/addons/website_rating/controllers/website_mail.py @@ -1,19 +1,17 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from werkzeug.exceptions import NotFound - from odoo import http from odoo.http import request -from odoo.addons.website_mail.controllers.main import WebsiteMail +from odoo.addons.portal.controllers.mail import PortalChatter -class WebsiteRating(WebsiteMail): +class WebsiteRating(PortalChatter): @http.route() - def website_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs): - result = super(WebsiteRating, self).website_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs) + def portal_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs): + result = super(WebsiteRating, self).portal_chatter_init(res_model, res_id, domain=domain, limit=limit, **kwargs) # get the rating statistics about the record if kwargs.get('rating_include'): record = request.env[res_model].browse(res_id) @@ -22,10 +20,10 @@ class WebsiteRating(WebsiteMail): return result @http.route() - def website_message_fetch(self, res_model, res_id, domain=False, limit=False, offset=False, **kw): - # add 'rating_include' in context, to fetch them in website_message_format + def portal_message_fetch(self, res_model, res_id, domain=False, limit=False, offset=False, **kw): + # add 'rating_include' in context, to fetch them in portal_message_format if kw.get('rating_include'): context = dict(request.context) context['rating_include'] = True request.context = context - return super(WebsiteRating, self).website_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw) + return super(WebsiteRating, self).portal_message_fetch(res_model, res_id, domain=domain, limit=limit, offset=offset, **kw) diff --git a/addons/website_rating/models/mail_message.py b/addons/website_rating/models/mail_message.py index 8e6c67fc601..2f1f59cc320 100644 --- a/addons/website_rating/models/mail_message.py +++ b/addons/website_rating/models/mail_message.py @@ -8,12 +8,8 @@ class MailMessage(models.Model): _inherit = 'mail.message' @api.multi - def website_message_format(self): - result = super(MailMessage, self).website_message_format() + def _portal_message_format(self, field_list): # inlude rating value in data if requested if self._context.get('rating_include'): - message_data = self.read(['rating_value']) - message_tree = dict((m['id'], m['rating_value']) for m in message_data) - for message_vals in result: - message_vals['rating_value'] = message_tree.get(message_vals['id'], False) # False for no rating - return result + field_list += ['rating_value'] + return super(MailMessage, self)._portal_message_format(field_list) diff --git a/addons/website_rating/static/src/js/website_mail.js b/addons/website_rating/static/src/js/website_mail.js index d9d114de502..d672af6788d 100644 --- a/addons/website_rating/static/src/js/website_mail.js +++ b/addons/website_rating/static/src/js/website_mail.js @@ -8,13 +8,13 @@ odoo.define('website_rating.thread', function(require) { var qweb = core.qweb; var _t = core._t; - var WebsiteMailThread = require('website_mail.thread').WebsiteMailThread; + var PortalChatter = require('portal.chatter').PortalChatter; /** * Extends Frontend Chatter to handle rating */ - WebsiteMailThread.include({ - events: _.extend({}, WebsiteMailThread.prototype.events, { + PortalChatter.include({ + events: _.extend({}, PortalChatter.prototype.events, { "mousemove .stars i" : "_onMoveStar", "mouseleave .stars i" : "_onMoveOutStar", "click .stars" : "_onClickStar", diff --git a/addons/website_rating/static/src/xml/website_mail.xml b/addons/website_rating/static/src/xml/website_mail.xml index ffe0df540c4..2f3dd132a8b 100644 --- a/addons/website_rating/static/src/xml/website_mail.xml +++ b/addons/website_rating/static/src/xml/website_mail.xml @@ -27,7 +27,7 @@ - +
    @@ -76,15 +76,15 @@ - - + + - + @@ -94,13 +94,13 @@ - - + + - + diff --git a/addons/website_rating/views/website_rating_templates.xml b/addons/website_rating/views/website_rating_templates.xml index c5c6684e371..058b643f2ae 100644 --- a/addons/website_rating/views/website_rating_templates.xml +++ b/addons/website_rating/views/website_rating_templates.xml @@ -8,7 +8,7 @@ -