[IMP]: Added Upstream patch for Mail

This commit is contained in:
Hansa Rathod 2018-07-10 18:38:51 +05:30
parent f4ba553e51
commit d4c0819ce9
20 changed files with 222 additions and 33 deletions

View File

@ -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):

View File

@ -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>

View File

@ -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:

View File

@ -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():

View File

@ -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()}

View File

@ -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) {

View File

@ -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) {

View File

@ -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
*/ */

View File

@ -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) {

View File

@ -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

View File

@ -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());

View File

@ -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);

View File

@ -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 ?

View File

@ -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%;

View File

@ -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 {

View File

@ -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">

View File

@ -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);

View File

@ -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");
} }

View File

@ -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')

View File

@ -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>