[MERGE] forward port of branch saas-2 up to revid 4965 chs@openerp.com-20131025103939-5gta1eifjhx1tsmi
bzr revid: chs@openerp.com-20131025104326-4k5hayl0bwdgt0mm
This commit is contained in:
commit
d91819cb43
|
@ -34,6 +34,7 @@
|
|||
<field name="lang"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="module" group="base.group_no_one"/>
|
||||
<field name="type"/>
|
||||
<field name="res_id"/>
|
||||
</group>
|
||||
|
|
|
@ -47,33 +47,8 @@ class base_module_import(osv.osv_memory):
|
|||
}
|
||||
|
||||
def importzip(self, cr, uid, ids, context):
|
||||
(data,) = self.browse(cr, uid, ids , context=context)
|
||||
module_data = data.module_file
|
||||
zip_data = base64.decodestring(module_data)
|
||||
fp = StringIO()
|
||||
fp.write(zip_data)
|
||||
try:
|
||||
file_data = zipfile.ZipFile(fp, 'r')
|
||||
except zipfile.BadZipfile:
|
||||
raise osv.except_osv(_('Error!'), _('File is not a zip file!'))
|
||||
init_file_name = sorted(file_data.namelist())[0]
|
||||
module_name = os.path.split(init_file_name)[0]
|
||||
|
||||
file_path = os.path.join(ADDONS_PATH, '%s.zip' % module_name)
|
||||
try:
|
||||
zip_file = open(file_path, 'wb')
|
||||
except IOError:
|
||||
raise osv.except_osv(_('Error!'),
|
||||
_('Can not create the module file: %s!') % \
|
||||
(file_path,) )
|
||||
zip_file.write(zip_data)
|
||||
zip_file.close()
|
||||
|
||||
self.pool.get('ir.module.module').update_list(cr, uid,
|
||||
{'module_name': module_name,})
|
||||
self.write(cr, uid, ids, {'state':'done', 'module_name': module_name},
|
||||
context)
|
||||
return False
|
||||
#TODO: drop this model and the corresponding view/action in trunk
|
||||
raise NotImplementedError('This feature is not available')
|
||||
|
||||
def action_module_open(self, cr, uid, ids, context):
|
||||
(data,) = self.browse(cr, uid, ids , context=context)
|
||||
|
|
|
@ -116,8 +116,7 @@ class _column(object):
|
|||
self.groups = False # CSV list of ext IDs of groups that can access this field
|
||||
self.deprecated = False # Optional deprecation warning
|
||||
for a in args:
|
||||
if args[a]:
|
||||
setattr(self, a, args[a])
|
||||
setattr(self, a, args[a])
|
||||
|
||||
def restart(self):
|
||||
pass
|
||||
|
|
|
@ -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 = '''<div>
|
||||
<span>
|
||||
</span>
|
||||
<p>Hello, <span>Raoul</span>
|
||||
<bold>You</bold> are
|
||||
pretty</p>
|
||||
<span>Really</span>
|
||||
</div>
|
||||
'''
|
||||
# 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 = '''<div> Blahble
|
||||
bluih blouh
|
||||
<blockquote>This is a quote
|
||||
<span>And this is quite a long quote, after all.</span>
|
||||
</blockquote>
|
||||
</div>'''
|
||||
# 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:
|
||||
|
|
|
@ -175,26 +175,45 @@ 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 stop_idx == -1:
|
||||
stop_idx = len(outertext)
|
||||
|
||||
truncate_idx = -1
|
||||
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
|
||||
if word:
|
||||
truncate_idx = node.text.find(word) + len(word)
|
||||
else:
|
||||
stop_idx = 0
|
||||
node.text = innertext + outertext[0:stop_idx]
|
||||
truncate_idx = position
|
||||
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]
|
||||
outertext = node.text[truncate_idx:]
|
||||
node.text = innertext
|
||||
|
||||
# create <span> ... <a href="#">read more</a></span> 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'})
|
||||
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)
|
||||
# tag node
|
||||
overtext_node.set('in_overlength', '1')
|
||||
# add newly created nodes in dom
|
||||
|
@ -223,17 +242,16 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300):
|
|||
html = '<div>%s</div>' % 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 +263,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():
|
||||
# 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 +298,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 +330,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 <span> -> back to \n
|
||||
html = etree.tostring(root, pretty_print=False)
|
||||
linebreaks = re.compile(r'<span>([\s]*[\r\n]+[\s]*)<\/span>', re.IGNORECASE | re.DOTALL)
|
||||
linebreaks = re.compile(r'<span[^>]*>([\s]*[\r\n]+[\s]*)<\/span>', re.IGNORECASE | re.DOTALL)
|
||||
html = _replace_matching_regex(linebreaks, html, '\n')
|
||||
|
||||
return html
|
||||
|
|
Loading…
Reference in New Issue