[MERGE] New view Messaging

This commit is contained in:
Aaron Bohy
2015-09-01 21:25:24 +02:00
159 changed files with 5893 additions and 9332 deletions
+3
View File
@@ -20,6 +20,9 @@ class BusController(openerp.http.Controller):
# override to add channels
def _poll(self, dbname, channels, last, options):
# update the user presence
if request.session.uid and 'im_presence' in options:
request.env['bus.presence'].update(options.get('im_presence'))
request.cr.close()
request._cr = None
return dispatch.poll(dbname, channels, last)
+3
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
import bus
import bus_presence
import res_users
import res_partner
-1
View File
@@ -53,7 +53,6 @@ class ImBus(models.Model):
"message": json_dump(message)
}
self.sudo().create(values)
self.env.cr.commit()
if random.random() < 0.01:
self.gc()
if channels:
+87
View File
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
import datetime
import random
import time
from openerp import api, fields, models
from openerp import tools
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.addons.bus.models.bus import TIMEOUT
DISCONNECTION_TIMER = TIMEOUT + 5
AWAY_TIMER = 600 # 10 minutes
class BusPresence(models.Model):
""" User Presence
Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not
attached to res_users to avoid database concurrence errors. Since the 'update' method is executed
at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'.
"""
_name = 'bus.presence'
_description = 'User Presence'
_sql_constraints = [('bus_user_presence_unique', 'unique(user_id)', 'A user can only have one IM status.')]
user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade')
last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now())
last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now())
status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline')
@api.model
def update(self, user_presence=True):
""" Register the given presence of the current user, and trigger a im_status change if necessary.
The status will not be written or sent if not necessary.
:param user_presence : True, if the user (self._uid) is still detected using its browser.
:type user_presence : boolean
"""
presence = self.search([('user_id', '=', self._uid)], limit=1)
# set the default values
send_notification = True
values = {
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'status': presence and presence.status or 'offline'
}
# update the user or a create a new one
if not presence: # create a new presence for the user
values['status'] = 'online'
values['user_id'] = self._uid
self.create(values)
else: # write the user presence if necessary
if user_presence:
values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
values['status'] = 'online'
else:
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold:
values['status'] = 'away'
send_notification = presence.status != values['status']
# write only if the last_poll is passed TIMEOUT, or if the status has changed
delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT)
if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification:
# Hide transaction serialization errors, which can be ignored, the presence update is not essential
with tools.mute_logger('openerp.sql_db'):
presence.write(values)
# avoid TransactionRollbackError
self.env.cr.commit() # TODO : check if still necessary
# notify if the status has changed
if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence
self.env['bus.bus'].sendone((self._cr.dbname, 'bus.presence'), {'id': self._uid, 'im_status': values['status']})
# gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it.
if random.random() < 0.01:
self.check_users_disconnection()
return True
@api.model
def check_users_disconnection(self):
""" Disconnect the users having a too old last_poll """
limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')])
presences.write({'status': 'offline'})
notifications = []
for presence in presences:
notifications.append([(self._cr.dbname, 'bus.presence'), {'id': presence.user_id.id, 'im_status': presence.status}])
self.env['bus.bus'].sendmany(notifications)
+43
View File
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
im_status = fields.Char('IM Status', compute='_compute_im_status')
@api.multi
def _compute_im_status(self):
self.env.cr.execute("""
SELECT P.id as id, B.status as im_status
FROM bus_presence B
JOIN res_users U ON B.user_id = U.id
JOIN res_partner P ON P.id = U.partner_id
WHERE P.id IN %s AND U.active = 't'
""", (tuple(self.ids),))
fetch_result = self.env.cr.dictfetchall()
result = dict(((user_presence['id'], user_presence['im_status']) for user_presence in fetch_result))
for partner in self:
partner.im_status = result.get(partner.id, 'offline')
@api.model
def im_search(self, name, limit=20):
""" Search partner with a name and return its id, name and im_status.
Note : the user must be logged
:param name : the partner name to search
:param limit : the limit of result to return
"""
name = '%' + name + '%'
excluded_partner_ids = [self.env.user.partner_id.id]
self.env.cr.execute("""
SELECT U.id as user_id, P.id as id, P.name as name, COALESCE(B.status, 'offline') as im_status
FROM res_users U
JOIN res_partner P ON P.id = U.partner_id
LEFT JOIN bus_presence B ON B.user_id = U.id
WHERE P.name ILIKE %s
AND P.id NOT IN %s
AND U.active = 't'
LIMIT %s
""", (name, tuple(excluded_partner_ids), limit))
result = self.env.cr.dictfetchall()
return result
@@ -12,7 +12,7 @@ class ResUsers(models.Model):
def _compute_im_status(self):
""" Compute the im_status of the users """
res = dict((uid, 'offline') for uid in self.ids)
presences = self.env['im_chat.presence'].search([('user_id', 'in', self.ids)])
presences = self.env['bus.presence'].search([('user_id', 'in', self.ids)])
for presence in presences:
res[presence.user_id.id] = presence.status
for user in self:
@@ -37,7 +37,7 @@ class ResUsers(models.Model):
# first query to find online employee
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM im_chat_presence S
FROM bus_presence S
JOIN res_users U ON S.user_id = U.id
JOIN res_partner P ON P.id = U.partner_id
WHERE '''+where_clause_base+'''
@@ -52,7 +52,7 @@ class ResUsers(models.Model):
# second query to find other online people
if len(result) < limit:
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM im_chat_presence S
FROM bus_presence S
JOIN res_users U ON S.user_id = U.id
JOIN res_partner P ON P.id = U.partner_id
WHERE '''+where_clause_base+'''
@@ -67,7 +67,7 @@ class ResUsers(models.Model):
if len(result) < limit:
self._cr.execute('''SELECT U.id as id, P.name as name, COALESCE(S.status, 'offline') as im_status
FROM res_users U
LEFT JOIN im_chat_presence S ON S.user_id = U.id
LEFT JOIN bus_presence S ON S.user_id = U.id
LEFT JOIN res_partner P ON P.id = U.partner_id
WHERE '''+where_clause_base+'''
AND U.id NOT IN %s
+3 -1
View File
@@ -1,2 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0
access_bus_bus,bus.bus public,model_bus_bus,,0,0,0,0
access_bus_presence,bus.presence,model_bus_presence,base.group_user,1,1,1,1
access_bus_presence_portal,bus.presence,model_bus_presence,base.group_portal,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_bus_bus bus.bus public model_bus_bus 0 0 0 0
3 access_bus_presence bus.presence model_bus_presence base.group_user 1 1 1 1
4 access_bus_presence_portal bus.presence model_bus_presence base.group_portal 1 1 1 1
+16
View File
@@ -9,12 +9,21 @@ bus.ERROR_DELAY = 10000;
bus.Bus = Widget.extend({
init: function(){
var self = this;
this._super();
this.options = {};
this.activated = false;
this.channels = [];
this.last = 0;
this.stop = false;
// bus presence
this.set("window_focus", true);
this.on("change:window_focus", self, function(e) {
self.options.im_presence = self.get("window_focus");
});
$(window).on("focus", _.bind(this.window_focus, this));
$(window).on("blur", _.bind(this.window_blur, this));
},
start_polling: function(){
if(!this.activated){
@@ -57,6 +66,13 @@ bus.Bus = Widget.extend({
delete_channel: function(channel){
this.channels = _.without(this.channels, channel);
},
// bus presence : window focus/unfocus
window_focus: function() {
this.set("window_focus", true);
},
window_blur: function() {
this.set("window_focus", false);
},
});
// singleton
-1
View File
@@ -35,5 +35,4 @@ Both goals and badges are flexibles and can be adapted to a large range of modul
],
'application': False,
'auto_install': False,
'qweb': ['static/src/xml/gamification.xml'],
}
@@ -1,146 +1,65 @@
@charset "UTF-8";
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_goal {
width: 230px;
min-height: 200px; }
min-height: 200px;
}
.openerp .oe_kanban_view .oe_kanban_card.oe_kanban_badge {
width: 250px;
min-height: 150px; }
min-height: 150px;
}
.openerp .oe_kanban_badge_avatars {
margin-top: 8px; }
.openerp .oe_kanban_badge_avatars img {
width: 30px;
height: 30px;
padding-left: 0;
margin-top: 3px;
border-radius: 2px;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); }
margin-top: 8px;
}
.openerp .oe_kanban_badge_avatars img {
width: 30px;
height: 30px;
padding-left: 0;
margin-top: 3px;
border-radius: 2px;
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.openerp .oe_margin_top_8 {
margin-top: 8px; }
margin-top: 8px;
}
.openerp .oe_margin_top_16 {
margin-top: 16px; }
margin-top: 16px;
}
.openerp .oe_kanban_goal .oe_goal_state_block {
width: 200px;
height: 130px;
margin: auto;
margin-bottom: -20px; }
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
font-size: 2.5em;
font-weight: bold;
padding-top: 30px; }
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa {
font-size: 3em;
margin-top: -17px;
margin-left: 17px; }
.openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p {
text-align: center; }
.openerp .oe_kanban_content .oe_goal_gauge:first-child {
margin: auto; }
.openerp .oe_kanban_content .oe_goal_gauge svg {
margin-top: -20px; }
.openerp .oe_no_overflow {
overflow: hidden; }
.openerp .oe_red {
color: red; }
.openerp .oe_green {
color: green; }
.openerp .oe_orange {
color: orange; }
.openerp .oe_form td .oe_no_padding {
margin-left: -6px; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion {
background-color: #d3def1; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion ul {
padding-left: 15px; }
.openerp .oe_timeline .oe_tl_inbox_aside .oe_gamification_suggestion span {
background-color: #f0eeee;
display: inline-block;
height: 25px;
vertical-align: middle;
font-size: 14px;
font-weight: bold;
margin-top: -20px;
padding-left: 0 5px 0px 5px; }
.openerp .oe_timeline .oe_goal {
background-color: #f0eeee;
border: solid 1px #cacaca;
margin-top: 20px;
padding-left: 10px; }
.openerp .oe_timeline .oe_goal .oe_update_challenge.fa, .openerp .oe_timeline .oe_goal .oe_goal_action.fa {
visibility: hidden;
font-size: 16px;
float: right;
position: relative; }
.openerp .oe_timeline .oe_goal div:hover > .oe_update_challenge, .openerp .oe_timeline .oe_goal div:hover > .oe_goal_action, .openerp .oe_timeline .oe_goal th:hover > .oe_goal_action {
visibility: visible; }
.openerp .oe_timeline .oe_goal .oe_goals_list {
padding-left: 0;
margin-top: 5px;
margin-bottom: 10px;
width: 100%; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress div {
display: inline-block; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress::before {
content: "•";
padding-right: 4px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached::before {
content: "✓";
padding-right: 0;
margin-left: -2px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_no_progress.oe_goal_reached .oe_cell {
text-decoration: line-through; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell.oe_goal_current {
font-size: 150%;
font-weight: bold;
min-width: 50px;
padding: 0 5px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box {
display: inline-block;
position: relative;
z-index: 0;
vertical-align: middle;
width: 100%;
border: solid 1px rgba(0, 0, 0, 0.03);
border-radius: 2px; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_outer_box.oe_no_progress {
border: none; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress_background {
background-color: white;
position: absolute;
height: 100%;
width: 100%;
z-index: -2;
top: 0;
left: 0; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_goal_progress {
background-color: #d4e9de;
position: absolute;
height: 100%;
width: 0;
z-index: -1;
top: 0;
left: 0; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_thead {
font-weight: normal;
padding: 5px;
text-align: center; }
.openerp .oe_timeline .oe_goal .oe_goals_list .oe_cell {
padding: 3px 0; }
.openerp .oe_timeline .oe_table.oe_goals_list .col0 {
font-size: 200%;
margin-bottom: -20px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state {
font-size: 2.5em;
font-weight: bold;
width: 25px;
text-align: center; }
.openerp .oe_timeline .oe_table.oe_goals_list .col1 {
padding: 0 5px; }
.openerp .oe_timeline .oe_table.oe_goals_list .col2 {
width: auto; }
.openerp .oe_timeline .oe_user_avatar {
width: 24px;
padding-right: 5px; }
.openerp .oe_timeline .oe_table {
display: table; }
.openerp .oe_timeline .oe_row {
display: table-row; }
.openerp .oe_timeline .oe_cell {
display: table-cell;
vertical-align: middle; }
padding-top: 30px;
}
.openerp .oe_kanban_goal .oe_goal_state_block .oe_goal_state .fa {
font-size: 3em;
margin-top: -17px;
margin-left: 17px;
}
.openerp .oe_kanban_goal .oe_goal_state_block, .openerp .oe_kanban_goal p {
text-align: center;
}
.openerp .oe_kanban_content .oe_goal_gauge:first-child {
margin: auto;
}
.openerp .oe_kanban_content .oe_goal_gauge svg {
margin-top: -20px;
}
.openerp .oe_no_overflow {
overflow: hidden;
}
.openerp .oe_red {
color: red;
}
.openerp .oe_green {
color: green;
}
.openerp .oe_orange {
color: orange;
}
.openerp .oe_form td .oe_no_padding {
margin-left: -6px;
}
@@ -61,118 +61,3 @@
// compensate padding from .openerp .oe_form td.oe_form_group_cell + .oe_form_group_cell
.oe_form td .oe_no_padding
margin-left: -6px
.oe_timeline
.oe_tl_inbox_aside
.oe_gamification_suggestion
background-color: #d3def1
ul
padding-left: 15px
span
background-color: #f0eeee
display: inline-block
height: 25px
vertical-align: middle
font-size: 14px
font-weight: bold
margin-top: -20px
padding-left: 0 5px 0px 5px
.oe_goal
background-color: #f0eeee
border: solid 1px #CACACA
margin-top: 20px
padding-left: 10px
.oe_update_challenge.fa, .oe_goal_action.fa
visibility: hidden
font-size: 16px
float: right
position: relative
div:hover > .oe_update_challenge, div:hover > .oe_goal_action, th:hover > .oe_goal_action
visibility: visible
.oe_goals_list
padding-left: 0
margin-top: 5px
margin-bottom: 10px
width: 100%
.oe_no_progress
div
display: inline-block
.oe_no_progress::before
content: "•"
padding-right: 4px
.oe_no_progress.oe_goal_reached::before
content: "✓"
padding-right: 0
margin-left: -2px
.oe_no_progress.oe_goal_reached
.oe_cell
text-decoration: line-through
.oe_cell.oe_goal_current
font-size: 150%
font-weight: bold
min-width: 50px
padding: 0 5px
.oe_goal_outer_box
display: inline-block
position: relative
z-index: 0
vertical-align: middle
width: 100%
border: solid 1px rgba(0,0,0,0.03)
border-radius: 2px
.oe_goal_outer_box.oe_no_progress
border: none
.oe_goal_progress_background
background-color: white
position: absolute
height: 100%
width: 100%
z-index: -2
top: 0
left: 0
.oe_goal_progress
background-color: rgb(212, 233, 222)
position: absolute
height: 100%
width: 0
z-index: -1
top: 0
left: 0
.oe_thead
font-weight: normal
padding: 5px
text-align: center
.oe_cell
padding: 3px 0
.oe_table.oe_goals_list
.col0
font-size: 200%
font-weight: bold
width: 25px
text-align: center
.col1
padding: 0 5px
.col2
width: auto
.oe_user_avatar
width: 24px
padding-right: 5px
.oe_table
display: table
.oe_row
display: table-row
.oe_cell
display: table-cell
vertical-align: middle
@@ -1,122 +0,0 @@
odoo.define('gamification.gamification', function(require) {
"use strict";
var mail = require('mail.mail');
var core = require('web.core');
var form_common = require('web.form_common');
var Model = require('web.DataModel');
var session = require('web.session');
var Widget = require('web.Widget');
var QWeb = core.qweb;
var Sidebar = Widget.extend({
template: 'gamification.UserWallSidebar',
init: function (parent, action) {
this._super(parent, action);
this.deferred = $.Deferred();
this.goals_info = {};
this.challenge_suggestions = {};
$(document).off('keydown.klistener');
},
events: {
// update a challenge and related goals
'click i.oe_update_challenge': function(event) {
var self = this;
var challenge_id = parseInt(event.currentTarget.id, 10);
var goals_updated = new Model('gamification.challenge').call('quick_update', [challenge_id]);
$.when(goals_updated).done(function() {
self.get_goal_todo_info();
});
},
// action to modify a goal
'click a.oe_goal_action': function(event) {
var self = this;
var goal_id = parseInt(event.currentTarget.id, 10);
var goal_action = new Model('gamification.goal').call('get_action', [goal_id]).then(function(res) {
goal_action.action = res;
});
$.when(goal_action).done(function() {
var action = self.do_action(goal_action.action);
$.when(action).done(function () {
new Model('gamification.goal').call('update', [[goal_id]]).then(function(res) {
self.get_goal_todo_info();
});
});
});
},
// get more info about a challenge request
'click a.oe_challenge_reply': function(event) {
var self = this;
var challenge_id = parseInt(event.currentTarget.id, 10);
var challenge_action = new Model('gamification.challenge').call('reply_challenge_wizard', [challenge_id]).then(function(res) {
challenge_action.action = res;
});
$.when(challenge_action).done(function() {
self.do_action(challenge_action.action).done(function () {
self.get_goal_todo_info();
});
});
}
},
start: function() {
var self = this;
this._super.apply(this, arguments);
self.get_goal_todo_info();
self.get_challenge_suggestions();
},
get_goal_todo_info: function() {
var self = this;
var challenges = new Model('res.users').call('get_serialised_gamification_summary', []).then(function(result) {
if (result.length === 0) {
self.$el.find(".oe_gamification_challenge_list").hide();
} else {
self.$el.find(".oe_gamification_challenge_list").empty();
_.each(result, function(item){
var $item = $(QWeb.render("gamification.ChallengeSummary", {challenge: item}));
self.render_money_fields($item);
self.render_user_avatars($item);
self.$el.find('.oe_gamification_challenge_list').append($item);
});
}
});
},
get_challenge_suggestions: function() {
var self = this;
var challenge_suggestions = new Model('res.users').call('get_challenge_suggestions', []).then(function(result) {
if (result.length === 0) {
self.$el.find(".oe_gamification_suggestion").hide();
} else {
var $item = $(QWeb.render("gamification.ChallengeSuggestion", {challenges: result}));
self.$el.find('.oe_gamification_suggestion').append($item);
}
});
},
render_money_fields: function(item) {
var self = this;
var FieldMonetary = core.form_widget_registry.get('monetary');
self.dfm = new form_common.DefaultFieldManager(self);
// Generate a FieldMonetary for each .oe_goal_field_monetary
item.find(".oe_goal_field_monetary").each(function() {
var currency_id = parseInt( $(this).attr('data-id'), 10);
var money_field = new FieldMonetary(self.dfm, {
attrs: {
modifiers: '{"readonly": true}'
}
});
money_field.set('currency', currency_id);
money_field.get_currency_info();
money_field.set('value', parseInt($(this).text(), 10));
money_field.replace($(this));
});
},
render_user_avatars: function(item) {
item.find(".oe_user_avatar").each(function() {
var user_id = parseInt( $(this).attr('data-id'), 10);
var url = session.url('/web/image', {model: 'res.users', field: 'image_small', id: user_id});
$(this).attr("src", url);
});
}
});
});
@@ -1,114 +0,0 @@
<templates>
<t t-name="gamification.UserWallSidebar" class="oe_gamification_user_wall_sidebar">
<div>
<div class="oe_gamification_suggestion"></div>
<div class="oe_gamification_challenge_list"></div>
</div>
</t>
<t t-name="gamification.ChallengeSummary">
<div class="oe_goal">
<div class="oe_gamification_title">
<i class="oe_update_challenge fa fa-refresh" rol="button" t-attf-id="{challenge.id}"/>
<span><t t-esc="challenge.name" /></span>
</div>
<t t-if="challenge.visibility_mode == 'personal'">
<div class="oe_table oe_goals_list">
<div t-foreach="challenge.lines" t-as="line" t-attf-class="oe_row oe_goal_outer_box #{line.state == 'reached' ? 'oe_goal_reached' : ''} #{line.display_mode != 'progress' ? 'oe_no_progress' : ''}">
<t t-if="line.display_mode == 'progress'">
<div class="oe_goal_progress_background"></div>
<div class="oe_goal_progress" t-attf-style="#{line.display_mode == 'progress' ? 'width: '+line.completeness+'%;' : 'width:0;'}"></div>
<div class="oe_cell oe_goal_current"><t t-esc="line.current" /></div>
</t>
<div class="oe_cell">
<t t-if="line.computation_mode != 'manually' and !line.action">
<span t-att-title="line.description"><t t-esc="line.name" /></span>
</t>
<t t-if="line.action or line.computation_mode == 'manually'">
<span t-att-title="line.description"><a class="oe_goal_action" t-att-id="line.id"><t t-esc="line.name" /></a></span>
</t>
<t t-if="line.display_mode == 'progress'"><br/>
<div class="oe_grey">
<t t-if="line.definition_condition == 'higher'">
Target:
</t>
<t t-if="line.definition_condition == 'lower'">
Target: &lt;=
</t>
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target"/></span>
<t t-if="line.suffix"><t t-esc="line.suffix"/></t>
</div>
</t>
</div>
</div>
</div>
</t>
<t t-if="challenge.visibility_mode == 'ranking'">
<div t-foreach="challenge.lines" t-as="line" class="oe_goals_list oe_table">
<div class="oe_row">
<div class="oe_cell oe_thead" colspan="3" t-attf-title="#{line.description ? line.description : ''}">
<strong><t t-esc="line.name"/></strong>
<br/>
<div class="oe_grey">
<t t-if="line.definition_condition == 'higher'">
Target:
</t>
<t t-if="line.definition_condition == 'lower'">
Target: &lt;=
</t>
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="line.target" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</div>
</div>
</div>
<div t-foreach="line.goals" t-as="goal" t-attf-class="#{goal.id == line.own_goal_id ? 'oe_bold' : ''}">
<div t-attf-class="oe_row oe_goal_outer_box #{goal.state == 'reached' ? 'oe_goal_reached' : ''} #{line.display_mode != 'progress' ? 'oe_no_progress' : ''}">
<t t-if="line.display_mode == 'progress'">
<div class="oe_goal_progress_background"></div>
<div class="oe_goal_progress" t-attf-style="#{line.display_mode == 'progress' ? 'width: '+goal.completeness+'%;' : 'width:0;'}"></div>
</t>
<div class="oe_cell col0"><t t-esc="goal.rank" /></div>
<div class="oe_cell col1"><img class="oe_user_avatar" t-attf-alt="#{goal.name}" t-attf-data-id="#{goal.user_id}"/></div>
<div class="oe_cell col2">
<t t-if="line.display_mode == 'progress'">
<!-- progress, action on current value -->
<t t-esc="goal.name"/><br/>
<t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</t>
<t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
<a class="oe_goal_action" t-att-id="line.own_goal_id">
<span t-attf-class="#{line.monetary ? 'oe_goal_field_monetary' : ''}" t-attf-data-id="#{line.monetary ? challenge.currency : ''}"><t t-esc="goal.current" /></span><t t-if="line.suffix"> <t t-esc="line.suffix"/></t>
</a>
</t>
</t>
<t t-if="line.display_mode != 'progress'">
<!-- not progress, action on user name -->
<t t-if="goal.id != line.own_goal_id or (line.computation_mode != 'manually' and !line.action)">
<t t-esc="goal.name"/>
</t>
<t t-if="goal.id == line.own_goal_id and (line.action or line.computation_mode == 'manually')">
<a class="oe_goal_action" t-att-id="line.own_goal_id"><t t-esc="goal.name"/></a>
</t>
</t>
</div>
</div>
</div>
</div>
</t>
</div>
</t>
<t t-name="gamification.ChallengeSuggestion">
<div class="oe_goal">
<h4>Invited Challenges</h4>
<ul t-foreach="challenges" t-as="challenge" class="oe_goals_list">
<li>
<strong><a class="oe_challenge_reply" t-attf-title="#{challenge.description.value ? challenge.description.value : ''}" t-attf-id="{challenge.id}" title="more details..."><t t-esc="challenge.name"/></a></strong>
</li>
</ul>
</div>
</t>
</templates>
@@ -4,7 +4,6 @@
<template id="assets_backend" name="gamification assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/gamification/static/src/css/gamification.css"/>
<script type="text/javascript" src="/gamification/static/src/js/gamification.js"></script>
</xpath>
</template>
</data>
-1
View File
@@ -41,7 +41,6 @@ You can manage:
'hr_data.xml',
'hr_tip_data.xml',
'res_config_view.xml',
'mail_hr_view.xml',
'views/hr.xml',
'hr_dashboard.xml',
],
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.actions.act_window" id="mail.mail_message_action_inbox">
<field name="name">Inbox</field>
<field name="res_model">mail.message</field>
<field name="view_type">form</field>
<field name="view_mode">timeline</field>
<field name="context">{
'default_model': 'res.users',
'default_res_id': uid,
'thread_model': 'res.partner',
'options' : {'view_mailbox': True,
'view_inbox': True,
'show_write_to_followers': True}
}</field>
<field name="domain">[
('needaction', '=', True)
]</field>
</record>
</data>
</openerp>
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
import models
import controllers
-28
View File
@@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
{
'name' : 'Instant Messaging',
'version': '1.0',
'summary': 'Chat between users',
'sequence': 115,
'category': 'Tools',
'complexity': 'easy',
'website': 'https://www.odoo.com/page/live-chat',
'description':
"""
Instant Messaging
=================
Allows users to chat with each other in real time. Find other users easily and
chat in real time. It support several chats in parallel.
""",
'data': [
'security/ir.model.access.csv',
'security/im_chat_session_security.xml',
'views/im_chat_session_templates.xml',
'views/im_chat_session_views.xml',
'data/im_chat_session_data.xml',
],
'depends': ['base', 'web', 'bus'],
'qweb': ['static/src/xml/*.xml'],
'application': True,
'installable': True,
}
-2
View File
@@ -1,2 +0,0 @@
# -*- coding: utf-8 -*
import main
-63
View File
@@ -1,63 +0,0 @@
# -*- coding: utf-8 -*
import base64
import openerp
from openerp import SUPERUSER_ID
from openerp.http import request
class ChatController(openerp.addons.bus.controllers.main.BusController):
def _default_request_uid(self):
""" For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none)
!!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access)
can access to the ressource, thanks to 'is_in_session', (for instance).
"""
return request.session.uid and request.session.uid or SUPERUSER_ID
# --------------------------
# Extends BUS Controller Poll
# --------------------------
def _poll(self, dbname, channels, last, options):
if request.session.uid:
request.env['im_chat.presence'].update(options.get('im_presence'))
# channel to receive message
channels.append((request.db, 'im_chat.session', request.session.uid))
## For performance issue, the real time status notification is disabled. This means a change of status are still braoadcasted
## but not received by anyone. Otherwise, all listening user restart their longpolling at the same time and cause a 'ConnectionPool Full Error'
## since there is not enought cursors for everyone. Now, when a user open his list of users, an RPC call is made to update his user status list.
##channels.append((request.db,'im_chat.presence'))
return super(ChatController, self)._poll(dbname, channels, last, options)
# --------------------------
# Anonymous routes (Common Methods)
# --------------------------
@openerp.http.route('/im_chat/post', type="json", auth="none")
def post(self, uuid, message_content, message_type):
request_uid = self._default_request_uid()
return request.env["im_chat.message"].sudo(request_uid).send_message(request.session.uid, uuid, message_content, message_type)
@openerp.http.route(['/im_chat/image/<string:uuid>/<int:user_id>', '/im_chat/image/<string:uuid>/<string:user_id>'], type='http', auth="none")
def image(self, uuid, user_id):
request_uid = self._default_request_uid()
# anonymous will have a user_id = False, interpreted by string
if isinstance(user_id, (basestring, unicode)):
user_id = False
# get image for the people in the channel
image_b64 = request.env['im_chat.session'].sudo(request_uid).session_user_image(uuid, user_id)
image_data = base64.b64decode(image_b64)
headers = [('Content-Type', 'image/png')]
headers.append(('Content-Length', len(image_data)))
return request.make_response(image_data, headers)
@openerp.http.route(['/im_chat/history'], type="json", auth="none")
def history(self, uuid, last_id=False, limit=20):
request_uid = self._default_request_uid()
return request.env["im_chat.message"].sudo(request_uid).fetch_message(uuid, last_id, limit)
# --------------------------
# User routes (Only Logged User Methods)
# --------------------------
@openerp.http.route('/im_chat/init', type="json", auth="user")
def init(self):
return request.env['im_chat.message'].get_init_notifications()
@@ -1,158 +0,0 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- Smileys -->
<record id="im_chat_smiley_smile" model="im_chat.shortcode">
<field name="source">:)</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/smile.png'/&gt;</field>
<field name="description">Emoji Smile</field>
</record>
<record id="im_chat_smiley_sad" model="im_chat.shortcode">
<field name="source">:(</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/sad.png'/&gt;</field>
<field name="description">Emoji Sad</field>
</record>
<record id="im_chat_smiley_laugh" model="im_chat.shortcode">
<field name="source">:D</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/laugh.png'/&gt;</field>
<field name="description">Emoji Laugh</field>
</record>
<record id="im_chat_smiley_wink" model="im_chat.shortcode">
<field name="source">;)</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/wink.png'/&gt;</field>
<field name="description">Emoji Wink</field>
</record>
<record id="im_chat_smiley_stuck" model="im_chat.shortcode">
<field name="source">O_0</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/stuck_out_tongue.png'/&gt;</field>
<field name="description">Emoji Stuck Out Tongue Winking Eye</field>
</record>
<record id="im_chat_smiley_scream" model="im_chat.shortcode">
<field name="source">:@</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/scream.png'/&gt;</field>
<field name="description">Emoji Scream</field>
</record>
<record id="im_chat_smiley_neutral" model="im_chat.shortcode">
<field name="source">:|</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/neutral.png'/&gt;</field>
<field name="description">Emoji Speechless/Neutral face</field>
</record>
<record id="im_chat_smiley_cheeky" model="im_chat.shortcode">
<field name="source">:p</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/yum.png'/&gt;</field>
<field name="description">Emoji Cheeky</field>
</record>
<record id="im_chat_smiley_confused" model="im_chat.shortcode">
<field name="source">:/</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/confused.png'/&gt;</field>
<field name="description">Emoji Confused</field>
</record>
<record id="im_chat_smiley_open_mouth" model="im_chat.shortcode">
<field name="source">:O</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/open_mouth.png'/&gt;</field>
<field name="description">Emoji Open Mouth</field>
</record>
<record id="im_chat_smiley_heart" model="im_chat.shortcode">
<field name="source">&amp;lt;3</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/heart.png'/&gt;</field>
<field name="description">Emoji Heart</field>
</record>
<!-- Other images -->
<record id="im_chat_smiley_thumbsup" model="im_chat.shortcode">
<field name="source">:+1</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/thumbsup.png'/&gt;</field>
<field name="description">Emoji Thumbs Up</field>
</record>
<record id="im_chat_smiley_thumbsdown" model="im_chat.shortcode">
<field name="source">:-1</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/thumbsdown.png'/&gt;</field>
<field name="description">Emoji Thumbs Down</field>
</record>
<record id="im_chat_smiley_kitten" model="im_chat.shortcode">
<field name="source">:kitten</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/cat.png'/&gt;</field>
<field name="description">Emoji Kitten</field>
</record>
<record id="im_chat_smiley_pinky" model="im_chat.shortcode">
<field name="source">:pinky</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/pinky.png'/&gt;</field>
<field name="description">Emoji Pinky</field>
</record>
<record id="im_chat_smiley_musti" model="im_chat.shortcode">
<field name="source">:musti</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/musti.png'/&gt;</field>
<field name="description">Emoji Musti</field>
</record>
<record id="im_chat_smiley_man_with_turban" model="im_chat.shortcode">
<field name="source">:turban</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/man_with_turban.png'/&gt;</field>
<field name="description">Emoji Man With Turban</field>
</record>
<record id="im_chat_smiley_troll" model="im_chat.shortcode">
<field name="source">:troll</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/trollface.png'/&gt;</field>
<field name="description">Emoji Troll Face</field>
</record>
<record id="im_chat_smiley_boar" model="im_chat.shortcode">
<field name="source">:boar</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/boar.png'/&gt;</field>
<field name="description">Emoji Boar</field>
</record>
<record id="im_chat_smiley_poop" model="im_chat.shortcode">
<field name="source">:poop</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/poop.png'/&gt;</field>
<field name="description">Emoji Poop</field>
</record>
<record id="im_chat_smiley_cookie" model="im_chat.shortcode">
<field name="source">:cookie</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/cookie.png'/&gt;</field>
<field name="description">Emoji Cookie</field>
</record>
<record id="im_chat_smiley_postal_horn" model="im_chat.shortcode">
<field name="source">:postal_horn</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/postal_horn.png'/&gt;</field>
<field name="description">Emoji Postal Horn</field>
</record>
<record id="im_chat_smiley_ghost" model="im_chat.shortcode">
<field name="source">:ghost</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/ghost.png'/&gt;</field>
<field name="description">Emoji Ghost</field>
</record>
<record id="im_chat_smiley_beer" model="im_chat.shortcode">
<field name="source">:beer</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/beer.png'/&gt;</field>
<field name="description">Emoji Beer</field>
</record>
<record id="im_chat_smiley_man" model="im_chat.shortcode">
<field name="source">:man</field>
<field name="substitution">&lt;img src='/im_chat/static/src/img/man.png'/&gt;</field>
<field name="description">Emoji Man</field>
</record>
</data>
</odoo>
-197
View File
@@ -1,197 +0,0 @@
# Bulgarian translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-11-18 12:00+0000\n"
"Last-Translator: Dimitar Dimitrov <dimitrovden@gmail.com>\n"
"Language-Team: Bulgarian <bg@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-11-19 06:43+0000\n"
"X-Generator: Launchpad (build 17252)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Съобщения"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Един потребител може да има само един IM статус."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Автор"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Отсъстващ(а)"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Приключен"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Дата на създаване"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Създадено от"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Създадено на"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr ""
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "IM състояние"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr ""
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Последно Присъствие"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Последно обновено от"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Последно обновено на"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Съобщение"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Съобщения"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Извън линия"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "На линия"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Отвoри"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Кажи нещо..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Сесия"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr ""
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr ""
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr ""
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Потребители"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Посетител"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "Неизвестен"
-198
View File
@@ -1,198 +0,0 @@
# German translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-09-18 13:08+0000\n"
"Last-Translator: Thorsten Vocks (OpenBig.org) <thorsten.vocks@big-"
"consulting.net>\n"
"Language-Team: German <de@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Mitteilungen"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Ein Benutzer kann nur einen eindeutigen Status haben."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Verfasser"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Abwesend"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Beendet"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Erstellt am"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Erstellt durch"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Erstellt am"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Zusammen gefaltet"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Sofortnachrichten-Status"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Letzte Abfrage"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Letzte Anwesenheit"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Zuletzt aktualisiert von"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Zuletzt aktualisiert am"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Nachricht"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mitteilungen"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Offline"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Online"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Offen"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Verbundene Teilnehmer"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Sag etwas ..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sitzung"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sitzung mit"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Teilnehmer"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Typ"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Benutzer"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Besucher"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "unbekannt"
-197
View File
@@ -1,197 +0,0 @@
# Spanish translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-08-18 14:41+0000\n"
"Last-Translator: Pedro Manuel Baeza <pedro.baeza@gmail.com>\n"
"Language-Team: Spanish <es@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d mensajes"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Un usuario sólo puede tener un estado de charla."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Ausente"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Cerrado"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Fecha de creación"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Creado por"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Creado en"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Replegado"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Estado del chat"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Última encuesta"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Última presencia"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Última actualización de"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Última actualización en"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Mensaje"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mensajes"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Desconectado"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "En línea"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Abrir"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Usuarios de sesión relacionados"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Diga algo..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sesión"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sesión a"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Usuarios de sesión"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Tipo"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Usuarios"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Visitante"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "desconocido"
-197
View File
@@ -1,197 +0,0 @@
# Croatian translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-11-15 12:35+0000\n"
"Last-Translator: Davor Bojkić <bole@dajmi5.com>\n"
"Language-Team: Croatian <hr@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-11-16 06:19+0000\n"
"X-Generator: Launchpad (build 17241)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d poruka"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Korisnik može imati samo jedan IM status"
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Odsutan"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Zatvoren"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Datum kreiranja"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Kreirao"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Vrijeme kreiranja"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Omotano"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "IM Status"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr ""
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Zadnja prijava"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Promijenio"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Vrijeme promjene"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Poruka"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Poruke"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Odspojen"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Online"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr ""
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Reci nešto..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr ""
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr ""
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr ""
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr ""
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr ""
-197
View File
@@ -1,197 +0,0 @@
# Hungarian translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-10-28 07:51+0000\n"
"Last-Translator: krnkris <Unknown>\n"
"Language-Team: Hungarian <hu@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-10-29 07:49+0000\n"
"X-Generator: Launchpad (build 17203)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Üzenetek"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Egy felhasználónak csak egy üzenőküldési állapota lehet."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Szerző"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Távollét"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Lezárt"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Létrehozás dátuma"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Készítette"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Létrehozás dátuma"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Behajtott"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "Azonosító ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Üzenetküldő-állapot"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Utolsó szavazás"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Utolsó jelenlét"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Utoljára frissítve, által"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Utoljára frissítve ekkor"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Üzenet"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Üzenetek"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Nem elérhető"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Elérhető"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Nyitott"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Szakasz felhasználók környezete"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Mondj valamit..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Munkafázis"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Munkafázis ehhez"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Munkafázis felhasználói"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Típus"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Felhasználók"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Látogató"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "ismeretlen"
-339
View File
@@ -1,339 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * im_chat
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-08-25 09:00+0000\n"
"PO-Revision-Date: 2015-08-25 09:00+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:89
#: code:addons/im_chat/static/src/js/im_chat_common.js:92
#, python-format
msgid "%d Messages"
msgstr ""
#. module: im_chat
#: code:addons/im_chat/models/im_chat_session.py:180
#, python-format
msgid "%s joined the conversation."
msgstr ""
#. module: im_chat
#: code:addons/im_chat/models/im_chat_session.py:202
#, python-format
msgid "%s left the conversation."
msgstr ""
#. module: im_chat
#: model:ir.actions.act_window,help:im_chat.im_chat_shortcode_action
msgid "A shortcode is a keyboard shortcut. For instance, you type #gm and it will be transformed into \"Good Morning\"."
msgstr ""
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_from_id
msgid "Author"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_im_chat_shortcode
msgid "Canned Response / Shortcode"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_im_chat_session
msgid "Chat Conversation"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_im_chat_conversation_state
msgid "Chat Conversation State"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_im_chat_message
msgid "Chat Message"
msgstr ""
#. module: im_chat
#: model:ir.actions.act_window,name:im_chat.im_chat_shortcode_action
msgid "Chat Shortcode"
msgstr ""
#. module: im_chat
#: model:ir.actions.act_window,help:im_chat.im_chat_shortcode_action
msgid "Click to define a new chat shortcode."
msgstr ""
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_create_date
msgid "Create Date"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_create_date
msgid "Create date"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_create_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_create_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_create_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_create_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_create_uid
msgid "Created by"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_create_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_create_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_create_date
msgid "Created on"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_description
msgid "Description"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_display_name
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_display_name
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_display_name
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_display_name
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_display_name
msgid "Display Name"
msgstr ""
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_id
msgid "ID"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_status
#: model:ir.model.fields,field_description:im_chat.field_res_users_im_status
msgid "IM Status"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message___last_update
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence___last_update
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session___last_update
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel___last_update
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode___last_update
msgid "Last Modified on"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_last_poll
msgid "Last Poll"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_last_presence
msgid "Last Presence"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_write_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_write_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_write_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_write_uid
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_write_uid
msgid "Last Updated by"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_write_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_write_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_write_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_write_date
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_write_date
msgid "Last Updated on"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_message
msgid "Message"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_type
msgid "Message type"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_message_ids
msgid "Messages"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr ""
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:47
#, python-format
msgid "Quit discussion"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_session_res_users_rel
msgid "Relation Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_common.js:24
#, python-format
msgid "Say something..."
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/xml/im_chat.xml:90
#, python-format
msgid "Search users..."
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_message_to_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_session_id
msgid "Session"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_user_ids
msgid "Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:52
#, python-format
msgid "Shortcode"
msgstr ""
#. module: im_chat
#: model:ir.ui.view,arch_db:im_chat.im_chat_shortcode_view_form
#: model:ir.ui.view,arch_db:im_chat.im_chat_shortcode_view_tree
msgid "Shortcodes"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_source
msgid "Shortcut"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:46
#, python-format
msgid "Shortcuts"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_state
msgid "Status"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_shortcode_substitution
msgid "Substitution"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,help:im_chat.field_im_chat_shortcode_substitution
msgid "The excaped html code replacing the shortcut"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,help:im_chat.field_im_chat_shortcode_source
msgid "The shortcut which must be replace in the Chat Messages"
msgstr ""
#. module: im_chat
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_uuid
msgid "UUID"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_im_chat_presence
msgid "User Chat Presence"
msgstr ""
#. module: im_chat
#: model:ir.model,name:im_chat.model_res_users
#: model:ir.model.fields,field_description:im_chat.field_im_chat_presence_user_id
#: model:ir.model.fields,field_description:im_chat.field_im_chat_session_res_users_rel_user_id
msgid "Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_common.js:26
#, python-format
msgid "Visitor"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:70
#, python-format
msgid "Warning"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat_backend.js:70
#, python-format
msgid "You are only 2 identified users. Just close the conversation to leave."
msgstr ""
-197
View File
@@ -1,197 +0,0 @@
# Macedonian translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-10-18 10:21+0000\n"
"Last-Translator: Tome Barbov <tome.barbov@eskon.com.mk>\n"
"Language-Team: Macedonian <mk@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-10-19 07:12+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Пораки"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Еден корисник може да има само еден IM статус."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Автор"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Отсутен"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Затворено"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Креирај датум"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Креирано од"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Креирано на"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "IM Статус"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr ""
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Последно присуство"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Последно ажурирање од"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Последно ажурирање на"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Порака"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Пораки"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Мета"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Исклучен"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Вклучен"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Отвори"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Речете нешто..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Сесија"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Сесија до"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Корисници на сесија"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Тип"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Корисници"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Посетител"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "непознато"
-197
View File
@@ -1,197 +0,0 @@
# Dutch translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-08-15 16:07+0000\n"
"Last-Translator: Mario Gielissen (Openworx) <Unknown>\n"
"Language-Team: Dutch <nl@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Berichten"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Een gebruiker kan maar een IM status hebben."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Auteur"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Afwezig"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Gesloten"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Aanmaakdatum"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Aangemaakt door"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Aangemaakt op"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Gevouwen"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "IM status"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Laatste pol"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Laatst aanwezig"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Laatste bijgewerkt door"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Laatst bijgewerkt op"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Bericht"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Berichten"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Offline"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Online"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Open"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Relatie gebruikers sessie"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Zeg iets ..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sessie"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sessie naar"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Gebruikers sessie"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Type"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Gebruikers"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Bezoeker"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "onbekend"
-198
View File
@@ -1,198 +0,0 @@
# Polish translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-08-27 21:10+0000\n"
"Last-Translator: Monika Grzelak (OpenGLOBE.pl) "
"<monika.grzelak@openglobe.pl>\n"
"Language-Team: Polish <pl@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Wiadomości"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Użytkownik może mieć tylko jeden status komunikatora."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Zaraz wracam"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Zamknięte"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Data utworzenia"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Utworzone przez"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Data utworzenia"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Zwinięte"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Status komunikatora"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Ostatnia ankieta"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Ostatnia obecność"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Ostatnio modyfikowane przez"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Data ostatniej modyfikacji"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Wiadomość"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Wiadomości"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr ""
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Niedostępny"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Dostępny"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Otwórz"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Uczestnicy sesji relacyjnej"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Powiedz coś..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sesja"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sesja do"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Użytkownicy sesji"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Typ"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Użytkownicy"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Gość"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "nieznane"
-197
View File
@@ -1,197 +0,0 @@
# Portuguese translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-09-17 17:42+0000\n"
"Last-Translator: Opencloud - PT <Unknown>\n"
"Language-Team: Portuguese <pt@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Mensagens"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Um utilizador pode apenas ter um estado IM."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Ausente"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Fechado"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Data de Criação"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Criado por"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Criado em"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Dobrado"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Estado IM"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Última Votação"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Última Presença"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Última Actualização por"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Última Actualização em"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Mensagem"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mensagens"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Desligado"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Ligado"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Abrir"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Utilizadores da Sessão Relacionados"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Diga algo..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sessão"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sessão a"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Utilizadores da Sessão"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Tipo"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Utilizadores"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Visitante"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "desconhecido"
-198
View File
@@ -1,198 +0,0 @@
# Brazilian Portuguese translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-09-06 19:08+0000\n"
"Last-Translator: Fábio Martinelli - http://zupy.com.br "
"<webmaster@zupy.com.br>\n"
"Language-Team: Brazilian Portuguese <pt_BR@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-09-24 09:14+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Mensagens"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Um usuário pode ter apenas um status"
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Ausente"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Fechado"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Data de criação"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Criado por"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Criado em"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Dobrado"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Status do mensageiro"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Última votação"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Última presença"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Última atualização por"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Última atualização em"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Mensagem"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mensagens"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Desconectado"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Conectado"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Aberto"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "Relação Sessões Usuários"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Diga alguma coisa..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sessão"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Sessão para"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Seção de usuários"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Digite"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Usuários"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Visitante"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "desconhecido"
-197
View File
@@ -1,197 +0,0 @@
# Romanian translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-10-16 04:31+0000\n"
"Last-Translator: Dorin <dhongu@gmail.com>\n"
"Language-Team: Romanian <ro@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-10-17 06:27+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Mesaje"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr ""
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Autor"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Absent"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Închis"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Data creării"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Creat de"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Creat la"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Îndoită"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Stare mesagerie instant"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr ""
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Ultima actualizare făcută de"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr ""
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Mesaj"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mesaje"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Deconectat"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Activ"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Deschide"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr ""
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr ""
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Sesiune"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr ""
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr ""
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Tip"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Utilizatori"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Vizitator"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "necunoscut"
-197
View File
@@ -1,197 +0,0 @@
# Turkish translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2014-09-23 16:27+0000\n"
"PO-Revision-Date: 2014-10-11 07:08+0000\n"
"Last-Translator: Ayhan KIZILTAN <Unknown>\n"
"Language-Team: Turkish <tr@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-10-12 07:19+0000\n"
"X-Generator: Launchpad (build 17196)\n"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:88
#, python-format
msgid "%d Messages"
msgstr "%d Mesaj"
#. module: im_chat
#: sql_constraint:im_chat.presence:0
msgid "A user can only have one IM status."
msgstr "Bir kullanıcı yalnozca bir IM durumunda olabilir."
#. module: im_chat
#: field:im_chat.message,from_id:0
msgid "Author"
msgstr "Yazan"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Away"
msgstr "Dışarıda"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Closed"
msgstr "Kapalı"
#. module: im_chat
#: field:im_chat.message,create_date:0
msgid "Create Date"
msgstr "Oluşturma Tarihi"
#. module: im_chat
#: field:im_chat.conversation_state,create_uid:0
#: field:im_chat.message,create_uid:0
#: field:im_chat.presence,create_uid:0
#: field:im_chat.session,create_uid:0
msgid "Created by"
msgstr "Oluşturan"
#. module: im_chat
#: field:im_chat.conversation_state,create_date:0
#: field:im_chat.presence,create_date:0
#: field:im_chat.session,create_date:0
msgid "Created on"
msgstr "Oluşturulma"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Folded"
msgstr "Katlanmış"
#. module: im_chat
#: field:im_chat.conversation_state,id:0
#: field:im_chat.message,id:0
#: field:im_chat.presence,id:0
#: field:im_chat.session,id:0
msgid "ID"
msgstr "ID"
#. module: im_chat
#: field:im_chat.presence,status:0
#: field:res.users,im_status:0
msgid "IM Status"
msgstr "Anlık İleti Durumu"
#. module: im_chat
#: field:im_chat.presence,last_poll:0
msgid "Last Poll"
msgstr "Son Anket"
#. module: im_chat
#: field:im_chat.presence,last_presence:0
msgid "Last Presence"
msgstr "Son Durum"
#. module: im_chat
#: field:im_chat.conversation_state,write_uid:0
#: field:im_chat.message,write_uid:0
#: field:im_chat.presence,write_uid:0
#: field:im_chat.session,write_uid:0
msgid "Last Updated by"
msgstr "Son Güncelleyen"
#. module: im_chat
#: field:im_chat.conversation_state,write_date:0
#: field:im_chat.message,write_date:0
#: field:im_chat.presence,write_date:0
#: field:im_chat.session,write_date:0
msgid "Last Updated on"
msgstr "Son Güncelleme"
#. module: im_chat
#: field:im_chat.message,message:0
#: selection:im_chat.message,type:0
msgid "Message"
msgstr "Mesaj"
#. module: im_chat
#: field:im_chat.session,message_ids:0
msgid "Messages"
msgstr "Mesajlar"
#. module: im_chat
#: selection:im_chat.message,type:0
msgid "Meta"
msgstr "Meta"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Offline"
msgstr "Çevrimdışı"
#. module: im_chat
#: selection:im_chat.presence,status:0
msgid "Online"
msgstr "Çevrimiçi"
#. module: im_chat
#: selection:im_chat.conversation_state,state:0
msgid "Open"
msgstr "Aç"
#. module: im_chat
#: field:im_chat.session,session_res_users_rel:0
msgid "Relation Session Users"
msgstr "İlgili Oturum Kullanıcısı"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:18
#, python-format
msgid "Say something..."
msgstr "Bir şey söyle..."
#. module: im_chat
#: field:im_chat.conversation_state,session_id:0
msgid "Session"
msgstr "Oturum"
#. module: im_chat
#: field:im_chat.message,to_id:0
msgid "Session To"
msgstr "Oturum Açılan"
#. module: im_chat
#: field:im_chat.session,user_ids:0
msgid "Session Users"
msgstr "Oturum Kullanıcısı"
#. module: im_chat
#: field:im_chat.message,type:0
msgid "Type"
msgstr "Türü"
#. module: im_chat
#: field:im_chat.session,uuid:0
msgid "UUID"
msgstr "UUID"
#. module: im_chat
#: field:im_chat.conversation_state,user_id:0
#: field:im_chat.presence,user_id:0
#: model:ir.model,name:im_chat.model_res_users
msgid "Users"
msgstr "Kullanıcılar"
#. module: im_chat
#. openerp-web
#: code:addons/im_chat/static/src/js/im_chat.js:20
#, python-format
msgid "Visitor"
msgstr "Ziyaretçi"
#. module: im_chat
#: field:im_chat.conversation_state,state:0
msgid "unknown"
msgstr "bilinmeyen"
-3
View File
@@ -1,3 +0,0 @@
# -*- coding: utf-8 -*-
import im_chat_session
import res_users
-462
View File
@@ -1,462 +0,0 @@
# -*- coding: utf-8 -*-
import cgi
import datetime
import random
import re
import time
import uuid
from openerp import tools, _
from openerp import api, fields, models
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.addons.bus.models.bus import TIMEOUT
DISCONNECTION_TIMER = TIMEOUT + 5
AWAY_TIMER = 600 # 10 minutes
class ImChatConversationState(models.Model):
""" User Session State
Adds a state on the m2m between user and session.
"""
_name = 'im_chat.conversation_state'
_description = 'Chat Conversation State'
_table = 'im_chat_session_res_users_rel'
state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Status', default='open')
session_id = fields.Many2one('im_chat.session', string='Session', required=True, ondelete='cascade')
user_id = fields.Many2one('res.users', string='Users', required=True, ondelete='cascade')
class ImChatSession(models.Model):
""" Session
Reprensenting a conversation between users. It manages anonymous user for method starting
with 'session_'.
"""
_name = 'im_chat.session'
_rec_name = 'uuid'
_description = 'Chat Conversation'
_order = 'id desc'
def _default_uuid(self):
return uuid.uuid4()
uuid = fields.Char(string='UUID', index=True, default=_default_uuid)
message_ids = fields.One2many('im_chat.message', 'to_id', string='Messages')
session_res_users_rel = fields.One2many('im_chat.conversation_state', 'session_id', string='Relation Session Users')
user_ids = fields.Many2many('res.users', 'im_chat_session_res_users_rel', 'session_id', 'user_id', string='Session Users')
# --------------------------
# Session Methods
# --------------------------
@api.multi
def session_info(self):
""" Get the header of the current session
:returns the values of the session
:rtype : dict
"""
self.ensure_one()
info = {
'uuid': self.uuid,
'users': self.session_user_info(),
'state': 'open',
}
# add user session state, if available and if user is logged
if self._uid:
domain = [('user_id', '=', self._uid), ('session_id', 'in', self.ids)]
session_state = self.env['im_chat.conversation_state'].search(domain, limit=1)
if session_state:
info['state'] = session_state.state
return info
@api.multi
def session_user_info(self):
""" Get the user infos for all the identified user in the session
:returns a list of user infos
:rtype : list(dict)
"""
self.ensure_one()
return self.env['res.users'].browse(self.user_ids.ids).read(['id', 'name', 'im_status'])
@api.model
def session_user_image(self, uuid, user_id):
""" Get the avatar of a user in the given session
:param uuid : the uuid of the session
:param user_id : the user identifier
"""
# get the session
image_b64 = False
if user_id:
session = self.env["im_chat.session"].search([('uuid', '=', uuid)], limit=1)
if session and session.sudo(user_id).is_in_session():
image_b64 = self.env["res.users"].sudo().browse(user_id).image_small
# set default image if not found
if not image_b64:
image_b64 = 'R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
return image_b64
@api.model
def session_get(self, user_to):
""" Get the canonical session between 2 users, create it if needed.
To reuse an old session, this one must be private, and contains only 2 users.
:param user_to : the identifier of the user
:returns a session header, or False if the user_to was False
:rtype : dict
"""
if user_to:
session = self.search([('user_ids', 'in', user_to), ('user_ids', 'in', [self._uid])], limit=1)
if not (session and len(session.user_ids) == 2 and session.is_private()):
session = self.create({'user_ids': [(6, 0, (user_to, self._uid))]})
return session.session_info()
# --------------------------
# Utils Methods
# --------------------------
@api.multi
def is_private(self):
""" Return true if the session is private between identified users (no external messages).
The only way to do that is to check if there is a message without author (from_id is False).
"""
self.ensure_one()
message_ids = self.env["im_chat.message"].search([('to_id', 'in', self.ids), ('from_id', '=', None)])
return len(message_ids) == 0
@api.multi
def is_in_session(self):
""" Return True if the current user is in the user_ids of the session. False otherwise.
Note : the user need to be logged
"""
self.ensure_one()
user_id = self._uid
return user_id and user_id in [u.id for u in self.user_ids]
@api.model
def update_state(self, uuid, state=None):
""" Update the fold_state of the given session. In order to syncronize web browser
tabs, the change will be broadcast to himself (the current user channel).
Note : the user need to be logged
:param status : the new status of the session for the current user.
"""
domain = [('user_id', '=', self._uid), ('session_id.uuid', '=', uuid)]
for session_state in self.env['im_chat.conversation_state'].search(domain):
if not state:
state = session_state.state
if session_state.state == 'open':
state = 'folded'
else:
state = 'open'
session_state.write({'state': state})
self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.session', self._uid), session_state.session_id.session_info())
# --------------------------
# User Actions
# --------------------------
@api.model
def add_user(self, uuid):
""" Add the current user to the session, and broadcast the meta message.
Note : the user need to be logged
:param uuid : the uuid of the session
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if user_id not in [u.id for u in session.user_ids]:
session.write({'user_ids': [(4, self._uid)]})
# notify the all the channel users and anonymous channel
notifications = []
for user in session.user_ids:
info = session.sudo(user.id).session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info])
# anonymous are not notified when a new user is added : cannot execute session_info as uid = None
info = session.sudo().session_info()
notifications.append([session.uuid, info])
self.env['bus.bus'].sendmany(notifications)
# send a message to the conversation
user = self.env['res.users'].browse(self._uid)
message = _("%s joined the conversation.") % (user.name,)
self.env["im_chat.message"].send_message(session.uuid, "meta", message)
@api.model
def quit_user(self, uuid):
""" Remove the current user from the given session.
Note : the user need to be logged
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 2:
return session._remove_user()
return False
@api.multi
def _remove_user(self):
""" Private implementation of removing the current user from the current session,
and broadcast the meta message.
Note : the user need to be logged
"""
self.ensure_one()
# send a message to the conversation
user = self.env['res.users'].browse(self._uid)
message = _("%s left the conversation.") % (user.name,)
self.env["im_chat.message"].send_message(self.uuid, "meta", message)
# close his session state
self.update_state(self.uuid, 'closed')
# remove the user from session
self.write({"user_ids": [(3, self._uid)]})
# notify the all the channel users and anonymous channel
notifications = []
for user in self.user_ids:
info = self.sudo(user.id).session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), info])
# anonymous are not notified when a new user left : cannot execute session_info as uid = None
info = self.sudo().session_info()
notifications.append([self.uuid, info])
self.env['bus.bus'].sendmany(notifications)
return True
class ImChatMessage(models.Model):
""" Session Messsages
The chat provide 2 message type : 'message' (normal user message)
or 'meta' (message relative to changes about sessions)
For anonymous message, the from_id is False.
Messages are sent to a session not to users.
"""
_name = 'im_chat.message'
_description = 'Chat Message'
_order = 'create_date asc'
@api.model
def _default_type(self):
return [('message', 'Message'), ('meta', 'Meta')]
create_date = fields.Datetime(string="Create date", required=True, index=True)
from_id = fields.Many2one('res.users', 'Author')
to_id = fields.Many2one('im_chat.session', 'Session', required=True, index=True)
type = fields.Selection('_default_type', 'Message type', default='message')
message = fields.Char('Message')
@api.model
def _escape_keep_url(self, message):
""" Escape the message and transform the url into clickable link
:param message : the message to escape url and transform them into clickable link
:returns the escaped message
"""
safe_message = ""
first = 0
last = 0
for m in re.finditer('(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?', message):
last = m.start()
safe_message += cgi.escape(message[first:last])
safe_message += '<a href="%s" target="_blank">%s</a>' % (cgi.escape(m.group(0)), m.group(0))
first = m.end()
last = m.end()
safe_message += cgi.escape(message[last:])
return safe_message
@api.model
def get_init_notifications(self):
""" Get unread messages and old messages received less than AWAY_TIMER
ago and the session_info for open or folded window
Note : the user need to be logged
"""
# get the message since the AWAY_TIMER
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('to_id.user_ids', 'in', [self._uid]), ('create_date', '>', threshold)]
# get the message since the last poll of the user
presence = self.env['im_chat.presence'].search([('user_id', '=', self._uid)], limit=1)
if presence:
domain.append(('create_date', '>', presence.last_poll))
messages = self.search_read(domain, ['from_id', 'to_id', 'create_date', 'type', 'message'], order='id asc')
# get the session of the messages and the not-closed ones
session_ids = [m['to_id'][0] for m in messages]
domain = [('user_id', '=', self._uid), '|', ('state', '!=', 'closed'), ('session_id', 'in', session_ids)]
session_states = self.env['im_chat.conversation_state'].search(domain)
# re-open the session where a message have been recieved recently
session_states.filtered(lambda r: r.state == 'closed').write({'state': 'folded'})
# create the notifications (session infos first, then messages)
notifications = []
for state in session_states:
session_infos = state.session_id.session_info()
notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), session_infos])
for message in messages:
notifications.append([(self._cr.dbname, 'im_chat.session', self._uid), message])
return notifications
@api.model
def send_message(self, from_uid, uuid, message_content, message_type='message'):
""" Post and broadcast a message for the given session, return the message id
:param from_uid : user id of the author of the message
:param uuid : the uuid of the session
:param message_content : the content of the message
:param message_type : the type of the message
:returns the identifier of the new created message
:rtype : integer
"""
session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1)
# escape the new message
message_content = self._escape_keep_url(message_content)
message_content = self.env['im_chat.shortcode'].replace_shortcode(message_content)
# create the chat message
values = {
"from_id": from_uid,
"to_id": session.id,
"type": message_type,
"message": message_content,
}
message = self.create(values)
# broadcast the message
data = message.read(['from_id', 'to_id', 'create_date', 'type', 'message'])[0]
notifications = []
notifications.append([uuid, data])
for user in session.sudo().user_ids: # sudo required for portal user
notifications.append([(self._cr.dbname, 'im_chat.session', user.id), data])
self.env['bus.bus'].sendmany(notifications)
return message.id
@api.model
def fetch_message(self, uuid, last_id=False, limit=20):
""" Return messages for the given session uuid. If the session is public, fetch_message
as 'sudo', else (private session) fetch them as the current user. The security rule
'message_rule_1' will check the access right.
:param uuid : uuid of the session to fetch messages
:param last_id : last message id to start the research
:param limit : maximum number of messages to fetch
:returns list of messages values
:rtype : list(dict)
"""
session = self.env['im_chat.session'].search([('uuid', '=', uuid)], limit=1)
if session and session.is_in_session():
domain = [("to_id.uuid", "=", uuid)]
if last_id:
domain.append(("id", "<", last_id))
return self.search_read(domain, ['id', 'create_date', 'to_id', 'from_id', 'type', 'message'], limit=limit, order="id desc")
return False
class ImChatShortcode(models.Model):
""" Shortcode
Canned Responses, allowing the user to defined shortcuts in its chat message.
These shortcode are globals and are available for every user. Smiley use this mecanism.
"""
_name = 'im_chat.shortcode'
_description = 'Canned Response / Shortcode'
source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages")
substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut")
description = fields.Char('Description')
@api.model
def create(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(ImChatShortcode, self).create(values)
@api.multi
def write(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(ImChatShortcode, self).write(values)
def _sanitize_shorcode(self, substitution):
""" Sanitize the shortcode substitution :
- HTML substitution : only allow the img tag (smiley)
- escape other substitutions to avoid XSS
"""
is_img_tag = re.match(r'''^<img\s+src=('|")([^'"]*)\1\s*/?>$''', substitution, re.M|re.I)
if is_img_tag:
return substitution
return cgi.escape(substitution)
@api.model
def replace_shortcode(self, message):
for shortcode in self.search([]):
regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source)
message = re.sub(regex, " " + shortcode.substitution + " ", message)
return message
class ImChatPresence(models.Model):
""" User Chat Presence
Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not
attached to res_users to avoid database concurrence errors. Since the 'update' method is executed
at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'.
"""
_name = 'im_chat.presence'
_description = 'User Chat Presence'
_sql_constraints = [('im_chat_user_status_unique', 'unique(user_id)', 'A user can only have one IM status.')]
user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade')
last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now())
last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now())
status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline')
@api.model
def update(self, user_presence=True):
""" Register the given presence of the current user, and trigger a im_status change if necessary.
The status will not be written or sent if not necessary.
:param user_presence : True, if the user (self._uid) is still detected using its browser.
:type user_presence : boolean
"""
presence = self.search([('user_id', '=', self._uid)], limit=1)
# set the default values
send_notification = True
values = {
'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'status' : presence and presence.status or 'offline'
}
# update the user or a create a new one
if not presence: # create a new presence for the user
values['status'] = 'online'
values['user_id'] = self._uid
self.create(values)
else: # write the user presence if necessary
if user_presence:
values['last_presence'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
values['status'] = 'online'
else:
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
if datetime.datetime.strptime(presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < threshold:
values['status'] = 'away'
send_notification = presence.status != values['status']
# write only if the last_poll is passed TIMEOUT, or if the status has changed
delta = datetime.datetime.utcnow() - datetime.datetime.strptime(presence.last_poll, DEFAULT_SERVER_DATETIME_FORMAT)
if delta > datetime.timedelta(seconds=TIMEOUT) or send_notification:
# Hide transaction serialization errors, which can be ignored, the presence update is not essential
with tools.mute_logger('openerp.sql_db'):
presence.write(values)
# avoid TransactionRollbackError
self.env.cr.commit() # TODO : check if still necessary
# notify if the status has changed
if send_notification: # TODO : add user_id to the channel tuple to allow using user_watch in controller presence
self.env['bus.bus'].sendone((self._cr.dbname, 'im_chat.presence'), {'id': self._uid, 'im_status': values['status']})
# gc : disconnect the users having a too old last_poll. 1 on 100 chance to do it.
if random.random() < 0.01:
self.check_users_disconnection()
return True
@api.model
def check_users_disconnection(self):
""" Disconnect the users having a too old last_poll """
limit_date = (datetime.datetime.utcnow() - datetime.timedelta(0, DISCONNECTION_TIMER)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
presences = self.search([('last_poll', '<', limit_date), ('status', '!=', 'offline')])
presences.write({'status': 'offline'})
notifications = []
for presence in presences:
notifications.append([(self._cr.dbname, 'im_chat.presence'), {'id': presence.user_id.id, 'im_status': presence.status}])
self.env['bus.bus'].sendmany(notifications)
@@ -1,37 +0,0 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="im_chat_message_rule_user" model="ir.rule">
<field name="name">Can only read messages that you sent or messages sent to you</field>
<field name="model_id" ref="model_im_chat_message"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
<field name="domain_force">[('to_id.user_ids', 'in', [user.id])]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="im_chat_session_rule_user" model="ir.rule">
<field name="name">Can only modify your session</field>
<field name="model_id" ref="model_im_chat_session"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
<field name="domain_force">[('user_ids', 'in', [user.id])]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
<record id="im_chat_session_relation_rule_user" model="ir.rule">
<field name="name">Can only modify your own session relations</field>
<field name="model_id" ref="model_im_chat_conversation_state"/>
<field name="groups" eval="[(4, ref('base.group_user')), (4, ref('base.group_portal'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="1"/>
</record>
</data>
</odoo>
@@ -1,11 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_im_chat_message,im_chat.message,model_im_chat_message,base.group_user,1,0,1,0
access_im_chat_session,im_chat.session,model_im_chat_session,base.group_user,1,1,1,0
access_im_chat_conversation_state,im_chat.conversation_state,model_im_chat_conversation_state,base.group_user,1,1,1,0
access_im_chat_presence,im_chat.presence,model_im_chat_presence,base.group_user,1,1,1,1
access_im_chat_shortcode,im_chat.shortcode,model_im_chat_shortcode,base.group_user,1,1,1,1
access_im_chat_message_portal,im_chat.message,model_im_chat_message,base.group_portal,1,0,1,0
access_im_chat_session_portal,im_chat.session,model_im_chat_session,base.group_portal,1,1,1,0
access_im_chat_conversation_state_portal,im_chat.conversation_state,model_im_chat_conversation_state,base.group_portal,1,1,1,0
access_im_chat_presence_portal,im_chat.presence,model_im_chat_presence,base.group_portal,1,1,1,1
access_im_chat_shortcode_portal,im_chat.shortcode,model_im_chat_shortcode,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_im_chat_message im_chat.message model_im_chat_message base.group_user 1 0 1 0
3 access_im_chat_session im_chat.session model_im_chat_session base.group_user 1 1 1 0
4 access_im_chat_conversation_state im_chat.conversation_state model_im_chat_conversation_state base.group_user 1 1 1 0
5 access_im_chat_presence im_chat.presence model_im_chat_presence base.group_user 1 1 1 1
6 access_im_chat_shortcode im_chat.shortcode model_im_chat_shortcode base.group_user 1 1 1 1
7 access_im_chat_message_portal im_chat.message model_im_chat_message base.group_portal 1 0 1 0
8 access_im_chat_session_portal im_chat.session model_im_chat_session base.group_portal 1 1 1 0
9 access_im_chat_conversation_state_portal im_chat.conversation_state model_im_chat_conversation_state base.group_portal 1 1 1 0
10 access_im_chat_presence_portal im_chat.presence model_im_chat_presence base.group_portal 1 1 1 1
11 access_im_chat_shortcode_portal im_chat.shortcode model_im_chat_shortcode base.group_portal 1 0 0 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.
Binary file not shown.
@@ -1,114 +0,0 @@
.oe_im {
position: fixed;
background-color: #eaeaea;
width: 250px;
border-left: 1px solid #AEB9BD;
z-index: 500;
-webkit-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
-moz-box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
box-shadow: -2px 0px 5px 0px rgba(50, 50, 50, 0.25);
}
@media print {
.oe_im, .oe_im_chatview {
visibility: hidden;
}
}
/* button */
.oe_systray #oe_topbar_imbutton_icon {
color: white;
}
.oe_topbar_item.oe_topbar_imbutton .fa {
position: relative;
top: -8px;
}
/* search stuff */
.oe_im_frame_header {
position: relative;
padding: 16px;
}
.oe_im_frame_header .oe_im_searchbox {
width: 200px;
padding: 1px 21px 1px 19px;
font-size: 13px;
border: none;
border-bottom: 1px solid #c6c6c6;
background: none;
margin-left: 20px;
height: 38px;
}
.oe_im_frame_header .oe_im_searchbox:focus {
outline: none;
}
.oe_im_frame_header .oe_im_search_icon {
position: absolute;
color: #888;
top: 28px;
left: 8px;
font-size: 16px;
font-weight: 300 !important;
}
.oe_im_frame_header .oe_im_search_clear {
display: none;
position: absolute;
right: 11px;
top: 4px;
font-size: 26px;
color: #b6b6b6;
cursor: pointer;
}
.oe_im_frame_header .oe_im_search_clear:hover {
color: #888;
}
/* users */
.oe_im_users {
padding-bottom: 38px;
height: 100%;
overflow-y: scroll;
}
.oe_im_user {
position: relative;
padding: 2px 8px;
cursor: pointer;
font-size: 13px;
margin-bottom: 10px;
}
.oe_im_user:hover {
background: lightGrey;
}
.oe_im_user_clip {
display: inline-block;
width: 26px;
height: 26px;
margin-right: 4px;
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
}
.oe_im_user_avatar {
width: 26px;
height: auto;
}
.oe_im_user_name {
width: 162px;
line-height: 26px;
padding-right: 15px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
position: relative;
}
.oe_im_user_online {
display: none;
position: absolute;
top: 9.5px;
right: 11px;
width: 11px;
height: 11px;
vertical-align: middle;
border: 0;
}
@@ -1,259 +0,0 @@
.openerp_style { /* base style of openerp */
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
color: #4c4c4c;
font-size: 13px;
background: white;
}
/* button */
.oe_chat_button {
position: fixed;
bottom: 0px;
right: 6px;
display: inline-block;
min-width: 100px;
background-color: rgba(60, 60, 60, 0.6);
font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif;
font-size: 14px;
font-weight: bold;
padding: 10px;
color: white;
text-shadow: rgb(59, 76, 88) 1px 1px 0px;
border: 1px solid rgb(80, 80, 80);
border-bottom: 0px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
z-index:500;
}
/* conversations */
.oe_im_chatview {
position: fixed;
overflow: hidden;
margin-bottom: 5px;
margin-right: 6px;
background-color: #fff;
border: 1px solid #DEDEDE;
width: 240px;
z-index:500;
}
@media screen and (max-width:360px) {
.oe_im_chatview {
width: 100%;
margin-right: 0;
}
.oe_im_chatview .oe_im_chatview_content {
width: 100% !important;
}
.oe_im_chatview .oe_im_chatview_input {
width: 100% !important;
}
}
.oe_im_chatview .oe_im_chatview_disconnected {
display:none;
z-index: 100;
width: 100%;
background: #E8EBEF;
padding: 5px;
font-size: 11px;
color: #999;
line-height: 14px;
height: 28px;
overflow: hidden;
}
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_disconnected {
display: block;
}
/* ####### Chat View : Header ####### */
.oe_im_chatview_header {
padding: 3px 6px 2px;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
border-radius: 3px 3px 0 0;
-moz-border-radius: 3px 3px 0 0;
-webkit-border-radius: 3px 3px 0 0;
border-bottom: 1px solid #AEB9BD;
cursor: pointer;
}
.oe_im_chatview_header .header_name{
max-width: 75%;
word-wrap: break-word;
display: inline-block;
height: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.oe_im_chatview_header .right_buttons {
float: right;
}
.oe_im_chatview_header .right_buttons > * {
display: inline-block;
border: 0;
cursor: pointer;
color: gray ;
background: transparent;
font-size: 16px;
line-height: 16px;
margin-left: 5px;
-webkit-appearance: none;
}
.oe_im_chatview_header .right_buttons .dropdown-menu{
right: 0;
left: auto;
}
.oe_im_chatview_header .right_buttons .dropdown-menu > li{
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.428571429;
color: #4c4c4c;
white-space: nowrap;
}
.oe_im_chatview .oe_im_chatview_content {
overflow: auto;
height: 287px;
width: 240px;
}
.oe_im_chatview.oe_im_chatview_disconnected_status .oe_im_chatview_content {
height: 249px;
}
.oe_im_chatview .oe_im_chatview_footer {
position: relative;
padding: 3px;
border-top: 1px solid #AEB9BD;
background: #DEDEDE;
background: -moz-linear-gradient(#FCFCFC, #DEDEDE);
background: -webkit-gradient(linear, left top, left bottom, from(#FCFCFC), to(#DEDEDE));
-moz-border-radius: 0 0 3px 3px;
-webkit-border-radius: 0 0 3px 3px;
border-radius: 0 0 3px 3px;
}
.oe_im_chatview .oe_im_chatview_input {
width: 222px;
font-family: Lato, Helvetica, sans-serif;
font-size: 13px;
color: #333;
padding: 1px 5px;
border: 1px solid #AEB9BD;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: inset 0 1px 4px rgba(0,0,0,0.2);
-webkit-box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
box-shadow: inset 0 1px 4px rgba(0, 0, 0, 0.2);
}
.oe_im_chatview .oe_im_chatview_message_bubble {
background: white;
position: relative;
min-height: 32px;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
}
/* Smiley size */
.oe_im_chatview .oe_im_chatview_bubble_item img{
width: 18px;
}
.oe_im_chatview .oe_im_chatview_technical_bubble {
background: #D8D8D8;
position: relative;
min-height: 32px;
padding: 3px;
margin: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
font-style:italic;
}
.oe_im_chatview .oe_im_chatview_clip {
position: relative;
float: left;
width: 26px;
height: 26px;
margin-right: 4px;
-moz-box-shadow: 0 0 2px 1px rgba(0,0,0,0.25);
-webkit-box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.25);
}
.oe_im_chatview .oe_im_chatview_avatar {
float: left;
width: 26px;
height: auto;
clip: rect(0, 26px, 26px, 0);
max-width: 100%;
width: auto 9;
height: auto;
vertical-align: middle;
border: 0;
-ms-interpolation-mode: bicubic;
}
.oe_im_chatview .oe_im_chatview_time {
position: absolute;
right: 0px;
top: 0px;
margin: 3px;
text-align: right;
line-height: 13px;
font-size: 11px;
color: #999;
width: 60px;
overflow: hidden;
}
.oe_im_chatview .oe_im_chatview_from {
margin: 0 0 2px 0;
line-height: 14px;
font-weight: bold;
font-size: 12px;
width: 140px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
color: #3A87AD;
}
.oe_im_chatview .oe_im_chatview_bubble_list {
}
.oe_im_chatview .oe_im_chatview_bubble_item {
margin: 0 0 2px 30px;
line-height: 14px;
word-wrap: break-word;
}
.oe_im_chatview_online {
margin-top: -4px;
width: 11px;
height: 11px;
}
.oe_im_chatview .oe_im_chatview_date_separator {
background: transparent;
position: relative;
padding: 3px;
color: #aaa;
margin: 3px;
text-align: right;
font-size: 12px;
}
@@ -1,301 +0,0 @@
odoo.define('im_chat.im_chat', function (require) {
"use strict";
// to do: make this work in website
var bus = require('bus.bus');
var core = require('web.core');
var data = require('web.data');
var Model = require('web.DataModel');
var session = require('web.session');
var SystrayMenu = require('web.SystrayMenu');
var utils = require('web.utils');
var web_client = require('web.web_client');
var Widget = require('web.Widget');
var im_chat_common = require('im_chat.im_chat_common');
var _t = core._t;
var QWeb = core.qweb;
var USERS_LIMIT = 20;
// ########## CONVERSATION extentions ###############
im_chat_common.ConversationManager.include({
_message_receive: function(message){
var self = this;
var session_id = message.to_id[0];
var conv = this.sessions[message.to_id[1]];
if(!conv){
// fetch the session, and init it with the message
var def_session = new Model("im_chat.session").call("session_info", [], {"ids" : [session_id]}).then(function(session){
conv = self.session_apply(session, {'force_open': true});
conv.message_receive(message);
});
}else{
this._super(message);
}
},
});
im_chat_common.Conversation.include({
// user actions
prepare_action_menu: function(){
this._super();
this._add_action(_t('Shortcuts'), 'im_chat_option_shortcut', 'fa fa-info-circle', this.action_shorcode);
this._add_action(_t('Quit discussion'), 'im_chat_option_quit', 'fa fa-minus-square', this.action_quit_session);
},
action_shorcode: function(e){
return web_client.action_manager.do_action({
type: 'ir.actions.act_window',
name : _t('Shortcode'),
res_model: 'im_chat.shortcode',
view_mode: 'tree,form',
view_type: 'tree',
views: [[false, 'list'], [false, 'form']],
target: "new",
limit: 80,
flags: {
action_buttons: true,
pager: true,
}
});
},
action_quit_session: function(e){
var self = this;
var Session = new Model("im_chat.session");
return Session.call("quit_user", [this.get("session").uuid]).then(function(res) {
if(! res){
self.do_warn(_t("Warning"), _t("You are only 2 identified users. Just close the conversation to leave."));
}
});
},
// session
session_update_state: function(state){
var self = this;
var args = arguments;
var super_call = this._super;
// broadcast the state changing
return new Model("im_chat.session").call("update_state", [], {"uuid" : this.get("session").uuid, "state" : state}).then(function(){
super_call.apply(self, args);
});
},
// window title
window_title_change: function() {
this.super();
var title = undefined;
if (this.get("waiting_messages") !== 0) {
title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"))
}
web_client.set_title_part("im_messages", title);
},
// TODO : change this way
add_user: function(user){
return new Model("im_chat.session").call("add_user", [this.get("session").uuid , user.id]);
},
});
// ###### BACKEND : contact panel, top menu button #########
var UserWidget = Widget.extend({
template: "im_chat.UserWidget",
events: {
"click": "activate_user",
},
init: function(parent, user) {
this._super(parent);
this.set("id", user.id);
this.set("name", user.name);
this.set("im_status", user.im_status);
this.set("image_url", user.image_url);
},
start: function() {
this.$el.data("user", {id:this.get("id"), name:this.get("name")});
this.$el.draggable({helper: "clone"});
this.on("change:im_status", this, this.update_status);
this.update_status();
},
update_status: function(){
this.$(".oe_im_user_online").toggle(this.get('im_status') !== 'offline');
var img_src = (this.get('im_status') == 'away' ? '/im_chat/static/src/img/yellow.png' : '/im_chat/static/src/img/green.png');
this.$(".oe_im_user_online").attr('src', img_src);
},
activate_user: function() {
this.trigger("user_clicked", this.get("id"));
},
});
var InstantMessaging = Widget.extend({
template: "im_chat.InstantMessaging",
events: {
"keydown .oe_im_searchbox": "input_change",
"keyup .oe_im_searchbox": "input_change",
"change .oe_im_searchbox": "input_change",
},
init: function(parent) {
this._super(parent);
this.shown = false;
this.set("right_offset", 0);
this.set("current_search", "");
this.users = [];
this.widgets = {};
// listen bus
this.bus = bus.bus;
this.bus.on("notification", this, this.on_notification);
},
start: function() {
var self = this;
// ui
this.conv_manager = new im_chat_common.ConversationManager(this);
this.on("change:right_offset", this.conv_manager, _.bind(function() {
this.conv_manager.set("right_offset", this.get("right_offset"));
}, this));
$(window).scroll(_.bind(this.position_compute, this));
$(window).resize(_.bind(this.position_compute, this));
this.$el.css("right", -this.$el.outerWidth());
this.position_compute();
// business
this.on("change:current_search", this, this.user_search);
// add a drag & drop listener
self.conv_manager.on("im_session_activated", self, function(conv) {
conv.$el.droppable({
drop: function(event, ui) {
conv.add_user(ui.draggable.data("user"));
}
});
});
// fetch the unread message and the recent activity (e.i. to re-init in case of refreshing page)
return session.rpc("/im_chat/init", {}).then(function(notifications) {
_.each(notifications, function(notif){
self.conv_manager.on_notification(notif, {'load_history': true});
});
// start polling
self.bus.start_polling();
});
},
on_notification: function(notification){
var channel = notification[0];
var message = notification[1];
// user status notification
if(channel[1] === 'im_chat.presence'){
if(message.im_status){
this.user_update_status([message]);
}
}
},
// ui
position_compute: function() {
var top = 48;
this.$el.css("top", top);
this.$el.css("bottom", 0);
},
input_change: function() {
this.set("current_search", this.$(".oe_im_searchbox").val());
},
switch_display: function() {
this.position_compute();
var fct = _.bind(function(place) {
this.set("right_offset", place + this.$el.outerWidth());
this.$(".oe_im_searchbox").focus();
}, this);
var opt = {
step: fct,
};
if (this.shown) {
this.$el.animate({
right: -this.$el.outerWidth(),
}, opt);
} else {
if (! bus.bus.activated) {
this.do_warn("Instant Messaging is not activated on this server. Try later.", "");
return;
}
// update the list of user status when show the IM
this.user_search();
this.$el.animate({
right: 0,
}, opt);
}
this.shown = ! this.shown;
},
// user methods
user_search: function(e) {
var self = this;
var user_model = new Model("res.users");
return user_model.call('im_search', [this.get("current_search"), USERS_LIMIT]).then(function(result){
self.$(".oe_im_input").val("");
var old_widgets = self.widgets;
self.widgets = {};
self.users = [];
_.each(result, function(user) {
user.image_url = session.url('/web/image', {model:'res.users', field: 'image_small', id: user.id});
var widget = new UserWidget(self, user);
widget.appendTo(self.$(".oe_im_users"));
widget.on("user_clicked", self, self.user_clicked);
self.widgets[user.id] = widget;
self.users.push(user);
});
_.each(old_widgets, function(w) {
w.destroy();
});
});
},
user_clicked: function(user_id) {
var self = this;
var sessions = new Model("im_chat.session");
return sessions.call("session_get", [user_id]).then(function(session) {
self.conv_manager.session_apply(session, {'focus': true, 'force_open': true});
});
},
user_update_status: function(users_list){
var self = this;
_.each(users_list, function(el) {
if (self.widgets[el.id]) {
self.widgets[el.id].set("im_status", el.im_status);
}
});
}
});
var ImTopButton = Widget.extend({
template:'im_chat.ImTopButton',
events: {
"click": "clicked",
},
start: function() {
// Create the InstantMessaging widget and put it in the DOM
var im = new InstantMessaging(this);
web_client.im_messaging = im;
im.appendTo(web_client.$el);
// Bind the click action to the ImTopButton
this.on("clicked", im, im.switch_display);
return this._super();
},
clicked: function(ev) {
ev.preventDefault();
this.trigger("clicked");
},
});
// Put the ImTopButton widget in the systray menu if the user is an employee
var Users = new Model('res.users');
Users.call('has_group', ['base.group_user']).done(function(is_employee) {
if (is_employee) {
SystrayMenu.Items.push(ImTopButton);
}
});
return {
InstantMessaging: InstantMessaging,
UserWidget: UserWidget,
};
});
@@ -1,433 +0,0 @@
odoo.define('im_chat.im_chat_common', function (require) {
"use strict";
// to do: make this work in website
var bus = require('bus.bus');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');
var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
var NBR_LIMIT_HISTORY = 20;
var ConversationManager = Widget.extend({
init: function(parent, options) {
var self = this;
this._super(parent);
this.options = _.clone(options) || {};
_.defaults(this.options, {
inputPlaceholder: _t("Say something..."),
defaultMessage: null,
defaultUsername: _t("Visitor"),
force_open: false,
load_history: true,
focus: false,
});
// business
this.sessions = {};
this.bus = bus.bus;
this.bus.on("notification", this, this.on_notification);
this.bus.options.im_presence = true;
// ui
this.set("right_offset", 0);
this.set("bottom_offset", 0);
this.on("change:right_offset", this, this.calc_positions);
this.on("change:bottom_offset", this, this.calc_positions);
this.set("window_focus", true);
this.on("change:window_focus", self, function(e) {
self.bus.options.im_presence = self.get("window_focus");
});
this.set("waiting_messages", 0);
this.on("change:waiting_messages", this, this.window_title_change);
$(window).on("focus", _.bind(this.window_focus, this));
$(window).on("blur", _.bind(this.window_blur, this));
this.window_title_change();
},
on_notification: function(notification, options) {
var self = this;
var channel = notification[0];
var message = notification[1];
var regex_uuid = new RegExp(/(\w{8}(-\w{4}){3}-\w{12}?)/g);
// Concern im_chat : if the channel is the im_chat.session or im_chat.status, or a 'private' channel (aka the UUID of a session)
if((Array.isArray(channel) && (channel[1] === 'im_chat.session')) || (regex_uuid.test(channel))){
// message to display in the chatview
if (message.type === "message" || message.type === "meta") {
self.message_receive(message);
}
// activate the received session
if(message.uuid){
this.session_apply(message, options);
}
}
},
// window focus unfocus beep and title
window_focus: function() {
this.set("window_focus", true);
this.set("waiting_messages", 0);
},
window_blur: function() {
this.set("window_focus", false);
},
window_beep: function() {
if (typeof(Audio) === "undefined") {
return;
}
var audio = new Audio();
var ext = audio.canPlayType("audio/ogg; codecs=vorbis") ? ".ogg" : ".mp3";
audio.src = session.url("/im_chat/static/src/audio/ting") + ext;
audio.play();
},
window_title_change: function() {
var title = undefined;
if (this.get("waiting_messages") !== 0) {
title = _.str.sprintf(_t("%d Messages"), this.get("waiting_messages"));
this.window_beep();
}
},
// sessions and messages
session_apply: function(active_session, options){
// options used by this function : force_open and focus (load_history can be usefull)
var self = this;
options = _.extend(_.clone(this.options), options || {});
// force open
var session = _.clone(active_session);
if(options['force_open']){
session.state = 'open';
}
// create/get the conversation widget
var conv = this.sessions[session.uuid];
if (! conv) {
if(session.state !== 'closed'){
conv = new Conversation(this, this, session, options);
conv.appendTo($("body"));
conv.on("destroyed", this, _.bind(this.session_delete, this));
this.sessions[session.uuid] = conv;
this.calc_positions();
}
}else{
conv.set("session", session);
}
// if force open, broadcast it
if(options['force_open'] && active_session.state !== 'open'){
conv.session_update_state('open');
}
// apply the focus
if (options['focus']){
conv.focus();
}
return conv;
},
session_delete: function(uuid){
delete this.sessions[uuid];
this.calc_positions();
},
message_receive: function(message) {
var self = this;
var session_id = message.to_id[0];
var uuid = message.to_id[1];
var from_id = message['from_id'] ? message['from_id'][0] : false;
if (! this.get("window_focus") && from_id != this.get_current_uid()) {
this.set("waiting_messages", this.get("waiting_messages") + 1);
}
this._message_receive(message);
},
_message_receive: function(message){
var uuid = message.to_id[1];
var conv = this.sessions[uuid];
conv.message_receive(message);
},
// others
get_current_uid: function(){
return session && !_.isUndefined(session.uid) ? session.uid : false;
},
calc_positions: function() {
var self = this;
var current = this.get("right_offset");
_.each(this.sessions, function(s) {
s.set("bottom_position", self.get("bottom_offset"));
s.set("right_position", current);
current += s.$().outerWidth(true);
});
},
destroy: function() {
$(window).off("unload", this.unload);
$(window).off("focus", this.window_focus);
$(window).off("blur", this.window_blur);
return this._super();
}
});
var Conversation = Widget.extend({
className: "openerp_style oe_im_chatview",
events: {
"keydown input": "keydown",
"click .oe_im_chatview_close": "click_close",
"click .oe_im_chatview_header": "click_header"
},
init: function(parent, c_manager, session, options) {
this._super(parent);
this.c_manager = c_manager;
this.options = options || {};
this.loading_history = true;
this.set("messages", []);
this.set("session", session);
this.set("right_position", 0);
this.set("bottom_position", 0);
this.set("pending", 0);
this.inputPlaceholder = this.options.defaultInputPlaceholder;
},
start: function() {
var self = this;
self._super.apply(this, arguments);
self.prepare_action_menu();
// ui
self.$().append(QWeb.render("im_chat.Conversation", {widget: self}));
self.on("change:right_position", self, self.calc_pos);
self.on("change:bottom_position", self, self.calc_pos);
self.on("change:pending", self, _.bind(function() {
if (self.get("pending") === 0) {
self.$(".nbr_messages").text("");
} else {
self.$(".nbr_messages").text("(" + self.get("pending") + ")");
}
}, self));
self.full_height = self.$().height();
self.calc_pos();
// business
self.bind_action_menu();
self.on("change:session", self, self.session_update);
self.on("change:messages", self, self.render_messages);
self.$('.oe_im_chatview_content').on('scroll',function(){
if($(this).scrollTop() === 0){
self.message_history();
}
});
if(self.options['load_history']){ // load history if asked
self.message_history();
}
// prepare the header and the correct state
self.session_update();
},
// action menu
_add_action: function(label, style_class, icon_fa_class, callback){
this.actions.push({
'label': label,
'class': style_class,
'icon_class': icon_fa_class,
'callback': callback,
});
},
prepare_action_menu: function(){
// override this method to add action with _add_action()
this.actions = [];
},
bind_action_menu: function(){
var self = this;
_.each(this.actions, function(action){
if(action.callback){
self.$('.button_option_group .' + action.class).on('click', _.bind(action.callback, self));
}
});
},
// ui
show: function(){
this.$().animate({
height: this.full_height
});
this.set("pending", 0);
},
hide: function(){
this.$().animate({
height: this.$(".oe_im_chatview_header").outerHeight()
});
},
calc_pos: function() {
this.$().css("right", this.get("right_position"));
this.$().css("bottom", this.get("bottom_position"));
},
// session
session_update_state: function(state){
var session = this.get('session');
session.state = state;
this.set('session', session);
},
session_update: function(){
// built the name
var names = [];
_.each(this.get("session").users, function(user){
if( (session.uid !== user.id) && !(_.isUndefined(session.uid) && !user.id) ){
names.push(user.name);
}
});
this.$(".header_name").text(names.join(", "));
this.$(".header_name").attr('title', names.join(", "));
// update the fold state
if(this.get("session").state){
if(this.get("session").state === 'closed'){
this.destroy();
}else{
if(this.get("session").state === 'open'){
this.show();
}else{
this.hide();
}
}
}
},
// messages
message_history: function(){
var self = this;
if(this.loading_history){
var data = {uuid: self.get("session").uuid, limit: NBR_LIMIT_HISTORY};
var lastid = _.first(this.get("messages")) ? _.first(this.get("messages")).id : false;
if(lastid){
data.last_id = lastid;
}
session.rpc("/im_chat/history", data).then(function(messages){
if(messages){
self.insert_messages(messages);
if(messages.length != NBR_LIMIT_HISTORY){
self.loading_history = false;
}
}else{
self.loading_history = false;
}
});
}
},
message_receive: function(message) {
if (this.get('session').state === 'open') {
this.set("pending", 0);
} else {
this.set("pending", this.get("pending") + 1);
}
this.insert_messages([message]);
this._go_bottom();
},
message_send: function(message, type) {
var self = this;
var send_it = function() {
return session.rpc("/im_chat/post", {uuid: self.get("session").uuid, message_type: type, message_content: message});
};
var tries = 0;
send_it().fail(function(error, e) {
e.preventDefault();
tries += 1;
if (tries < 3)
return send_it();
});
},
insert_messages: function(messages){
var self = this;
// avoid duplicated messages
messages = _.filter(messages, function(m){ return !_.contains(_.pluck(self.get("messages"), 'id'), m.id) ; });
// escape the message content and set the timezone
_.map(messages, function(m){
if(!m.from_id){
m.from_id = [false,self. get_anonymous_name()];
}
m.create_date = moment(time.str_to_datetime(m.create_date)).format('YYYY-MM-DD HH:mm:ss');
return m;
});
this.set("messages", _.sortBy(this.get("messages").concat(messages), function(m){ return m.id; }));
},
render_messages: function(){
var self = this;
var res = {};
var last_date_day, last_user_id = -1;
_.each(this.get("messages"), function(current){
// add the url of the avatar for all users in the conversation
current.from_id[2] = session.url(_.str.sprintf("/im_chat/image/%s/%s", self.get('session').uuid, current.from_id[0]));
var date_day = current.create_date.split(" ")[0];
if(date_day !== last_date_day){
res[date_day] = [];
last_user_id = -1;
}
last_date_day = date_day;
if(current.type == "message"){ // traditionnal message
if(last_user_id === current.from_id[0]){
_.last(res[date_day]).push(current);
}else{
res[date_day].push([current]);
}
last_user_id = current.from_id[0];
}else{ // meta message
res[date_day].push([current]);
last_user_id = -1;
}
});
// render and set the content of the chatview
// TODO jem : when refactoring this, don't forget to pre-process date in this function before render quweb template
// since, moment will not be define in qweb on the website pages, because the helper (see csn) is in core.js and cannot be
// imported in the frontend.
this.$('.oe_im_chatview_content_bubbles').html($(QWeb.render("im_chat.Conversation_content", {"list": res})));
this._go_bottom();
},
// utils and event
get_anonymous_name: function(){
var name = this.options["defaultUsername"];
_.each(this.get('session').users, function(u){
if(!u.id){
name = u.name;
}
});
return name;
},
keydown: function(e) {
if(e && e.which == 27) {
if(this.$el.prev().find('.oe_im_chatview_input').length > 0){
this.$el.prev().find('.oe_im_chatview_input').focus();
}else{
this.$el.next().find('.oe_im_chatview_input').focus();
}
e.stopPropagation();
this.session_update_state('closed');
}
if(e && e.which !== 13) {
return;
}
var mes = this.$("input").val();
if (! mes.trim()) {
return;
}
this.$("input").val("");
this.message_send(mes, "message");
},
_go_bottom: function() {
this.$(".oe_im_chatview_content").scrollTop(this.$(".oe_im_chatview_content").get(0).scrollHeight);
},
focus: function() {
this.$(".oe_im_chatview_input").focus();
},
click_header: function(event){
var classes = event.target.className.split(' ');
if(_.contains(classes, 'header_name') || _.contains(classes, 'oe_im_chatview_header')){
this.session_update_state();
}
},
click_close: function(event) {
this.session_update_state('closed');
},
destroy: function() {
this.trigger("destroyed", this.get('session').uuid);
return this._super();
}
});
return {
Conversation: Conversation,
ConversationManager: ConversationManager,
};
});
-103
View File
@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="im_chat.Conversation">
<div class="oe_im_chatview_header">
<span class="header_name"></span>
<span class="nbr_messages"/>
<span class="right_buttons">
<!-- options -->
<div t-if="widget.actions.length !== 0" class="btn-group button_option_group">
<span class="dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-cogs"></i>
</span>
<ul class="dropdown-menu" role="menu">
<t t-foreach="widget.actions" t-as="action">
<li t-att-class="action.class">
<i t-att-class="action.icon_class"></i> <t t-esc="action.label"/>
</li>
</t>
</ul>
</div>
<div class="oe_im_chatview_close">×</div>
</span>
</div>
<div class="oe_im_chatview_content">
<div class="oe_im_chatview_status"/>
<div class="oe_im_chatview_content_bubbles"></div>
</div>
<div class="oe_im_chatview_footer">
<input class="oe_im_chatview_input" t-att-placeholder="widget.inputPlaceholder" />
</div>
</t>
<t t-name="im_chat.Conversation_content">
<t t-foreach="_.keys(list)" t-as="date">
<div class="oe_im_chatview_date_separator">
<t t-esc="window.moment(date).format('LL')"/>
</div>
<t t-foreach="list[date]" t-as="bubble">
<t t-if="bubble[0].type === 'message'">
<t t-call="im_chat.Conversation_message_bubble">
<t t-set="messages" t-value="bubble"/>
</t>
</t>
<t t-if="bubble[0].type === 'meta'">
<t t-call="im_chat.Conversation_technical_bubble">
<t t-set="messages" t-value="bubble"/>
</t>
</t>
</t>
</t>
</t>
<t t-name="im_chat.Conversation_message_bubble">
<div class="oe_im_chatview_message_bubble">
<div class="oe_im_chatview_clip">
<img class="oe_im_chatview_avatar" t-att-src="_.last(messages).from_id[2]"/>
</div>
<div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
<div class="oe_im_chatview_bubble_list">
<t t-foreach="messages" t-as="m">
<div class="oe_im_chatview_bubble_item"><t t-raw="m.message"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
</div>
</t>
<t t-name="im_chat.Conversation_technical_bubble">
<div class="oe_im_chatview_technical_bubble">
<div class="oe_im_chatview_from"><t t-esc="_.last(messages).from_id[1]"/></div>
<div>
<t t-foreach="messages" t-as="m">
<div><t t-raw="m.message"/></div>
</t>
</div>
<div class="oe_im_chatview_time"><t t-esc="window.moment((_.last(messages).create_date)).format('LT')"/></div>
</div>
</t>
<t t-name="im_chat.UserWidget">
<div class="oe_im_user ui-draggable">
<span class="oe_im_user_clip">
<img t-att-src="widget.get('image_url')" class="oe_im_user_avatar"/>
</span>
<span class="oe_im_user_name"><t t-esc="widget.get('name')"/></span>
<img t-att-src="'/im_chat/static/src/img/green.png'" t-att-data-im-user-id="widget.get('id')" class="oe_im_user_online"/>
</div>
</t>
<t t-name="im_chat.InstantMessaging">
<div class="oe_im">
<div class="oe_im_frame_header">
<i class="fa fa-search oe_im_search_icon"/>
<input class="oe_im_searchbox" placeholder="Search users..."/>
</div>
<div class="oe_im_users"></div>
<div class="oe_im_content"></div>
</div>
</t>
<t t-name="im_chat.ImTopButton">
<li t-att-title='_t("Display Instant Messaging")'>
<a href="#">
<i id="oe_topbar_imbutton_icon" class="fa fa-comments"/>
</a>
</li>
</t>
</templates>
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="assets_backend" name="im_chat assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/im_chat/static/src/css/im_chat_common.css"/>
<link rel="stylesheet" href="/im_chat/static/src/css/im_chat_backend.css"/>
<script type="text/javascript" src="/im_chat/static/src/js/im_chat_common.js"></script>
<script type="text/javascript" src="/im_chat/static/src/js/im_chat_backend.js"></script>
</xpath>
</template>
</data>
</odoo>
+7 -3
View File
@@ -21,16 +21,20 @@ Help your customers with this chat, and analyse their feedback.
'data': [
"security/im_livechat_channel_security.xml",
"security/ir.model.access.csv",
"views/im_chat_session_views.xml",
"views/mail_channel_views.xml",
"views/im_livechat_channel_views.xml",
"views/im_livechat_channel_templates.xml",
"report/im_livechat_report_views.xml",
"report/im_livechat_report_channel_views.xml",
"report/im_livechat_report_operator_views.xml",
"data/im_livechat_channel_data.xml"
],
'demo': [
"data/im_livechat_channel_demo.xml",
],
'depends' : ["mail", "rating", "im_chat"],
'depends': ["mail", "rating"],
'qweb': [
'static/src/xml/im_livechat_backend.xml',
],
'installable': True,
'auto_install': False,
'application': True,
+24 -62
View File
@@ -1,52 +1,23 @@
# -*- coding: utf-8 -*-
import json
from openerp import http, _
from openerp import SUPERUSER_ID
from openerp.http import request
class LivechatController(http.Controller):
@http.route('/im_livechat/support/<string:dbname>/<int:channel_id>', type='http', auth='none')
def support_page(self, dbname, channel_id, **kwargs):
Channel = request.env['im_livechat.channel']
info = Channel.get_channel_infos(channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["channel_name"] = Channel.browse(channel_id).name
return request.render('im_livechat.support_page', info)
@http.route('/im_livechat/support/<int:channel_id>', type='http', auth='public')
def support_page(self, channel_id, **kwargs):
channel = request.env['im_livechat.channel'].sudo().browse(channel_id)
return request.render('im_livechat.support_page', {'channel': channel})
@http.route('/im_livechat/loader/<string:dbname>/<int:channel_id>', type='http', auth='none')
def loader(self, dbname, channel_id, **kwargs):
info = request.env['im_livechat.channel'].sudo().get_channel_infos(channel_id)
info["dbname"] = dbname
info["channel"] = channel_id
info["username"] = kwargs.get("username", _("Visitor"))
# find the country from the request
country_id = False
country_code = request.session.geoip and request.session.geoip.get('country_code') or False
if country_code:
countries = request.env['res.country'].search([('code', '=', country_code)])
if countries:
country_id = countries[0]
# extract url
url = request.httprequest.headers.get('Referer') or request.httprequest.base_url
# find the match rule for the given country and url
rule = request.env['im_livechat.channel.rule'].match_rule(channel_id, url, country_id)
if rule:
if rule.action == 'hide_button':
# don't return the initialization script, since its blocked (in the country)
return
rule_data = {
'action' : rule.action,
'auto_popup_timer' : rule.auto_popup_timer,
'regex_url' : rule.regex_url,
}
info['rule'] = json.dumps(rule and rule_data or False)
return request.render('im_livechat.loader', info)
@http.route('/im_livechat/loader/<int:channel_id>', type='http', auth='public')
def loader(self, channel_id, **kwargs):
username = kwargs.get("username", _("Visitor"))
channel = request.env['im_livechat.channel'].sudo().browse(channel_id)
info = request.env['im_livechat.channel'].match_rules(request, channel.id, username=username)
return request.render('im_livechat.loader', {'info': info}) if info else False
@http.route('/im_livechat/get_session', type="json", auth="none")
@http.route('/im_livechat/get_session', type="json", auth='public')
def get_session(self, channel_id, anonymous_name, **kwargs):
# if geoip, add the country name to the anonymous name
if request.session.geoip:
@@ -54,43 +25,34 @@ class LivechatController(http.Controller):
# if the user is identifiy (eg: portal user on the frontend), don't use the anonymous name. The user will be added to session.
if request.session.uid:
anonymous_name = False
return request.env['im_livechat.channel'].sudo(request.session.uid).get_channel_session(channel_id, anonymous_name)
return request.env["im_livechat.channel"].get_mail_channel(channel_id, anonymous_name)
@http.route('/im_livechat/available', type='json', auth="none")
def available(self, db, channel):
user_id = request.session.uid or SUPERUSER_ID
channel = request.env['im_livechat.channel'].sudo(user_id).browse(channel)
return len(channel.get_available_users()) > 0
@http.route('/rating/livechat/feedback', type='json', auth='none')
def rating_livechat_feedback(self, uuid, rate, reason=None, **kwargs):
Session = request.env['im_chat.session']
@http.route('/im_livechat/feedback', type='json', auth='public')
def feedback(self, uuid, rate, reason=None, **kwargs):
Channel = request.env['mail.channel']
Rating = request.env['rating.rating']
session_ids = Session.sudo().search([('uuid', '=', uuid)])
if session_ids:
session = session_ids[0]
channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1)
if channel:
# limit the creation : only ONE rating per session
values = {
'rating': rate,
}
if not session.rating_ids:
if not channel.rating_ids:
values.update({
'res_id': session.id,
'res_model': 'im_chat.session',
'res_id': channel.id,
'res_model': 'mail.channel',
'rating': rate,
'feedback': reason,
})
# find the partner related to the user of the conversation
rated_partner_id = False
if session.user_ids:
rated_partner_id = session.user_ids[0] and session.user_ids[0].partner_id.id or False
values['rated_partner_id'] = rated_partner_id
# find the partner (operator)
if channel.channel_partner_ids:
values['rated_partner_id'] = channel.channel_partner_ids[0] and channel.channel_partner_ids[0].id or False
# create the rating
rating = Rating.sudo().create(values)
else:
if reason:
values['feedback'] = reason
rating = session.rating_ids[0]
rating = channel.rating_ids[0]
rating.write(values)
return rating.id
return False
@@ -20,7 +20,7 @@
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model">im_chat.session</field>
<field name="model">mail.channel</field>
<field name="function">cron_remove_empty_session</field>
<field name="active" eval="True"/>
</record>
@@ -28,47 +28,20 @@
</data>
<data>
<!-- Custom Filters -->
<record id="im_livechat_report_filter_session_by_day" model="ir.filters">
<field name="name">Sessions by day</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('nbr_speakers','&gt;', 1)]</field>
<field name="context">{'group_by': ['start_date:day', 'session_name']}</field>
<field name="is_default">v</field>
<field name="user_id" eval="False"/>
</record>
<record id="im_livechat_report_filter_session_last_24h" model="ir.filters">
<field name="name">Last 24h stats</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') )]</field>
<field name="context">{'group_by': ['start_date:day', 'session_name']}</field>
<field name="user_id" eval="False"/>
</record>
<record id="im_livechat_report_filter_operator_stats" model="ir.filters">
<field name="name">Operators stats</field>
<field name="model_id">im_livechat.report</field>
<field name="domain">[('nbr_speakers','&gt;', 1), ('user_id', '!=', False)]</field>
<field name="context">{'group_by': ['start_date:month', 'user_id', 'uuid']}</field>
<field name="user_id" eval="False"/>
</record>
<!-- Smiley for rating to be displayed in the conversation -->
<record id="im_chat_shortcode_rating_0" model="im_chat.shortcode">
<record id="mail_shortcode_rating_0" model="mail.shortcode">
<field name="source">:rating_0</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_0.png'/&gt;</field>
<field name="description">Rating Smiley Bad</field>
</record>
<record id="im_chat_shortcode_rating_5" model="im_chat.shortcode">
<record id="mail_shortcode_rating_5" model="mail.shortcode">
<field name="source">:rating_5</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_5.png'/&gt;</field>
<field name="description">Rating Smiley Okay</field>
</record>
<record id="im_chat_shorcode_rating_10" model="im_chat.shortcode">
<record id="mail_shorcode_rating_10" model="mail.shortcode">
<field name="source">:rating_10</field>
<field name="substitution">&lt;img src='/rating/static/src/img/rating_10.png'/&gt;</field>
<field name="description">Rating Smiley Good</field>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*
import im_livechat_channel
import im_chat_session
import mail_channel
import rating
@@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models
from openerp import SUPERUSER_ID
class ImChatSession(models.Model):
""" Chat Session
Reprensenting a conversation between users.
It extends the base method for anonymous usage.
"""
_name = 'im_chat.session'
_inherit = ['im_chat.session', 'rating.mixin']
anonymous_name = fields.Char('Anonymous Name')
create_date = fields.Datetime('Create Date', required=True)
channel_id = fields.Many2one('im_livechat.channel', 'Channel')
fullname = fields.Char('Complete Name', compute='_compute_fullname')
@api.multi
@api.depends('anonymous_name', 'user_ids')
def _compute_fullname(self):
""" built the complete name of the session """
for session in self:
names = []
for user in session.user_ids:
names.append(user.name)
if session.anonymous_name:
names.append(session.anonymous_name)
session.fullname = ', '.join(names)
@api.multi
def is_in_session(self):
""" Return True if the current user is in the user_ids of the session. False otherwise.
If this is executed as sudo, this might be the anonymous user.
"""
self.ensure_one()
if self.anonymous_name and self._uid == SUPERUSER_ID:
return True
else:
return super(ImChatSession, self).is_in_session()
@api.multi
def session_user_info(self):
""" Get the user infos for all the identified user in the session + the anonymous if anonymous session
:returns a list of user infos
:rtype : list(dict)
"""
self.ensure_one()
users_infos = super(ImChatSession, self).session_user_info()
# identify the operator for the 'welcome message'
for user_profile in users_infos:
user_profile['is_operator'] = bool(user_profile['id'] == self.env.context.get('im_livechat_operator_id'))
if self.anonymous_name:
users_infos.append({'id': False, 'name': self.anonymous_name, 'im_status': 'online', 'is_operator': False})
return users_infos
@api.model
def quit_user(self, uuid):
""" Remove the current user from the given session.
Note : an anonymous user cannot leave the session, since he is not registered.
Required to modify the base comportement, since a session can contain only 1 identified user.
:param uuid : the uuid of the session to quit
"""
session = self.search([('uuid', '=', uuid)], limit=1)
if session.anonymous_name:
# an identified user can leave an anonymous session if there is still another idenfied user in it
if self._uid and self._uid in [u.id for u in session.user_ids] and len(session.user_ids) > 1:
self._remove_user()
return True
return False
else:
return super(ImChatSession, self).quit_user(uuid)
@api.model
def cron_remove_empty_session(self):
groups = self.env['im_chat.message'].read_group([], ['to_id'], ['to_id'])
not_empty_session_ids = [group['to_id'][0] for group in groups]
empty_sessions = self.search([('id', 'not in', not_empty_session_ids), ('channel_id', '!=', False)])
empty_sessions.unlink()
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import json
import openerp
import random
import re
@@ -24,13 +25,12 @@ class ImLivechatChannel(models.Model):
def _default_user_ids(self):
return [(6, 0, [self._uid])]
# attribute fields
name = fields.Char('Name', required=True, help="The name of the channel")
button_text = fields.Char('Text of the Button', default='Have a Question? Chat with us.',
help="Default text displayed on the Livechat Support Button")
default_message = fields.Char('Welcome Message', default='How may I help you?',
help="This is an automated 'welcome' message that your visitor will see when they initiate a new chat session.")
help="This is an automated 'welcome' message that your visitor will see when they initiate a new conversation.")
input_placeholder = fields.Char('Chat Input Placeholder')
# computed fields
@@ -38,9 +38,8 @@ class ImLivechatChannel(models.Model):
help="URL to a static page where you client can discuss with the operator of the channel.")
are_you_inside = fields.Boolean(string='Are you inside the matrix?',
compute='_are_you_inside', store=False, readonly=True)
script_internal = fields.Text('Script (internal)', compute='_compute_script_internal', store=False, readonly=True)
script_external = fields.Text('Script (external)', compute='_compute_script_external', store=False, readonly=True)
nbr_session = fields.Integer('Number of session', compute='_compute_nbr_session', store=False, readonly=True)
nbr_channel = fields.Integer('Number of conversation', compute='_compute_nbr_channel', store=False, readonly=True)
rating_percentage_satisfaction = fields.Integer('% Happy', compute='_compute_percentage_satisfaction', store=False, default=-1)
# images fields
@@ -57,7 +56,7 @@ class ImLivechatChannel(models.Model):
# relationnal fields
user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids)
session_ids = fields.One2many('im_chat.session', 'channel_id', 'Sessions')
channel_ids = fields.One2many('mail.channel', 'livechat_channel_id', 'Sessions')
rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id', 'Rules')
@@ -75,17 +74,6 @@ class ImLivechatChannel(models.Model):
self.image_small = False
self.image_medium = False
@api.multi
def _compute_script_internal(self):
view = self.env['ir.model.data'].get_object('im_livechat', 'internal_loader')
values = {
"url": self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
"dbname": self._cr.dbname,
}
for record in self:
values["channel"] = record.id
record.script_internal = view.render(values)
@api.multi
def _compute_script_external(self):
view = self.env['ir.model.data'].get_object('im_livechat', 'external_loader')
@@ -94,26 +82,26 @@ class ImLivechatChannel(models.Model):
"dbname": self._cr.dbname,
}
for record in self:
values["channel"] = record.id
values["channel_id"] = record.id
record.script_external = view.render(values)
@api.multi
def _compute_web_page_link(self):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
for record in self:
record.web_page = "%s/im_livechat/support/%s/%i" % (base_url, self._cr.dbname, record.id)
record.web_page = "%s/im_livechat/support/%i" % (base_url, record.id)
@api.multi
@api.depends('session_ids')
def _compute_nbr_session(self):
@api.depends('channel_ids')
def _compute_nbr_channel(self):
for record in self:
record.nbr_session = len(record.session_ids)
record.nbr_channel = len(record.channel_ids)
@api.multi
@api.depends('session_ids.rating_ids')
@api.depends('channel_ids.rating_ids')
def _compute_percentage_satisfaction(self):
for record in self:
repartition = record.session_ids.rating_get_grades()
repartition = record.channel_ids.rating_get_grades()
total = sum(repartition.values())
if total > 0:
happy = repartition['great']
@@ -121,7 +109,6 @@ class ImLivechatChannel(models.Model):
else:
record.rating_percentage_satisfaction = -1
# --------------------------
# Action Methods
# --------------------------
@@ -143,7 +130,7 @@ class ImLivechatChannel(models.Model):
"""
self.ensure_one()
action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating')
action['domain'] = [('res_id', 'in', [s.id for s in self.session_ids]), ('res_model', '=', 'im_chat.session')]
action['domain'] = [('res_id', 'in', [s.id for s in self.channel_ids]), ('res_model', '=', 'mail.channel')]
return action
# --------------------------
@@ -158,33 +145,77 @@ class ImLivechatChannel(models.Model):
return self.sudo().user_ids.filtered(lambda user: user.im_status == 'online')
@api.model
def get_channel_session(self, channel_id, anonymous_name):
""" return a session given a channel : create on with a registered user, or return false otherwise """
def get_mail_channel(self, livechat_channel_id, anonymous_name):
""" Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise
:param livechat_channel_id : the identifier if the im_livechat.channel
:param anonymous_name : the name of the anonymous person of the channel
:type livechat_channel_id : int
:type anonymous_name : str
:return : channel header
:rtype : dict
"""
# get the avalable user of the channel
users = self.sudo().browse(channel_id).get_available_users()
users = self.sudo().browse(livechat_channel_id).get_available_users()
if len(users) == 0:
return False
user_id = random.choice(users).id
# user to add to the session
user_to_add = [(4, user_id)]
if self.env.uid:
user_to_add.append((4, self.env.uid))
# choose the res.users operator and get its partner id
user = random.choice(users)
operator_partner_id = user.partner_id.id
# partner to add to the mail.channel
channel_partner_to_add = [(4, operator_partner_id)]
if self.env.uid: # if the user if logged (portal user), he can be identify
channel_partner_to_add.append((4, self.env.user.partner_id.id))
# create the session, and add the link with the given channel
session = self.env["im_chat.session"].sudo().create({'user_ids': user_to_add, 'channel_id': channel_id, 'anonymous_name': anonymous_name})
return session.sudo().with_context(im_livechat_operator_id=user_id).session_info()
mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({
'channel_partner_ids': channel_partner_to_add,
'livechat_channel_id': livechat_channel_id,
'anonymous_name': anonymous_name,
'channel_type': 'livechat',
'name': ', '.join([anonymous_name, user.name]),
'public': 'public',
})
return mail_channel.sudo().with_context(im_livechat_operator_partner_id=operator_partner_id).channel_info()[0]
@api.model
def get_channel_infos(self, channel_id):
url = self.env['ir.config_parameter'].get_param('web.base.url')
channel = self.browse(channel_id)
return {
"url": url,
'buttonText': channel.button_text,
'inputPlaceholder': channel.input_placeholder,
'defaultMessage': channel.default_message,
"channelName": channel.name,
'button_text': channel.button_text,
'input_placeholder': channel.input_placeholder,
'default_message': channel.default_message,
"channel_name": channel.name,
"channel_id": channel.id,
}
@api.model
def match_rules(self, request, channel_id, username='Visitor'):
info = {
'server_url': self.env['ir.config_parameter'].get_param('web.base.url'),
'options': self.sudo().get_channel_infos(channel_id),
}
info['options']["default_username"] = username
# find the country from the request
country_id = False
country_code = request.session.geoip and request.session.geoip.get('country_code') or False
if country_code:
country_ids = self.env['res.country'].sudo().search([('code', '=', country_code)])
if country_ids:
country_id = country_ids[0]
# extract url
url = request.httprequest.headers.get('Referer') or request.httprequest.base_url
# find the match rule for the given country and url
rule = self.env['im_livechat.channel.rule'].sudo().match_rule(channel_id, url, country_id)
if rule:
if rule.action == 'hide_button':
# don't return the initialization script, since its blocked (in the country)
return False
rule_data = {
'action': rule.action,
'auto_popup_timer': rule.auto_popup_timer,
'regex_url': rule.regex_url,
}
info['rule'] = rule and rule_data or False
return info
class ImLivechatChannelRule(models.Model):
@@ -214,7 +245,6 @@ class ImLivechatChannelRule(models.Model):
sequence = fields.Integer('Matching order', default=10,
help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.")
def match_rule(self, channel_id, url, country_id=False):
""" determine if a rule of the given channel match with the given url
:param channel_id : the identifier of the channel_id
+71
View File
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models
from openerp import SUPERUSER_ID
class MailChannel(models.Model):
""" Chat Session
Reprensenting a conversation between users.
It extends the base method for anonymous usage.
"""
_name = 'mail.channel'
_inherit = ['mail.channel', 'rating.mixin']
anonymous_name = fields.Char('Anonymous Name')
create_date = fields.Datetime('Create Date', required=True)
channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')])
livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel')
@api.multi
def _channel_message_notifications(self, message):
""" When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when
clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header
should be added to the notification, since the user cannot be listining to the channel.
"""
notifications = super(MailChannel, self)._channel_message_notifications(message)
if not message.author_id:
unpinned_channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: not cp.is_pinned)
if unpinned_channel_partner:
unpinned_channel_partner.write({'is_pinned': True})
notifications = self._channel_channel_notifications(unpinned_channel_partner.mapped('partner_id').ids) + notifications
return notifications
@api.multi
def channel_info(self):
""" Extends the channel header by adding the livechat operator and the 'anonymous' profile
:rtype : list(dict)
"""
channel_infos = super(MailChannel, self).channel_info()
# add the operator id
if self.env.context.get('im_livechat_operator_partner_id'):
partner_name = self.env['res.partner'].browse(self.env.context.get('im_livechat_operator_partner_id')).name_get()[0]
for channel_info in channel_infos:
channel_info['operator_pid'] = partner_name
# add the anonymous name
channel_infos_dict = dict((c['id'], c) for c in channel_infos)
for channel in self:
if channel.anonymous_name:
channel_infos_dict[channel.id]['anonymous_name'] = channel.anonymous_name
return channel_infos_dict.values()
@api.model
def channel_fetch_slot(self):
values = super(MailChannel, self).channel_fetch_slot()
pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True)]).mapped('channel_id')
values['channel_livechat'] = self.search([('channel_type', '=', 'livechat'), ('public', 'in', ['public']), ('id', 'in', pinned_channels.ids)]).channel_info()
return values
@api.model
def cron_remove_empty_session(self):
self.env.cr.execute("""
SELECT id as id
FROM mail_channel C
WHERE NOT EXISTS (
SELECT *
FROM mail_message_mail_channel_rel R
WHERE R.mail_channel_id = C.id
) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL;
""")
empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()]
self.browse(empty_channel_ids).unlink()
+2 -3
View File
@@ -11,9 +11,8 @@ class Rating(models.Model):
def _compute_res_name(self):
# cannot change the rec_name of session since it is use to create the bus channel
# so, need to override this method to set the same alternative rec_name as in reporting
if self.res_model == 'im_chat.session':
if self.res_model == 'mail.channel':
current_object = self.env[self.res_model].sudo().browse(self.res_id)
self.res_name = ('%s / %s') % (current_object.channel_id.name, current_object.id)
self.res_name = ('%s / %s') % (current_object.livechat_channel_id.name, current_object.id)
else:
super(Rating, self)._compute_res_name()
+2 -1
View File
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
import im_livechat_report
import im_livechat_report_channel
import im_livechat_report_operator
@@ -1,55 +0,0 @@
# -*- coding: utf-8 -*-
from openerp import api, fields, models, tools
class im_livechat_report(models.Model):
""" Livechat Support Report """
_name = "im_livechat.report"
_description = "Livechat Support Report"
_order = 'start_date, session_name'
_auto = False
session_name = fields.Char('Session Name', readonly=True)
uuid = fields.Char('UUID', readonly=True)
start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation")
start_date_hour = fields.Char('Hour of start Date of session', readonly=True)
duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)")
time_in_session = fields.Float('Time in session', digits=(16, 2), readonly=True, group_operator="avg", help="Average time the user spend in the conversation")
time_to_answer = fields.Float('Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor")
nbr_messages = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation")
nbr_user_messages = fields.Integer('Average of messages/user', readonly=True, group_operator="avg", help="Average number of message per user")
nbr_speakers = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers")
user_id = fields.Many2one('res.users', 'User', readonly=True)
session_id = fields.Many2one('im_chat.session', 'Session', readonly=True)
channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True)
def init(self, cr):
# Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view !
tools.drop_view_if_exists(cr, 'im_livechat_report')
cr.execute("""
CREATE OR REPLACE VIEW im_livechat_report AS (
SELECT
min(M.id) as id,
S.uuid as uuid,
CONCAT(C.name, ' / ', S.id) as session_name,
S.create_date as start_date,
to_char(date_trunc('hour', S.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour,
EXTRACT('epoch' from ((SELECT (max(create_date)-min(create_date)) FROM im_chat_message WHERE to_id=S.id AND from_id = U.id))) as time_in_session,
EXTRACT('epoch' from ((SELECT min(create_date) FROM im_chat_message WHERE to_id=S.id AND from_id IS NOT NULL)-(SELECT min(create_date) FROM im_chat_message WHERE to_id=S.id AND from_id IS NULL))) as time_to_answer,
EXTRACT('epoch' from (max((SELECT (max(create_date)) FROM im_chat_message WHERE to_id=S.id))-S.create_date)) as duration,
(SELECT count(distinct COALESCE(from_id, 0)) FROM im_chat_message WHERE to_id=S.id) as nbr_speakers,
(SELECT count(id) FROM im_chat_message WHERE to_id=S.id) as nbr_messages,
count(M.id) as nbr_user_messages,
U.id as user_id,
S.channel_id as channel_id,
S.id as session_id
FROM im_chat_message M
LEFT JOIN im_chat_session S on (S.id = M.to_id)
LEFT JOIN res_users U on (U.id = M.from_id)
LEFT JOIN im_livechat_channel C on (S.channel_id = C.id)
WHERE S.channel_id IS NOT NULL
GROUP BY U.id, M.to_id, S.id, C.name, S.uuid
)
""")
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from openerp import fields, models, tools
class ImLivechatReportChannel(models.Model):
""" Livechat Support Report on the Channels """
_name = "im_livechat.report.channel"
_description = "Livechat Support Report"
_order = 'start_date, technical_name'
_auto = False
uuid = fields.Char('UUID', readonly=True)
channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True)
channel_name = fields.Char('Channel Name', readonly=True)
technical_name = fields.Char('Code', readonly=True)
livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True)
start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation")
start_date_hour = fields.Char('Hour of start Date of session', readonly=True)
duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)")
nbr_speaker = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers")
nbr_message = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation")
partner_id = fields.Many2one('res.partner', 'Opertor', readonly=True)
def init(self, cr):
# Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view !
tools.drop_view_if_exists(cr, 'im_livechat_report_channel')
cr.execute("""
CREATE OR REPLACE VIEW im_livechat_report_channel AS (
SELECT
C.id as id,
C.uuid as uuid,
C.id as channel_id,
C.name as channel_name,
CONCAT(L.name, ' / ', C.id) as technical_name,
C.livechat_channel_id as livechat_channel_id,
C.create_date as start_date,
to_char(date_trunc('hour', C.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour,
EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration,
count(distinct P.id) as nbr_speaker,
count(distinct M.id) as nbr_message,
MAX(S.partner_id) as partner_id
FROM mail_channel C
JOIN mail_message_mail_channel_rel R ON (C.id = R.mail_channel_id)
JOIN mail_message M ON (M.id = R.mail_message_id)
JOIN mail_channel_partner S ON (S.channel_id = C.id)
JOIN im_livechat_channel L ON (L.id = C.livechat_channel_id)
LEFT JOIN res_partner P ON (M.author_id = P.id)
GROUP BY C.id, C.name, C.livechat_channel_id, L.name, C.create_date, C.uuid
)
""")
@@ -2,42 +2,42 @@
<odoo>
<data>
<record id="im_livechat_report_view_pivot" model="ir.ui.view">
<field name="name">im_livechat.report.pivot</field>
<field name="model">im_livechat.report</field>
<record id="im_livechat_report_channel_view_pivot" model="ir.ui.view">
<field name="name">im_livechat.report.channel.pivot</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<pivot string="Livechat Support Statistics" disable_linking="True">
<field name="session_name" type="row"/>
<field name="technical_name" type="row"/>
<field name="duration" type="measure"/>
<field name="nbr_messages" type="measure"/>
<field name="time_to_answer" type="measure"/>
<field name="nbr_message" type="measure"/>
</pivot>
</field>
</record>
<record id="im_livechat_report_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.graph</field>
<field name="model">im_livechat.report</field>
<record id="im_livechat_report_channel_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.channel.graph</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<graph string="Livechat Support Statistics">
<field name="session_name"/>
<field name="nbr_messages" type="measure"/>
<field name="technical_name"/>
<field name="nbr_message" type="measure"/>
</graph>
</field>
</record>
<record id="im_livechat_report_view_search" model="ir.ui.view">
<field name="name">im_livechat.report.search</field>
<field name="model">im_livechat.report</field>
<record id="im_livechat_report_channel_view_search" model="ir.ui.view">
<field name="name">im_livechat.report.channel.search</field>
<field name="model">im_livechat.report.channel</field>
<field name="arch" type="xml">
<search string="Search report">
<filter name="missed_session" string="Missed sessions" domain="[('nbr_speakers','&lt;=', 1)]"/>
<filter name="treated_session" string="Treated sessions" domain="[('nbr_speakers','&gt;', 1)]"/>
<filter name="missed_session" string="Missed sessions" domain="[('nbr_speaker','&lt;=', 1)]"/>
<filter name="treated_session" string="Treated sessions" domain="[('nbr_speaker','&gt;', 1)]"/>
<filter name="last_24h" string="Last 24h" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d') )]"/>
<filter name="last_week" string="Last Week" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(weeks=1)).strftime('%%Y-%%m-%%d') )]"/>
<group expand="0" string="Group By...">
<filter name="group_by_session" string="Session" domain="[]" context="{'group_by':'session_name'}"/>
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_user" string="Operator" domain="[('user_id','!=', False)]" context="{'group_by':'user_id'}"/>
<filter name="group_by_operator" string="Operator" domain="[('partner_id','!=', False)]" context="{'group_by':'partner_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_hour" string="Creation date (hour)" domain="[]" context="{'group_by':'start_date_hour'}"/>
<filter name="group_by_day" string="Creation date (day)" domain="[]" context="{'group_by':'start_date:day'}"/>
@@ -49,22 +49,29 @@
</field>
</record>
<record id="im_livechat_report_action" model="ir.actions.act_window">
<field name="name">Livechat Support Report</field>
<field name="res_model">im_livechat.report</field>
<record id="im_livechat_report_channel_action" model="ir.actions.act_window">
<field name="name">Livechat Support Report Channel</field>
<field name="res_model">im_livechat.report.channel</field>
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="help">Livechat Support Analysis allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc.</field>
<field name="context">{"search_default_last_week":1}</field>
<field name="help">Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc.</field>
</record>
<menuitem
id="menu_reporting_livechat"
name="Report"
parent="menu_livechat_root"
sequence="50"
groups="im_livechat_group_manager"
action="im_livechat_report_action"/>
groups="im_livechat_group_manager"/>
<menuitem
id="menu_reporting_livechat_channel"
name="Session Statistics"
parent="menu_reporting_livechat"
sequence="10"
action="im_livechat_report_channel_action"/>
</data>
</odoo>
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from openerp import fields, models, tools
class ImLivechatReportOperator(models.Model):
""" Livechat Support Report on the Operator """
_name = "im_livechat.report.operator"
_description = "Livechat Support Report"
_order = 'livechat_channel_id, partner_id'
_auto = False
partner_id = fields.Many2one('res.partner', 'Operator', readonly=True)
livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True)
nbr_channel = fields.Integer('# of channel', readonly=True, group_operator="sum", help="Number of conversation")
channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True)
start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation")
time_to_answer = fields.Float('Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor")
duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)")
def init(self, cr):
# Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view !
tools.drop_view_if_exists(cr, 'im_livechat_report_operator')
cr.execute("""
CREATE OR REPLACE VIEW im_livechat_report_operator AS (
SELECT
row_number() OVER () AS id,
P.id as partner_id,
L.id as livechat_channel_id,
count(C.id) as nbr_channel,
C.id as channel_id,
C.create_date as start_date,
EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration,
EXTRACT('epoch' from ((SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id=P.id AND R.mail_channel_id = C.id AND R.mail_message_id = M.id)-(SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id IS NULL AND R.mail_channel_id = C.id AND R.mail_message_id = M.id))) as time_to_answer
FROM im_livechat_channel_im_user O
JOIN res_users U ON (O.user_id = U.id)
JOIN res_partner P ON (U.partner_id = P.id)
LEFT JOIN im_livechat_channel L ON (L.id = O.channel_id)
LEFT JOIN mail_channel C ON (C.livechat_channel_id = L.id)
GROUP BY P.id, L.id, C.id, C.create_date
)
""")
@@ -0,0 +1,66 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="im_livechat_report_operator_view_pivot" model="ir.ui.view">
<field name="name">im_livechat.report.operator.pivot</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<pivot string="Livechat Support Statistics" disable_linking="True">
<field name="partner_id" type="row"/>
<field name="duration" type="measure"/>
<field name="nbr_channel" type="measure"/>
</pivot>
</field>
</record>
<record id="im_livechat_report_operator_view_graph" model="ir.ui.view">
<field name="name">im_livechat.report.operator.graph</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<graph string="Livechat Support Statistics">
<field name="partner_id"/>
<field name="nbr_channel" type="measure"/>
</graph>
</field>
</record>
<record id="im_livechat_report_operator_view_search" model="ir.ui.view">
<field name="name">im_livechat.report.operator.search</field>
<field name="model">im_livechat.report.operator</field>
<field name="arch" type="xml">
<search string="Search report">
<filter name="last_24h" string="Last 24h" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(days=1)).strftime('%%Y-%%m-%%d') )]"/>
<filter name="last_week" string="Last Week" domain="[('start_date','&gt;', (context_today() - datetime.timedelta(weeks=1)).strftime('%%Y-%%m-%%d') )]"/>
<group expand="0" string="Group By...">
<filter name="group_by_channel" string="Channel" domain="[]" context="{'group_by':'channel_id'}"/>
<filter name="group_by_operator" string="Operator" domain="[('partner_id','!=', False)]" context="{'group_by':'partner_id'}"/>
<separator orientation="vertical" />
<filter name="group_by_hour" string="Creation date (hour)" domain="[]" context="{'group_by':'start_date_hour'}"/>
<filter name="group_by_day" string="Creation date (day)" domain="[]" context="{'group_by':'start_date:day'}"/>
<filter name="group_by_week" string="Creation date (week)" domain="[]" context="{'group_by':'start_date:week'}"/>
<filter name="group_by_month" string="Creation date (month)" domain="[]" context="{'group_by':'start_date:month'}" />
<filter name="group_by_year" string="Creation date (year)" domain="[]" context="{'group_by':'start_date:year'}"/>
</group>
</search>
</field>
</record>
<record id="im_livechat_report_operator_action" model="ir.actions.act_window">
<field name="name">Livechat Support Report Channel</field>
<field name="res_model">im_livechat.report.operator</field>
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
<field name="context">{"search_default_last_week":1}</field>
<field name="help">Livechat Support Channel Statistics allows you to easily check and analyse your company livechat session performance. Extract informations about the missed sessions, the audiance, the duration of a session, etc.</field>
</record>
<menuitem
id="menu_reporting_livechat_operator"
name="Operator Analysis"
parent="menu_reporting_livechat"
sequence="10"
action="im_livechat_report_operator_action"/>
</data>
</odoo>
@@ -21,22 +21,22 @@
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<record id="im_chat_message_rule_manager" model="ir.rule">
<record id="mail_message_rule_manager" model="ir.rule">
<field name="name">Live Support Managers can read messages from live support</field>
<field name="model_id" ref="im_chat.model_im_chat_message"/>
<field name="model_id" ref="mail.model_mail_message"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.im_livechat_group_manager')])]"/>
<field name="domain_force">[('to_id.channel_id', '!=', None)]</field>
<field name="domain_force">[('mail.channel_ids.livechat_channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<record id="im_chat_session_rule_manager" model="ir.rule">
<record id="mail_channel_rule_manager" model="ir.rule">
<field name="name">Live Support Managers can see all session from live support</field>
<field name="model_id" ref="im_chat.model_im_chat_session"/>
<field name="model_id" ref="mail.model_mail_channel"/>
<field name="groups" eval="[(6,0,[ref('im_livechat.im_livechat_group_manager')])]"/>
<field name="domain_force">[('channel_id', '!=', None)]</field>
<field name="domain_force">[('mail.channel_ids.livechat_channel_id', '!=', None)]</field>
<field name="perm_unlink" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_read" eval="1"/>
@@ -1,8 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_livechat_channel,im_livechat.channel,model_im_livechat_channel,,1,0,0,0
access_livechat_channel_user,im_livechat.channel,model_im_livechat_channel,im_livechat_group_user,1,1,1,0
access_livechat_channel_manager,im_livechat.channel,model_im_livechat_channel,im_livechat_group_manager,1,1,1,1
access_livechat_support_report,im_livechat.report,model_im_livechat_report,im_livechat_group_manager,1,1,1,1
access_livechat_channel_user,im_livechat.channel.user,model_im_livechat_channel,im_livechat_group_user,1,1,1,0
access_livechat_channel_manager,im_livechat.channel.manager,model_im_livechat_channel,im_livechat_group_manager,1,1,1,1
access_livechat_support_report_channel,im_livechat.report.channel,model_im_livechat_report_channel,im_livechat_group_manager,1,1,1,1
access_livechat_support_report_operator,im_livechat.report.operator,model_im_livechat_report_operator,im_livechat_group_manager,1,1,1,1
access_livechat_channel_rule,im_livechat.channel.rule,model_im_livechat_channel_rule,,1,0,0,0
access_livechat_channel_rule_user,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,0
access_livechat_channel_rule_manager,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_user,1,1,1,1
access_livechat_channel_rule_manager,im_livechat.channel.rule,model_im_livechat_channel_rule,im_livechat_group_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_livechat_channel im_livechat.channel model_im_livechat_channel 1 0 0 0
3 access_livechat_channel_user im_livechat.channel im_livechat.channel.user model_im_livechat_channel im_livechat_group_user 1 1 1 0
4 access_livechat_channel_manager im_livechat.channel im_livechat.channel.manager model_im_livechat_channel im_livechat_group_manager 1 1 1 1
5 access_livechat_support_report access_livechat_support_report_channel im_livechat.report im_livechat.report.channel model_im_livechat_report model_im_livechat_report_channel im_livechat_group_manager 1 1 1 1
6 access_livechat_support_report_operator im_livechat.report.operator model_im_livechat_report_operator im_livechat_group_manager 1 1 1 1
7 access_livechat_channel_rule im_livechat.channel.rule model_im_livechat_channel_rule 1 0 0 0
8 access_livechat_channel_rule_user im_livechat.channel.rule model_im_livechat_channel_rule im_livechat_group_user 1 1 1 0
9 access_livechat_channel_rule_manager im_livechat.channel.rule model_im_livechat_channel_rule im_livechat_group_user im_livechat_group_manager 1 1 1 1
@@ -1,42 +1,75 @@
.openerp { /* base style of openerp */
font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
color: #4c4c4c;
font-size: 13px;
}
.o_mail_chat_im_window * {
box-sizing: border-box;
}
.o_livechat_button {
width: 230px;
position: fixed;
bottom: 0px;
right: 6px;
display: inline-block;
min-width: 100px;
background-color: rgba(60, 60, 60, 0.6);
font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif;
font-size: 14px;
font-weight: bold;
padding: 10px;
color: white;
text-shadow: rgb(59, 76, 88) 1px 1px 0px;
border: 1px solid rgb(80, 80, 80);
border-bottom: 0px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: pointer;
z-index:500;
}
/* Livechat Rating : feedback smiley */
.oe_im_chatview .oe_livechat_rating{
.o_mail_chat_im_window_content .o_livechat_rating{
padding: 15px;
vertical-align: middle;
}
.oe_im_chatview .oe_livechat_rating .oe_im_feedback_text {
color: white;
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_feedback_text {
vertical-align: middle;
font-size: 14px;
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices{
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices{
height:40px;
text-align: center;
margin: 10px 0 30px 0;
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img{
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img{
width: 50px;
opacity: 0.60;
cursor: pointer;
margin: 5px;
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img:hover{
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img:hover{
opacity:1;
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_choices img.selected{
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_choices img.selected{
opacity: 1;
}
/* feedback reason */
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason {
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason {
margin: 10px 0;
display: none; /* hidden by default */
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason textarea {
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason textarea {
width: 100%;
height: 70px;
resize: none;
}
.oe_im_chatview .oe_livechat_rating .oe_livechat_rating_reason_button input{
.o_mail_chat_im_window_content .o_livechat_rating .o_livechat_rating_reason_button input{
float: right;
margin-left : 10px;
}
+148 -141
View File
@@ -7,23 +7,24 @@ var user_session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var Widget = require('web.Widget');
var im_chat_common = require('im_chat.im_chat_common');
var mail_chat_common = require('mail.chat_common');
var _t = core._t;
var QWeb = core.qweb;
var COOKIE_NAME = 'livechat_conversation';
/*
The state of anonymous session is hold by the client and not the server.
Override the method managing the state of normal conversation.
* Conversation Patch
*
* The state of anonymous session is hold by the client and not the
* server. Override the method managing the state of normal conversation.
*/
im_chat_common.Conversation.include({
init: function(){
mail_chat_common.Conversation.include({
init: function(parent, session, options){
this._super.apply(this, arguments);
this.shown = true;
this.feedback = false;
this.options.default_username = session.anonymous_name || this.options.default_username;
},
show: function(){
this._super.apply(this, arguments);
@@ -33,7 +34,7 @@ im_chat_common.Conversation.include({
this._super.apply(this, arguments);
this.shown = false;
},
session_update_state: function(state){
session_fold: function(state){
// set manually the new state
if(state === 'closed'){
this.destroy();
@@ -49,30 +50,38 @@ im_chat_common.Conversation.include({
this.show();
}
}
// session and cookie update
utils.set_cookie('im_livechat_session', JSON.stringify(this.get('session')), 60*60);
}
// session and cookie update
this._super(state);
utils.set_cookie(COOKIE_NAME, JSON.stringify(this.get('session')), 60*60);
},
click_close: function(event) {
on_click_close: function(event) {
var self = this;
event.stopPropagation();
if(!this.feedback && (this.get('messages').length > 1)){
this.feedback = new Feedback(this);
this.$(".oe_im_chatview_content").empty();
this.$(".oe_im_chatview_input").prop('disabled', true);
this.feedback.appendTo( this.$(".oe_im_chatview_content"));
this.$(".o_mail_chat_im_window_content").empty();
this.$(".o_mail_chat_im_window_input").prop('disabled', true);
this.feedback.appendTo(this.$(".o_mail_chat_im_window_content"));
// bind event to close conversation
this.feedback.on("feedback_sent", this, this.click_close);
var args = arguments;
var _super = this._super;
this.feedback.on("feedback_sent", this, function(){
_super.apply(self, args);
});
}else{
this._super.apply(this, arguments);
utils.set_cookie(COOKIE_NAME, "", -1);
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
}
},
});
// To avoid exeption when the anonymous has close his
// conversation and when operator send him a message.
im_chat_common.ConversationManager.include({
/*
* Livechat Conversation Manager
*
* To avoid exeption when the anonymous has close his conversation and when operator send
* him a message. Extending ConversationManager is better than monkey-patching it.
*/
var LivechatConversationManager = mail_chat_common.ConversationManager.extend({
message_receive: function(message) {
try{
this._super(message);
@@ -80,145 +89,143 @@ im_chat_common.ConversationManager.include({
}
});
var LiveSupport = Widget.extend({
init: function(server_url, db, channel, options, rule) {
this._super();
options = options || {};
_.defaults(options, {
buttonText: _t("Chat with one of our collaborators"),
inputPlaceholder: null,
defaultMessage: _t("How may I help you?"),
defaultUsername: _t("Visitor"),
/**
* Livechat Button
*
* This widget is the Button to start a conversation with an operator on the livechat channel
* (defined in options.channel_id). This will auto-popup if the rule say so, create the conversation
* window if a session is saved in a cookie, ...
*/
var LivechatButton = Widget.extend({
template: 'im_livechat.LivechatButton',
events: {
"click": "on_click"
},
init: function(parent, server_url, options, rule) {
this._super.apply(this, arguments);
this.server_url = server_url;
this.options = _.defaults(options || {}, {
placeholder: _t('Ask something ...'),
default_username: _t("Visitor"),
button_text: _t("Chat with one of our collaborators"),
default_message: _t("How may I help you?"),
});
user_session.setup(server_url, {use_cors: false});
this.load_template(db, channel, options, rule);
this.rule = rule || false;
this.mail_channel = false;
},
_get_template_list: function(){
return ['/im_livechat/static/src/xml/im_livechat.xml', '/im_chat/static/src/xml/im_chat.xml'];
willStart: function(){
return $.when(this.load_qweb_template(), this._super());
},
load_template: function(db, channel, options, rule){
start: function(){
var self = this;
return this._super.apply(this, arguments).then(function(){
// set up the manager
self.manager = new LivechatConversationManager(self, self.options);
self.manager.set("bottom_offset", self.$el.outerHeight());
// check if a session already exists in a cookie
var cookie = utils.get_cookie('im_livechat_session');
if(cookie){
self.set_conversation(JSON.parse(cookie), false);
}else{ // if not session, apply the rule
var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup') ? JSON.parse(utils.get_cookie('im_livechat_auto_popup')) : true;
self.auto_popup_cookie = auto_popup_cookie;
if (self.rule.action === 'auto_popup' && auto_popup_cookie){
setTimeout(function() {
self.on_click();
}, self.rule.auto_popup_timer*1000);
}
}
});
},
load_qweb_template: function(){
var self = this;
// load the qweb templates
var defs = [];
var templates = this._get_template_list();
var templates = ['/mail/static/src/xml/mail_chat_common.xml', '/im_livechat/static/src/xml/im_livechat.xml'];
_.each(templates, function(tmpl){
defs.push(user_session.rpc('/web/proxy/load', {path: tmpl}).then(function(xml) {
QWeb.add_template(xml);
}));
});
return $.when.apply($, defs).then(function() {
self.setup(db, channel, options, rule);
return $.when.apply($, defs);
},
load_mail_channel: function(display_warning){
var self = this;
return user_session.rpc('/im_livechat/get_session', {
"channel_id" : this.options.channel_id,
"anonymous_name" : this.options.default_username,
}, {shadow: true}).then(function(channel){
if(channel){
self.set_conversation(channel, true);
}else{
if(display_warning){
self.alert_available_message();
}
}
});
},
setup: function(db, channel, options, rule){
on_click: function(event){
var self = this;
var cookie = utils.get_cookie(COOKIE_NAME);
if(cookie){
self.build_button(channel, options, JSON.parse(cookie), rule);
}else{
user_session.rpc("/im_livechat/available", {db: db, channel: channel}).then(function(activated) {
if(activated){
self.build_button(channel, options, false, rule);
}
});
if(!this.mail_channel){
this.load_mail_channel(event !== undefined);
}
},
build_button: function(channel, options, session, rule){
var button = new ChatButton(null, channel, options, session);
button.appendTo($("body"));
var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup') ? JSON.parse(utils.get_cookie('im_livechat_auto_popup')) : true;
if (rule.action === 'auto_popup' && auto_popup_cookie){
setTimeout(function() {
button.click();
}, rule.auto_popup_timer*1000);
}
}
});
var ChatButton = Widget.extend({
className: "openerp_style oe_chat_button hidden-print",
events: {
"click": "click"
},
init: function(parent, channel, options, session) {
this._super(parent);
this.channel = channel;
this.options = options;
this.text = options.buttonText;
this.session = session || false;
this.conv = false;
this.no_session_message = _t("None of our collaborators seems to be available, please try again later.");
},
start: function() {
this.$().append(QWeb.render("im_livechat.chatButton", {widget: this}));
// set up the manager
this.manager = new im_chat_common.ConversationManager(this, this.options);
this.manager.set("bottom_offset", $('.oe_chat_button').outerHeight());
if(this.session){
this.set_conversation(this.session);
}
},
click: function() {
set_conversation: function(mail_channel, welcome_message){
var self = this;
if (!this.conv){
user_session.rpc("/im_livechat/get_session", {
"channel_id" : self.channel,
"anonymous_name" : this.options.defaultUsername
}, {shadow: true}).then(function(session) {
if (! session) {
alert(self.no_session_message);
return;
}
session.state = 'open';
// save the session in a cookie
utils.set_cookie(COOKIE_NAME, JSON.stringify(session), 60*60);
// create the conversation with the received session
self.set_conversation(session, true);
});
}
},
set_conversation: function(session, welcome_message){
var self = this;
this.session = session;
if(session.state === 'closed'){
return;
}
this.conv = this.manager.session_apply(session);
// set the mail_channel and create the Conversation Window
this.mail_channel = mail_channel;
this.conv = this.manager.session_apply(mail_channel, {
'load_history': !welcome_message,
'focus': welcome_message
});
this.conv.on("destroyed", this, function() {
bus.bus.stop_polling();
delete self.conv;
delete self.session;
delete self.mail_channel;
utils.set_cookie('im_livechat_session', "", -1); // delete the session cookie
});
// start the polling
bus.bus.add_channel(session.uuid);
bus.bus.start_polling();
// add the automatic welcome message
if(welcome_message){
this.send_welcome_message();
}
// setup complet when the conversation window is appended
this.manager._session_defs[mail_channel.id].then(function(){
// start the polling
bus.bus.add_channel(mail_channel.uuid);
bus.bus.start_polling();
// add the automatic welcome message
if(welcome_message){
self.send_welcome_message();
}
// create the cookies
utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
utils.set_cookie('im_livechat_session', JSON.stringify(mail_channel), 60*60);
})
},
send_welcome_message: function(){
var self = this;
if(this.session.users.length > 0){
if (self.options.defaultMessage) {
setTimeout(function(){
var operator = _.last(_.filter(self.session.users, function(user){return user.is_operator}));
self.conv.message_receive({
id : 1,
type: "message",
message: self.options.defaultMessage,
create_date: time.datetime_to_str(new Date()),
from_id: [operator.id, operator.name],
to_id: [0, self.session.uuid]
});
}, 1000);
}
if(this.mail_channel.operator_pid && this.options.default_message) {
setTimeout(function(){
self.conv.message_receive({
id : 1,
message_type: "comment",
model: 'mail.channel',
body: self.options.default_message,
date: time.datetime_to_str(new Date()),
author_id: self.mail_channel.operator_pid,
channel_ids: [self.mail_channel.id],
tracking_value_ids: [],
attachment_ids: [],
});
}, 1000);
}
}
});
},
alert_available_message: function(){
alert(_t("None of our collaborators seems to be available, please try again later."));
},
})
/* Rating livechat object */
/*
* Rating for Livechat
*
* This widget display the 3 rating smileys, and a textarea to add a reason (only for red
* smileys), and sent the user feedback to the server.
*/
var Feedback = Widget.extend({
template : "im_livechat.feedback",
init: function(parent){
@@ -231,20 +238,20 @@ var Feedback = Widget.extend({
start: function(){
this._super.apply(this.arguments);
// bind events
this.$('.oe_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this));
this.$('.o_livechat_rating_choices img').on('click', _.bind(this.click_smiley, this));
this.$('#rating_submit').on('click', _.bind(this.click_send, this));
},
click_smiley: function(ev){
var self = this;
this.rating = parseInt($(ev.currentTarget).data('value'));
this.$('.oe_livechat_rating_choices img').removeClass('selected');
this.$('.oe_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');
this.$('.o_livechat_rating_choices img').removeClass('selected');
this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');
// only display textearea if bad smiley selected
var close_conv = false;
if(this.rating === 0){
this.$('.oe_livechat_rating_reason').show();
this.$('.o_livechat_rating_reason').show();
}else{
this.$('.oe_livechat_rating_reason').hide();
this.$('.o_livechat_rating_reason').hide();
close_conv = true;
}
this._send_feedback(close_conv).then(function(){
@@ -260,7 +267,7 @@ var Feedback = Widget.extend({
_send_feedback: function(close){
var self = this;
var uuid = this.conversation.get('session').uuid;
return user_session.rpc('/rating/livechat/feedback', {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
return user_session.rpc('/im_livechat/feedback', {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
if(close){
self.trigger("feedback_sent"); // will close the conversation
self.conversation.message_send(_.str.sprintf(_t("I rated you with :rating_%d"), self.rating), "message");
@@ -271,7 +278,7 @@ var Feedback = Widget.extend({
return {
Feedback: Feedback,
LiveSupport: LiveSupport,
LivechatButton: LivechatButton,
};
});
@@ -0,0 +1,53 @@
odoo.define('im_livechat.chat_backend', function (require) {
"use strict";
var mail_chat_backend = require('mail.chat_backend');
/**
* Patch for the client action to integrate Livechat Mail Channel in a particular slot
*/
mail_chat_backend.ChatMailThread.include({
init: function(parent, action){
this._super.apply(this, arguments);
this.set('channel_livechat', []);
},
start: function(){
var self = this;
this.on("change:channel_livechat", this, function(){
self.channel_render('channel_livechat');
});
self.$el.on('click', '.o_mail_chat_channel_unpin', _.bind(self.on_click_channel_unpin, self));
return this._super.apply(this, arguments).then(function(){
});
},
on_click_channel_unpin: function(event){
var self = this;
var channel_id = this.$(event.currentTarget).data('channel-id');
var channel = this.channels[channel_id];
return this.channel_pin(channel.uuid, false).then(function(){
self.channel_remove(channel_id);
// if unpin current channel, switch to inbox
if(self.get('current_channel_id') === channel_id){
self.set('current_channel_id', 'channel_inbox');
}
});
},
channel_change: function(){
// update the default username
var current_channel = this.channels[this.get('current_channel_id')];
if(current_channel){
this.options.default_username = current_channel.anonymous_name || this.options.default_username
}
return this._super.apply(this, arguments);
},
// utils function
get_channel_slot: function(channel){
if(channel.channel_type === 'livechat'){
return 'channel_livechat';
}
return this._super.apply(this, arguments);
},
});
});
@@ -2,28 +2,29 @@
<templates xml:space="preserve">
<t t-name="im_livechat.chatButton">
<t t-esc="widget.text"/>
<t t-name="im_livechat.LivechatButton">
<div class="openerp o_livechat_button hidden-print" t-att-data-livechat-channel-id="widget.options.channel_id">
<t t-esc="widget.options.button_text"/>
</div>
</t>
<t t-name="im_livechat.feedback">
<div class="oe_livechat_rating">
<div class="oe_im_feedback_text">
<div class="o_livechat_rating">
<div class="o_livechat_rating_feedback_text">
Did we correctly answer your question ?
</div>
<div class="oe_livechat_rating_choices">
<div class="o_livechat_rating_choices">
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_10.png'" alt="Good" data-value="10"/>
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_5.png'" alt="OK" data-value="5"/>
<img t-att-src="widget.server_origin + '/rating/static/src/img/rating_0.png'" alt="Bad" data-value="0" />
</div>
<div class="oe_livechat_rating_reason">
<div class="o_livechat_rating_reason">
<textarea id="reason" placeholder="Explain your note"></textarea>
<div class="oe_livechat_rating_reason_button">
<div class="o_livechat_rating_reason_button">
<input type="button" id="rating_submit" value="Send" />
</div>
</div>
</div>
</t>
</templates>
</templates>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-extend="mail.chat.ChatMailThread.channels">
<t t-jquery="li > a" t-operation="append">
<t t-if="channel.channel_type === 'livechat'">
<span class="fa fa-times pull-right o_mail_chat_channel_unpin" t-att-title="_t('Close')" t-att-data-channel-id="channel.id"/>
</t>
</t>
</t>
<t t-extend="mail.chat.ChatMailThread">
<t t-jquery=".o_mail_chat_sidebar" t-operation="append">
<!-- Livechat list -->
<h4 class="o_mail_chat_sidebar_title"><t t-esc="_t('Livechat')"/></h4>
<t t-call="mail.chat.ChatMailThread.channels">
<t t-set="channel_slot" t-value="'channel_livechat'"/>
</t>
</t>
</t>
</template>
@@ -2,8 +2,20 @@
<odoo>
<data>
<!-- Template rendering the external HTML support page -->
<template id="support_page" name="Livechat Support Page">
<!--
Integrate Livechat Conversation in the Mail Chat Client Action
-->
<template id="assets_backend" name="im_livechat assets backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat_backend.js"></script>
</xpath>
</template>
<!--
Template rendering the external HTML support page
-->
<template id="support_page" name="Livechat : Support Page">
&lt;!DOCTYPE html&gt;
<html style="height: 100%">
<head>
@@ -12,7 +24,7 @@
<title><t t-esc="channel_name"/> Livechat Support Page</title>
<!-- Call the external Bundle to render the css, js, and js loader tags -->
<t t-call="im_livechat.external_loader"/>
<t t-raw="channel.script_external"/>
<style type="text/css">
body {
@@ -63,36 +75,32 @@
<body>
<div class="main" style="opacity: 1;">
<h1 class="channel_name"><t t-esc="channel_name"/></h1>
<h1 class="channel_name"><t t-esc="channel.name"/></h1>
<div>Live Chat Powered by <strong>Odoo</strong>.</div>
</div>
</body>
</html>
</template>
<!-- Template rendering all the scripts required to execute the Livechat from an external page (which not contain Odoo) -->
<template id="external_loader" name="All the scripts to launch the LiveSupport from an external Web Page">
<!--
Template rendering all the scripts required to execute the Livechat from an external page (which not contain Odoo)
-->
<template id="external_loader" name="Livechat : external_script field of livechat channel">
<!-- css style -->
<link t-att-href="'%s/web/css/im_livechat.external_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib (internal and external) -->
<script t-att-src="'%s/web/js/im_livechat.external_lib' % (url)" type="text/javascript" async="async" />
<script t-att-src="'%s/web/js/im_livechat.external_lib' % (url)" type="text/javascript"/>
<!-- the loader -->
<script t-att-src="'%s/im_livechat/loader/%s/%i' % (url, dbname, channel)" type="text/javascript" async="async" />
<script t-att-src="'%s/im_livechat/loader/%i' % (url, channel_id)" type="text/javascript"/>
</template>
<!-- Template rendering all the scripts required to execute the Livechat from a page containing Odoo -->
<template id="internal_loader" name="All the scripts to launch the LiveSupport from an internal Web Page">
<!-- css style -->
<link t-att-href="'%s/web/css/im_livechat.internal_lib' % (url)" rel="stylesheet"/>
<!-- js of all the required lib -->
<script t-att-src="'%s/web/js/im_livechat.internal_lib' % (url)" type="text/javascript" />
<!-- the loader -->
<script t-att-src="'%s/im_livechat/loader/%s/%i' % (url, dbname, channel)" type="text/javascript" />
</template>
<!-- Bundle of External Librairies of the Livechat -->
<template id="external_lib" name="External Librairies of the Livechat, required to make it work">
<!--
Bundle of External Librairies of the Livechat (Odoo + required modules)
-->
<template id="external_lib" name="Livechat : External Librairies of the Livechat, required to make it work">
<!-- Momentjs -->
<script type="text/javascript" src="/web/static/lib/moment/moment.js"></script>
<!-- OpenERP minimal lib -->
<script type="text/javascript" src="/web/static/lib/underscore/underscore.js"></script>
<script type="text/javascript" src="/web/static/lib/underscore.string/lib/underscore.string.js"></script>
@@ -101,7 +109,6 @@
<script type="text/javascript" src="/web/static/lib/jquery/jquery.browser.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.timeago/jquery.timeago.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
<!-- Qweb2 lib -->
<script type="text/javascript" src="/web/static/lib/qweb/qweb2.js"></script>
<!-- Odoo JS Framework -->
@@ -116,48 +123,46 @@
<script type="text/javascript" src="/web/static/src/js/framework/session.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/core.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/session_instance.js"></script>
<!-- add the internal lib -->
<t t-call="im_livechat.internal_lib"/>
</template>
<!-- Bundle of Librairies of the Bus, Chat, Livechat -->
<template id="internal_lib" name="Librairies of the Livechat">
<!-- Momentjs -->
<script type="text/javascript" src="/web/static/lib/moment/moment.js"></script>
<!-- IM module -->
<!-- Bus, Mail, Livechat -->
<script type="text/javascript" src="/bus/static/src/js/bus.js"></script>
<script type="text/javascript" src="/im_chat/static/src/js/im_chat_common.js"></script>
<script type="text/javascript" src="/mail/static/src/js/mail_utils.js"></script>
<script type="text/javascript" src="/mail/static/src/js/mail_chat_common.js"></script>
<script type="text/javascript" src="/im_livechat/static/src/js/im_livechat.js"></script>
<!-- CSS -->
<link rel="stylesheet" href="/im_chat/static/src/css/im_chat_common.css"></link>
<link rel="stylesheet" href="/mail/static/src/less/mail_chat_common.less"></link>
<link rel="stylesheet" href="/im_livechat/static/src/css/im_livechat.css"/>
</template>
<!-- the js code to initialize the LiveSupport object -->
<template id="loader" name="Javascript initializing the LiveSupport">
<t t-translate="off">
$(function() {
odoo.define('im_livechat.livesupport', function (require) {
var im_livechat = require('im_livechat.im_livechat');
window.livesupport = new im_livechat.LiveSupport(
"<t t-esc="url"/>",
"<t t-esc="dbname"/>",
<t t-esc="channel"/>,
{
buttonText: "<t t-esc="buttonText"/>",
inputPlaceholder: "<t t-esc="inputPlaceholder"/>",
defaultMessage: "<t t-esc="defaultMessage"/>" || '',
auto: window.oe_im_livechat_auto || false,
defaultUsername: "<t t-esc="username"/>" || undefined,
},
<t t-raw="rule"/>
);
<template id="loader" name="Livechat : Javascript appending the livechat button">
<t t-if="not info">
<t t-set="info" t-value="request.env['im_livechat.channel'].match_rules(request, channel)"/>
</t>
<t t-if="info" t-translate="off">
document.addEventListener("DOMContentLoaded", function(event) {
odoo.define('web.session', function (require) {
var Session = require('web.Session');
var modules = odoo._modules;
return new Session(undefined, "<t t-esc="info['server_url']"/>", {modules:modules, use_cors: false});
});
odoo.define('im_livechat.livesupport', function (require) {
var im_livechat = require('im_livechat.im_livechat');
var button = new im_livechat.LivechatButton(
$('body'),
"<t t-esc="info['server_url']"/>",
<t t-raw="json.dumps(info.get('options', {}))"/>,
<t t-raw="json.dumps(info.get('rule', False))"/>
);
button.appendTo($('body'));
window.livechat_button = button;
});
});
});
</t>
</template>
</data>
</odoo>
@@ -29,7 +29,7 @@
<field name="web_page" widget="url"/>
<field name="are_you_inside"/>
<field name="user_ids"/>
<field name="nbr_session"/>
<field name="nbr_channel"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
@@ -44,7 +44,7 @@
<t t-esc="(record.user_ids.raw_value || []).length"/> Operators
<br/>
<i class="fa fa-comments"></i>
<t t-esc="record.nbr_session.raw_value"/> Sessions
<t t-esc="record.nbr_channel.raw_value"/> Sessions
</p>
</div>
<div class="o_kanban_button">
@@ -66,8 +66,8 @@
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" type="action" attrs="{'invisible':[('nbr_session','=', 0)]}" name="%(im_chat_session_action_from_channel)d" icon="fa-comments">
<field string="Sessions" name="nbr_session" widget="statinfo"/>
<button class="oe_stat_button" type="action" attrs="{'invisible':[('nbr_channel','=', 0)]}" name="%(mail_channel_action_from_livechat_channel)d" icon="fa-comments">
<field string="Sessions" name="nbr_channel" widget="statinfo"/>
</button>
<button name="action_view_rating" attrs="{'invisible':[('rating_percentage_satisfaction','=', -1)]}" class="oe_stat_button" type="object" icon="fa-smile-o">
<field string="% Happy" name="rating_percentage_satisfaction" widget="statinfo"/>
@@ -192,7 +192,7 @@
id="session_history"
name="History"
parent="menu_livechat_root"
action="im_chat_session_action"
action="mail_channel_action"
groups="im_livechat_group_user"
sequence="10"/>
@@ -2,9 +2,9 @@
<openerp>
<data>
<record id="im_chat_session_view_search" model="ir.ui.view">
<field name="name">im_chat.session.search</field>
<field name="model">im_chat.session</field>
<record id="mail_channel_view_search" model="ir.ui.view">
<field name="name">mail.channel.search</field>
<field name="model">mail.channel</field>
<field name="arch" type="xml">
<search string="Search history">
<filter name="id" string="My sessions" domain="[('user_ids','in', uid)]"/>
@@ -20,28 +20,28 @@
</field>
</record>
<record id="im_chat_session_view_tree" model="ir.ui.view">
<field name="name">im_chat.session.tree</field>
<field name="model">im_chat.session</field>
<record id="mail_channel_view_tree" model="ir.ui.view">
<field name="name">mail.channel.tree</field>
<field name="model">mail.channel</field>
<field name="arch" type="xml">
<tree string="History" create="false">
<field name="id"/>
<field name="fullname"/>
<field name="name"/>
<field name="create_date"/>
</tree>
</field>
</record>
<record id="im_chat_session_view_form" model="ir.ui.view">
<field name="name">im_chat.session.form</field>
<field name="model">im_chat.session</field>
<record id="mail_channel_view_form" model="ir.ui.view">
<field name="name">mail.channel.form</field>
<field name="model">mail.channel</field>
<field name="arch" type="xml">
<form string="Session Form" create="false" edit="false">
<sheet>
<group>
<group string="General">
<field name="id"/>
<field name="fullname" />
<field name="name" />
<field name="create_date" readonly="1"/>
</group>
<group string="Feedback">
@@ -68,8 +68,8 @@
<field name="message_ids" type="tree">
<tree string="History">
<field name="create_date" />
<field name="from_id" />
<field name="message" />
<field name="author_id" />
<field name="body" />
</tree>
</field>
</sheet>
@@ -78,25 +78,25 @@
</record>
<record id="im_chat_session_action" model="ir.actions.act_window">
<record id="mail_channel_action" model="ir.actions.act_window">
<field name="name">History</field>
<field name="res_model">im_chat.session</field>
<field name="res_model">mail.channel</field>
<field name="view_type">form</field>
<field name="view_mode">list,form</field>
<field name="domain">[('channel_id', '!=', None)]</field>
<field name="context">{'group_by': ['channel_id', 'create_date:month']}</field>
<field name="domain">[('livechat_channel_id', '!=', None)]</field>
<field name="context">{'group_by': ['livechat_channel_id', 'create_date:month']}</field>
</record>
<record id="im_chat_session_action_from_channel" model="ir.actions.act_window">
<record id="mail_channel_action_from_livechat_channel" model="ir.actions.act_window">
<field name="name">Sessions</field>
<field name="res_model">im_chat.session</field>
<field name="res_model">mail.channel</field>
<field name="view_mode">list,form</field>
<field name="domain">[('channel_id', 'in', [active_id])]</field>
<field name="domain">[('livechat_channel_id', 'in', [active_id])]</field>
<field name="context">{
'search_default_channel_id': [active_id],
'default_channel_id': active_id,
'search_default_livechat_channel_id': [active_id],
'default_livechat_channel_id': active_id,
}</field>
<field name="search_view_id" ref="im_chat_session_view_search"/>
<field name="search_view_id" ref="mail_channel_view_search"/>
</record>
</data>
+1 -1
View File
@@ -16,7 +16,7 @@ Ask your functionnal question directly to the Odoo Operators with the livechat s
'data': [
"views/im_odoo_support.xml"
],
'depends' : ["web", "im_chat"],
'depends' : ["web", "mail"],
'qweb': [
'static/src/xml/im_odoo_support.xml'
],
+7 -3
View File
@@ -25,7 +25,7 @@ Main Features
* Includes an extensible generic email composition assistant, that can turn into a mass-mailing assistant and is capable of interpreting simple *placeholder expressions* that will be replaced with dynamic data when each email is actually sent.
""",
'website': 'https://www.odoo.com/page/enterprise-social-network',
'depends': ['base', 'base_setup'],
'depends': ['base', 'base_setup', 'bus'],
'data': [
'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
@@ -34,11 +34,12 @@ Main Features
'views/mail_message_views.xml',
'views/mail_mail_views.xml',
'views/mail_followers_views.xml',
'views/mail_thread_views.xml',
'views/mail_channel_views.xml',
'views/mail_shortcode_views.xml',
'views/res_config_views.xml',
'data/mail_data.xml',
'data/mail_channel_data.xml',
'data/mail_shortcode_data.xml',
'security/mail_security.xml',
'security/ir.model.access.csv',
'views/mail_alias_views.xml',
@@ -57,7 +58,10 @@ Main Features
'installable': True,
'application': True,
'qweb': [
'static/src/xml/mail.xml',
'static/src/xml/mail_thread.xml',
'static/src/xml/mail_chatter.xml',
'static/src/xml/mail_chat_common.xml',
'static/src/xml/mail_chat_backend.xml',
'static/src/xml/mail_followers.xml',
'static/src/xml/announcement.xml',
],
+2
View File
@@ -1 +1,3 @@
# -*- coding: utf-8 -*
import main
import bus
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*
import base64
import openerp
from openerp import SUPERUSER_ID
from openerp.http import request
class MailChatController(openerp.addons.bus.controllers.main.BusController):
def _default_request_uid(self):
""" For Anonymous people, they receive the access right of SUPERUSER_ID since they have NO access (auth=none)
!!! Each time a method from this controller is call, there is a check if the user (who can be anonymous and Sudo access)
can access to the ressource.
"""
return request.session.uid and request.session.uid or SUPERUSER_ID
# --------------------------
# Extends BUS Controller Poll
# --------------------------
def _poll(self, dbname, channels, last, options):
if request.session.uid:
partner_id = request.env.user.partner_id.id
if partner_id:
for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]):
channels.append((request.db, 'mail.channel', mail_channel.id))
# personal channel
channels.append((request.db, 'res.partner', partner_id))
return super(MailChatController, self)._poll(dbname, channels, last, options)
# --------------------------
# Anonymous routes (Common Methods)
# --------------------------
@openerp.http.route('/mail/chat_post', type="json", auth="none")
def mail_chat_post(self, uuid, message_content, **kwargs):
request_uid = self._default_request_uid()
# find the author from the user session, which can be None
author_id = False # message_post accept 'False' author_id, but not 'None'
if request.session.uid:
author_id = request.env['res.users'].sudo().browse(request.session.uid).partner_id.id
# post a message without adding followers to the channel. email_from=False avoid to get author from email data
mail_channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1)
message = mail_channel.sudo(request_uid).with_context(mail_create_nosubscribe=True).message_post(author_id=author_id, email_from=False, body=message_content, message_type='comment', subtype='mail.mt_comment', **kwargs)
return message and message.id or False
@openerp.http.route(['/mail/chat_history'], type="json", auth="none")
def mail_chat_history(self, uuid, last_id=False, limit=20):
request_uid = self._default_request_uid()
channel = request.env["mail.channel"].sudo(request_uid).search([('uuid', '=', uuid)], limit=1)
return channel.sudo(request_uid).channel_fetch_message(last_id, limit)
@openerp.http.route('/mail/chat_init', type="json", auth="none")
def mail_chat_init(self):
result = {
'emoji': request.env['mail.shortcode'].sudo().search_read([('shortcode_type', '=', 'image')], ['source', 'substitution', 'description'])
}
# include the previous notifications, only for identified user
if request.session.uid:
request_uid = self._default_request_uid()
result['notifications'] = request.env['mail.channel'].sudo(request_uid).get_init_notifications()
return result
+13
View File
@@ -205,3 +205,16 @@ class MailController(http.Controller):
except:
return self._redirect_to_messaging()
return werkzeug.utils.redirect('/mail/view?%s' % url_encode({'model': model, 'res_id': res_id}))
@http.route('/mail/needaction', type='json', auth='user')
def needaction(self):
return request.env['res.partner'].get_needaction_count()
@http.route('/mail/client_action', type='json', auth='user')
def mail_client_action(self):
values = {
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
'chatter_needaction_auto': request.env.user.chatter_needaction_auto,
'channel_slots': request.env['mail.channel'].channel_fetch_slot()
}
return values
-11
View File
@@ -41,17 +41,6 @@
<field name="args">()</field>
</record>
<!-- After installation of the module, open the related menu -->
<record id="action_client_messaging_menu" model="ir.actions.client">
<field name="name">Open Messaging Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('mail.mail_thread_menu_root')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_messaging_menu"/>
<field name="state">open</field>
</record>
<!-- Admin should not receivve emails at creation -->
<record id="base.partner_root" model="res.partner">
<field name="notify_email">none</field>
+183
View File
@@ -0,0 +1,183 @@
<?xml version="1.0"?>
<odoo>
<data>
<!-- Smileys -->
<record id="mail_smiley_smile" model="mail.shortcode">
<field name="source">:)</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/smile.png'/&gt;</field>
<field name="description">Emoji Smile</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_sad" model="mail.shortcode">
<field name="source">:(</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/sad.png'/&gt;</field>
<field name="description">Emoji Sad</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_laugh" model="mail.shortcode">
<field name="source">:D</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/laugh.png'/&gt;</field>
<field name="description">Emoji Laugh</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_wink" model="mail.shortcode">
<field name="source">;)</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/wink.png'/&gt;</field>
<field name="description">Emoji Wink</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_stuck" model="mail.shortcode">
<field name="source">O_0</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/stuck_out_tongue.png'/&gt;</field>
<field name="description">Emoji Stuck Out Tongue Winking Eye</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_scream" model="mail.shortcode">
<field name="source">:@</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/scream.png'/&gt;</field>
<field name="description">Emoji Scream</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_neutral" model="mail.shortcode">
<field name="source">:|</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/neutral.png'/&gt;</field>
<field name="description">Emoji Speechless/Neutral face</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_cheeky" model="mail.shortcode">
<field name="source">:p</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/yum.png'/&gt;</field>
<field name="description">Emoji Cheeky</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_confused" model="mail.shortcode">
<field name="source">:/</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/confused.png'/&gt;</field>
<field name="description">Emoji Confused</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_open_mouth" model="mail.shortcode">
<field name="source">:O</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/open_mouth.png'/&gt;</field>
<field name="description">Emoji Open Mouth</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_heart" model="mail.shortcode">
<field name="source">&amp;lt;3</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/heart.png'/&gt;</field>
<field name="description">Emoji Heart</field>
<field name="shortcode_type">image</field>
</record>
<!-- Other images -->
<record id="mail_smiley_thumbsup" model="mail.shortcode">
<field name="source">:+1</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/thumbsup.png'/&gt;</field>
<field name="description">Emoji Thumbs Up</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_thumbsdown" model="mail.shortcode">
<field name="source">:-1</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/thumbsdown.png'/&gt;</field>
<field name="description">Emoji Thumbs Down</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_kitten" model="mail.shortcode">
<field name="source">:kitten</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/cat.png'/&gt;</field>
<field name="description">Emoji Kitten</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_pinky" model="mail.shortcode">
<field name="source">:pinky</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/pinky.png'/&gt;</field>
<field name="description">Emoji Pinky</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_musti" model="mail.shortcode">
<field name="source">:musti</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/musti.png'/&gt;</field>
<field name="description">Emoji Musti</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_man_with_turban" model="mail.shortcode">
<field name="source">:turban</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/man_with_turban.png'/&gt;</field>
<field name="description">Emoji Man With Turban</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_troll" model="mail.shortcode">
<field name="source">:troll</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/trollface.png'/&gt;</field>
<field name="description">Emoji Troll Face</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_boar" model="mail.shortcode">
<field name="source">:boar</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/boar.png'/&gt;</field>
<field name="description">Emoji Boar</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_poop" model="mail.shortcode">
<field name="source">:poop</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/poop.png'/&gt;</field>
<field name="description">Emoji Poop</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_cookie" model="mail.shortcode">
<field name="source">:cookie</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/cookie.png'/&gt;</field>
<field name="description">Emoji Cookie</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_postal_horn" model="mail.shortcode">
<field name="source">:postal_horn</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/postal_horn.png'/&gt;</field>
<field name="description">Emoji Postal Horn</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_ghost" model="mail.shortcode">
<field name="source">:ghost</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/ghost.png'/&gt;</field>
<field name="description">Emoji Ghost</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_beer" model="mail.shortcode">
<field name="source">:beer</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/beer.png'/&gt;</field>
<field name="description">Emoji Beer</field>
<field name="shortcode_type">image</field>
</record>
<record id="mail_smiley_man" model="mail.shortcode">
<field name="source">:man</field>
<field name="substitution">&lt;img src='/mail/static/src/img/smiley/man.png'/&gt;</field>
<field name="description">Emoji Man</field>
<field name="shortcode_type">image</field>
</record>
</data>
</odoo>
+2 -1
View File
@@ -9,9 +9,10 @@ import mail_mail
import mail_thread
import mail_channel
import mail_template
import mail_shortcode
import res_partner
import res_users
import res_config
import update
import ir_actions
import html2text
import html2text
+336 -6
View File
@@ -1,19 +1,28 @@
# -*- coding: utf-8 -*-
import datetime
import time
import uuid
from openerp import _, api, fields, models, modules, tools
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.exceptions import UserError
from openerp.addons.bus.models.bus_presence import AWAY_TIMER
class ChannelPartner(models.Model):
_name = 'mail.channel.partner'
_description = 'Last Seen Many2many'
_table = 'mail_channel_partner'
_rec_name = 'partner_id'
partner_id = fields.Many2one('res.partner', string='Recipient')
channel_id = fields.Many2one('mail.channel', string='Channel')
seen_message_id = fields.Many2one('mail.message', string='Last Seen')
fold_state = fields.Selection([('open', 'Open'), ('folded', 'Folded'), ('closed', 'Closed')], string='Conversation Fold State', default='open')
is_minimized = fields.Boolean("Conversation is minimied")
is_pinned = fields.Boolean("Is pinned on the interface", default=True)
class Channel(models.Model):
@@ -86,7 +95,7 @@ class Channel(models.Model):
def create(self, vals):
# Create channel and alias
channel = super(Channel, self.with_context(
alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True)
alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True, mail_create_nosubscribe=True)
).create(vals)
channel.alias_id.write({"alias_force_thread_id": channel.id, 'alias_parent_thread_id': channel.id})
@@ -122,22 +131,19 @@ class Channel(models.Model):
self._subscribe_users()
return result
def _notify(self, message):
# DO SOMETHING USEFULL
return True
def _subscribe_users(self):
for mail_channel in self:
mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]})
@api.multi
def action_follow(self):
return self.write({'channel_partner_ids': [(4, self.env.user.partner_id.id)]})
return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]})
@api.multi
def action_unfollow(self):
return self.write({'channel_partner_ids': [(3, self.env.user.partner_id.id)]})
@api.multi
def message_get_email_values(self, notif_mail=None):
self.ensure_one()
@@ -161,3 +167,327 @@ class Channel(models.Model):
headers['X-Forge-To'] = list_to
res['headers'] = repr(headers)
return res
@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):
# auto pin 'direct_message' channel partner
self.filtered(lambda channel: channel.channel_type == 'direct_message').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
# apply shortcode (text only) subsitution
body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text')
message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs)
return message
#------------------------------------------------------
# Instant Messaging API
#------------------------------------------------------
# A channel header should be broadcasted:
# - when adding user to channel (only to the new added partners)
# - when folding/minimizing a channel (only to the user making the action)
# A message should be broadcasted:
# - when a message is posted on a channel (to the channel, using _notify() method)
# Anonymous method
@api.multi
def _broadcast(self, partner_ids):
""" Broadcast the current channel header to the given partner ids
:param partner_ids : the partner to notify
"""
notifications = self._channel_channel_notifications(partner_ids)
self.env['bus.bus'].sendmany(notifications)
@api.multi
def _channel_channel_notifications(self, partner_ids):
""" Generate the bus notifications of current channel for the given partner ids
:param partner_ids : the partner to send the current channel header
:returns list of bus notifications (tuple (bus_channe, message_content))
"""
notifications = []
for partner in self.env['res.partner'].browse(partner_ids):
user_id = partner.user_ids and partner.user_ids[0] or False
if user_id:
for channel_info in self.sudo(user_id).channel_info():
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
return notifications
@api.multi
def _notify(self, message):
""" Broadcast the given message on the current channels.
Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)).
A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field
of the received message indicates on wich mail channel the message should be displayed.
:param : mail.message to broadcast
"""
message.ensure_one()
notifications = self._channel_message_notifications(message)
self.env['bus.bus'].sendmany(notifications)
@api.multi
def _channel_message_notifications(self, message):
""" Generate the bus notifications for the given message
:param message : the mail.message to sent
:returns list of bus notifications (tuple (bus_channe, message_content))
"""
message_values = message.message_format()[0]
notifications = []
for channel in self:
message_values['channel_ids'] = [channel.id]
notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)])
# add uuid to allow anonymous to listen
if channel.public == 'public':
notifications.append([channel.uuid, dict(message_values)])
return notifications
@api.multi
def channel_info(self):
""" Get the informations header for the current channels
:returns a list of channels values
:rtype : list(dict)
"""
channel_infos = []
partner_channels = self.env['mail.channel.partner']
# find the channel partner state, if logged user
if self.env.user and self.env.user.partner_id:
partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)])
# for each channel, build the information header and include the logged partner information
for channel in self:
info = {
'id': channel.id,
'name': channel.name,
'uuid': channel.uuid,
'state': 'open',
'is_minimized': False,
'channel_type': channel.channel_type,
'public': channel.public,
}
# add the partner for 'direct mesage' channel
if channel.channel_type == 'chat':
info['direct_partner'] = channel.sudo().channel_partner_ids.filtered(lambda p: p.id != self.env.user.partner_id.id).read(['id', 'name', 'im_status'])
# add user session state, if available and if user is logged
if partner_channels.ids:
partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id)
if len(partner_channel) >= 1:
partner_channel = partner_channel[0]
info['state'] = partner_channel.fold_state or 'open'
info['is_minimized'] = partner_channel.is_minimized
info['seen_message_id'] = partner_channel.seen_message_id.id
# add needaction and unread counter, since the user is logged
info['message_needaction_counter'] = channel.message_needaction_counter
info['message_unread_counter'] = channel.message_unread_counter
channel_infos.append(info)
return channel_infos
@api.multi
def channel_fetch_message(self, last_id=False, limit=20):
""" Return message values of the current channel.
:param last_id : last message id to start the research
:param limit : maximum number of messages to fetch
:returns list of messages values
:rtype : list(dict)
"""
self.ensure_one()
domain = [("channel_ids", "in", self.ids)]
if last_id:
domain.append(("id", "<", last_id))
return self.env['mail.message'].message_fetch(domain=domain, limit=limit)
# User methods
@api.model
def channel_get(self, partners_to, pin=True):
""" Get the canonical private channel between some partners, create it if needed.
To reuse an old channel (conversation), this one must be private, and contains
only the given partners.
:param partners_to : list of res.partner ids to add to the conversation
:param pin : True if getting the channel should pin it for the current user
:returns a channel header, or False if the users_to was False
:rtype : dict
"""
if partners_to:
partners_to.append(self.env.user.partner_id.id)
# determine type according to the number of partner in the channel
self.env.cr.execute("""
SELECT P.channel_id as channel_id
FROM mail_channel C, mail_channel_partner P
WHERE P.channel_id = C.id
AND C.public LIKE 'private'
AND P.partner_id IN %s
AND channel_type LIKE 'chat'
GROUP BY P.channel_id
HAVING COUNT(P.partner_id) = %s
""", (tuple(partners_to), len(partners_to),))
result = self.env.cr.dictfetchall()
if result:
# get the existing channel between the given partners
channel = self.browse(result[0].get('channel_id'))
# pin up the channel for the current partner
if pin:
self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True})
else:
# create a new one
channel = self.create({
'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to],
'public': 'private',
'channel_type': 'chat',
'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')),
})
# broadcast the channel header to the other partner (not me)
channel._broadcast(partners_to)
return channel.channel_info()[0]
return False
@api.model
def channel_fold(self, uuid, state=None):
""" Update the fold_state of the given session. In order to syncronize web browser
tabs, the change will be broadcast to himself (the current user channel).
Note: the user need to be logged
:param state : the new status of the session for the current user.
"""
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
for session_state in self.env['mail.channel.partner'].search(domain):
if not state:
state = session_state.fold_state
if session_state.fold_state == 'open':
state = 'folded'
else:
state = 'open'
session_state.write({
'fold_state': state,
'is_minimized': bool(state != 'closed'),
})
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0])
@api.model
def channel_minimize(self, uuid, minimized=True):
values = {
'fold_state': minimized and 'open' or 'closed',
'is_minimized': minimized
}
domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
channel_partners = self.env['mail.channel.partner'].search(domain)
channel_partners.write(values)
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0])
@api.model
def channel_pin(self, uuid, pinned=False):
# add the person in the channel, and pin it (or unpin it)
channel = self.search([('uuid', '=', uuid)])
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)])
if channel_partners:
channel_partners.write({'is_pinned': pinned})
@api.multi
def channel_seen(self):
self.ensure_one()
if self.channel_message_ids.ids:
last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id})
@api.multi
def channel_invite(self, partner_ids):
""" Add the given partner_ids to the current channels and broadcast the channel header to them.
:param partner_ids : list of partner id to add
"""
partners = self.env['res.partner'].browse(partner_ids)
# add the partner
for channel in self:
partners_to_add = partners - channel.channel_partner_ids
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
# broadcast the channel header to the added partner
self._broadcast(partner_ids)
#------------------------------------------------------
# Instant Messaging View Specific (Slack Client Action)
#------------------------------------------------------
@api.model
def get_init_notifications(self):
""" Get unread messages and old messages received less than AWAY_TIMER
ago of minimized channel ONLY. This aims to set the minimized channel
when refreshing the page.
Note : the user need to be logged
"""
# get current user's minimzed channel
minimized_channels = self.env['mail.channel.partner'].search([('is_minimized', '=', True), ('partner_id', '=', self.env.user.partner_id.id)]).mapped('channel_id')
# get the message since the AWAY_TIMER
threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('channel_ids', 'in', minimized_channels.ids), ('create_date', '>', threshold)]
# get the message since the last poll of the user
presence = self.env['bus.presence'].search([('user_id', '=', self._uid)], limit=1)
if presence:
domain.append(('create_date', '>', presence.last_poll))
# do the message search
message_values = self.env['mail.message'].message_fetch(domain=domain)
# create the notifications (channel infos first, then messages)
notifications = []
for channel_info in minimized_channels.channel_info():
notifications.append([(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info])
for message_value in message_values:
for channel_id in message_value['channel_ids']:
if channel_id in minimized_channels.ids:
message_value['channel_ids'] = [channel_id]
notifications.append([(self._cr.dbname, 'mail.channel', channel_id), dict(message_value)])
return notifications
@api.model
def channel_fetch_slot(self):
""" Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
the mapping between partner_id/channel_id for direct_message channels.
:returns dict : the grouped channels and the mapping
"""
values = {}
my_partner_id = self.env.user.partner_id.id
pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id')
# get the group/public channels
values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
# get the pinned 'direct message' channel
direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)])
values['channel_direct_message'] = direct_message_channels.channel_info()
# get the partner of the pinned 'direct message' channels with mapping partner/channel
direct_channel_partner = direct_message_channels.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id.id != my_partner_id)
values['mapping'] = dict((item.partner_id.id, item.channel_id.id) for item in direct_channel_partner)
# partners of the 'direct message' channels
values['partners'] = direct_message_channels.mapped('channel_partner_ids').filtered(lambda partner: partner.id != my_partner_id).read(['id', 'name', 'im_status'])
# get the private group
values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
return values
@api.model
def channel_search_to_join(self, name=None, domain=None):
""" Return the channel info of the channel the current partner can join
:param name : the name of the researched channels
:param domain : the base domain of the research
:returns dict : channel dict
"""
if not domain:
domain = []
domain += [('channel_type', '=', 'channel'), ('channel_partner_ids', 'not in', [self.env.user.partner_id.id])]
if name:
domain.append(('name', 'ilike', '%'+name+'%'))
return self.search(domain).read(['name', 'public', 'uuid', 'channel_type'])
@api.multi
def channel_join_and_get_info(self):
self.ensure_one()
self.action_follow()
return self.channel_info()[0]
@api.model
def channel_create(self, name, privacy='public'):
""" Create a channel and add the current partner, broadcast it (to make the user directly
listen to it when polling)
:param name : the name of the channel to create
:param privacy : privacy of the channel. Should be 'public' or 'private'.
:return dict : channel header
"""
# create the channel
new_channel = self.create({'name': name, 'public': privacy})
new_channel.action_follow()
return new_channel.channel_info()[0]
+79 -11
View File
@@ -248,12 +248,12 @@ class Message(models.Model):
tracking_value_ids.append(tracking_tree[tracking_value.id])
message_dict.update({
'is_author': pid == author[0],
#'is_author': pid == author[0],
'author_id': author,
'partner_ids': partner_ids,
'attachment_ids': attachment_ids,
'tracking_value_ids': tracking_value_ids,
'user_pid': pid
#'user_pid': pid
})
return True
@@ -281,7 +281,8 @@ class Message(models.Model):
'record_name': self.record_name,
'subject': self.subject,
'date': self.date,
'needaction': self.needaction,
#'needaction': self.needaction, JEM : not used.
'needaction_partner_ids': self.needaction_partner_ids.ids,
'parent_id': parent_id,
'is_private': is_private,
'author_id': False,
@@ -291,12 +292,13 @@ class Message(models.Model):
'is_favorite': self.starred,
'attachment_ids': [],
'tracking_value_ids': [],
'channel_ids': self.channel_ids.ids,
}
@api.cr_uid_context
def message_read_wrapper(self, cr, uid, ids=None, domain=None, context=None,
thread_level=0, parent_id=False, limit=None, child_limit=None):
return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context,
return self.message_read(cr, uid, ids, domain=domain, thread_level=thread_level, context=context,
parent_id=parent_id, limit=limit, child_limit=child_limit)
@api.multi
@@ -308,7 +310,7 @@ class Message(models.Model):
well formed threads, if uid has access to them.
After reading the messages, expandable messages are added in the
message list. It consists in messages holding the 'read more' data:
message list. It consists in messages holding the 'read more' data:
number of messages to read, domain to apply.
:param list ids: optional IDs to fetch
@@ -320,7 +322,7 @@ class Message(models.Model):
:param int limit: number of messages to fetch, before adding the
ancestors and expandables
:param int child_limit: number of child messages to fetch
:return dict:
:return dict:
- int: number of messages read (status 'unread' to 'read')
- list: list of threads [[messages_of_thread1], [messages_of_thread2]]
"""
@@ -397,7 +399,7 @@ class Message(models.Model):
'nb_messages': more_count,
'parent_id': parent}
if parent and thread_level:
if parent and thread_level:
#insert expandable before parent message
parent_tree[parent].insert(len(parent_tree[parent])-1, exp)
else:
@@ -408,7 +410,7 @@ class Message(models.Model):
parent_list = parent_tree.values()
parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item]), reverse=True)
#add 'thread' expandable
#add 'thread' expandable
if thread_level:
exp_domain = domain + [('id', '<', min(self.ids)), ('id', 'not in', parent_ids), ('parent_id', 'not in', parent_ids)]
more_count = self.search_count(exp_domain)
@@ -419,15 +421,79 @@ class Message(models.Model):
'parent_id': parent_id}])
nb_read = 0
if context and 'mail_read_set_read' in context and context['mail_read_set_read']:
if context and 'mail_read_set_read' in context and context['mail_read_set_read']:
nb_read = self.set_message_read(True, create_missing=False)
else:
nb_read = 0
parent_list = []
parent_list = []
return {'nb_read': nb_read, 'threads': parent_list}
@api.model
def message_fetch(self, domain, limit=20):
return self.search(domain, limit=limit).message_format()
@api.multi
def message_format(self):
""" Get the message values in the format for web client. Since message values can be broadcasted,
computed fields MUST NOT BE READ and broadcasted.
:returns list(dict).
Example :
{
'body': HTML content of the message
'model': u'res.partner',
'record_name': u'Agrolait',
'attachment_ids': [
{
'file_type_icon': u'webimage',
'id': 45,
'name': u'sample.png',
'filename': u'sample.png'
}
],
'needaction_partner_ids': [], # list of partner ids
'res_id': 7,
'tracking_value_ids': [
{
'old_value': "",
'changed_field': "Customer",
'id': 2965,
'new_value': "Axelor"
}
],
'author_id': (3, u'Administrator'),
'email_from': 'sacha@pokemon.com' # email address or False
'subtype_id': (1, u'Discussions'),
'channel_ids': [], # list of channel ids
'date': '2015-06-30 08:22:33',
'partner_ids': [[7, "Sacha Du Bourg-Palette"]], # list of partner name_get
'message_type': u'comment',
'id': 59,
'subject': False
'is_note': True # only if the subtype is internal
}
"""
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
'channel_ids', 'partner_ids', # recipients
'needaction_partner_ids', # list of partner ids for whom the message is a needaction
'starred_partner_ids', # list of partner ids for whom the message is starred
])
message_tree = dict((m.id, m) for m in self)
self._message_read_dict_postprocess(message_values, message_tree)
# add is_note flag
internal_subtype_ids = self.env['mail.message.subtype'].search([('internal', '=', True)]).ids
for message in message_values:
if message['subtype_id'] and message['subtype_id'][0]:
message['is_note'] = bool(message['subtype_id'][0] in internal_subtype_ids)
return message_values
#------------------------------------------------------
# mail_message internals
#------------------------------------------------------
@@ -750,7 +816,9 @@ class Message(models.Model):
self_sudo = self.sudo()
# TDE CHECK: add partners / channels as arguments to be able to notify a message with / without computation ??
self.ensure_one() # tde: not sure, just for testing, will see
self.ensure_one() # tde: not sure, just for testinh, will see
partners = self.env['res.partner'] | self.partner_ids
channels = self.env['mail.channel'] | self.channel_ids
# all followers of the mail.message document have to be added as partners and notified
# and filter to employees only if the subtype is internal
+59
View File
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
import cgi
import re
from openerp import api, fields, models
class MailShortcode(models.Model):
""" Shortcode
Canned Responses, allowing the user to defined shortcuts in its message. Should be applied before storing message in database.
Emoji allowing replacing text with image for visual effect. Should be applied when the message is displayed (only for final rendering).
These shortcodes are global and are available for every user.
"""
_name = 'mail.shortcode'
_description = 'Canned Response / Shortcode'
source = fields.Char('Shortcut', required=True, index=True, help="The shortcut which must be replace in the Chat Messages")
substitution = fields.Char('Substitution', required=True, index=True, help="The excaped html code replacing the shortcut")
description = fields.Char('Description')
shortcode_type = fields.Selection([('image', 'Smiley'), ('text', 'Canned Response')], required=True, default='text',
help="* Smiley are only used for HTML code to display an image "\
"* Text (default value) is use to substitute text with another text")
@api.model
def create(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(MailShortcode, self).create(values)
@api.multi
def write(self, values):
if values.get('substitution'):
values['substitution'] = self._sanitize_shorcode(values['substitution'])
return super(MailShortcode, self).write(values)
def _sanitize_shorcode(self, substitution):
""" Sanitize the shortcode substitution :
- HTML substitution : only allow the img tag (emoji)
- escape other substitutions to avoid XSS
"""
is_img_tag = re.match(r'''^<img\s+src=('|")([^'"]*)\1\s*/?>$''', substitution, re.M | re.I)
if is_img_tag:
return substitution
return cgi.escape(substitution)
@api.model
def apply_shortcode(self, message, shortcode_type=None):
""" Apply the substitution on the given text
:param message : the text where the shortcode will be applied
:param shortcode_type : shortcode type that should be applied during the substitution
"""
domain = []
if shortcode_type:
domain.append(('shortcode_type', '=', shortcode_type))
for shortcode in self.search(domain):
regex = '(?:^|\s)(%s)(?:\s|$)' % re.escape(shortcode.source)
message = re.sub(regex, " " + shortcode.substitution + " ", message)
return message
+7 -4
View File
@@ -346,6 +346,9 @@ class MailThread(models.AbstractModel):
('res_model', 'in', [False, self._name]),
('internal', '=', True)], ['name', 'description', 'sequence'])
options['internal_subtypes'] = internal_subtypes
# emoji list
options['emoji_list'] = self.env['mail.shortcode'].search([('shortcode_type', '=', 'image')]).read(['source', 'description', 'substitution'])
# save options on the node
node.set('options', json.dumps(options))
res['arch'] = etree.tostring(doc)
return res
@@ -501,7 +504,7 @@ class MailThread(models.AbstractModel):
""" When redirecting towards the Inbox, choose which action xml_id has
to be fetched. This method is meant to be inherited, at least in portal
because portal users have a different Inbox action than classic users. """
return 'mail.mail_message_action_inbox'
return 'mail.mail_channel_action_client_chat'
@api.multi
def _notification_link_helper(self, link_type, **kwargs):
@@ -1855,9 +1858,9 @@ class MailThread(models.AbstractModel):
@api.multi
def message_set_read(self):
# Nothing done here currently. Will be implemented with the final
# slack modeling.
return True
messages = self.env['mail.message'].search([('model', '=', self._name), ('res_id', 'in', self.ids), ('needaction', '=', True)])
messages.write({'needaction_partner_ids': [(3, self.env.user.partner_id.id)]})
return messages.ids
@api.multi
def message_change_thread(self, new_thread):
+9
View File
@@ -173,3 +173,12 @@ class Partner(models.Model):
emails.send()
return True
@api.model
def get_needaction_count(self):
""" compute the number of needaction of the current user """
self.env.cr.execute("""
SELECT count(*) as needaction_count
FROM mail_message_res_partner_needaction_rel R
WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
return self.env.cr.dictfetchall()[0].get('needaction_count')
+3 -1
View File
@@ -27,4 +27,6 @@ access_mail_tracking_value_system,mail.tracking.value.system,model_mail_tracking
access_mail_thread_all,mail.thread.all,model_mail_thread,,1,1,1,1
access_publisher_warranty_contract_all,publisher.warranty.contract.all,model_publisher_warranty_contract,,1,1,1,1
access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1
access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
27 access_mail_thread_all mail.thread.all model_mail_thread 1 1 1 1
28 access_publisher_warranty_contract_all publisher.warranty.contract.all model_publisher_warranty_contract 1 1 1 1
29 access_mail_template mail.template model_mail_template base.group_user 1 1 1 0
30 access_mail_template_system mail.template_system model_mail_template base.group_system 1 1 1 1
31 access_mail_shortcode mail.shortcode model_mail_shortcode base.group_user 1 1 1 1
32 access_mail_shortcode_portal mail.shortcode.portal model_mail_shortcode base.group_portal 1 0 0 0
File diff suppressed because it is too large Load Diff
-104
View File
@@ -1,104 +0,0 @@
/* ------------------------------ */
/* Group Form */
/* ------------------------------ */
/* Resize sheet width */
.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet {
min-height: 0px;
max-height: none;
}
.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_sheet {
min-height: 0px;
max-height: none;
padding: 0px 18px;
}
.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_avatar {
padding-top: 3px;
}
/* Resize footer width */
.o_web_client .oe_form div.oe_mail_channel_footer {
max-width: 80%;
}
/* Resize group description */
.o_web_client .oe_form_sheetbg.o_web_client_mail_channel_sheet .oe_form_field_text > textarea {
height: 60px;
}
/* ------------------------------ */
/* Group Kanban */
/* ------------------------------ */
.oe_group_vignette {
padding: 8px 0;
min-height: 100px;
}
.oe_group_image, .oe_group_details {
display: inline-block;
vertical-align: top;
}
.oe_group_image {
width: 100px;
height: 100px;
text-align: center;
overflow: hidden;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
-o-border-radius: 3px;
-ms-border-radius: 3px;
border-radius: 3px;
border-collapse: separate;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
-o-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.oe_group_photo {
width: 100px;
height: 100px;
clip: rect(0px, 100px, 100px, 0px);
border:none;
}
.oe_group_details {
width: 220px;
font-size: 13px;
padding: 2px 5px;
color: #4c4c4c;
min-height: 120px;
}
.oe_group_details h4 {
margin: 0;
font-size: 13px;
}
.oe_group_details ul {
margin: 3px 0 5px;
padding: 0;
list-style: none;
}
.o_web_client .oe_group_details li {
margin: 2px 0;
}
.o_web_client .oe_group_button {
bottom: 0;
}
.o_web_client .oe_group_button .oe_group_join {
color: white;
background-color: #3465A4;
background-image: -webkit-linear-gradient(top, #729FCF, #3465A4);
background-image: -moz-linear-gradient(top, #729FCF, #3465A4);
background-image: -ms-linear-gradient(top, #729FCF, #3465A4);
background-image: -o-linear-gradient(top, #729FCF, #3465A4);
background-image: linear-gradient(to bottom, #729FCF, #3465A4);
}

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Some files were not shown because too many files have changed in this diff Show More