From af29bf7bc92c291e50f7b7a7c08b713c1f3214b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 22 Oct 2013 15:50:37 +0200 Subject: [PATCH 01/25] [FIX] tools: mail: fixed shortening of html content. Fixed length computation of text in html nodes: multiples successive whitespaces are considered as one whitespaces; better truncate position when adding a read more link; now always protect words (placed after the first word that exceeds the shorten position); pre nodes are preserved about whitespaces; when the read more link should go into a quote, it instead goes at the end of the first parent node not being quoted instead of at a wrong position. Added tests for shorten position. bzr revid: tde@openerp.com-20131022135037-igauu2kkglvdrqu7 --- openerp/tests/test_mail.py | 55 +++++++++++++++++++++++++++- openerp/tools/mail.py | 74 +++++++++++++++++++++++++------------- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/openerp/tests/test_mail.py b/openerp/tests/test_mail.py index c8bbc0c862b..df1b5e15a11 100755 --- a/openerp/tests/test_mail.py +++ b/openerp/tests/test_mail.py @@ -149,6 +149,59 @@ class TestCleaner(unittest2.TestCase): for text in out_lst: self.assertNotIn(text, new_html, 'html_email_cleaner did not remove unwanted content') + def test_05_shorten(self): + # TEST: shorten length + test_str = '''
+ + +

Hello, Raoul + You are + pretty

+Really +
+''' + # shorten at 'H' of Hello -> should shorten after Hello, + html = html_email_clean(test_str, shorten=True, max_length=1, remove=True) + self.assertIn('Hello,', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('Raoul', html, 'html_email_cleaner: shorten error or too long') + self.assertIn('read more', html, 'html_email_cleaner: shorten error about read more inclusion') + # shorten at 'are' -> should shorten after are + html = html_email_clean(test_str, shorten=True, max_length=17, remove=True) + self.assertIn('Hello,', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('Raoul', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('are', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('pretty', html, 'html_email_cleaner: shorten error or too long') + self.assertNotIn('Really', html, 'html_email_cleaner: shorten error or too long') + self.assertIn('read more', html, 'html_email_cleaner: shorten error about read more inclusion') + + # TEST: shorten in quote + test_str = '''
Blahble + bluih blouh +
This is a quote + And this is quite a long quote, after all. +
+
''' + # shorten in the quote + html = html_email_clean(test_str, shorten=True, max_length=25, remove=True) + self.assertIn('Blahble', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('bluih', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('blouh', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('quote', html, 'html_email_cleaner: shorten error or too long') + self.assertIn('read more', html, 'html_email_cleaner: shorten error about read more inclusion') + # shorten in second word + html = html_email_clean(test_str, shorten=True, max_length=9, remove=True) + self.assertIn('Blahble', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('bluih', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('blouh', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('quote', html, 'html_email_cleaner: shorten error or too long') + self.assertIn('read more', html, 'html_email_cleaner: shorten error about read more inclusion') + # shorten waaay too large + html = html_email_clean(test_str, shorten=True, max_length=900, remove=True) + self.assertIn('Blahble', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('bluih', html, 'html_email_cleaner: shorten error or too short') + self.assertIn('blouh', html, 'html_email_cleaner: shorten error or too short') + self.assertNotIn('quote', html, 'html_email_cleaner: shorten error or too long') + def test_10_email_text(self): """ html_email_clean test for text-based emails """ new_html = html_email_clean(test_mail_examples.TEXT_1, remove=True) @@ -230,7 +283,7 @@ class TestCleaner(unittest2.TestCase): for ext in test_mail_examples.BUG_1_OUT: self.assertNotIn(ext, new_html, 'html_email_cleaner did not removed invalid content') - new_html = html_email_clean(test_mail_examples.BUG2, remove=True, shorten=True, max_length=4000) + new_html = html_email_clean(test_mail_examples.BUG2, remove=True, shorten=True, max_length=250) for ext in test_mail_examples.BUG_2_IN: self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed valid content') for ext in test_mail_examples.BUG_2_OUT: diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index b287ded9d6d..2e84f0b4420 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -175,20 +175,38 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): iteration += 1 new_node = _insert_new_node(node, -1, new_node_tag, text[idx:] + (cur_node.tail or ''), None, {}) - def _truncate_node(node, position, find_first_blank=True): + def _truncate_node(node, position, simplify_whitespaces=True): + """ Truncate a node text at a given position. This algorithm will shorten + at the end of the word whose ending character exceeds position. + + :param bool simplify_whitespaces: whether to try to count all successive + whitespaces as one character. This + option should not be True when trying + to keep 'pre' consistency. + """ if node.text is None: node.text = '' - # truncate text - end_position = position if len(node.text) >= position else len(node.text) - innertext = node.text[0:end_position] - outertext = node.text[end_position:] - if find_first_blank: - stop_idx = outertext.find(' ') + + if simplify_whitespaces: + cur_char_nbr = 0 + word = None + node_words = node.text.strip(' \t\r\n').split() + for word in node_words: + cur_char_nbr += len(word) + if cur_char_nbr >= position: + break + stop_idx = node.text.find(word) + len(word) if stop_idx == -1: - stop_idx = len(outertext) + stop_idx = len(node.text) else: - stop_idx = 0 - node.text = innertext + outertext[0:stop_idx] + stop_idx = position + stop_idx = stop_idx if len(node.text) >= stop_idx else len(node.text) + + # compose new text bits + innertext = node.text[0:stop_idx] + outertext = node.text[stop_idx:] + node.text = innertext + # create ... read more node read_more_node = _create_node('span', ' ... ', None, {'class': 'oe_mail_expand'}) read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'}) @@ -223,17 +241,16 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): html = '
%s
' % html root = lxml.html.fromstring(html) - # remove all tails and replace them by a span element, because managing text and tails can be a pain in the ass - for node in root.getiterator(): + quote_tags = re.compile(r'(\n(>)+[^\n\r]*)') + signature = re.compile(r'([-]{2,}[\s]?[\r\n]{1,2}[\s\S]+)') + for node in root.iter(): + # remove all tails and replace them by a span element, because managing text and tails can be a pain in the ass if node.tail: tail_node = _create_node('span', node.tail) node.tail = None node.addnext(tail_node) - # form node and tag text-based quotes and signature - quote_tags = re.compile(r'(\n(>)+[^\n\r]*)') - signature = re.compile(r'([-]{2,}[\s]?[\r\n]{1,2}[\s\S]+)') - for node in root.getiterator(): + # form node and tag text-based quotes and signature _tag_matching_regex_in_text(quote_tags, node, 'span', {'text_quote': '1'}) _tag_matching_regex_in_text(signature, node, 'span', {'text_signature': '1'}) @@ -245,7 +262,10 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): quote_begin = False overlength = False cur_char_nbr = 0 - for node in root.getiterator(): + for node in root.iter(): + # node_text = re.sub('\s{2,}', ' ', node.text and node.text.strip(' \t\r\n') or '') # do not take into account multiple spaces that are displayed as max 1 space in html + node_text = ' '.join((node.text and node.text.strip(' \t\r\n') or '').split()) + # root: try to tag the client used to write the html if 'WordSection1' in node.get('class', '') or 'MsoNormal' in node.get('class', ''): root.set('msoffice', '1') @@ -277,27 +297,30 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): # 1/ truncate the text at the next available space # 2/ create a 'read more' node, next to current node # 3/ add the truncated text in a new node, next to 'read more' node - if shorten and not overlength and cur_char_nbr + len(node.text or '') > max_length: + if shorten and not overlength and cur_char_nbr + len(node_text) > max_length: node_to_truncate = node while node_to_truncate.get('in_quote') and node_to_truncate.getparent() is not None: node_to_truncate = node_to_truncate.getparent() overlength = True node_to_truncate.set('truncate', '1') - node_to_truncate.set('truncate_position', str(max_length - cur_char_nbr)) - cur_char_nbr += len(node.text or '') + if node_to_truncate == node: + node_to_truncate.set('truncate_position', str(max_length - cur_char_nbr)) + else: + node_to_truncate.set('truncate_position', str(len(node.text or ''))) + cur_char_nbr += len(node_text) # Tree modification # ------------------------------------------------------------ for node in root.iter(): if node.get('truncate'): - _truncate_node(node, int(node.get('truncate_position', '0'))) + _truncate_node(node, int(node.get('truncate_position', '0')), node.tag != 'pre') # Post processing # ------------------------------------------------------------ to_remove = [] - for node in root.getiterator(): + for node in root.iter(): if node.get('in_quote') or node.get('in_overlength'): # copy the node tail into parent text if node.tail and not node.get('tail_remove'): @@ -306,17 +329,20 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): to_remove.append(node) if node.get('tail_remove'): node.tail = '' + # clean node + for attribute_name in ['in_quote', 'tail_remove', 'in_overlength', 'msoffice', 'hotmail', 'truncate', 'truncate_position']: + node.attrib.pop(attribute_name, None) for node in to_remove: if remove: node.getparent().remove(node) else: if not 'oe_mail_expand' in node.get('class', ''): # trick: read more link should be displayed even if it's in overlength - node_class = node.get('class', '') + ' ' + 'oe_mail_cleaned' + node_class = node.get('class', '') + ' oe_mail_cleaned' node.set('class', node_class) # html: \n that were tail of elements have been encapsulated into -> back to \n html = etree.tostring(root, pretty_print=False) - linebreaks = re.compile(r'([\s]*[\r\n]+[\s]*)<\/span>', re.IGNORECASE | re.DOTALL) + linebreaks = re.compile(r']*>([\s]*[\r\n]+[\s]*)<\/span>', re.IGNORECASE | re.DOTALL) html = _replace_matching_regex(linebreaks, html, '\n') return html From 57c11338ae23c8389da6c4969e897e7b95b782bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 22 Oct 2013 16:29:08 +0200 Subject: [PATCH 02/25] [IMP] tools: mail: added a protection in a string.find, could have a None argument bzr revid: tde@openerp.com-20131022142908-sol44xaprx1b0b0a --- openerp/tools/mail.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 2e84f0b4420..6ab7119236c 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -195,7 +195,10 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): cur_char_nbr += len(word) if cur_char_nbr >= position: break - stop_idx = node.text.find(word) + len(word) + if word: + stop_idx = node.text.find(word) + len(word) + else: + stop_idx = len(node.text) if stop_idx == -1: stop_idx = len(node.text) else: From 7f61d18ef918ba065179e4893e994841a5f63c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Oct 2013 14:27:44 +0200 Subject: [PATCH 03/25] [CLEAN] tools: cleaned modified code bzr revid: tde@openerp.com-20131023122744-3b3hayy4f8ss2bjx --- openerp/tools/mail.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 6ab7119236c..5530ae4797b 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -187,6 +187,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): if node.text is None: node.text = '' + truncate_idx = -1 if simplify_whitespaces: cur_char_nbr = 0 word = None @@ -196,18 +197,15 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): if cur_char_nbr >= position: break if word: - stop_idx = node.text.find(word) + len(word) - else: - stop_idx = len(node.text) - if stop_idx == -1: - stop_idx = len(node.text) + truncate_idx = node.text.find(word) + len(word) else: - stop_idx = position - stop_idx = stop_idx if len(node.text) >= stop_idx else len(node.text) + truncate_idx = position + if truncate_idx == -1 or truncate_idx >= len(node.text): + truncate_idx = len(node.text) - 1 # compose new text bits - innertext = node.text[0:stop_idx] - outertext = node.text[stop_idx:] + innertext = node.text[0:truncate_idx] + outertext = node.text[truncate_idx:] node.text = innertext # create ... read more node @@ -215,7 +213,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'}) read_more_node.append(read_more_link_node) # create outertext node - overtext_node = _create_node('span', outertext[stop_idx:]) + overtext_node = _create_node('span', outertext[truncate_idx:]) # tag node overtext_node.set('in_overlength', '1') # add newly created nodes in dom From e14de7a63737978de323791e4dbf872e0af6a1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 23 Oct 2013 14:38:39 +0200 Subject: [PATCH 04/25] [FIX] Fixed previous commit, car go to len of the string. bzr revid: tde@openerp.com-20131023123839-gvf9ugft2b2xmo9a --- openerp/tools/mail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 5530ae4797b..431fb561faf 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -200,8 +200,8 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): truncate_idx = node.text.find(word) + len(word) else: truncate_idx = position - if truncate_idx == -1 or truncate_idx >= len(node.text): - truncate_idx = len(node.text) - 1 + if truncate_idx == -1 or truncate_idx > len(node.text): + truncate_idx = len(node.text) # compose new text bits innertext = node.text[0:truncate_idx] From b1c221e9c48edba65801c71933aeeb0e38c480ff Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 17:53:40 +0200 Subject: [PATCH 05/25] [FIX] hr: notification about new employee joining should only be sent to employees (base.group_user) In the future we should directly use the `Whole Company` mail.group, but this does not work well enough now, as recipients will not be able to directly go to the employee record (e.g. to follow her) bzr revid: odo@openerp.com-20131023155340-z2e78xo3vb7yq008 --- addons/hr/hr.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/addons/hr/hr.py b/addons/hr/hr.py index d19f1f7de8b..7eb601deb41 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -236,11 +236,13 @@ class hr_employee(osv.osv): employee_id = super(hr_employee, self).create(cr, uid, data, context=create_ctx) employee = self.browse(cr, uid, employee_id, context=context) if employee.user_id: + res_users = self.pool['res.users'] # send a copy to every user of the company - company_id = employee.user_id.partner_id.company_id.id - partner_ids = self.pool.get('res.partner').search(cr, uid, [ - ('company_id', '=', company_id), - ('user_ids', '!=', False)], context=context) + # TODO: post to the `Whole Company` mail.group when we'll be able to link to the employee record + _model, group_id = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user') + user_ids = res_users.search(cr, uid, [('company_id', '=', employee.user_id.company_id.id), + ('groups_id', 'in', group_id)]) + partner_ids = list(set(u.partner_id.id for u in res_users.browse(cr, uid, user_ids, context=context))) else: partner_ids = [] self.message_post(cr, uid, [employee_id], From d7f9722a83401cd8c61d7a09570ac359594d86d3 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 23 Oct 2013 18:26:46 +0200 Subject: [PATCH 06/25] [FIX] web: bind RouteMap using environ to allow correct redirections bzr revid: chs@openerp.com-20131023162646-9t8iu2okkddg56yi --- addons/web/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/http.py b/addons/web/http.py index 4bda58117d4..90dfca1615e 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -1043,7 +1043,7 @@ class Root(object): Tries to discover the controller handling the request for the path specified in the request. """ path = request.httprequest.path - urls = self.get_db_router(request.db).bind("") + urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ) func, arguments = urls.match(path) arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")]) From 5add0d31f31ec8f1a3fcbc27572b18d704bb3908 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 18:28:07 +0200 Subject: [PATCH 07/25] [IMP] hr.employee: default search should includes work emails (useful e.g. for trigrams) bzr revid: odo@openerp.com-20131023162807-qjw68383a7612op6 --- addons/hr/hr_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index d15b4eb81a7..1cf73286dfd 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -117,7 +117,7 @@ hr.employee - + From a0fab6769490730bfd0ca9d66e727de79e31f6b3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 18:29:28 +0200 Subject: [PATCH 08/25] [FIX] hr_holidays: leave holiday overlap should ignore cancelled/refused ones Obviously a cancelled leave does not really overlap with a new one. bzr revid: odo@openerp.com-20131023162928-56vdsjxr8sa4n3jv --- addons/hr_holidays/hr_holidays.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 1af2fc00193..682508e0ca0 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -145,7 +145,9 @@ class hr_holidays(osv.osv): def _check_date(self, cr, uid, ids): for holiday in self.browse(cr, uid, ids): - holiday_ids = self.search(cr, uid, [('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '<>', holiday.id)]) + holiday_ids = self.search(cr, uid, [('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), + ('employee_id', '=', holiday.employee_id.id), ('id', '<>', holiday.id), + ('state', 'not in', ['cancel', 'refuse'])]) if holiday_ids: return False return True From 0354391b1014c7fe6f6498e4c5cbf84aab8f4949 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 18:30:29 +0200 Subject: [PATCH 09/25] [IMP] crm: defaults can be literals bzr revid: odo@openerp.com-20131023163029-z3q0ve4jykanibkv --- addons/crm/crm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm/crm.py b/addons/crm/crm.py index dc7ebf3ca95..6c2db63b0be 100644 --- a/addons/crm/crm.py +++ b/addons/crm/crm.py @@ -78,8 +78,8 @@ class crm_case_stage(osv.osv): } _defaults = { - 'sequence': lambda *args: 1, - 'probability': lambda *args: 0.0, + 'sequence': 1, + 'probability': 0.0, 'on_change': True, 'fold': False, 'type': 'both', From 9dce48185faac1ede01da77a5da1488288f275ad Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 18:31:08 +0200 Subject: [PATCH 10/25] [IMP] portal_hr: avoid shadowing parent _description for no reason bzr revid: odo@openerp.com-20131023163108-ic3xp7yt3lx4bmkx --- addons/portal_hr_employees/hr_employee.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/portal_hr_employees/hr_employee.py b/addons/portal_hr_employees/hr_employee.py index 52d01170a2a..b6d3ed1a7b5 100644 --- a/addons/portal_hr_employees/hr_employee.py +++ b/addons/portal_hr_employees/hr_employee.py @@ -24,7 +24,6 @@ from openerp.osv import fields, osv class crm_contact_us(osv.TransientModel): """ Add employees list to the portal's contact page """ _inherit = 'portal_crm.crm_contact_us' - _description = 'Contact form for the portal' _columns = { 'employee_ids' : fields.many2many('hr.employee', string='Employees', readonly=True), } @@ -40,7 +39,6 @@ class crm_contact_us(osv.TransientModel): } class hr_employee(osv.osv): - _description = 'Portal employee' _inherit = 'hr.employee' """ From 9b7398820e86506bd77fb3f94d8a74d37a5ee625 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 23 Oct 2013 18:32:47 +0200 Subject: [PATCH 11/25] [DOC] stock: for the record, rationale for stock.location.complete_name being stored bzr revid: odo@openerp.com-20131023163247-bir34amewsxiby2t --- addons/stock/stock.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index d818ca794d5..af70faeef41 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -159,6 +159,10 @@ class stock_location(osv.osv): \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products """, select = True), # temporarily removed, as it's unused: 'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True), + + # as discussed on bug 765559, the main purpose of this field is to allow sorting the list of locations + # according to the displayed names, and reversing that sort by clicking on a column. It does not work for + # translated values though - so it needs fixing. 'complete_name': fields.function(_complete_name, type='char', size=256, string="Location Name", store={'stock.location': (_get_sublocations, ['name', 'location_id'], 10)}), From dbe253b0e490317dd26c772fd22bb578348a7d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 24 Oct 2013 10:39:55 +0200 Subject: [PATCH 12/25] [FIX] Fixed bug due to copy-and-paste: outertext is now valid bzr revid: tde@openerp.com-20131024083955-ipbsf8rxbzm0mag3 --- openerp/tools/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 431fb561faf..a1b0a23dcf5 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -213,7 +213,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'}) read_more_node.append(read_more_link_node) # create outertext node - overtext_node = _create_node('span', outertext[truncate_idx:]) + overtext_node = _create_node('span', outertext) # tag node overtext_node.set('in_overlength', '1') # add newly created nodes in dom From 6b7ccc16b7883367ef4810a823ba66b85e725b06 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 28 Oct 2013 14:50:50 +0100 Subject: [PATCH 13/25] [FIX] mail: mail_message: avoid crash when checking access rules against invalid ids bzr revid: chs@openerp.com-20131028135050-4hqx129ouy7z5b03 --- addons/mail/mail_message.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 3719dbc1c63..de8e4c68fda 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -212,7 +212,7 @@ class mail_message(osv.Model): _defaults = { 'type': 'email', - 'date': fields.datetime.now(), + 'date': fields.datetime.now, 'author_id': lambda self, cr, uid, ctx=None: self._get_default_author(cr, uid, ctx), 'body': '', 'email_from': lambda self, cr, uid, ctx=None: self._get_default_from(cr, uid, ctx), @@ -698,8 +698,9 @@ class mail_message(osv.Model): """ model_record_ids = {} for id in msg_ids: - if msg_val[id]['model'] and msg_val[id]['res_id']: - model_record_ids.setdefault(msg_val[id]['model'], dict()).setdefault(msg_val[id]['res_id'], set()).add(msg_val[id]['res_id']) + vals = msg_val.get(id, {}) + if vals.get('model') and vals.get('res_id'): + model_record_ids.setdefault(vals['model'], set()).add(vals['res_id']) return model_record_ids if uid == SUPERUSER_ID: @@ -711,7 +712,7 @@ class mail_message(osv.Model): partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id # Read mail_message.ids to have their values - message_values = dict.fromkeys(ids) + message_values = dict.fromkeys(ids, {}) cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,)) for id, rmod, rid, author_id, parent_id in cr.fetchall(): message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id} @@ -745,10 +746,10 @@ class mail_message(osv.Model): ], context=context) notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)] elif operation == 'create': - for doc_model, doc_dict in model_record_ids.items(): + for doc_model, doc_ids in model_record_ids.items(): fol_ids = fol_obj.search(cr, SUPERUSER_ID, [ ('res_model', '=', doc_model), - ('res_id', 'in', list(doc_dict.keys())), + ('res_id', 'in', list(doc_ids)), ('partner_id', '=', partner_id), ], context=context) fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)] @@ -759,9 +760,9 @@ class mail_message(osv.Model): other_ids = other_ids.difference(set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) document_related_ids = [] - for model, doc_dict in model_record_ids.items(): + for model, doc_ids in model_record_ids.items(): model_obj = self.pool[model] - mids = model_obj.exists(cr, uid, doc_dict.keys()) + mids = model_obj.exists(cr, uid, list(doc_ids)) if hasattr(model_obj, 'check_mail_message_access'): model_obj.check_mail_message_access(cr, uid, mids, operation, context=context) else: From 12690a7cfec84d3314eaf2f2aedcfc1154b3c971 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 28 Oct 2013 15:57:12 +0100 Subject: [PATCH 14/25] [FIX] project_issue: correct issue escalation bzr revid: chs@openerp.com-20131028145712-a6egn04g1g2va5cs --- addons/project_issue/project_issue.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 351b55fdcfc..cf0f79e18a5 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -465,18 +465,20 @@ class project_issue(osv.Model): return stage_ids[0] return False - def case_escalate(self, cr, uid, ids, context=None): - cases = self.browse(cr, uid, ids) - for case in cases: + def case_escalate(self, cr, uid, ids, context=None): # FIXME rename this method to issue_escalate + for issue in self.browse(cr, uid, ids, context=context): data = {} - if case.project_id.project_escalation_id: - data['project_id'] = case.project_id.project_escalation_id.id - if case.project_id.project_escalation_id.user_id: - data['user_id'] = case.project_id.project_escalation_id.user_id.id - if case.task_id: - self.pool.get('project.task').write(cr, uid, [case.task_id.id], {'project_id': data['project_id'], 'user_id': False}) - else: + esc_proj = issue.project_id.project_escalation_id + if not esc_proj: raise osv.except_osv(_('Warning!'), _('You cannot escalate this issue.\nThe relevant Project has not configured the Escalation Project!')) + + data['project_id'] = esc_proj.id + if esc_proj.user_id: + data['user_id'] = esc_proj.user_id.id + issue.write(data) + + if issue.task_id: + issue.task_id.write({'project_id': esc_proj.id, 'user_id': False}) return True # ------------------------------------------------------- From 06a3255576922aac31f52b916fff9e8416a3bfce Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 28 Oct 2013 16:56:00 +0100 Subject: [PATCH 15/25] [IMP] res_partner: default search field can be based on display_name + have email matching by default This more closely matches the behavior of name_search, until name_search is actually available in search views. bzr revid: odo@openerp.com-20131028155600-1dwd35sjht2of63m --- openerp/addons/base/res/res_partner_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index f02021313c0..943aedf49f7 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -329,7 +329,7 @@ + filter_domain="['|','|',('display_name','ilike',self),('ref','=',self),('email,'ilike',self)]"/> From 5db5d655d6f2abf2220bad5db09ecf5f02c7dee0 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 28 Oct 2013 16:56:39 +0100 Subject: [PATCH 16/25] [IMP] res.users: default search field should match emails too bzr revid: odo@openerp.com-20131028155639-it1q12xt5iul1oac --- openerp/addons/base/res/res_users_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml index 770cb45149b..a5f988428e9 100644 --- a/openerp/addons/base/res/res_users_view.xml +++ b/openerp/addons/base/res/res_users_view.xml @@ -196,7 +196,7 @@ res.users - + From d1d35b3f573a2f0407abed51b1d8f2b689b745d7 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 28 Oct 2013 17:01:00 +0100 Subject: [PATCH 17/25] [FIX] mail.thread: message_redirect_action should not crash when the message has been deleted bzr revid: odo@openerp.com-20131028160100-yhebao3adbzz00cn --- addons/mail/mail_thread.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 9692b94f159..9ed63ae14fa 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -577,7 +577,8 @@ class mail_thread(osv.AbstractModel): return action if msg_id and not (model and res_id): msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context) - model, res_id = msg.model, msg.res_id + if msg.exists(): + model, res_id = msg.model, msg.res_id # if model + res_id found: try to redirect to the document or fallback on the Inbox if model and res_id: From abe6df638d3fa79e62ff50f16435b6ad35c6a370 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 29 Oct 2013 09:53:56 +0100 Subject: [PATCH 18/25] [FIX] Reverted modification of behavior in 3858 and replaced it with a fix that solves the core problem (the fact that the function redirect_with_hash() didn't produced the expected effect in safari). To solve that problem, the method was simply modified to use the same trick already used in IE that has the same problem than safari. bzr revid: nicolas.vanhoren@openerp.com-20131029085356-hb2sk4g7j3xh4zar --- addons/web/controllers/main.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9584c53ca5b..9df5355b9c1 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -91,13 +91,16 @@ db_list = http.db_list db_monodb = http.db_monodb def redirect_with_hash(url, code=303): + redirect_code = "" % url if request.httprequest.user_agent.browser == 'msie': try: version = float(request.httprequest.user_agent.version) if version < 10: - return "" % url + return redirect_code except Exception: pass + elif request.httprequest.user_agent.browser == 'safari': + return redirect_code return werkzeug.utils.redirect(url, code) def module_topological_sort(modules): @@ -529,14 +532,12 @@ class Home(http.Controller): def index(self, s_action=None, db=None, debug=False, **kw): debug = debug != False - try: - lst = http.db_list() - except openerp.exceptions.AccessDenied: - lst = [] - + lst = http.db_list(True) if db not in lst: db = None guessed_db = http.db_monodb(request.httprequest) + if guessed_db is None and len(lst) > 0: + guessed_db = lst[0] def redirect(db): query = dict(urlparse.parse_qsl(request.httprequest.query_string, keep_blank_values=True)) @@ -544,11 +545,8 @@ class Home(http.Controller): redirect = request.httprequest.path + '?' + urllib.urlencode(query) return redirect_with_hash(redirect) - if guessed_db is None and db is None and lst: - return redirect(lst[0]) - - if guessed_db is None and lst: - guessed_db = lst[0] + if db is None and guessed_db is not None: + return redirect(guessed_db) if db is not None and db != guessed_db: request.session.logout() From 866148efa4543a63db43d428642bcfcd9b375e7a Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Tue, 29 Oct 2013 10:01:50 +0100 Subject: [PATCH 19/25] [FIX] Corrected the redirect_with_hash() method that produced an incorrect url in IE and safari bzr revid: nicolas.vanhoren@openerp.com-20131029090150-u397wkzdo9an9n0w --- addons/web/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9df5355b9c1..04570b68ccf 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -91,7 +91,7 @@ db_list = http.db_list db_monodb = http.db_monodb def redirect_with_hash(url, code=303): - redirect_code = "" % url + redirect_code = "" % url if request.httprequest.user_agent.browser == 'msie': try: version = float(request.httprequest.user_agent.version) From be9bb538bc9343e069ccecc5266eda2b07bed037 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 29 Oct 2013 20:28:38 +0100 Subject: [PATCH 20/25] [FIX] crm.phonecall: correct view to call correct on_change method bzr revid: chs@openerp.com-20131029192838-6c6z22enbcgotqma --- addons/crm/crm_phonecall_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm/crm_phonecall_view.xml b/addons/crm/crm_phonecall_view.xml index 585286f5456..a027a910520 100644 --- a/addons/crm/crm_phonecall_view.xml +++ b/addons/crm/crm_phonecall_view.xml @@ -120,7 +120,7 @@ + on_change="on_change_partner_id(partner_id)"/> From 53c15261979cc7d406264970168933ee0c55ac8a Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 30 Oct 2013 16:26:28 +0100 Subject: [PATCH 21/25] [FIX]crm: crm_lead_to_opportunity wizard, the wizard was not choosing duplicated leads/opps which was having probability null in database bzr revid: dle@openerp.com-20131030152628-tn12sxhbqbdqh3m4 --- addons/crm/wizard/crm_lead_to_opportunity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 48097944ec4..f239ad739c1 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -59,11 +59,11 @@ class crm_lead2opportunity_partner(osv.osv_memory): if partner_id: # Search for opportunities that have the same partner and that arent done or cancelled - ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), ('probability', '<', '100')]) + ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), '|', ('probability', '=', False), ('probability', '<', '100')]) for id in ids: tomerge.add(id) if email: - ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), ('probability', '<', '100')]) + ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), '|', ('probability', '=', False), ('probability', '<', '100')]) for id in ids: tomerge.add(id) From 95631cdf9f8d4d379ba30879ff61ed35ee177e85 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 30 Oct 2013 18:18:46 +0100 Subject: [PATCH 22/25] [FIX]crm_partner_assign: set probabilities of stages 'Assigned' and 'To recycle' to 10% and 5% (respectively), to avoid to match the subtype message 'Opportunity Lost' when changing to this specific stages, in the chatter (tracking). bzr revid: dle@openerp.com-20131030171846-bz141fhan7bqir44 --- addons/crm_partner_assign/crm_partner_assign_data.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/crm_partner_assign/crm_partner_assign_data.xml b/addons/crm_partner_assign/crm_partner_assign_data.xml index b4347b1621b..3f6d13c3d53 100644 --- a/addons/crm_partner_assign/crm_partner_assign_data.xml +++ b/addons/crm_partner_assign/crm_partner_assign_data.xml @@ -4,14 +4,14 @@ Assigned - + lead To Recycle - + lead From d9ca5363961e465ce43ac75f7ba26c977880005d Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 31 Oct 2013 11:33:33 +0100 Subject: [PATCH 23/25] [FIX] web: correct date and datetime parsing at end of month. As `new Date()` return the current date, when we are the 31, setting the month to a month with less than 31 days will change the date to next day (1st of next month). This result to a date(time) object one month ahead of the wanted date(time). bzr revid: chs@openerp.com-20131031103333-vt68132ptj9sbr04 --- addons/web/static/src/js/openerpframework.js | 6 ++-- addons/web/static/test/framework.js | 37 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index 027452b42fb..1d688505a06 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -1265,7 +1265,7 @@ openerp.str_to_datetime = function(str) { if ( !res ) { throw new Error("'" + str + "' is not a valid datetime"); } - var tmp = new Date(); + var tmp = new Date(0); tmp.setUTCFullYear(parseFloat(res[1])); tmp.setUTCMonth(parseFloat(res[2]) - 1); tmp.setUTCDate(parseFloat(res[3])); @@ -1297,7 +1297,7 @@ openerp.str_to_date = function(str) { if ( !res ) { throw new Error("'" + str + "' is not a valid date"); } - var tmp = new Date(); + var tmp = new Date(0); tmp.setFullYear(parseFloat(res[1])); tmp.setMonth(parseFloat(res[2]) - 1); tmp.setDate(parseFloat(res[3])); @@ -1424,4 +1424,4 @@ if (typeof(define) !== "undefined") { // amd window.openerp = declare($, _, QWeb2); } -})(); \ No newline at end of file +})(); diff --git a/addons/web/static/test/framework.js b/addons/web/static/test/framework.js index 3619ab87e65..88bc413a279 100644 --- a/addons/web/static/test/framework.js +++ b/addons/web/static/test/framework.js @@ -427,12 +427,49 @@ ropenerp.testing.section('server-formats', { date3.getUTCHours(), date3.getUTCMinutes(), date3.getUTCSeconds(), date3.getUTCMilliseconds()], [2009, 5 - 1, 4, 12, 34, 23, 845]); }); + test('Parse server datetime on 31', function() { + var wDate = window.Date; + var s = openerp.testing.Stack(); + return s.push(function() { + window.Date = function() { + return wDate('2013-10-31 12:34:56'); + }; + }, function() { + window.Date = wDate; + }).execute(function() { + return openerp.str_to_datetime('2013-11-11 02:45:21'); + }).then(function(date) { + deepEqual( + [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), + date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()], + [2013, 11 - 1, 11, 2, 45, 21]); + }); + + }); test('Parse server date', function () { var date = openerp.str_to_date("2009-05-04"); deepEqual( [date.getFullYear(), date.getMonth(), date.getDate()], [2009, 5 - 1, 4]); }); + test('Parse server date on 31', function() { + var wDate = window.Date; + var s = openerp.testing.Stack(); + return s.push(function() { + window.Date = function() { + return wDate('2013-10-31 12:34:56'); + }; + }, function() { + window.Date = wDate; + }).execute(function() { + return openerp.str_to_date('2013-11-11'); + }).then(function(date) { + deepEqual( + [date.getFullYear(), date.getMonth(), date.getDate()], + [2013, 11 - 1, 11]); + }); + + }); test('Parse server time', function () { var date = openerp.str_to_time("12:34:23"); deepEqual( From 12e262e5c97d7f4faf411dcb4bf8815ad437b078 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 31 Oct 2013 12:19:45 +0100 Subject: [PATCH 24/25] [FIX] web: correct "date(time) on 31" tests bzr revid: chs@openerp.com-20131031111945-u3wi11e4rykpslme --- addons/web/static/test/framework.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/addons/web/static/test/framework.js b/addons/web/static/test/framework.js index 88bc413a279..237bdc195ca 100644 --- a/addons/web/static/test/framework.js +++ b/addons/web/static/test/framework.js @@ -427,12 +427,15 @@ ropenerp.testing.section('server-formats', { date3.getUTCHours(), date3.getUTCMinutes(), date3.getUTCSeconds(), date3.getUTCMilliseconds()], [2009, 5 - 1, 4, 12, 34, 23, 845]); }); - test('Parse server datetime on 31', function() { + test('Parse server datetime on 31', {asserts: 1}, function() { var wDate = window.Date; - var s = openerp.testing.Stack(); + var s = ropenerp.testing.Stack(); return s.push(function() { - window.Date = function() { - return wDate('2013-10-31 12:34:56'); + window.Date = function(v) { + if (_.isUndefined(v)) { + v = '2013-10-31 12:34:56'; + } + return new wDate(v); }; }, function() { window.Date = wDate; @@ -452,21 +455,24 @@ ropenerp.testing.section('server-formats', { [date.getFullYear(), date.getMonth(), date.getDate()], [2009, 5 - 1, 4]); }); - test('Parse server date on 31', function() { + test('Parse server date on 31', {asserts: 1}, function() { var wDate = window.Date; - var s = openerp.testing.Stack(); + var s = ropenerp.testing.Stack(); return s.push(function() { - window.Date = function() { - return wDate('2013-10-31 12:34:56'); + window.Date = function(v) { + if (_.isUndefined(v)) { + v = '2013-10-31 12:34:56'; + } + return new wDate(v); }; }, function() { window.Date = wDate; }).execute(function() { - return openerp.str_to_date('2013-11-11'); + return openerp.str_to_date('2013-11-21'); }).then(function(date) { deepEqual( [date.getFullYear(), date.getMonth(), date.getDate()], - [2013, 11 - 1, 11]); + [2013, 11 - 1, 21]); }); }); From 3df2242b68ef58ccb728f8d376c7d2d2a12236be Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Thu, 31 Oct 2013 18:49:16 +0100 Subject: [PATCH 25/25] [FIX]crm_partner_assign: lead mass email template, partner_name field was more appropriate for having the name of the partner. Contact name is less often filled. bzr revid: dle@openerp.com-20131031174916-3a99a7xqe3gbq1xn --- addons/crm_partner_assign/crm_partner_assign_data.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm_partner_assign/crm_partner_assign_data.xml b/addons/crm_partner_assign/crm_partner_assign_data.xml index 3f6d13c3d53..3b5dbeb4322 100644 --- a/addons/crm_partner_assign/crm_partner_assign_data.xml +++ b/addons/crm_partner_assign/crm_partner_assign_data.xml @@ -31,7 +31,7 @@
    % for lead in ctx['partner_leads']: -
  1. ${lead.lead_id.name or 'Subject Undefined'}, ${lead.lead_id.contact_name or 'Contact Name Undefined'}, ${lead.lead_id.country_id and lead.lead_id.country_id.name or 'Country Undefined' }, ${lead.lead_id.email_from or 'Email Undefined'}, ${lead.lead_id.phone or ''}

  2. +
  3. ${lead.lead_id.name or 'Subject Undefined'}, ${lead.lead_id.partner_name or lead.lead_id.contact_name or 'Contact Name Undefined'}, ${lead.lead_id.country_id and lead.lead_id.country_id.name or 'Country Undefined' }, ${lead.lead_id.email_from or 'Email Undefined'}, ${lead.lead_id.phone or ''}

  4. % endfor