[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:
@@ -1,2 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import rating
|
||||
from . import mail_thread
|
||||
from . import mail_message
|
||||
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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 <= percent and percent <= 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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 > 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 & 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">
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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) <= 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">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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&field=image_small&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&field=author_avatar&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">
|
||||
|
||||
Reference in New Issue
Block a user