[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.
from datetime import date, datetime, timedelta
import pytz
from flectra import api, exceptions, fields, models, _
@ -77,7 +78,7 @@ class MailActivity(models.Model):
summary = fields.Char('Summary')
note = fields.Html('Note')
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
user_id = fields.Many2one(
'res.users', 'Assigned to',
@ -108,8 +109,16 @@ class MailActivity(models.Model):
@api.depends('date_deadline')
def _compute_state(self):
today = date.today()
today_default = date.today()
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)
diff = (date_deadline - today)
if diff.days == 0:
@ -132,7 +141,8 @@ class MailActivity(models.Model):
@api.onchange('recommended_activity_type_id')
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
def _check_access(self, operation):

View File

@ -505,7 +505,15 @@ class Channel(models.Model):
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]})
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)
# 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.")
else:
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>
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>

View File

@ -5,6 +5,7 @@ import logging
import re
from email.utils import formataddr
from openerp.http import request
from flectra import _, api, fields, models, modules, SUPERUSER_ID, tools
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
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
safari = request and request.httprequest.user_agent.browser == 'safari'
attachments_tree = dict((attachment['id'], {
'id': attachment['id'],
'filename': attachment['datas_fname'],
'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)
# 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']]
subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description'])
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:
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['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
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([
('res_model', '=', self._name),
('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
def _search_follower_channels(self, operator, operand):
@ -139,7 +140,8 @@ class MailThread(models.AbstractModel):
followers = self.env['mail.followers'].sudo().search([
('res_model', '=', self._name),
('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.depends('message_follower_ids')
@ -149,7 +151,8 @@ class MailThread(models.AbstractModel):
('res_id', 'in', self.ids),
('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:
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)
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:
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
def _get_message_unread(self):
@ -1373,7 +1378,13 @@ class MailThread(models.AbstractModel):
located in tools. """
if not body:
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
to_remove = []
for node in root.iter():

View File

@ -58,6 +58,8 @@ class Users(models.Model):
# create a 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
@api.multi
@ -127,16 +129,19 @@ class Users(models.Model):
def activity_user_count(self):
query = """SELECT m.id, count(*), act.res_model as model,
CASE
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
WHEN now()::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 'today'
WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
END AS states
FROM mail_activity AS act
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;
"""
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()
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()}

View File

@ -32,6 +32,7 @@ function _readActivities(self, ids) {
model: 'mail.activity',
method: 'read',
args: [ids],
context: (self.record && self.record.getContext()) || self.getSession().user_context,
}).then(function (activities) {
// convert create_date and date_deadline to moments
_.each(activities, function (activity) {
@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
method: 'action_feedback',
args: [[id]],
kwargs: {feedback: feedback},
context: this.record.getContext(),
});
},
_scheduleActivity: function (id, previous_activity_type_id, callback) {

View File

@ -149,7 +149,16 @@ function make_message (data) {
_.each(_.keys(emoji_substitutions), function (key) {
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
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> ');
// 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) {

View File

@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px';
return Widget.extend({
template: "mail.ChatWindow",
custom_events: {
escape_pressed: '_onEscapePressed'
escape_pressed: '_onEscapePressed',
document_viewer_closed: '_onDocumentViewerClose',
},
events: {
'click .o_chat_composer': '_onComposerClick',
@ -180,6 +181,9 @@ return Widget.extend({
}
this.focus_input();
},
_onDocumentViewerClose: function (ev) {
this.focus_input();
},
/**
* @private
*/

View File

@ -514,6 +514,9 @@ var BasicComposer = Widget.extend(chat_mixin, {
on_click_add_attachment: function () {
this.$('input.o_input_file').click();
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) {
@ -565,6 +568,8 @@ var BasicComposer = Widget.extend(chat_mixin, {
if (this.mention_manager.is_open()) {
event.stopPropagation();
this.mention_manager.reset_suggestions();
} else if (this.ignoreEscape) {
this.ignoreEscape = false;
} else {
this.trigger_up("escape_pressed");
}
@ -789,6 +794,7 @@ var BasicComposer = Widget.extend(chat_mixin, {
* @param {MouseEvent} event
*/
_onAttachmentView: function (event) {
event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id');
var attachments = this.get('attachment_ids');
if (activeAttachmentID) {

View File

@ -25,6 +25,7 @@ var DocumentViewer = Widget.extend({
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
'keydown': '_onKeydown',
'keyup': '_onKeyUp',
'mousedown .o_viewer_img': '_onStartDrag',
'mousemove .o_viewer_content': '_onDrag',
'mouseup .o_viewer_content': '_onEndDrag'
@ -152,6 +153,7 @@ var DocumentViewer = Widget.extend({
_onClose: function (e) {
e.preventDefault();
this.$el.modal('hide');
this.trigger_up('document_viewer_closed');
},
/**
* When popup close complete destroyed modal even DOM footprint too
@ -232,6 +234,20 @@ var DocumentViewer = Widget.extend({
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
* @param {MouseEvent} e

View File

@ -122,7 +122,7 @@ var MessagingMenu = Widget.extend({
if (channelID === 'channel_inbox') {
var resID = $(event.currentTarget).data('res_id');
var resModel = $(event.currentTarget).data('res_model');
if (resModel && resID) {
if (resModel && resModel !== 'mail.channel' && resID) {
this.do_action({
type: 'ir.actions.act_window',
res_model: resModel,
@ -130,7 +130,11 @@ var MessagingMenu = Widget.extend({
res_id: resID
});
} 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 () {
self.trigger_up('hide_app_switcher');
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
*/
_onAttachmentView: function (event) {
event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id');
if (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
// 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) {
attrs = attrs || {};
if (attrs.target === undefined) {
@ -70,7 +70,7 @@ function linkify(text, attrs) {
return key + '="' + _.escape(value) + '"';
}).join(' ');
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>';
});
}
@ -92,6 +92,7 @@ function strip_html (node, transform_children) {
function inline (node, transform_children) {
if (node.nodeType === 3) return node.data;
if (node.nodeType === 8) return "";
if (node.tagName === "BR") return " ";
if (node.tagName.match(/^(A|P|DIV|PRE|BLOCKQUOTE)$/)) return 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
function parse_email (text) {
var result = text.match(/(.*)<(.*@.*)>/);
if (result) {
return [_.str.trim(result[1]), _.str.trim(result[2])];
if (text){
var result = text.match(/(.*)<(.*@.*)>/);
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) {
return [_.str.trim(result[1]), _.str.trim(result[1])];
}
return [text, false];
}
}*/
// Replaces textarea text into html text (add <p>, <a>)
// TDE note : should be done server-side, in Python -> use mail.compose.message ?

View File

@ -51,7 +51,7 @@
border: none;
}
.o_composer_button_full_composer {
.o-position-absolute(0, 0);
.o-position-absolute(auto, 0);
}
@media (max-width: @screen-xs-max) {
.o_composer_button_send {
@ -136,7 +136,7 @@
&.o_chat_inline_composer {
.o_composer_container {
.o-flex(1, 0, auto);
.o-flex(1, 1, auto);
}
.o_composer {
padding: @o-mail-chatter-gap @o-mail-chatter-gap 0 @o-mail-chatter-gap;
@ -339,6 +339,7 @@
}
.o_modal_fullscreen {
z-index: @o-chat-window-zindex + 1; // To overlap chat window
.o_viewer_content {
width: 100%;
height: 100%;

View File

@ -107,7 +107,7 @@
&.o_mail_note {
background-color: @mail-thread-note;
padding-left: @grid-gutter-width*0.3;
border-bottom: 1px solid @gray-lighter-dark;
border-bottom: 1px solid @gray-lighter-darker;
}
.o_mail_subject {

View File

@ -76,7 +76,7 @@
</t>
<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-if="widget.attachment.length != 1">

View File

@ -50,6 +50,84 @@ QUnit.module('mail', {}, function () {
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) {
assert.expect(2);

View File

@ -8,7 +8,7 @@ QUnit.module('mail', {}, function () {
QUnit.module('Mail utils');
QUnit.test('add_link utility function', function (assert) {
assert.expect(7);
assert.expect(15);
var testInputs = {
'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);
if (willLinkify) {
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 {
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['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')
def test_message_process_cid(self):
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 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">
<field name="default_external_email_server"/>
</div>