[MERGE] website_*: new website chatter

This commit brings a new frontend chatter.
The old one displayed all message on the website
page, and that could be very slow when many many
messages are posted on a document.

The new chatter is more dynamic: we introduce
a pager, to fetch and display only a few messages.
It also still support to post a message if you are
not logged in, using a token mecanism (see
website_quote).

Moreover, a better integration with rating: in the
eshop, when posting a comment, the visitor can add
a rating value (stars).
This commit is contained in:
Jérome Maes
2017-06-30 11:08:31 +02:00
49 changed files with 1112 additions and 724 deletions
+3
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
from . import rating
from . import mail_thread
from . import mail_message
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MailMessage(models.Model):
_inherit = 'mail.message'
rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings')
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=True)
@api.multi
@api.depends('rating_ids', 'rating_ids.rating')
def _compute_rating_value(self):
ratings = self.env['rating.rating'].search([('message_id', 'in', self.ids), ('consumed', '=', True)], order='create_date DESC')
mapping = dict((r.message_id.id, r.rating) for r in ratings)
for message in self:
message.rating_value = mapping.get(message.id, 0.0)
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
@api.multi
@api.returns('self', lambda value: value.id)
def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs):
rating_value = kwargs.get('rating_value')
message = super(MailThread, self).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs)
if rating_value:
ir_model = self.env['ir.model'].sudo().search([('model', '=', self._name)])
self.env['rating.rating'].create({
'rating': float(rating_value) if rating_value is not None else False,
'res_model_id': ir_model.id,
'res_id': self.id,
'message_id': message.id,
'consumed': True,
'partner_id': self.env.user.partner_id.id,
})
return message
@@ -1,48 +0,0 @@
/* Star Widget */
.o_rating_star_card{
margin-bottom: 5px;
}
.o_rating_star_card .stars {
display: inline-block;
color: #FACC2E;
margin-right: 15px;
}
.o_rating_star_card .stars i {
margin-right: -3px;
text-align: center;
}
.o_rating_star_card .stars.enabled{
cursor: pointer;
}
.o_rating_star_card .rate_text{
display: inline-block;
}
/* Rating Card */
.o_rating_card .progress {
position: relative;
height: 25px;
margin-bottom: 3px;
}
.o_rating_card .progress-bar {
background-color: #FACC2E;
}
.o_rating_card .progress > .progress_type {
position: absolute;
left: 0px;
font-weight: 800;
padding: 3px 10px 2px 10px;
color: rgb(120, 100, 100);
width: 100%;
}
.o_rating_card .progress > .progress_completed {
position: absolute;
right: 0px;
font-weight: 800;
padding: 3px 10px 2px;
}
@@ -1,127 +0,0 @@
odoo.define('rating.rating', function (require) {
"use strict";
var Widget = require('web.Widget');
var translation = require('web.translation');
var _t = translation._t;
// Star Widget
var RatingStarWidget = Widget.extend({
template: 'rating.rating_star_card',
events: {
"mousemove .stars i" : "moveOnStars",
"mouseleave .stars i" : "moveOut",
"click .stars" : "clickOnStar",
"mouseleave .stars" : "moveOutStars",
},
init: function(parent, options){
this._super.apply(this, arguments);
this.options = _.defaults(options || {}, {
'rating_default_value': 0,
'rating_disabled': 0,
});
this.labels = {
'0': "",
'1': _t("I hate it"),
'2': _t("I don't like it"),
'3': _t("It's okay"),
'4': _t("I like it"),
'5': _t("I love it"),
};
this.user_click = false; // user has click or not
this.set("star_value", 0);
this.on("change:star_value", this, this.changeStars);
},
start: function(){
this.$input = this.$('input');
this.star_list = this.$('.stars').find('i');
// set the default value
this.set("star_value", this.options.rating_default_value);
this.is_editable = !this.options.rating_disabled;
},
attachTo: function(el){
this._super.apply(this, arguments);
// set the default value and bind event
var default_value = this.$('input').data('default') || this.options['rating_default_value'];
default_value = this.roundToHalf(default_value);
this.set('star_value', default_value);
// is_editable value from DOM
this.is_editable = !this.$input.data('is_disabled') || this.options['rating_disabled'];
},
changeStars: function(){
var val = this.get("star_value");
var index = Math.floor(val);
var decimal = val - index;
// reset the stars
this.star_list.removeClass('fa-star fa-star-half-o').addClass('fa-star-o');
this.$('.stars').find("i:lt("+index+")").removeClass('fa-star-o fa-star-half-o').addClass('fa-star');
if(decimal){
this.$('.stars').find("i:eq("+(index)+")").removeClass('fa-star-o fa-star fa-star-half-o').addClass('fa-star-half-o');
}
this.$input.val(val);
this.$('.rate_text .label').text(this.labels[index]);
},
moveOut: function(){
if(!this.user_click && this.is_editable){
this.set("star_value", 0);
}
this.user_click = false;
},
moveOnStars: function(e){
if(this.is_editable){
this.$('.rate_text').show();
var index = this.$('.stars i').index(e.currentTarget);
this.set("star_value", index+1);
}
},
clickOnStar: function(e){
if(this.is_editable){
this.user_click = true;
}
},
moveOutStars: function(e){
this.$('.rate_text').hide();
},
roundToHalf: function(value) {
var converted = parseFloat(value); // Make sure we have a number
var decimal = (converted - parseInt(converted, 10));
decimal = Math.round(decimal * 10);
if(decimal == 5){
return (parseInt(converted, 10)+0.5);
}
if((decimal < 3) || (decimal > 7)){
return Math.round(converted);
}else{
return (parseInt(converted, 10)+0.5);
}
},
});
var page_widgets = {};
$(document).ready(function(){
// Rating Card
$('[data-toggle="rating-popover"]').popover({
html : true,
trigger: 'hover',
title: function() {
return $(this).parent().find('.rating_popover').find('.popover-title').html();
},
content: function() {
return $(this).parent().find('.rating_popover').find('.popover-content').html();
}
});
// Rating Star Widget instances
$('.o_rating_star_card').each(function(index, elem){
page_widgets[index] = new RatingStarWidget();
page_widgets[index].attachTo(elem);
})
});
return {
RatingStarWidget: RatingStarWidget,
rating_star_widgets: page_widgets,
};
});
@@ -1,21 +0,0 @@
<templates id="template" xml:space="preserve">
<t t-name="rating.rating_star_card">
<div class="o_rating_star_card">
<div t-att-class="'stars ' + (widget.options.rating_disabled ? 'disabled' : 'enabled')">
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
</div>
<div t-if="!widget.options.rating_disabled" class="rate_text">
<span class="label label-info"></span>
</div>
<input type="hidden" readonly="readonly"
name="rating"
t-att-data-default="widget.options.rating_default_value || 0.0"
t-att-data-is_disabled="widget.options.rating_disabled ? 1 : 0"/>
</div>
</t>
</templates>
-85
View File
@@ -1,91 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Bundle for Rating Widget (in assets common to be used in website modules) -->
<template id="assets_common" inherit_id="web.assets_common" name="Rating common assets">
<xpath expr='.' position="inside">
<script src="/rating/static/src/js/rating_common.js"></script>
<link href="/rating/static/src/css/rating_common.css" rel="stylesheet"/>
</xpath>
</template>
<!-- Rating Star Widget (Generic widget)
:param String rating_input_name : the name of the input field to send the rating
:param Float rating_default_value : the rate to initialize the widget with
:param Boolean rating_disabled : is the widget disabled (False), or it is just used to display information (True). Default value is False
-->
<template id="rating_star_card" name="Rating Star Widget">
<div class="o_rating_star_card">
<div t-attf-class="stars #{rating_disabled and 'disabled' or 'enabled'}">
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
</div>
<div t-if="not rating_disabled" class="rate_text">
<span class="label label-info"></span>
</div>
<input type="hidden" readonly="readonly"
t-att-name="rating_input_name or 'rating'"
t-att-data-default="rating_default_value or 0.0"
t-att-data-is_disabled="rating_disabled and 1 or 0"/>
</div>
</template>
<!-- Rating card : display the result (avg, total, ...) for rating
:param dict rating_stat : a dictionnary with
'avg' : containing the rating note average
'total' : the number of rating for the object
'percent' : a dict where the key is each possible rate, and the value is
the percent of rating having this rate.
:param float max_rate : the upper bound of the rate to display (in the popover)
:param float min_rate : the lower bound of the rate to display (in the popover)
-->
<template id="rating_card" name="Rating Card">
<div class="o_rating_card">
<div class="text-center well well-sm">
<!-- popover 'button' -->
<div class="rating"
data-placement="top"
data-toggle="rating-popover">
<t t-call="rating.rating_star_card">
<t t-set="rating_default_value" t-value="'%.2f' % (rating_stat.get('avg', 0.0),)"/>
<t t-set="rating_disabled" t-value="True"/>
</t>
<i class="fa fa-user"> <t t-esc="rating_stat.get('total', 0)"/></i>
</div>
<!-- popover core -->
<div class="popover rating_popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-content">
<t t-foreach="rating_stat.get('percent')" t-as="percent">
<t t-if="min_rate &lt;= percent and percent &lt;= max_rate">
<div class="progress" style="width:200px;">
<div class="progress-bar" role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
t-att-aria-valuenow="rating_stat['percent'][percent]"
t-attf-style="width: #{rating_stat['percent'][percent]}%">
<span class="sr-only">
<t t-esc="rating_stat['percent'][percent]"/>%
</span>
</div>
<span class="progress_type">
<t t-esc="percent"/> <i class="fa fa-star"/>
</span>
<span class="progress_completed">
<t t-esc="rating_stat['percent'][percent]"/>%
</span>
</div>
</t>
</t>
</div>
</div>
</div>
</div>
</template>
<!-- External page : thanks message -->
<template id="rating_external_page_view" name="Rating Page Done">
<t t-call="web.layout">
+2 -8
View File
@@ -140,14 +140,8 @@ class BlogPost(models.Model):
teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser')
teaser_manual = fields.Text(string='Teaser Content')
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: [
'&', '&', ('model', '=', self._name), ('message_type', '=', 'comment'), ('path', '=', False)
],
string='Website Messages',
help="Website communication history",
)
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment'), ('path', '=', False)])
# creation / update stuff
create_date = fields.Datetime('Created on', index=True, readonly=True)
published_date = fields.Datetime('Published Date')
@@ -299,13 +299,11 @@
</template>
<!-- Options: Blog Post: user can reply -->
<template id="opt_blog_post_complete_comment" name="Allow blog post comment"
inherit_id="website_blog.blog_post_complete" active="False" customize_show="True">
<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[@class='o_blog_post_complete o_sharing_links']" position="after">
<section id="comments" class="read_width">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="blog_post"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</section>
</xpath>
@@ -517,7 +517,6 @@
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="lead"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
@@ -800,7 +799,6 @@
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="opportunity"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
+1 -8
View File
@@ -19,14 +19,7 @@ class Event(models.Model):
_inherit = ['event.event', 'website.seo.metadata', 'website.published.mixin']
website_published = fields.Boolean(track_visibility='onchange')
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: [
'&', ('model', '=', self._name), ('message_type', '=', 'comment')
],
string='Website Messages',
help="Website communication history",
)
is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating")
website_menu = fields.Boolean(
+1 -5
View File
@@ -209,11 +209,7 @@ class Post(models.Model):
('link', 'Article'),
('discussion', 'Discussion')],
string='Type', default='question', required=True)
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: ['&', ('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])],
string='Post Messages', help="Comments on forum post",
)
website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])
# history
create_date = fields.Datetime('Asked on', index=True, readonly=True)
+2 -1
View File
@@ -11,9 +11,10 @@
'data': [
'views/website_mail_templates.xml',
'data/mail_channel_data.xml',
'security/website_mail_security.xml',
],
'qweb': [
'static/src/xml/chatter_message.xml'
'static/src/xml/website_mail.xml'
],
'installable': True,
'auto_install': True,
+62 -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,49 @@ 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)]
# 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,5 +1,6 @@
# -*- 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
@@ -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
+12
View File
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
website_message_ids = fields.One2many('mail.message', 'res_id', string='Website Messages',
domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')], auto_join=True,
help="Website communication history")
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_message_rule_public" model="ir.rule">
<field name="name">mail.message: portal/public: read published messages</field>
<field name="model_id" ref="model_mail_message"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_portal')), (4, ref('base.group_public'))]"/>
</record>
</data>
</odoo>
@@ -1,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,237 @@
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('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){
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
//--------------------------------------------------------------------------
_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);
},
});
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..."></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>
@@ -134,7 +134,6 @@
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="project"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
@@ -254,7 +253,6 @@
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="task"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
@@ -136,7 +136,6 @@
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="issue"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
@@ -438,15 +438,12 @@
<template id="chatter" name="Allow Comments &amp; History" customize_show="True" inherit_id="website_quote.so_quotation">
<xpath expr="//div[@id='quote_content']" position="inside">
<section id="discussion" class="hidden-print">
<h1 class="page-header">History</h1>
<!-- Options:Quotation Chatter: user can reply -->
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="quotation"/>
<t t-set="token_field" t-value="'access_token'"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</section>
<h1 class="page-header">History</h1>
<!-- Options:Quotation Chatter: user can reply -->
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="quotation"/>
<t t-set="token_field" t-value="'access_token'"/>
</t>
</xpath>
<xpath expr="//a[@data-target='#modaldecline']" position="before">
<a type="submit" href="#discussion">
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
+17
View File
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Website Rating',
'category': 'Hidden',
'version': '1.0',
'description': """
Bridge module for rating on website stuff.
""",
'depends': ['rating', 'website_mail'],
'data': [
'views/website_rating_templates.xml',
],
'qweb': ['static/src/xml/*.xml'],
'auto_installable': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import website_mail
@@ -0,0 +1,31 @@
# -*- 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
class WebsiteRating(WebsiteMail):
@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)
# get the rating statistics about the record
if kwargs.get('rating_include'):
record = request.env[res_model].browse(res_id)
if hasattr(record, 'rating_get_stats'):
result['rating_stats'] = record.rating_get_stats([('website_published', '=', True)])
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
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)
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mail_message
from . import rating
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MailMessage(models.Model):
_inherit = 'mail.message'
@api.multi
def website_message_format(self):
result = super(MailMessage, self).website_message_format()
# 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
@@ -0,0 +1,234 @@
odoo.define('website_rating.thread', function(require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var qweb = core.qweb;
var _t = core._t;
var WebsiteMailThread = require('website_mail.thread').WebsiteMailThread;
/**
* Extends Frontend Chatter to handle rating
*/
WebsiteMailThread.include({
events: _.extend({}, WebsiteMailThread.prototype.events, {
"mousemove .stars i" : "_onMoveStar",
"mouseleave .stars i" : "_onMoveOutStar",
"click .stars" : "_onClickStar",
"mouseleave .stars" : "_onMouseleaveStar",
"click .o_website_rating_select": "_onClickStarDomain",
"click .o_website_rating_select_text": "_onClickStarDomainReset",
}),
init: function(parent, options){
this._super.apply(this, arguments);
// options
if(!_.contains(this.options, 'display_rating')){
this.options = _.defaults(this.options, {
'display_rating': false,
'rating_default_value': 0.0,
});
}
// rating card
this.set('rating_card_values', {});
this.set('rating_value', false);
this.on("change:rating_value", this, this._onChangeRatingDomain);
// rating star
this.labels = {
'0': "",
'1': _t("I hate it"),
'2': _t("I don't like it"),
'3': _t("It's okay"),
'4': _t("I like it"),
'5': _t("I love it"),
};
this.user_click = false; // user has click or not
this.set("star_value", this.options.rating_default_value);
this.on("change:star_value", this, this._onChangeStarValue);
},
willStart: function(){
var self = this;
return this._super.apply(this, arguments).then(function(result){
// rating card
if(result['rating_stats']){
var rating_data = {
'avg': self.round_to_half(result['rating_stats']['avg']),
'percent': [],
};
_.each(_.keys(result['rating_stats']['percent']), function(rating){
if(0 < rating && rating <= 5){
rating_data['percent'].push({
'num': rating,
'percent': result['rating_stats']['percent'][rating],
});
}
});
self.set('rating_card_values', rating_data);
}
});
},
start: function(){
var self = this;
return this._super.apply(this, arguments).then(function(){
// rating stars
self.$input = self.$('input[name="rating_value"]');
self.$star_list = self.$('.stars').find('i');
self.set("star_value", self.options.rating_default_value); // set the default value to trigger the display of star widget
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Update the messages format
*
* @param {Array<Object>}
* @returns {Array}
*/
preprocessMessages: function(messages){
var self = this;
var messages = this._super.apply(this, arguments);
if(this.options['display_rating']){
_.each(messages, function(m){
m['rating_value'] = self.round_to_half(m['rating_value']);
});
}
return messages;
},
/**
* Round the given value with a precision of 0.5.
* Example : 1.2 --> 1.0
* 1.7 --> 1.5
* 1.9 --> 2.0
* @param Float
* @returns Float
**/
round_to_half: function(value) {
var converted = parseFloat(value); // Make sure we have a number
var decimal = (converted - parseInt(converted, 10));
decimal = Math.round(decimal * 10);
if(decimal == 5){
return (parseInt(converted, 10)+0.5);
}
if((decimal < 3) || (decimal > 7)){
return Math.round(converted);
}else{
return (parseInt(converted, 10)+0.5);
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_loadTemplates: function(){
return $.when(this._super(), ajax.loadXML('/website_rating/static/src/xml/website_mail.xml', qweb));
},
_messageFetchPrepareParams: function(){
var params = this._super.apply(this, arguments);
if(this.options['display_rating']){
params['rating_include'] = true;
}
return params;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} event
*/
_onClickStar: function(e){
this.user_click = true;
this.$input.val(this.get("star_value"));
},
/**
* @private
* @param {MouseEvent} event
*/
_onClickStarDomain: function(e){
var $tr = this.$(e.currentTarget);
var num = $tr.data('star');
if($tr.css('opacity') == 1){
this.set('rating_value', num);
this.$('.o_website_rating_select').css({
'opacity': 0.5,
});
this.$('.o_website_rating_select_text[data-star="'+num+'"]').css({
'visibility': 'visible',
'opacity': 1,
});
this.$('.o_website_rating_select[data-star="'+num+'"]').css({
'opacity': 1,
});
}
},
/**
* @private
* @param {MouseEvent} event
*/
_onClickStarDomainReset: function(e){
e.stopPropagation();
this.set('rating_value', false);
this.$('.o_website_rating_select_text').css('visibility', 'hidden');
this.$('.o_website_rating_select').css({
'opacity': 1,
});
},
/**
* @private
*/
_onChangeRatingDomain: function(){
var domain = [];
if(this.get('rating_value')){
domain = [['rating_value', '=', this.get('rating_value')]];
}
this._changeCurrentPage(1, domain);
},
/**
* @private
*/
_onChangeStarValue: function(){
var val = this.get("star_value");
var index = Math.floor(val);
var decimal = val - index;
// reset the stars
this.$star_list.removeClass('fa-star fa-star-half-o').addClass('fa-star-o');
this.$('.stars').find("i:lt("+index+")").removeClass('fa-star-o fa-star-half-o').addClass('fa-star');
if(decimal){
this.$('.stars').find("i:eq("+(index)+")").removeClass('fa-star-o fa-star fa-star-half-o').addClass('fa-star-half-o');
}
this.$('.rate_text .label').text(this.labels[index]);
},
_onMouseleaveStar: function(e){
this.$('.rate_text').hide();
},
/**
* @private
* @param {MouseEvent} event
*/
_onMoveStar: function(e){
var index = this.$('.stars i').index(e.currentTarget);
this.$('.rate_text').show();
this.set("star_value", index+1);
},
/**
* @private
*/
_onMoveOutStar: function(){
if(!this.user_click){
this.set("star_value", parseInt(this.$input.val()));
}
this.user_click = false;
},
});
});
@@ -0,0 +1,70 @@
/* static stars */
@rating-star-color: #FACC2E;
.o_website_rating_static{
color: @rating-star-color;
}
.o_website_rating_card_container {
.o_message_counter {
color: @gray;
}
/* progress bars */
table.o_website_rating_progress_table {
width: 100%;
overflow: visible;
.o_website_rating_table_star_num{
min-width: 30px;
}
.o_website_rating_select[style*="opacity: 1"] {
cursor: pointer;
}
.o_website_rating_table_progress{
min-width: 120px;
> .progress {
margin-bottom: 5px;
margin-left: 5px;
margin-right: 5px;
}
.o_rating_progressbar{
background-color: @rating-star-color;
}
}
.o_website_rating_table_percent {
text-align: right;
padding-left: 5px;
font-size: @font-size-small;
}
.o_website_rating_table_reset {
.o_website_rating_select_text {
visibility: hidden;
}
}
}
}
/* Star Widget */
.o_rating_star_card{
margin-bottom: 5px;
.stars {
display: inline-block;
color: #FACC2E;
margin-right: 15px;
}
.stars i {
margin-right: -3px;
text-align: center;
}
.stars.enabled{
cursor: pointer;
}
.rate_text{
display: inline-block;
}
}
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="website_rating.rating_stars_static">
<t t-set="val_integer" t-value="Math.floor(val)"/>
<t t-set="val_decimal" t-value="val - val_integer"/>
<t t-set="empty_star" t-value="5 - (val_integer+Math.ceil(val_decimal))"/>
<div class="o_website_rating_static">
<t t-foreach="_.range(0, val_integer)" t-as="num">
<i class="fa fa-star"></i>
</t>
<t t-if="val_decimal">
<i class="fa fa-star-half-o"></i>
</t>
<t t-foreach="_.range(0, empty_star)" t-as="num">
<i class="fa fa-star-o"></i>
</t>
</div>
</t>
<t t-name="website_rating.rating_card">
<div class="row o_website_rating_card_container">
<div class="col-md-3 col-md-offset-2" t-if="!_.isEmpty(widget.get('rating_card_values'))">
<p><strong>Average</strong></p>
<div class="o_website_rating_avg text-center">
<h1><t t-esc="widget.get('rating_card_values')['avg']"/></h1>
<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"/>
</div>
</div>
<div class="col-md-6" t-if="!_.isEmpty(widget.get('rating_card_values'))">
<p><strong>Details</strong></p>
<div class="o_website_rating_progress_bars">
<table class="o_website_rating_progress_table">
<t t-foreach="widget.get('rating_card_values')['percent']" t-as="percent">
<tr class="o_website_rating_select" t-att-data-star="percent['num']" style="opacity: 1">
<td class="o_website_rating_table_star_num" t-att-data-star="percent['num']">
<t t-esc="percent['num']"/> stars
</td>
<td class="o_website_rating_table_progress">
<div class="progress">
<div class="progress-bar o_rating_progressbar" role="progressbar" t-att-aria-valuenow="percent['percent']" aria-valuemin="0" aria-valuemax="100" t-att-style="'width:' + percent['percent'] + '%;'">
</div>
</div>
</td>
<td class="o_website_rating_table_percent">
<strong><t t-esc="percent['percent']"/>%</strong>
</td>
<td class="o_website_rating_table_reset">
<span class="btn btn-link o_website_rating_select_text" t-att-data-star="percent['num']">Remove selection</span>
</td>
</tr>
</t>
</table>
</div>
</div>
</div>
</t>
<t t-name="website_rating.rating_star_input">
<div class="o_rating_star_card" t-if="widget.options['display_rating']">
<div class="stars enabled">
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
<i class="fa fa-star-o"></i>
</div>
<div class="rate_text">
<span class="label label-info"></span>
</div>
<input type="hidden" readonly="readonly" name="rating_value" value="0.0"/>
</div>
</t>
<!--
Inherited templates from website_mail 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-call="website_rating.rating_star_input"/>
</t>
</t>
<t t-extend="website_mail.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">
<t t-set="val" t-value="message.rating_value"/>
</t>
</t>
</t>
</t>
<t t-extend="website_mail.chatter">
<t t-jquery="t[t-call='website_mail.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>
</t>
</templates>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website rating frontend assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_rating/static/src/js/website_mail.js"></script>
<link type="text/less" rel='stylesheet' href='/website_rating/static/src/less/website_rating.less'/>
</xpath>
</template>
<template id="message_thread" inherit_id="website_mail.message_thread">
<xpath expr="//div[@id='discussion']" position="attributes">
<attribute name="t-att-data-display_rating">display_rating or False</attribute>
</xpath>
</template>
</odoo>
+1 -1
View File
@@ -10,7 +10,7 @@ Odoo E-Commerce
==================
""",
'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_account', 'website_mail', 'website_form', 'rating'],
'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_account', 'website_mail', 'website_form', 'website_rating'],
'data': [
'security/ir.model.access.csv',
'security/website_sale.xml',
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import backend
from . import main
from . import website_mail
-8
View File
@@ -277,7 +277,6 @@ class WebsiteSale(http.Controller):
def product(self, product, category='', search='', **kwargs):
product_context = dict(request.env.context, active_id=product.id)
ProductCategory = request.env['product.public.category']
Rating = request.env['rating.rating']
if category:
category = ProductCategory.browse(int(category)).exists()
@@ -296,11 +295,6 @@ class WebsiteSale(http.Controller):
to_currency = pricelist.currency_id
compute_currency = lambda price: from_currency.compute(price, to_currency)
# get the rating attached to a mail.message, and the rating stats of the product
ratings = Rating.search([('message_id', 'in', product.website_message_ids.ids)])
rating_message_values = dict([(record.message_id.id, record.rating) for record in ratings])
rating_product = product.rating_get_stats([('website_published', '=', True)])
if not product_context.get('pricelist'):
product_context['pricelist'] = pricelist.id
product = product.with_context(product_context)
@@ -317,8 +311,6 @@ class WebsiteSale(http.Controller):
'main_object': product,
'product': product,
'get_attribute_value_ids': self.get_attribute_value_ids,
'rating_message_values': rating_message_values,
'rating_product': rating_product
}
return request.render("website_sale.product", values)
@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug import urls
from odoo import http
from odoo.http import request
from odoo.addons.website_mail.controllers.main import WebsiteMail
class WebsiteMailController(WebsiteMail):
@http.route(['/website_mail/post/json'], type='json', auth='public', website=True)
def chatter_json(self, res_model='', res_id=None, message='', **kw):
params = kw.copy()
params.pop('rating', False)
message_data = super(WebsiteMailController, self).chatter_json(res_model=res_model, res_id=res_id, message=message, **params)
if message_data and kw.get('rating') and res_model == 'product.template': # restrict rating only for product template
res_model_id = request.env.ref('product.model_product_template').id
rating = request.env['rating.rating'].create({
'rating': float(kw.get('rating')),
'res_model_id': res_model_id,
'res_id': res_id,
'message_id': message_data['id'],
'consumed': True,
})
message_data.update({
'rating_default_value': rating.rating,
'rating_disabled': True,
})
return message_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):
params = kw.copy()
params.pop('rating')
response = super(WebsiteMailController, self).chatter_post(res_model=res_model, res_id=res_id, message=message, redirect=redirect, **params)
if kw.get('rating') and res_model == 'product.template': # restrict rating only for product template
try:
fragment = urls.url_parse(response.location).fragment
message_id = int(fragment.replace('message-', ''))
res_model_id = request.env.ref('product.model_product_template').id
request.env['rating.rating'].create({
'rating': float(kw.get('rating')),
'res_model_id': res_model_id,
'res_id': res_id,
'message_id': message_id,
'consumed': True,
})
except Exception:
pass
return response
-1
View File
@@ -4,7 +4,6 @@
from . import crm_team
from . import ir_http
from . import product
from . import rating
from . import sale_order
from . import website
from . import website_config_settings
-5
View File
@@ -109,11 +109,6 @@ class ProductTemplate(models.Model):
_name = 'product.template'
_mail_post_access = 'read'
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: ['&', ('model', '=', self._name), ('message_type', '=', 'comment')],
string='Website Comments',
)
website_description = fields.Html('Description for the website', sanitize_attributes=False, translate=html_translate)
alternative_product_ids = fields.Many2many('product.template', 'product_alternative_rel', 'src_id', 'dest_id',
string='Alternative Products', help='Suggest more expensive alternatives to '
+12 -49
View File
@@ -17,7 +17,6 @@
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_payment.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_validate.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_buy.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_rating.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
</xpath>
</template>
@@ -552,32 +551,20 @@
<!-- Product options: OpenChatter -->
<template id="product_comment" inherit_id="website_sale.product" active="False" customize_show="True" name="Discussion and Rating">
<xpath expr="//div[@id='product_details']" position="inside">
<t t-call="rating.rating_card">
<t t-set="rating_stat" t-value="rating_product"/>
<t t-set="max_rate" t-value="5"/>
<t t-set="min_rate" t-value="1"/>
</t>
</xpath>
<xpath expr="//div[@t-field='product.website_description']" position="after">
<hr class="mb32"/>
<section class="container">
<div class="row col-md-10 col-md-offset-1">
<div class="text-muted">
<h3 class="fa fa-comment-o">
<a id="comments">
<t t-if="len(product.website_message_ids) &lt;= 1" ><t t-esc="len(product.website_message_ids)"/> comment</t>
<t t-if="len(product.website_message_ids) > 1"><t t-esc="len(product.website_message_ids)"/> comments</t>
</a>
</h3>
<div class="o_shop_discussion_rating">
<section class="container mt16 mb16">
<hr/>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="product"/>
<t t-set="display_rating" t-value="True"/>
</t>
</div>
</div>
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="product"/>
<t t-set="chatter_mode" t-value="'json'"/>
<t t-set="rating_enable" t-value="True"/>
</t>
</div>
</section>
</section>
</div>
</xpath>
</template>
@@ -704,30 +691,6 @@
</ul>
</template>
<!-- extend the generic mail thread message template to add rating widget -->
<template id="message_thread_rating" inherit_id="website_mail.message_thread" name="Chatter with rating">
<xpath expr="//section[contains(@class, 'o_website_mail_thread')]" position="attributes">
<attribute name="t-att-data-rating-enabled">rating_message_values and 1 or 0</attribute>
</xpath>
<xpath expr="//textarea[@name='message']" position="before">
<t t-if="rating_enable">
<t t-call="rating.rating_star_card">
<t t-set="rating_input_name" t-value="'rating'" />
</t>
</t>
</xpath>
<xpath expr="//ul[contains(@class, 'o_website_comments')]//h5" position="after">
<t t-if="rating_enable">
<div t-if="rating_message_values.get(message.id)">
<t t-call="rating.rating_star_card">
<t t-set="rating_default_value" t-value="rating_message_values.get(message.id)" />
<t t-set="rating_disabled" t-value="True" />
</t>
</div>
</t>
</xpath>
</template>
<template id="wizard_checkout" name="Wizard Checkout">
<ul class="wizard pull-right hidden-xs">
<li t-att-class="step==10 and 'text-primary' or 'text-muted'" id="step10">
-46
View File
@@ -168,52 +168,6 @@ class WebsiteSlides(http.Controller):
response.mimetype = 'application/pdf'
return response
@http.route('''/slides/slide/<model("slide.slide", "[('channel_id.can_see', '=', True)]"):slide>/comment''', type='http', auth="public", methods=['POST'], website=True)
def slide_comment(self, slide, **post):
""" Controller for message_post. Public user can post; their name and
email is used to find or create a partner and post as admin with the
right partner. Their comments are unpublished by default. Logged
users can post as usual. """
# TDE TODO :
# - subscribe partner instead of user writing the message ?
# - public user -> cannot create mail.message ?
if not post.get('comment'):
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
# public user: check or find author based on email, do not subscribe public user
# and do not publish their comments by default to avoid direct spam
if request.uid == request.website.user_id.id:
if not post.get('email'):
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
# TDE FIXME: public user has no right to create mail.message, should
# be investigated - using SUPERUSER_ID meanwhile
contextual_slide = slide.sudo().with_context(mail_create_nosubcribe=True)
# TDE FIXME: check in mail_thread, find partner from emails should maybe work as public user
partner_id = slide.sudo()._find_partner_from_emails([post.get('email')])[0]
if partner_id:
partner = request.env['res.partner'].sudo().browse(partner_id)
else:
partner = request.env['res.partner'].sudo().create({
'name': post.get('name', post['email']),
'email': post['email']
})
post_kwargs = {
'author_id': partner.id,
'website_published': False,
'email_from': partner.email,
}
# logged user: as usual, published by default
else:
contextual_slide = slide
post_kwargs = {}
contextual_slide.message_post(
body=post['comment'],
message_type='comment',
subtype='mt_comment',
**post_kwargs
)
return werkzeug.utils.redirect(request.httprequest.referrer + "#discuss")
@http.route('''/slides/slide/<model("slide.slide", "[('channel_id.can_see', '=', True), ('download_security', '=', 'public')]"):slide>/download''', type='http', auth="public", website=True)
def slide_download(self, slide):
if slide.download_security == 'public' or (slide.download_security == 'user' and request.session.uid):
+1 -4
View File
@@ -261,6 +261,7 @@ class Slide(models.Model):
_name = 'slide.slide'
_inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin']
_description = 'Slides'
_mail_post_access = 'read'
_PROMOTIONAL_FIELDS = [
'__last_update', 'name', 'image_thumb', 'image_medium', 'slide_type', 'total_views', 'category_id',
@@ -326,10 +327,6 @@ class Slide(models.Model):
# website
date_published = fields.Datetime('Publish Date')
website_message_ids = fields.One2many(
'mail.message', 'res_id',
domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')],
string='Website Messages', help="Website communication history")
likes = fields.Integer('Likes')
dislikes = fields.Integer('Dislikes')
# views
+3 -43
View File
@@ -543,49 +543,9 @@
</h4>
</div>
<div t-att-class="comments and 'tab-pane fade active in' or 'tab-pane fade'" id="discuss">
<form id="comment" class="js_website_submit_form" t-attf-action="/slides/slide/#{slide.id}/comment" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="media">
<span class="pull-left" t-if="not is_public_user">
<img class="img img-circle media-object"
t-att-src="'/web/image?model=res.partner&amp;field=image_small&amp;id='+str(user_id.partner_id.id)"
style="width: 30px"/>
</span>
<div class="media-body">
<div t-if="is_public_user" class="col-md-6">
<input type="name" class="form-control mb8" required="true" name="name" placeholder="Your Name"/>
<input type="email" class="form-control mb8" required="true" name="email" placeholder="Email Address"/>
</div>
<div t-if="is_public_user" class="col-md-6">
<p class="text-muted small">
Post comment with email address (confirmation of email is required in order to publish comment on website) or please
<a t-attf-href="/web?redirect=#{request.httprequest.url}">login</a>
to post comment
</p>
</div>
<div class="col-md-12">
<textarea rows="3" name="comment" class="o_slides_comment form-control" required="true" placeholder="Write a comment..."></textarea>
<button type="submit" class="btn btn-primary mt8">Post</button>
</div>
</div>
</div>
</form>
<ul class="media-list mt8">
<li t-foreach="comments" t-as="message" class="media">
<span class="pull-left">
<img class="media-object img img-circle"
t-att-src="'/web/image?model=mail.message&amp;field=author_avatar&amp;id='+str(message.id)" style="width: 30px"/>
</span>
<div class="media-body">
<t t-call="website.publish_short">
<t t-set="object" t-value="message"/>
</t>
<span t-field="message.author_id" style="display: inline-block;" t-options='{"widget": "contact", "fields": ["name"]}'/>
<span class="text-muted">on<span t-field="message.date"/></span>
<div t-field="message.body" class="discussion_scroll_post"></div>
</div>
</li>
</ul>
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="slide"/>
</t>
</div>
<div class="tab-pane fade oe_slides_transcript" id="transcript">
<t t-if="slide.index_content">