[IMP]: Added Upstream patch for Mail
This commit is contained in:
parent
f4ba553e51
commit
d4c0819ce9
@ -2,6 +2,7 @@
|
|||||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
from flectra import api, exceptions, fields, models, _
|
from flectra import api, exceptions, fields, models, _
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ class MailActivity(models.Model):
|
|||||||
summary = fields.Char('Summary')
|
summary = fields.Char('Summary')
|
||||||
note = fields.Html('Note')
|
note = fields.Html('Note')
|
||||||
feedback = fields.Html('Feedback')
|
feedback = fields.Html('Feedback')
|
||||||
date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today)
|
date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today)
|
||||||
# description
|
# description
|
||||||
user_id = fields.Many2one(
|
user_id = fields.Many2one(
|
||||||
'res.users', 'Assigned to',
|
'res.users', 'Assigned to',
|
||||||
@ -108,8 +109,16 @@ class MailActivity(models.Model):
|
|||||||
|
|
||||||
@api.depends('date_deadline')
|
@api.depends('date_deadline')
|
||||||
def _compute_state(self):
|
def _compute_state(self):
|
||||||
today = date.today()
|
today_default = date.today()
|
||||||
|
|
||||||
for record in self.filtered(lambda activity: activity.date_deadline):
|
for record in self.filtered(lambda activity: activity.date_deadline):
|
||||||
|
today = today_default
|
||||||
|
tz = record.user_id.sudo().tz
|
||||||
|
if tz:
|
||||||
|
today_utc = pytz.UTC.localize(datetime.utcnow())
|
||||||
|
today_tz = today_utc.astimezone(pytz.timezone(tz))
|
||||||
|
today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day)
|
||||||
|
|
||||||
date_deadline = fields.Date.from_string(record.date_deadline)
|
date_deadline = fields.Date.from_string(record.date_deadline)
|
||||||
diff = (date_deadline - today)
|
diff = (date_deadline - today)
|
||||||
if diff.days == 0:
|
if diff.days == 0:
|
||||||
@ -132,7 +141,8 @@ class MailActivity(models.Model):
|
|||||||
|
|
||||||
@api.onchange('recommended_activity_type_id')
|
@api.onchange('recommended_activity_type_id')
|
||||||
def _onchange_recommended_activity_type_id(self):
|
def _onchange_recommended_activity_type_id(self):
|
||||||
self.activity_type_id = self.recommended_activity_type_id
|
if self.recommended_activity_type_id:
|
||||||
|
self.activity_type_id = self.recommended_activity_type_id
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _check_access(self, operation):
|
def _check_access(self, operation):
|
||||||
|
@ -505,7 +505,15 @@ class Channel(models.Model):
|
|||||||
partners_to_add = partners - channel.channel_partner_ids
|
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]})
|
channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
|
||||||
for partner in partners_to_add:
|
for partner in partners_to_add:
|
||||||
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
if partner.id != self.env.user.partner_id.id:
|
||||||
|
notification = _('<div class="o_mail_notification">%(author)s invited %(new_partner)s to <a href="#" class="o_channel_redirect" data-oe-id="%(channel_id)s">#%(channel_name)s</a></div>') % {
|
||||||
|
'author': self.env.user.display_name,
|
||||||
|
'new_partner': partner.display_name,
|
||||||
|
'channel_id': channel.id,
|
||||||
|
'channel_name': channel.name,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (channel.id, channel.name,)
|
||||||
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
|
self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
|
||||||
|
|
||||||
# broadcast the channel header to the added partner
|
# broadcast the channel header to the added partner
|
||||||
@ -674,7 +682,7 @@ class Channel(models.Model):
|
|||||||
msg += _(" This channel is private. People must be invited to join it.")
|
msg += _(" This channel is private. People must be invited to join it.")
|
||||||
else:
|
else:
|
||||||
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
|
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
|
||||||
msg = _("You are in a private conversation with <b>@%s</b>.") % channel_partners[0].partner_id.name
|
msg = _("You are in a private conversation with <b>@%s</b>.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
|
||||||
msg += _("""<br><br>
|
msg += _("""<br><br>
|
||||||
You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
|
You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
|
||||||
You can mention a channel by typing <b>#channel</b>.<br>
|
You can mention a channel by typing <b>#channel</b>.<br>
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
|
from openerp.http import request
|
||||||
|
|
||||||
from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools
|
from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools
|
||||||
from flectra.exceptions import UserError, AccessError
|
from flectra.exceptions import UserError, AccessError
|
||||||
@ -299,11 +300,12 @@ class Message(models.Model):
|
|||||||
|
|
||||||
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
|
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
|
||||||
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
|
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
|
||||||
|
safari = request and request.httprequest.user_agent.browser == 'safari'
|
||||||
attachments_tree = dict((attachment['id'], {
|
attachments_tree = dict((attachment['id'], {
|
||||||
'id': attachment['id'],
|
'id': attachment['id'],
|
||||||
'filename': attachment['datas_fname'],
|
'filename': attachment['datas_fname'],
|
||||||
'name': attachment['name'],
|
'name': attachment['name'],
|
||||||
'mimetype': attachment['mimetype'],
|
'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'],
|
||||||
}) for attachment in attachments_data)
|
}) for attachment in attachments_data)
|
||||||
|
|
||||||
# 3. Tracking values
|
# 3. Tracking values
|
||||||
@ -422,7 +424,18 @@ class Message(models.Model):
|
|||||||
subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']]
|
subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']]
|
||||||
subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description'])
|
subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description'])
|
||||||
subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes)
|
subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes)
|
||||||
|
|
||||||
|
# fetch notification status
|
||||||
|
notif_dict = {}
|
||||||
|
notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False)])
|
||||||
|
for notif in notifs:
|
||||||
|
mid = notif.mail_message_id.id
|
||||||
|
if not notif_dict.get(mid):
|
||||||
|
notif_dict[mid] = {'partner_id': list()}
|
||||||
|
notif_dict[mid]['partner_id'].append(notif.res_partner_id.id)
|
||||||
|
|
||||||
for message in message_values:
|
for message in message_values:
|
||||||
|
message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', [])
|
||||||
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal']
|
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal']
|
||||||
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
|
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
|
||||||
if message['model'] and self.env[message['model']]._original_module:
|
if message['model'] and self.env[message['model']]._original_module:
|
||||||
|
@ -126,7 +126,8 @@ class MailThread(models.AbstractModel):
|
|||||||
followers = self.env['mail.followers'].sudo().search([
|
followers = self.env['mail.followers'].sudo().search([
|
||||||
('res_model', '=', self._name),
|
('res_model', '=', self._name),
|
||||||
('partner_id', operator, operand)])
|
('partner_id', operator, operand)])
|
||||||
return [('id', 'in', followers.mapped('res_id'))]
|
# using read() below is much faster than followers.mapped('res_id')
|
||||||
|
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _search_follower_channels(self, operator, operand):
|
def _search_follower_channels(self, operator, operand):
|
||||||
@ -139,7 +140,8 @@ class MailThread(models.AbstractModel):
|
|||||||
followers = self.env['mail.followers'].sudo().search([
|
followers = self.env['mail.followers'].sudo().search([
|
||||||
('res_model', '=', self._name),
|
('res_model', '=', self._name),
|
||||||
('channel_id', operator, operand)])
|
('channel_id', operator, operand)])
|
||||||
return [('id', 'in', followers.mapped('res_id'))]
|
# using read() below is much faster than followers.mapped('res_id')
|
||||||
|
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@api.depends('message_follower_ids')
|
@api.depends('message_follower_ids')
|
||||||
@ -149,7 +151,8 @@ class MailThread(models.AbstractModel):
|
|||||||
('res_id', 'in', self.ids),
|
('res_id', 'in', self.ids),
|
||||||
('partner_id', '=', self.env.user.partner_id.id),
|
('partner_id', '=', self.env.user.partner_id.id),
|
||||||
])
|
])
|
||||||
following_ids = followers.mapped('res_id')
|
# using read() below is much faster than followers.mapped('res_id')
|
||||||
|
following_ids = [res['res_id'] for res in followers.read(['res_id'])]
|
||||||
for record in self:
|
for record in self:
|
||||||
record.message_is_follower = record.id in following_ids
|
record.message_is_follower = record.id in following_ids
|
||||||
|
|
||||||
@ -161,9 +164,11 @@ class MailThread(models.AbstractModel):
|
|||||||
])
|
])
|
||||||
# Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False)
|
# Cases ('message_is_follower', '=', True) or ('message_is_follower', '!=', False)
|
||||||
if (operator == '=' and operand) or (operator == '!=' and not operand):
|
if (operator == '=' and operand) or (operator == '!=' and not operand):
|
||||||
return [('id', 'in', followers.mapped('res_id'))]
|
# using read() below is much faster than followers.mapped('res_id')
|
||||||
|
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||||
else:
|
else:
|
||||||
return [('id', 'not in', followers.mapped('res_id'))]
|
# using read() below is much faster than followers.mapped('res_id')
|
||||||
|
return [('id', 'not in', [res['res_id'] for res in followers.read(['res_id'])])]
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_message_unread(self):
|
def _get_message_unread(self):
|
||||||
@ -1373,7 +1378,13 @@ class MailThread(models.AbstractModel):
|
|||||||
located in tools. """
|
located in tools. """
|
||||||
if not body:
|
if not body:
|
||||||
return body, attachments
|
return body, attachments
|
||||||
root = lxml.html.fromstring(body)
|
try:
|
||||||
|
root = lxml.html.fromstring(body)
|
||||||
|
except ValueError:
|
||||||
|
# In case the email client sent XHTML, fromstring will fail because 'Unicode strings
|
||||||
|
# with encoding declaration are not supported'.
|
||||||
|
root = lxml.html.fromstring(body.encode('utf-8'))
|
||||||
|
|
||||||
postprocessed = False
|
postprocessed = False
|
||||||
to_remove = []
|
to_remove = []
|
||||||
for node in root.iter():
|
for node in root.iter():
|
||||||
|
@ -58,6 +58,8 @@ class Users(models.Model):
|
|||||||
|
|
||||||
# create a welcome message
|
# create a welcome message
|
||||||
user._create_welcome_message()
|
user._create_welcome_message()
|
||||||
|
# Auto-subscribe to channels
|
||||||
|
self.env['mail.channel'].search([('group_ids', 'in', user.groups_id.ids)])._subscribe_users()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@ -127,16 +129,19 @@ class Users(models.Model):
|
|||||||
def activity_user_count(self):
|
def activity_user_count(self):
|
||||||
query = """SELECT m.id, count(*), act.res_model as model,
|
query = """SELECT m.id, count(*), act.res_model as model,
|
||||||
CASE
|
CASE
|
||||||
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
|
WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today'
|
||||||
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
|
WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
|
||||||
WHEN now()::date - act.date_deadline::date < 0 Then 'planned'
|
WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
|
||||||
END AS states
|
END AS states
|
||||||
FROM mail_activity AS act
|
FROM mail_activity AS act
|
||||||
JOIN ir_model AS m ON act.res_model_id = m.id
|
JOIN ir_model AS m ON act.res_model_id = m.id
|
||||||
WHERE user_id = %s
|
WHERE user_id = %(user_id)s
|
||||||
GROUP BY m.id, states, act.res_model;
|
GROUP BY m.id, states, act.res_model;
|
||||||
"""
|
"""
|
||||||
self.env.cr.execute(query, [self.env.uid])
|
self.env.cr.execute(query, {
|
||||||
|
'today': fields.Date.context_today(self),
|
||||||
|
'user_id': self.env.uid,
|
||||||
|
})
|
||||||
activity_data = self.env.cr.dictfetchall()
|
activity_data = self.env.cr.dictfetchall()
|
||||||
model_ids = [a['id'] for a in activity_data]
|
model_ids = [a['id'] for a in activity_data]
|
||||||
model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}
|
model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}
|
||||||
|
@ -32,6 +32,7 @@ function _readActivities(self, ids) {
|
|||||||
model: 'mail.activity',
|
model: 'mail.activity',
|
||||||
method: 'read',
|
method: 'read',
|
||||||
args: [ids],
|
args: [ids],
|
||||||
|
context: (self.record && self.record.getContext()) || self.getSession().user_context,
|
||||||
}).then(function (activities) {
|
}).then(function (activities) {
|
||||||
// convert create_date and date_deadline to moments
|
// convert create_date and date_deadline to moments
|
||||||
_.each(activities, function (activity) {
|
_.each(activities, function (activity) {
|
||||||
@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
|
|||||||
method: 'action_feedback',
|
method: 'action_feedback',
|
||||||
args: [[id]],
|
args: [[id]],
|
||||||
kwargs: {feedback: feedback},
|
kwargs: {feedback: feedback},
|
||||||
|
context: this.record.getContext(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_scheduleActivity: function (id, previous_activity_type_id, callback) {
|
_scheduleActivity: function (id, previous_activity_type_id, callback) {
|
||||||
|
@ -149,7 +149,16 @@ function make_message (data) {
|
|||||||
_.each(_.keys(emoji_substitutions), function (key) {
|
_.each(_.keys(emoji_substitutions), function (key) {
|
||||||
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
|
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
|
||||||
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
|
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
|
||||||
|
var msg_bak = msg.body;
|
||||||
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
|
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
|
||||||
|
|
||||||
|
// Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis,
|
||||||
|
// the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome).
|
||||||
|
// Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are
|
||||||
|
// already replaced by the unicode counterpart).
|
||||||
|
if (_.str.count(msg.body, 'o_mail_emoji') > 200) {
|
||||||
|
msg.body = msg_bak;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function property_descr(channel) {
|
function property_descr(channel) {
|
||||||
|
@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px';
|
|||||||
return Widget.extend({
|
return Widget.extend({
|
||||||
template: "mail.ChatWindow",
|
template: "mail.ChatWindow",
|
||||||
custom_events: {
|
custom_events: {
|
||||||
escape_pressed: '_onEscapePressed'
|
escape_pressed: '_onEscapePressed',
|
||||||
|
document_viewer_closed: '_onDocumentViewerClose',
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
'click .o_chat_composer': '_onComposerClick',
|
'click .o_chat_composer': '_onComposerClick',
|
||||||
@ -180,6 +181,9 @@ return Widget.extend({
|
|||||||
}
|
}
|
||||||
this.focus_input();
|
this.focus_input();
|
||||||
},
|
},
|
||||||
|
_onDocumentViewerClose: function (ev) {
|
||||||
|
this.focus_input();
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -514,6 +514,9 @@ var BasicComposer = Widget.extend(chat_mixin, {
|
|||||||
on_click_add_attachment: function () {
|
on_click_add_attachment: function () {
|
||||||
this.$('input.o_input_file').click();
|
this.$('input.o_input_file').click();
|
||||||
this.$input.focus();
|
this.$input.focus();
|
||||||
|
// set ignoreEscape to avoid escape_pressed event when file selector dialog is opened
|
||||||
|
// when user press escape to cancel file selector dialog then escape_pressed event should not be trigerred
|
||||||
|
this.ignoreEscape = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
setState: function (state) {
|
setState: function (state) {
|
||||||
@ -565,6 +568,8 @@ var BasicComposer = Widget.extend(chat_mixin, {
|
|||||||
if (this.mention_manager.is_open()) {
|
if (this.mention_manager.is_open()) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
this.mention_manager.reset_suggestions();
|
this.mention_manager.reset_suggestions();
|
||||||
|
} else if (this.ignoreEscape) {
|
||||||
|
this.ignoreEscape = false;
|
||||||
} else {
|
} else {
|
||||||
this.trigger_up("escape_pressed");
|
this.trigger_up("escape_pressed");
|
||||||
}
|
}
|
||||||
@ -789,6 +794,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
|
|||||||
* @param {MouseEvent} event
|
* @param {MouseEvent} event
|
||||||
*/
|
*/
|
||||||
_onAttachmentView: function (event) {
|
_onAttachmentView: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
var activeAttachmentID = $(event.currentTarget).data('id');
|
var activeAttachmentID = $(event.currentTarget).data('id');
|
||||||
var attachments = this.get('attachment_ids');
|
var attachments = this.get('attachment_ids');
|
||||||
if (activeAttachmentID) {
|
if (activeAttachmentID) {
|
||||||
|
@ -25,6 +25,7 @@ var DocumentViewer = Widget.extend({
|
|||||||
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
|
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
|
||||||
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
|
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
|
||||||
'keydown': '_onKeydown',
|
'keydown': '_onKeydown',
|
||||||
|
'keyup': '_onKeyUp',
|
||||||
'mousedown .o_viewer_img': '_onStartDrag',
|
'mousedown .o_viewer_img': '_onStartDrag',
|
||||||
'mousemove .o_viewer_content': '_onDrag',
|
'mousemove .o_viewer_content': '_onDrag',
|
||||||
'mouseup .o_viewer_content': '_onEndDrag'
|
'mouseup .o_viewer_content': '_onEndDrag'
|
||||||
@ -152,6 +153,7 @@ var DocumentViewer = Widget.extend({
|
|||||||
_onClose: function (e) {
|
_onClose: function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.$el.modal('hide');
|
this.$el.modal('hide');
|
||||||
|
this.trigger_up('document_viewer_closed');
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* When popup close complete destroyed modal even DOM footprint too
|
* When popup close complete destroyed modal even DOM footprint too
|
||||||
@ -232,6 +234,20 @@ var DocumentViewer = Widget.extend({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Close popup on ESCAPE keyup
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {KeyEvent} e
|
||||||
|
*/
|
||||||
|
_onKeyUp: function (e) {
|
||||||
|
switch (e.which) {
|
||||||
|
case $.ui.keyCode.ESCAPE:
|
||||||
|
e.preventDefault();
|
||||||
|
this._onClose(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {MouseEvent} e
|
* @param {MouseEvent} e
|
||||||
|
@ -122,7 +122,7 @@ var MessagingMenu = Widget.extend({
|
|||||||
if (channelID === 'channel_inbox') {
|
if (channelID === 'channel_inbox') {
|
||||||
var resID = $(event.currentTarget).data('res_id');
|
var resID = $(event.currentTarget).data('res_id');
|
||||||
var resModel = $(event.currentTarget).data('res_model');
|
var resModel = $(event.currentTarget).data('res_model');
|
||||||
if (resModel && resID) {
|
if (resModel && resModel !== 'mail.channel' && resID) {
|
||||||
this.do_action({
|
this.do_action({
|
||||||
type: 'ir.actions.act_window',
|
type: 'ir.actions.act_window',
|
||||||
res_model: resModel,
|
res_model: resModel,
|
||||||
@ -130,7 +130,11 @@ var MessagingMenu = Widget.extend({
|
|||||||
res_id: resID
|
res_id: resID
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
|
var clientChatOptions = {clear_breadcrumbs: true};
|
||||||
|
if (resModel && resModel === 'mail.channel' && resID) {
|
||||||
|
clientChatOptions.active_id = resID;
|
||||||
|
}
|
||||||
|
this.do_action('mail.mail_channel_action_client_chat', clientChatOptions)
|
||||||
.then(function () {
|
.then(function () {
|
||||||
self.trigger_up('hide_app_switcher');
|
self.trigger_up('hide_app_switcher');
|
||||||
core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());
|
core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());
|
||||||
|
@ -328,6 +328,7 @@ var Thread = Widget.extend({
|
|||||||
* @param {MouseEvent} event
|
* @param {MouseEvent} event
|
||||||
*/
|
*/
|
||||||
_onAttachmentView: function (event) {
|
_onAttachmentView: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
var activeAttachmentID = $(event.currentTarget).data('id');
|
var activeAttachmentID = $(event.currentTarget).data('id');
|
||||||
if (activeAttachmentID) {
|
if (activeAttachmentID) {
|
||||||
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
|
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
|
||||||
|
@ -60,7 +60,7 @@ function _parse_and_transform(nodes, transform_function) {
|
|||||||
|
|
||||||
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
// Suggested URL Javascript regex of http://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url
|
||||||
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
|
// Adapted to make http(s):// not required if (and only if) www. is given. So `should.notmatch` does not match.
|
||||||
var url_regexp = /\b(?:https?:\/\/|(www\.))[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi;
|
var url_regexp = /\b(?:https?:\/\/\d{1,3}(?:\.\d{1,3}){3}|(?:https?:\/\/|(?:www\.))[-a-z0-9@:%._\+~#=]{2,256}\.[a-z]{2,13})\b(?:[-a-z0-9@:%_\+.~#?&'$//=]*)/gi;
|
||||||
function linkify(text, attrs) {
|
function linkify(text, attrs) {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
if (attrs.target === undefined) {
|
if (attrs.target === undefined) {
|
||||||
@ -70,7 +70,7 @@ function linkify(text, attrs) {
|
|||||||
return key + '="' + _.escape(value) + '"';
|
return key + '="' + _.escape(value) + '"';
|
||||||
}).join(' ');
|
}).join(' ');
|
||||||
return text.replace(url_regexp, function (url) {
|
return text.replace(url_regexp, function (url) {
|
||||||
var href = (!/^(f|ht)tps?:\/\//i.test(url)) ? "http://" + url : url;
|
var href = (!/^https?:\/\//i.test(url)) ? "http://" + url : url;
|
||||||
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
|
return '<a ' + attrs + ' href="' + href + '">' + url + '</a>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -92,6 +92,7 @@ function strip_html (node, transform_children) {
|
|||||||
|
|
||||||
function inline (node, transform_children) {
|
function inline (node, transform_children) {
|
||||||
if (node.nodeType === 3) return node.data;
|
if (node.nodeType === 3) return node.data;
|
||||||
|
if (node.nodeType === 8) return "";
|
||||||
if (node.tagName === "BR") return " ";
|
if (node.tagName === "BR") return " ";
|
||||||
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
|
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return transform_children();
|
||||||
node.innerHTML = transform_children();
|
node.innerHTML = transform_children();
|
||||||
@ -100,16 +101,23 @@ function inline (node, transform_children) {
|
|||||||
|
|
||||||
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
|
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
|
||||||
function parse_email (text) {
|
function parse_email (text) {
|
||||||
var result = text.match(/(.*)<(.*@.*)>/);
|
if (text){
|
||||||
if (result) {
|
var result = text.match(/(.*)<(.*@.*)>/);
|
||||||
return [_.str.trim(result[1]), _.str.trim(result[2])];
|
if (result) {
|
||||||
|
return [_.str.trim(result[1]), _.str.trim(result[2])];
|
||||||
|
}
|
||||||
|
result = text.match(/(.*@.*)/);
|
||||||
|
if (result) {
|
||||||
|
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
||||||
|
}
|
||||||
|
return [text, false];
|
||||||
}
|
}
|
||||||
result = text.match(/(.*@.*)/);
|
/* result = text.match(/(.*@.*)/);
|
||||||
if (result) {
|
if (result) {
|
||||||
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
||||||
}
|
}
|
||||||
return [text, false];
|
return [text, false];
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// Replaces textarea text into html text (add <p>, <a>)
|
// Replaces textarea text into html text (add <p>, <a>)
|
||||||
// TDE note : should be done server-side, in Python -> use mail.compose.message ?
|
// TDE note : should be done server-side, in Python -> use mail.compose.message ?
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.o_composer_button_full_composer {
|
.o_composer_button_full_composer {
|
||||||
.o-position-absolute(0, 0);
|
.o-position-absolute(auto, 0);
|
||||||
}
|
}
|
||||||
@media (max-width: @screen-xs-max) {
|
@media (max-width: @screen-xs-max) {
|
||||||
.o_composer_button_send {
|
.o_composer_button_send {
|
||||||
@ -136,7 +136,7 @@
|
|||||||
|
|
||||||
&.o_chat_inline_composer {
|
&.o_chat_inline_composer {
|
||||||
.o_composer_container {
|
.o_composer_container {
|
||||||
.o-flex(1, 0, auto);
|
.o-flex(1, 1, auto);
|
||||||
}
|
}
|
||||||
.o_composer {
|
.o_composer {
|
||||||
padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap;
|
padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap;
|
||||||
@ -339,6 +339,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_modal_fullscreen {
|
.o_modal_fullscreen {
|
||||||
|
z-index: @o-chat-window-zindex + 1; // To overlap chat window
|
||||||
.o_viewer_content {
|
.o_viewer_content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -107,7 +107,7 @@
|
|||||||
&.o_mail_note {
|
&.o_mail_note {
|
||||||
background-color: @mail-thread-note;
|
background-color: @mail-thread-note;
|
||||||
padding-left: @grid-gutter-width*0.3;
|
padding-left: @grid-gutter-width*0.3;
|
||||||
border-bottom: 1px solid @gray-lighter-dark;
|
border-bottom: 1px solid @gray-lighter-darker;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_mail_subject {
|
.o_mail_subject {
|
||||||
|
@ -76,7 +76,7 @@
|
|||||||
</t>
|
</t>
|
||||||
|
|
||||||
<t t-name="DocumentViewer">
|
<t t-name="DocumentViewer">
|
||||||
<div class="modal o_modal_fullscreen" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog" aria-hidden="true">
|
||||||
<t t-call="DocumentViewer.Content"/>
|
<t t-call="DocumentViewer.Content"/>
|
||||||
|
|
||||||
<t t-if="widget.attachment.length != 1">
|
<t t-if="widget.attachment.length != 1">
|
||||||
|
@ -50,6 +50,84 @@ QUnit.module('mail', {}, function () {
|
|||||||
parent.destroy();
|
parent.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('open document viewer and close using ESCAPE key should reset focus to chat window', function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
|
||||||
|
function createParent(params) {
|
||||||
|
var widget = new Widget();
|
||||||
|
|
||||||
|
testUtils.addMockEnvironment(widget, params);
|
||||||
|
return widget;
|
||||||
|
}
|
||||||
|
var messages = [{
|
||||||
|
attachment_ids: [{
|
||||||
|
filename: 'image1.jpg',
|
||||||
|
id:1,
|
||||||
|
mimetype: 'image/jpeg',
|
||||||
|
name: 'Test Image 1',
|
||||||
|
url: '/web/content/1?download=true'
|
||||||
|
}],
|
||||||
|
author_id: ["1", "John Doe"],
|
||||||
|
body: "A message",
|
||||||
|
date: moment("2016-12-20 09:35:40"),
|
||||||
|
displayed_author: "John Doe",
|
||||||
|
id: 1,
|
||||||
|
is_note: false,
|
||||||
|
is_starred: false,
|
||||||
|
model: 'partner',
|
||||||
|
res_id: 2
|
||||||
|
}];
|
||||||
|
var parent = createParent({
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if(_.str.contains(route, '/mail/attachment/preview/') ||
|
||||||
|
_.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
return $.when(canvas.toDataURL());
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
var chatWindow = new ChatWindow(parent, 1, "user", false, messages.length, {});
|
||||||
|
chatWindow.appendTo($('#qunit-fixture'));
|
||||||
|
chatWindow.render(messages);
|
||||||
|
|
||||||
|
testUtils.intercept(chatWindow, 'get_messages', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
var requested_msgs = _.filter(messages, function (msg) {
|
||||||
|
return _.contains(event.data.options.ids, msg.id);
|
||||||
|
});
|
||||||
|
event.data.callback($.when(requested_msgs));
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
testUtils.intercept(chatWindow, 'get_bus', function(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.data.callback(new Bus());
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
chatWindow.on('document_viewer_closed', null, function () {
|
||||||
|
assert.ok(true, "chat window should trigger a close document viewer event");
|
||||||
|
});
|
||||||
|
assert.strictEqual(chatWindow.$('.o_thread_message .o_attachment').length, 1,
|
||||||
|
"there should be three attachment on message");
|
||||||
|
// click on first image attachement
|
||||||
|
chatWindow.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first().click();
|
||||||
|
// check focus is on document viewer popup and then press escape to close it
|
||||||
|
assert.strictEqual(document.activeElement, $('.o_modal_fullscreen')[0], "Modal popup should have focus");
|
||||||
|
assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[src*="/web/image/1?unique=1"]').length, 1,
|
||||||
|
"Modal popup should open with first image src");
|
||||||
|
// trigger ESCAPE keyup on document viewer popup
|
||||||
|
var upKeyEvent = jQuery.Event("keyup", {which: 27});
|
||||||
|
$('.o_modal_fullscreen').trigger(upKeyEvent);
|
||||||
|
assert.strictEqual(document.activeElement, chatWindow.$input[0],
|
||||||
|
"input should be focused");
|
||||||
|
var upKeyEvent = jQuery.Event( "keyup", {which: 27});
|
||||||
|
chatWindow.$('.o_composer_input').trigger(upKeyEvent);
|
||||||
|
assert.strictEqual(chatWindow.folded, false, "Closed chat Window");
|
||||||
|
parent.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) {
|
QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ QUnit.module('mail', {}, function () {
|
|||||||
QUnit.module('Mail utils');
|
QUnit.module('Mail utils');
|
||||||
|
|
||||||
QUnit.test('add_link utility function', function (assert) {
|
QUnit.test('add_link utility function', function (assert) {
|
||||||
assert.expect(7);
|
assert.expect(15);
|
||||||
|
|
||||||
var testInputs = {
|
var testInputs = {
|
||||||
'http://admin:password@example.com:8/%2020': true,
|
'http://admin:password@example.com:8/%2020': true,
|
||||||
@ -24,6 +24,7 @@ QUnit.test('add_link utility function', function (assert) {
|
|||||||
var output = utils.parse_and_transform(content, utils.add_link);
|
var output = utils.parse_and_transform(content, utils.add_link);
|
||||||
if (willLinkify) {
|
if (willLinkify) {
|
||||||
assert.strictEqual(output.indexOf('<a '), 0, "There should be a link");
|
assert.strictEqual(output.indexOf('<a '), 0, "There should be a link");
|
||||||
|
assert.strictEqual(output.indexOf('</a>'), (output.length - 4), "Link should match the whole text");
|
||||||
} else {
|
} else {
|
||||||
assert.strictEqual(output.indexOf('<a '), -1, "There should be no link");
|
assert.strictEqual(output.indexOf('<a '), -1, "There should be no link");
|
||||||
}
|
}
|
||||||
|
@ -366,6 +366,18 @@ class TestMailgateway(TestMail):
|
|||||||
self.assertEqual(res['body'], '')
|
self.assertEqual(res['body'], '')
|
||||||
self.assertEqual(res['attachments'][0][0], 'thetruth.pdf')
|
self.assertEqual(res['attachments'][0][0], 'thetruth.pdf')
|
||||||
|
|
||||||
|
@mute_logger('flectra.addons.mail.models.mail_thread')
|
||||||
|
def test_message_parse_eml(self):
|
||||||
|
""" Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
|
||||||
|
"""
|
||||||
|
self.env['mail.thread'].message_process('mail.channel', MAIL_EML_ATTACHMENT)
|
||||||
|
|
||||||
|
@mute_logger('flectra.addons.mail.models.mail_thread')
|
||||||
|
def test_message_parse_xhtml(self):
|
||||||
|
""" Test that the parsing of mail with embedded emails as eml(msg) which generates empty attachments, can be processed.
|
||||||
|
"""
|
||||||
|
self.env['mail.thread'].message_process('mail.channel', MAIL_XHTML)
|
||||||
|
|
||||||
@mute_logger('flectra.addons.mail.models.mail_thread')
|
@mute_logger('flectra.addons.mail.models.mail_thread')
|
||||||
def test_message_process_cid(self):
|
def test_message_process_cid(self):
|
||||||
new_groups = self.format_and_process(MAIL_MULTIPART_IMAGE, subject='My Frogs', to='groups@example.com')
|
new_groups = self.format_and_process(MAIL_MULTIPART_IMAGE, subject='My Frogs', to='groups@example.com')
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-6 o_setting_box" title="Using your own email server is required to send/receive emails in Community and Enterprise versions. Online users already benefit from a ready-to-use email server (@mycompany.flectra.com).">
|
<div class="col-xs-12 col-md-6 o_setting_box" title="Using your own email server is required to send/receive emails in Community versions. Online users already benefit from a ready-to-use email server (@mycompany.flectra.com).">
|
||||||
<div class="o_setting_left_pane">
|
<div class="o_setting_left_pane">
|
||||||
<field name="default_external_email_server"/>
|
<field name="default_external_email_server"/>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user