[MOV] portal,website_mail: move chatter frontend to portal

This split responsiblilies :
- portal provides basic chatter for portal document, and
website documents (slides, blog, ...).
- website_mail provides comments moderation (unpublished mail
message)
- website_rating integrate rating when posting comments
This commit is contained in:
Jérome Maes
2017-08-16 14:56:40 +02:00
parent 2972976962
commit 3cfd64f63f
31 changed files with 594 additions and 539 deletions
+1
View File
@@ -3,3 +3,4 @@
from . import web
from . import portal
from . import mail
+103
View File
@@ -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)
}
+3 -1
View File
@@ -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
+24
View File
@@ -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
@@ -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<Object>}
* @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,
};
});
@@ -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;
}
}
@@ -0,0 +1,117 @@
<templates id="template" xml:space="preserve">
<t t-name="portal.chatter_message_count">
<t t-set="count" t-value="widget.get('message_count')"/>
<div class="o_message_counter">
<t t-if="count">
<span class="o_message_count fa fa-comments"> <t t-esc="count"/></span>
<t t-if="count == 1">comment</t>
<t t-else="">comments</t>
</t>
<t t-else="">
There are no comments for now.
</t>
</div>
</t>
<t t-name="portal.chatter_composer">
<div class="o_portal_chatter_composer" t-if="widget.options['allow_composer']">
<t t-if="!widget.options['display_composer']">
<h4>Leave a comment</h4>
<p>You must be <a t-attf-href="/web/login?redirect=#{window.encodeURI(window.location.href + '#discussion')}">logged in</a> to post a comment.</p>
</t>
<t t-if="widget.options['display_composer']">
<div class="media">
<div class="media-left">
<img class="o_portal_chatter_avatar pull-left" t-attf-src="/web/image/res.partner/#{widget.options['partner_id']}/image_small/50x50"/>
</div>
<div class="media-body">
<form class="o_portal_chatter_composer_form" t-attf-action="/mail/chatter_post" method="POST">
<input type="hidden" name="csrf_token" t-att-value="widget.options['csrf_token']"/>
<div class="mb32">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
<input type='hidden' name="res_model" t-att-value="widget.options['res_model']"/>
<input type='hidden' name="res_id" t-att-value="widget.options['res_id']"/>
<input type='hidden' name="token" t-att-value="widget.options['token']" t-if="widget.options['token']"/>
<input type='hidden' name="sha_in" t-att-value="widget.options['sha_in']" t-if="widget.options['sha_in']"/>
<input type='hidden' name="sha_time" t-att-value="widget.options['sha_time']" t-if="widget.options['sha_time']"/>
<input type='hidden' name="token_field" t-att-value="widget.options['token_field']" t-if="widget.options['token_field']"/>
<div class="alert alert-danger mt8 mb0 o_portal_chatter_composer_error" style="display:none;">
Oops! Something went wrong. Try to reload the page and log in.
</div>
<button t-attf-class="o_portal_chatter_composer_btn btn btn-primary mt8 o_website_message_post_helper" type="submit">Send</button>
</div>
</form>
</div>
</div>
</t>
<hr/>
</div>
</t>
<t t-name="portal.chatter_messages">
<div class="o_portal_chatter_messages">
<t t-foreach="widget.get('messages')" t-as="message">
<div class="media o_portal_chatter_message" t-att-id="'message-' + message.id">
<div class="media-left">
<img class="media-object o_portal_chatter_avatar" t-att-src="message.author_avatar_url" alt="avatar"/>
</div>
<div class="media-body">
<div class="o_portal_chatter_message_title">
<h5 class="media-heading">
<t t-esc="message.author_id[1]"/>
</h5>
<p class="o_portal_chatter_puslished_date"><t t-esc="message.published_date_str"/></p>
</div>
<t t-raw="message.body"/>
<div class="o_portal_chatter_attachments">
<div class="col-md-2 col-sm-3 text-center" t-foreach='message.attachment_ids' t-as='attachment'>
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
<div class='oe_attachment_name'><t t-raw='attachment.name'/></div>
</a>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-name="portal.pager">
<div class="o_portal_chatter_pager">
<t t-if="!_.isEmpty(widget.get('pager'))">
<ul class="pagination" t-if="widget.get('pager')['pages'].length &gt; 1">
<li t-if="widget.get('pager')['page'] != widget.get('pager')['page_previous']" t-att-data-page="widget.get('pager')['page_previous']" class="o_portal_chatter_pager_btn">
<a href="#"><i class="fa fa-chevron-left"/></a>
</li>
<t t-foreach="widget.get('pager')['pages']" t-as="page">
<li t-att-data-page="page" t-att-class="page == widget.get('pager')['page'] ? 'o_portal_chatter_pager_btn active' : 'o_portal_chatter_pager_btn'">
<a href="#"><t t-esc="page"/></a>
</li>
</t>
<li t-if="widget.get('pager')['page'] != widget.get('pager')['page_next']" t-att-data-page="widget.get('pager')['page_next']" class="o_portal_chatter_pager_btn">
<a href="#"><i class="fa fa-chevron-right"/></a>
</li>
</ul>
</t>
</div>
</t>
<t t-name="portal.chatter">
<div class="o_portal_chatter">
<div class="o_portal_chatter_header">
<t t-call="portal.chatter_message_count"/>
</div>
<hr/>
<t t-call="portal.chatter_composer"/>
<t t-call="portal.chatter_messages"/>
<div class="o_portal_chatter_footer">
<t t-call="portal.pager"/>
</div>
</div>
</t>
</templates>
+2
View File
@@ -5,10 +5,12 @@
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.ui.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.wrapwrap.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/bootstrap.extend.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/portal_chatter.less"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/portal/static/src/js/portal.js"></script>
<script type="text/javascript" src="/portal/static/src/js/portal_tour.js"></script>
<script type="text/javascript" src="/portal/static/src/js/portal_chatter.js"></script>
</xpath>
</template>
</odoo>
+18
View File
@@ -336,4 +336,22 @@
<li><a href="/my/home" role="menuitem">My Account</a></li>
</xpath>
</template>
<!--
Generic chatter template for the frontend
This template provide the container of the chatter. The rest is done in js.
To use this template, you need to call it after setting the following variable in your template or in your controller:
:object browserecord : the mail_thread object
:message_per_page int (optional): number of message per chatter page
:token string (optional): if you want your chatter to be available for non-logged user,
you can use a token to verify the identity of the user;
the message will be posted with the identity of the partner_id of the object
:token_field string (optional): name of the field that contains the token on the object (default to 'token')
-->
<template id="message_thread">
<div id="discussion" class="hidden-print o_portal_chatter"
t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-token_field="token_field" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
</div>
</template>
</odoo>
@@ -302,7 +302,7 @@
<template id="opt_blog_post_complete_comment" name="Allow blog post comment" inherit_id="website_blog.blog_post_complete" active="False" customize_show="True">
<xpath expr="//div[hasclass('o_blog_post_complete','o_sharing_links')]" position="after">
<section id="comments" class="read_width">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="blog_post"/>
</t>
</section>
@@ -507,7 +507,7 @@
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="lead"/>
</t>
</div>
@@ -772,7 +772,7 @@
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="opportunity"/>
</t>
</div>
+4 -1
View File
@@ -6,7 +6,10 @@
'category': 'Hidden',
'summary': 'Website Module for Mail',
'version': '0.1',
'description': """Glue module holding mail improvements for website.""",
'description': """
Module holding mail improvements for website.
It is responsible of comments moderation for published documents (forum, slides, blog, ...)
""",
'depends': ['website', 'mail'],
'data': [
'views/website_mail_templates.xml',
-95
View File
@@ -1,56 +1,8 @@
# -*- 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 WebsiteMail(http.Controller):
@@ -108,50 +60,3 @@ class WebsiteMail(http.Controller):
('partner_id', '=', partner.id)
]))
return values
@http.route(['/website_mail/post/post'], type='http', methods=['POST'], auth='public', website=True)
def website_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('/website_mail/init', type='json', auth='public', website=True)
def website_chatter_init(self, res_model, res_id, domain=False, limit=False, **kwargs):
is_user_public = bool(request.env.user == request.website.user_id)
message_data = self.website_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('/website_mail/fetch', type='json', auth='public', website=True)
def website_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).website_message_format(),
'message_count': Message.search_count(domain)
}
-1
View File
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_thread
from . import mail_message
from . import update
+4 -11
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo import api, fields, models, _
from odoo.osv import expression
from odoo.tools import html2plaintext
from odoo.exceptions import AccessError
@@ -58,13 +58,6 @@ class MailMessage(models.Model):
return super(MailMessage, self).check_access_rule(operation=operation)
@api.multi
def website_message_format(self):
message_values = self.read([
'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',
])
message_tree = dict((m.id, m) for m in self.sudo())
self._message_read_dict_postprocess(message_values, message_tree)
return message_values
def _portal_message_format(self, fields_list):
fields_list += ['website_published']
return super(MailMessage, self)._portal_message_format(fields_list)
@@ -4,7 +4,7 @@
<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="model_mail_message"/>
<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>
@@ -2,37 +2,3 @@
.js_follow[data-follow='off'] .js_unfollow_btn {
display: none;
}
.o_website_mail_chatter {
padding: 10px;
.o_website_mail_avatar{
width: 45px;
height: 45px;
}
.o_website_mail_header {
margin-bottom: 15px;
}
.o_website_mail_composer {
margin-bottom: 15px;
}
.o_website_mail_messages {
margin-bottom: 15px;
.o_website_mail_message_title {
p {
font-size:85%;
color:@odoo-main-color-muted;
margin: 0px;
}
}
}
.o_website_mail_pager {
text-align: center;
}
}
@@ -1,237 +1,20 @@
odoo.define('website_mail.thread', function (require) {
odoo.define('website_mail.thread', 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;
var PortalChatter = require('portal.chatter').PortalChatter;
/**
* Widget WebsiteMailThread
*
* - Fetch message fron controller
* - Display chatter: pager, total message, composer (according to access right)
* - Provider API to filter displayed messages
* Extends Frontend Chatter to handle rating
*/
var WebsiteMailThread = Widget.extend({
template: 'website_mail.chatter',
events: {
"click .o_website_mail_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: '/website_mail/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: '/website_mail/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<Object>}
* @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('/website_mail/static/src/xml/website_mail.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) {
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);
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_website_mail_messages').html(qweb.render("website_mail.chatter_messages", {widget: this}));
},
_renderMessageCount: function () {
this.$('.o_message_counter').replaceWith(qweb.render("website_mail.chatter_message_count", {widget: this}));
},
_renderPager: function () {
this.$('.o_website_mail_pager').replaceWith(qweb.render("website_mail.pager", {widget: this}));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onChangeDomain: function () {
var self = this;
this.messageFetch().then(function () {
var p = self.get('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);
PortalChatter.include({
_loadTemplates: function(){
return $.when(this._super(), ajax.loadXML('/website_mail/static/src/xml/website_mail.xml', qweb));
},
});
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 {
WebsiteMailThread: WebsiteMailThread,
};
});
@@ -9,124 +9,15 @@
</t>
</t>
<t t-name="website_mail.chatter_message_count">
<t t-set="count" t-value="widget.get('message_count')"/>
<div class="o_message_counter">
<t t-if="count">
<span class="o_message_count fa fa-comments"> <t t-esc="count"/></span>
<t t-if="count == 1">comment</t>
<t t-else="">comments</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-else="">
There are no comments for now.
</t>
</div>
</t>
</t>
<t t-name="website_mail.chatter_composer">
<div class="o_website_mail_composer" t-if="widget.options['allow_composer']">
<t t-if="!widget.options['display_composer']">
<h4>Leave a comment</h4>
<p>You must be <a t-attf-href="/web/login?redirect=#{window.encodeURI(window.location.href + '#discussion')}">logged in</a> to post a comment.</p>
</t>
<t t-if="widget.options['display_composer']">
<div class="media">
<div class="media-left">
<img class="o_website_mail_avatar pull-left" t-attf-src="/web/image/res.partner/#{widget.options['partner_id']}/image_small/50x50"/>
</div>
<div class="media-body">
<form class="o_website_mail_composer_form" t-attf-action="/website_mail/post/post" method="POST">
<input type="hidden" name="csrf_token" t-att-value="widget.options['csrf_token']"/>
<div class="mb32">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
<input type='hidden' name="res_model" t-att-value="widget.options['res_model']"/>
<input type='hidden' name="res_id" t-att-value="widget.options['res_id']"/>
<input type='hidden' name="token" t-att-value="widget.options['token']" t-if="widget.options['token']"/>
<input type='hidden' name="sha_in" t-att-value="widget.options['sha_in']" t-if="widget.options['sha_in']"/>
<input type='hidden' name="sha_time" t-att-value="widget.options['sha_time']" t-if="widget.options['sha_time']"/>
<input type='hidden' name="token_field" t-att-value="widget.options['token_field']" t-if="widget.options['token_field']"/>
<div class="alert alert-danger mt8 mb0 o_website_mail_composer_error" style="display:none;">
Oops! Something went wrong. Try to reload the page and log in.
</div>
<button t-attf-class="o_website_mail_composer_btn btn btn-primary mt8 o_website_message_post_helper" type="submit">Send</button>
</div>
</form>
</div>
</div>
</t>
<hr/>
</div>
</t>
<t t-name="website_mail.chatter_messages">
<div class="o_website_mail_messages">
<t t-foreach="widget.get('messages')" t-as="message">
<div class="media o_website_mail_message" t-att-id="'message-' + message.id">
<div class="media-left">
<img class="media-object o_website_mail_avatar" t-att-src="message.author_avatar_url" alt="avatar"/>
</div>
<div class="media-body">
<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>
<div class="o_website_mail_message_title">
<h5 class="media-heading">
<t t-esc="message.author_id[1]"/>
</h5>
<p class="o_website_mail_puslished_date"><t t-esc="message.published_date_str"/></p>
</div>
<t t-raw="message.body"/>
<div class="o_website_mail_attachments">
<div class="col-md-2 col-sm-3 text-center" t-foreach='message.attachment_ids' t-as='attachment'>
<a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank">
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/>
<div class='oe_attachment_name'><t t-raw='attachment.name'/></div>
</a>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-name="website_mail.pager">
<div class="o_website_mail_pager">
<t t-if="!_.isEmpty(widget.get('pager'))">
<ul class="pagination" t-if="widget.get('pager')['pages'].length &gt; 1">
<li t-if="widget.get('pager')['page'] != widget.get('pager')['page_previous']" t-att-data-page="widget.get('pager')['page_previous']" class="o_website_mail_pager_btn">
<a href="#"><i class="fa fa-chevron-left"/></a>
</li>
<t t-foreach="widget.get('pager')['pages']" t-as="page">
<li t-att-data-page="page" t-att-class="page == widget.get('pager')['page'] ? 'o_website_mail_pager_btn active' : 'o_website_mail_pager_btn'">
<a href="#"><t t-esc="page"/></a>
</li>
</t>
<li t-if="widget.get('pager')['page'] != widget.get('pager')['page_next']" t-att-data-page="widget.get('pager')['page_next']" class="o_website_mail_pager_btn">
<a href="#"><i class="fa fa-chevron-right"/></a>
</li>
</ul>
</t>
</div>
</t>
<t t-name="website_mail.chatter">
<div class="o_website_mail_chatter">
<div class="o_website_mail_header">
<t t-call="website_mail.chatter_message_count"/>
</div>
<hr/>
<t t-call="website_mail.chatter_composer"/>
<t t-call="website_mail.chatter_messages"/>
<div class="o_website_mail_footer">
<t t-call="website_mail.pager"/>
</div>
</div>
</t>
</templates>
@@ -25,21 +25,4 @@
</xpath>
</template>
<!--
Generic chatter template for the frontend
This template provide the container of the chatter. The rest is done in js.
To use this template, you need to call it after setting the following variable in your template or in your controller:
:object browserecord : the mail_thread object
:message_per_page int (optional): number of message per chatter page
:token string (optional): if you want your chatter to be available for non-logged user,
you can use a token to verify the identity of the user;
the message will be posted with the identity of the partner_id of the object
:token_field string (optional): name of the field that contains the token on the object (default to 'token')
-->
<template id="message_thread">
<div id="discussion" class="hidden-print o_website_mail_thread"
t-att-data-token="token" t-att-public-user="website.env.user == request.website.user_id" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-token_field="token_field" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'">
</div>
</template>
</odoo>
@@ -117,7 +117,7 @@
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="project"/>
</t>
</div>
@@ -235,7 +235,7 @@
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="task"/>
</t>
</div>
+1 -1
View File
@@ -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):
@@ -430,7 +430,7 @@
<xpath expr="//div[@id='quote_content']" position="inside">
<h1 class="page-header">History</h1>
<!-- Options:Quotation Chatter: user can reply -->
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="quotation"/>
<t t-set="token_field" t-value="'access_token'"/>
</t>
@@ -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)
+3 -7
View File
@@ -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)
@@ -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",
@@ -27,7 +27,7 @@
<t t-call="website_rating.rating_stars_static">
<t t-set="val" t-value="widget.get('rating_card_values')['avg'] || 0"/>
</t>
<t t-call="website_mail.chatter_message_count"/>
<t t-call="portal.chatter_message_count"/>
</div>
</div>
<div class="col-md-6" t-if="!_.isEmpty(widget.get('rating_card_values'))">
@@ -76,15 +76,15 @@
</t>
<!--
Inherited templates from website_mail to custom chatter with rating
Inherited templates from portal to custom chatter with rating
-->
<t t-extend="website_mail.chatter_composer">
<t t-jquery=".o_website_mail_composer_form input[name='csrf_token']" t-operation="after">
<t t-extend="portal.chatter_composer">
<t t-jquery=".o_portal_chatter_composer_form input[name='csrf_token']" t-operation="after">
<t t-call="website_rating.rating_star_input"/>
</t>
</t>
<t t-extend="website_mail.chatter_messages">
<t t-extend="portal.chatter_messages">
<t t-jquery="t[t-raw='message.body']" t-operation="before">
<t t-if="message['rating_value']">
<t t-call="website_rating.rating_stars_static">
@@ -94,13 +94,13 @@
</t>
</t>
<t t-extend="website_mail.chatter">
<t t-jquery="t[t-call='website_mail.chatter_message_count']" t-operation="replace">
<t t-extend="portal.chatter">
<t t-jquery="t[t-call='portal.chatter_message_count']" t-operation="replace">
<t t-if="widget.options['display_rating']">
<t t-call="website_rating.rating_card"/>
</t>
<t t-if="!widget.options['display_rating']">
<t t-call="website_mail.chatter_message_count"/>
<t t-call="portal.chatter_message_count"/>
</t>
</t>
</t>
@@ -8,7 +8,7 @@
</xpath>
</template>
<template id="message_thread" inherit_id="website_mail.message_thread">
<template id="message_thread" inherit_id="portal.message_thread">
<xpath expr="//div[@id='discussion']" position="attributes">
<attribute name="t-att-data-display_rating">display_rating or False</attribute>
</xpath>
+1 -1
View File
@@ -552,7 +552,7 @@
<hr/>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="product"/>
<t t-set="display_rating" t-value="True"/>
</t>
@@ -543,7 +543,7 @@
</h4>
</div>
<div t-att-class="comments and 'tab-pane fade active in' or 'tab-pane fade'" id="discuss">
<t t-call="website_mail.message_thread">
<t t-call="portal.message_thread">
<t t-set="object" t-value="slide"/>
</t>
</div>