[IMP] website_mail: make the frontend chatter lazy

Message are now fetched and display using javascript
to optimize performance.
Also the chatter is completely rewrite : add pager,
filter possiblity, ...

This commit is mainly technical: posting feature does
not change. Only the display with pager is new.
The goal is also to prepare frontend chatter for a
better integration with rating widget.

The mail.thread mixin is extended to manage the field
website_message_ids (display all messages that can be
seen on frontend by user (employee or not).

When fetching or posting message, a check is done to see
if we need to do it as sudo(). website_mail implement posting
messages with token or sha_in (using in website_quote).
The same verification is now done when fetching messages, via
'_special_access_object' method.
This commit is contained in:
Jérome Maes
2017-06-30 11:06:32 +02:00
parent 890e665b88
commit d79678fdd5
10 changed files with 497 additions and 195 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
'data/mail_channel_data.xml',
],
'qweb': [
'static/src/xml/chatter_message.xml'
'static/src/xml/website_mail.xml'
],
'installable': True,
'auto_install': True,
+65 -27
View File
@@ -1,11 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from hashlib import sha1
from time import time
from werkzeug.exceptions import NotFound
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.
@@ -27,13 +34,17 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_
"""
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 and record and token == getattr(record.sudo(), token_field, None):
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
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:
if not author_id:
raise NotFound()
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"),
@@ -98,25 +109,52 @@ class WebsiteMail(http.Controller):
]))
return values
@http.route(['/website_mail/post/json'], type='json', auth='public', website=True)
def chatter_json(self, res_model='', res_id=None, message='', **kw):
try:
msg = _message_post_helper(res_model, int(res_id), message, **kw)
except Exception:
return False
data = {
'id': msg.id,
'body': msg.body,
'date': msg.date,
'author': msg.author_id.name,
'image_url': '/mail/%s/%s/avatar/%s' % (msg.model, msg.res_id, msg.author_id.id)
}
return data
@http.route(['/website_mail/post/post'], type='http', methods=['POST'], auth='public', website=True)
def chatter_post(self, res_model='', res_id=None, message='', redirect=None, **kw):
def website_chatter_post(self, res_model, res_id, message, **kw):
url = request.httprequest.referrer
if message:
message = _message_post_helper(res_model, int(res_id), message, **kw)
url = url + "#message-%s" % (message.id,)
_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)]
# None published can see only the published messages
if not request.env.user.has_group('website.group_website_publisher'):
domain += [('website_published', '=', True)]
# 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)
}
@@ -56,3 +56,15 @@ class MailMessage(models.Model):
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))
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
@@ -1,4 +0,0 @@
.js_follow[data-follow='on'] .js_follow_btn ,
.js_follow[data-follow='off'] .js_unfollow_btn {
display: none;
}
@@ -0,0 +1,38 @@
.js_follow[data-follow='on'] .js_follow_btn ,
.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,80 +0,0 @@
odoo.define('website_mail.thread', function(require) {
'use strict';
var web_editor_base = require('web_editor.base');
var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var qweb = core.qweb;
// load qweb template
ajax.loadXML('/website_mail/static/src/xml/chatter_message.xml', qweb);
/**
* Widget WebsiteMailThread
*
* Widget sending message to the server, using json request. Used to not refresh all
* the page. Its DOM already exists on the page (The class of root element is
* '.o_website_mail_thread').
*/
var WebsiteMailThread = Widget.extend({
events:{
"click .o_website_chatter_json": "on_click",
},
on_click: function(e){
var self = this;
e.preventDefault();
var $button = this.$(e.currentTarget);
var $form = this.$('.o_website_chatter_form');
var $error = this.$('.o_website_chatter_error');
var action = $form.attr('action');
var data = this._get_form_data($form);
data.message = data.message.replace(/\n/g,"<br/>");
if (data.message) {
// make the 'send' button loading
$button.attr('disabled', true);
var button_bk = $button.html();
$button.prepend('<i class="fa fa-refresh fa-spin"></i> ');
// post message, shw/hide error message and empty textarea
ajax.jsonRpc(action, 'call', data).then(function (result) {
if (result) {
$error.fadeOut();
self.prepend_message(result);
$form.find('textarea').val('');
} else {
$error.fadeIn();
}
$button.html(button_bk);
$button.attr('disabled', false);
});
}
},
prepend_message: function(message_data){
var msg = qweb.render('website_mail.thread_message', message_data);
var elem = $(msg).hide().prependTo(this.$('.o_website_comments'));
elem.slideToggle();
return elem;
},
_get_form_data: function($form){
var unindexed_array = $form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function(n, i){
indexed_array[n.name] = n.value;
});
return indexed_array;
},
});
web_editor_base.ready().then(function(){
if($('.o_website_mail_thread').length) {
var mail_thread = new WebsiteMailThread($('body')).setElement($('.o_website_mail_thread'));
}
});
return {
WebsiteMailThread: WebsiteMailThread,
}
});
@@ -0,0 +1,232 @@
odoo.define('website_mail.thread', function(require) {
'use strict';
var web_editor_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 WebsiteMailThread
*
* - Fetch message fron controller
* - Display chatter: pager, total message, composer (according to access right)
* - Provider API to filter displayed messages
*/
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('current_page', this.options['pager_start']);
this.set('pager', {});
this.set('domain', this.options['domain']);
},
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.get('current_page')));
});
self.on("change:pager", self, self._renderPager);
self.on("change:domain", self, self._onChangeDomain);
self.on("change:current_page", self, self._onChangeCurrentPage);
// 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']));
});
},
//--------------------------------------------------------------------------
// 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
//--------------------------------------------------------------------------
/**
* @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.get('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 ajax.jsonRpc('/website_mail/fetch', 'call', data).then(function(result){
self.set('messages', self.preprocessMessages(result['messages']));
self.set('message_count', result['message_count']);
});
},
/**
* 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_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
//--------------------------------------------------------------------------
_onChangeCurrentPage: function(){
var self = this;
this.messageFetch().then(function(){
var p = self.get('current_page');
self.set('pager', self._pager(p));
});
},
_onChangeDomain: function(){
this.set('current_page', 1);
},
/**
* @private
* @param {MouseEvent} event
*/
_onClickPager: function(ev){
ev.preventDefault();
var page = $(ev.currentTarget).data('page');
this.set('current_page', page);
},
});
web_editor_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,
};
});
@@ -1,15 +0,0 @@
<templates id="template" xml:space="preserve">
<t t-name="website_mail.thread_message">
<li class="media" t-att-id="'message-' + id">
<div class="media-body">
<img class="media-object pull-left img-circle" t-att-src="image_url" style="width: 50px; margin-right: 10px;"/>
<div class="media-body">
<h5 class="media-heading">
<span t-esc="author"/> <small>just now</small>
</h5>
<div t-raw="body"/>
</div>
</div>
</li>
</t>
</templates>
@@ -0,0 +1,132 @@
<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="pull-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-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 t-else="">
There are no comments for now.
</t>
</div>
</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..." style="width: 75%"></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>
@@ -20,77 +20,26 @@
<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/message_post.js"></script>
<link rel='stylesheet' href='/website_mail/static/src/css/website_mail.css'/>
<script type="text/javascript" src="/website_mail/static/src/js/website_mail.js"></script>
<link rel='stylesheet' href='/website_mail/static/src/css/website_mail.less'/>
</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">
<!--
Generic chatter template for the frontend
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
: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')
:chatter_mode string: 'json' or 'post' depending on what kind of request you want
-->
<section id="discussion" class="hidden-print oe_website_portal o_website_mail_thread">
<t t-set="is_user_public" t-value="website.env.user == request.website.user_id"/>
<t t-set="can_comment" t-value="token or not is_user_public"/>
<section class="mb32 hidden-print" t-if="can_comment">
<form class="o_website_chatter_form" t-attf-action="/website_mail/post/#{chatter_mode}" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<img class="img pull-left img-circle" t-attf-src="/web/image/res.partner/#{user_id.partner_id.id}/image_small/50x50" style="width: 50px; margin-right: 10px;"/>
<div class="pull-left mb32" style="width: 75%%">
<textarea rows="4" name="message" class="form-control" placeholder="Write a message..."></textarea>
<input type='hidden' name="res_model" t-att-value="object._name"/>
<input type='hidden' name="res_id" t-att-value="object.id"/>
<input type='hidden' name="token" t-att-value="token" t-if="token"/>
<input type='hidden' name="token_field" t-att-value="token_field" t-if="token_field"/>
<div class="alert alert-danger mt8 mb0 o_website_chatter_error" style="display:none;">
Oops! Something went wrong. Try to reload the page and to log in.
</div>
<button t-attf-class="o_website_chatter_#{chatter_mode} btn btn-primary mt8 o_website_message_post_helper" type="submit">Send</button>
</div>
</form>
</section>
<section class="mb32 hidden-print" t-if="not can_comment">
<h3>Leave a comment</h3>
<p>You must be <a t-attf-href="/web/login?redirect=#{quote_plus(request.httprequest.url + '#discussion')}">logged in</a> to post a comment.</p>
</section>
<div class="clearfix"/>
<ul class="media-list o_website_comments">
<t t-set="messages" t-value="object.message_ids"/>
<t groups="base.group_portal,base.group_public">
<t t-set="messages" t-value="object.message_ids.filtered(lambda m: m.subtype_id.internal == False)"/>
</t>
<t t-foreach="messages" t-as="message">
<t t-if="message.website_published or website.env.user.sudo(website.env.user.id).has_group('website.group_website_publisher')">
<li class="media" t-if="message.message_type != 'notification'" t-att-id="'message-%s' % message.id">
<div class="media-body">
<img class="media-object pull-left img-circle" t-attf-src="/mail/#{message.model}/#{message.res_id}/avatar/#{message.author_id.id}"
style="width: 50px; margin-right: 10px;"/>
<div class="media-body">
<h5 class="media-heading">
<span t-field="message.author_id"/> <small>on <span t-field="message.date"/></small>
</h5>
<t t-set="object" t-value="message"/>
<t t-call="website.publish_short"/>
<div t-field="message.body"/>
<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-esc='attachment.name' /></div>
</a>
</div>
</div>
</div>
</li>
</t>
</t>
</ul>
</section>
<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>