diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 704488f6938..dfa6338fbdd 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -261,10 +261,14 @@ class ir_attachment(osv.osv): return len(result) if count else list(result) def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'): + if isinstance(ids, (int, long)): + ids = [ids] self.check(cr, uid, ids, 'read', context=context) return super(ir_attachment, self).read(cr, uid, ids, fields_to_read, context, load) def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, (int, long)): + ids = [ids] self.check(cr, uid, ids, 'write', context=context, values=vals) if 'file_size' in vals: del vals['file_size'] @@ -275,6 +279,8 @@ class ir_attachment(osv.osv): return super(ir_attachment, self).copy(cr, uid, id, default, context) def unlink(self, cr, uid, ids, context=None): + if isinstance(ids, (int, long)): + ids = [ids] self.check(cr, uid, ids, 'unlink', context=context) location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') if location: diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 302b11eb526..09622dadd17 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -184,7 +184,7 @@ class ir_fields_converter(orm.Model): def _str_id(self, cr, uid, model, column, value, context=None): return value, [] - _str_to_char = _str_to_text = _str_to_binary = _str_id + _str_to_reference = _str_to_char = _str_to_text = _str_to_binary = _str_id def _str_to_date(self, cr, uid, model, column, value, context=None): try: diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 396de27903e..14b854f0f1a 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -198,6 +198,7 @@ class ir_model(osv.osv): select=vals.get('select_level', '0'), update_custom_fields=True) self.pool[vals['model']]._auto_init(cr, ctx) + self.pool[vals['model']]._auto_end(cr, ctx) # actually create FKs! openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) return res @@ -356,6 +357,7 @@ class ir_model_fields(osv.osv): select=vals.get('select_level', '0'), update_custom_fields=True) self.pool[vals['model']]._auto_init(cr, ctx) + self.pool[vals['model']]._auto_end(cr, ctx) # actually create FKs! openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) return res @@ -470,6 +472,7 @@ class ir_model_fields(osv.osv): for col_name, col_prop, val in patch_struct[1]: setattr(obj._columns[col_name], col_prop, val) obj._auto_init(cr, ctx) + obj._auto_end(cr, ctx) # actually create FKs! openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname) return res diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index 0f9b6ea3886..6cbd90c3072 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -161,18 +161,18 @@ class ir_translation(osv.osv): ''' if context is None: context = {} - res = {} + res = dict.fromkeys(ids, False) for record in self.browse(cr, uid, ids, context=context): if record.type != 'model': res[record.id] = record.src else: model_name, field = record.name.split(',') model = self.pool.get(model_name) - #We need to take the context without the language information, because we want to read the - #value store in db and not on the one associate with current language. - context_wo_lang = context.copy() - context_wo_lang.pop('lang', None) - res[record.id] = model.read(cr, uid, record.res_id, [field], context=context_wo_lang)[field] + if model and model.exists(cr, uid, record.res_id, context=context): + # Pass context without lang, need to read real stored field, not translation + context_no_lang = dict(context, lang=None) + result = model.read(cr, uid, record.res_id, [field], context=context_no_lang) + res[record.id] = result[field] if result else False return res def _set_src(self, cr, uid, id, name, value, args, context=None): diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 9d599ba3aac..d58f4b3186b 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -305,18 +305,16 @@ class RegistryManager(object): r, c = cr.fetchone() # Check if the model registry must be reloaded (e.g. after the # database has been updated by another process). - if registry.base_registry_signaling_sequence != r: + if registry.base_registry_signaling_sequence > 1 and registry.base_registry_signaling_sequence != r: changed = True _logger.info("Reloading the model registry after database signaling.") registry = cls.new(db_name) - registry.base_registry_signaling_sequence = r # Check if the model caches must be invalidated (e.g. after a write # occured on another process). Don't clear right after a registry # has been reload. - elif registry.base_cache_signaling_sequence != c: + elif registry.base_cache_signaling_sequence > 1 and registry.base_cache_signaling_sequence != c: changed = True _logger.info("Invalidating all model caches after database signaling.") - registry.base_cache_signaling_sequence = c registry.clear_caches() registry.reset_any_cache_cleared() # One possible reason caches have been invalidated is the @@ -326,6 +324,8 @@ class RegistryManager(object): for column in model._columns.values(): if hasattr(column, 'digits_change'): column.digits_change(cr) + registry.base_registry_signaling_sequence = r + registry.base_cache_signaling_sequence = c finally: cr.close() return changed diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 372d7e463aa..e865287a8ea 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -207,27 +207,29 @@ class reference(_column): return model.name_get(cr, uid, [int(res_id)], context=context)[0][1] return tools.ustr(value) +# takes a string (encoded in utf8) and returns a string (encoded in utf8) +def _symbol_set_char(self, symb): + + #TODO: + # * we need to remove the "symb==False" from the next line BUT + # for now too many things rely on this broken behavior + # * the symb==None test should be common to all data types + if symb is None or symb == False: + return None + + # we need to convert the string to a unicode object to be able + # to evaluate its length (and possibly truncate it) reliably + u_symb = tools.ustr(symb) + return u_symb[:self.size].encode('utf8') + class char(_column): _type = 'char' def __init__(self, string="unknown", size=None, **args): _column.__init__(self, string=string, size=size or None, **args) - self._symbol_set = (self._symbol_c, self._symbol_set_char) - - # takes a string (encoded in utf8) and returns a string (encoded in utf8) - def _symbol_set_char(self, symb): - #TODO: - # * we need to remove the "symb==False" from the next line BUT - # for now too many things rely on this broken behavior - # * the symb==None test should be common to all data types - if symb is None or symb == False: - return None - - # we need to convert the string to a unicode object to be able - # to evaluate its length (and possibly truncate it) reliably - u_symb = tools.ustr(symb) - - return u_symb[:self.size].encode('utf8') + # self._symbol_set_char defined to keep the backward compatibility + self._symbol_f = self._symbol_set_char = lambda x: _symbol_set_char(self, x) + self._symbol_set = (self._symbol_c, self._symbol_f) class text(_column): @@ -1116,6 +1118,11 @@ class function(_column): self._symbol_f = integer._symbol_f self._symbol_set = integer._symbol_set + if type == 'char': + self._symbol_c = char._symbol_c + self._symbol_f = lambda x: _symbol_set_char(self, x) + self._symbol_set = (self._symbol_c, self._symbol_f) + def digits_change(self, cr): if self._type == 'float': if self.digits_compute: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b5440c2be44..cf96c8d27bd 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1010,8 +1010,10 @@ class BaseModel(object): raise except_orm('Error', ('Invalid function definition %s in object %s !\nYou must use the definition: store={object:(fnct, fields, priority, time length)}.' % (store_field, self._name))) self.pool._store_function.setdefault(object, []) - self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length)) - self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4])) + t = (self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length) + if not t in self.pool._store_function[object]: + self.pool._store_function[object].append((self._name, store_field, fnct, tuple(fields2) if fields2 else None, order, length)) + self.pool._store_function[object].sort(lambda x, y: cmp(x[4], y[4])) for (key, _, msg) in self._sql_constraints: self.pool._sql_error[self._table+'_'+key] = msg @@ -2887,8 +2889,12 @@ class BaseModel(object): """ Record the creation of a constraint for this model, to make it possible to delete it later when the module is uninstalled. Type can be either - 'f' or 'u' depending on the constraing being a foreign key or not. + 'f' or 'u' depending on the constraint being a foreign key or not. """ + if not self._module: + # no need to save constraints for custom models as they're not part + # of any module + return assert type in ('f', 'u') cr.execute(""" SELECT 1 FROM ir_model_constraint, ir_module_module @@ -4583,9 +4589,9 @@ class BaseModel(object): return browse_null() def _store_get_values(self, cr, uid, ids, fields, context): - """Returns an ordered list of fields.functions to call due to + """Returns an ordered list of fields.function to call due to an update operation on ``fields`` of records with ``ids``, - obtained by calling the 'store' functions of these fields, + obtained by calling the 'store' triggers of these fields, as setup by their 'store' attribute. :return: [(priority, model_name, [record_ids,], [function_fields,])] @@ -4594,42 +4600,46 @@ class BaseModel(object): stored_functions = self.pool._store_function.get(self._name, []) # use indexed names for the details of the stored_functions: - model_name_, func_field_to_compute_, id_mapping_fnct_, trigger_fields_, priority_ = range(5) + model_name_, func_field_to_compute_, target_ids_func_, trigger_fields_, priority_ = range(5) - # only keep functions that should be triggered for the ``fields`` + # only keep store triggers that should be triggered for the ``fields`` # being written to. - to_compute = [f for f in stored_functions \ + triggers_to_compute = [f for f in stored_functions \ if ((not f[trigger_fields_]) or set(fields).intersection(f[trigger_fields_]))] - mapping = {} - for function in to_compute: - # use admin user for accessing objects having rules defined on store fields - target_ids = [id for id in function[id_mapping_fnct_](self, cr, SUPERUSER_ID, ids, context) if id] + to_compute_map = {} + target_id_results = {} + for store_trigger in triggers_to_compute: + target_func_id_ = id(store_trigger[target_ids_func_]) + if not target_func_id_ in target_id_results: + # use admin user for accessing objects having rules defined on store fields + target_id_results[target_func_id_] = [i for i in store_trigger[target_ids_func_](self, cr, SUPERUSER_ID, ids, context) if i] + target_ids = target_id_results[target_func_id_] # the compound key must consider the priority and model name - key = (function[priority_], function[model_name_]) + key = (store_trigger[priority_], store_trigger[model_name_]) for target_id in target_ids: - mapping.setdefault(key, {}).setdefault(target_id,set()).add(tuple(function)) + to_compute_map.setdefault(key, {}).setdefault(target_id,set()).add(tuple(store_trigger)) - # Here mapping looks like: - # { (10, 'model_a') : { target_id1: [ (function_1_tuple, function_2_tuple) ], ... } - # (20, 'model_a') : { target_id2: [ (function_3_tuple, function_4_tuple) ], ... } - # (99, 'model_a') : { target_id1: [ (function_5_tuple, function_6_tuple) ], ... } + # Here to_compute_map looks like: + # { (10, 'model_a') : { target_id1: [ (trigger_1_tuple, trigger_2_tuple) ], ... } + # (20, 'model_a') : { target_id2: [ (trigger_3_tuple, trigger_4_tuple) ], ... } + # (99, 'model_a') : { target_id1: [ (trigger_5_tuple, trigger_6_tuple) ], ... } # } # Now we need to generate the batch function calls list # call_map = # { (10, 'model_a') : [(10, 'model_a', [record_ids,], [function_fields,])] } call_map = {} - for ((priority,model), id_map) in mapping.iteritems(): - functions_ids_maps = {} + for ((priority,model), id_map) in to_compute_map.iteritems(): + trigger_ids_maps = {} # function_ids_maps = # { (function_1_tuple, function_2_tuple) : [target_id1, target_id2, ..] } - for id, functions in id_map.iteritems(): - functions_ids_maps.setdefault(tuple(functions), []).append(id) - for functions, ids in functions_ids_maps.iteritems(): - call_map.setdefault((priority,model),[]).append((priority, model, ids, - [f[func_field_to_compute_] for f in functions])) + for target_id, triggers in id_map.iteritems(): + trigger_ids_maps.setdefault(tuple(triggers), []).append(target_id) + for triggers, target_ids in trigger_ids_maps.iteritems(): + call_map.setdefault((priority,model),[]).append((priority, model, target_ids, + [t[func_field_to_compute_] for t in triggers])) ordered_keys = call_map.keys() ordered_keys.sort() result = [] diff --git a/openerp/tests/test_mail.py b/openerp/tests/test_mail.py index f3d327b0281..c8bbc0c862b 100755 --- a/openerp/tests/test_mail.py +++ b/openerp/tests/test_mail.py @@ -223,6 +223,19 @@ class TestCleaner(unittest2.TestCase): for ext in test_mail_examples.THUNDERBIRD_1_OUT: self.assertNotIn(ext, new_html, 'html_email_cleaner did not erase signature / quoted content') + def test_70_read_more(self): + new_html = html_email_clean(test_mail_examples.BUG1, remove=True, shorten=True, max_length=100) + for ext in test_mail_examples.BUG_1_IN: + self.assertIn(ext, new_html, 'html_email_cleaner wrongly removed valid content') + 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) + 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: + self.assertNotIn(ext, new_html, 'html_email_cleaner did not removed invalid content') + def test_90_misc(self): # False boolean for text must return empty string new_html = html_email_clean(False) diff --git a/openerp/tests/test_mail_examples.py b/openerp/tests/test_mail_examples.py index 7631a28e662..54671106334 100644 --- a/openerp/tests/test_mail_examples.py +++ b/openerp/tests/test_mail_examples.py @@ -637,3 +637,322 @@ MSOFFICE_3 = """
MSOFFICE_3_IN = ['I saw your boss yesterday'] MSOFFICE_3_OUT = ['I noticed you recently downloaded OpenERP.', 'You indicated that you wish', 'Belgium: +32.81.81.37.00'] + + +# ------------------------------------------------------------ +# Test cases coming from bugs +# ------------------------------------------------------------ + +# bug: read more not apparent, strange message in read more span +BUG1 = """
Hi Migration Team,
+
+Paragraph 1, blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah.
+
+Paragraph 2, blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah.
+
+Paragraph 3, blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah blah blah blah blah blah blah 
+blah blah blah blah blah blah blah blah.
+
+Thanks.
+
+Regards,
+
+-- 
+Olivier Laurent
+Migration Manager
+OpenERP SA
+Chaussée de Namur, 40
+B-1367 Gérompont
+Tel: +32.81.81.37.00
+Web: http://www.openerp.com
""" + +BUG_1_IN = [ + 'Hi Migration Team', + 'Paragraph 1' +] +BUG_1_OUT = [ + 'Olivier Laurent', + 'Chaussée de Namur', + '81.81.37.00', + 'openerp.com', +] + + +BUG2 = """ +
+
+

+
+ -------- Original Message -------- + + + + + + + + + + + + + + + + + + + +
Subject: + Fwd: TR: OpenERP S.A. Payment Reminder
Date: Wed, 16 Oct 2013 14:11:13 +0200
From: Christine Herrmann <che@openerp.com>
To: online@openerp.com
+
+
+ +
+

+
+ -------- Message original -------- + + + + + + + + + + + + + + + + + + + + + + + +
Sujet: + TR: OpenERP S.A. Payment Reminder
Date : + Wed, 16 Oct 2013 10:34:45 -0000
De : Ida Siwatala <infos@inzoservices.com>
Répondre + + à : catchall@openerp.my.openerp.com
Pour : + Christine Herrmann (che) <che@openerp.com>
+
+
+
+
+

Bonjour,

+

+

 

+

Pourriez-vous + + me faire un retour sur ce point.

+

+

 

+

Cordialement

+

+

 

+
+
+

De : + Ida Siwatala [mailto:infos@inzoservices.com] +
+ Envoyé : vendredi 4 octobre 2013 20:03
+ À : 'Followers of + INZO-services-8-all-e-Maxime-Lisbonne-77176-Savigny-le-temple-France'
+ Objet : RE: OpenERP S.A. Payment Reminder

+
+
+

 

+

Bonsoir,

+

+

 

+

Je + + me permets de revenir vers vous par écrit , car j’ai + fait 2 appels vers votre service en exposant mon + problème, mais je n’ai pas eu de retour.

+

Cela + + fait un mois que j’ai fait la souscription de votre + produit, mais je me rends compte qu’il est pas adapté à + ma situation ( fonctionnalité manquante et surtout je + n’ai pas beaucoup de temps à passer à résoudre des + bugs).

+

C’est + + pourquoi , j’ai demandé qu’un accord soit trouvé avec + vous pour annuler le contrat (tout en vous payant le + mois d’utilisation de septembre).

+

+

 

+

Pourriez-vous + + me faire un retour sur ce point.

+

+

 

+

Cordialement,

+

+

 

+

Ida + + Siwatala

+

+

 

+

De : + che@openerp.com + [mailto:che@openerp.com] +
+ Envoyé : vendredi 4 octobre 2013 17:41
+ À : infos@inzoservices.com
+ Objet : OpenERP S.A. Payment Reminder

+

 

+
+

Dear + + INZO services,

+

Exception + + made if there was a mistake of ours, it seems that the + following amount stays unpaid. Please, take + appropriate measures in order to carry out this + payment in the next 8 days.

+

+

 

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Date de facturation

+
+

Description

+
+

Reference

+
+

Due Date

+
+

Amount (€)

+
+

Lit.

+
+

2013-09-24

+
+

2013/1121

+
+

Enterprise - Inzo Services + - Juillet 2013

+
+

2013-09-24

+
+

420.0

+

+

+

+

+

+

+

+
+

Amount + + due : 420.00 €

+

Would + + your payment have been carried out after this mail was + sent, please ignore this message. Do not hesitate to + contact our accounting department.

+


+ Best Regards,
+ Aurore Lesage
+ OpenERP
+ Chaussée de Namur, 40
+ B-1367 Grand Rosières
+ Tel: +32.81.81.37.00 - Fax: +32.81.73.35.01
+ E-mail : ale@openerp.com
+ Web: http://www.openerp.com

+
+
+
+ --
+ INZO services Sent by OpenERP + S.A. using OpenERP. + Access your messages and documents in + + OpenERP
+
-- 
+Christine Herrmann 
+
+OpenERP 
+Chaussée de Namur, 40 
+B-1367 Grand Rosières 
+Tel: +32.81.81.37.00 - Fax: +32.81.73.35.01 
+
+Web: http://www.openerp.com 
+
+
+
+
+
+
+ +
""" + +BUG_2_IN = [ + 'read more', + '...', +] +BUG_2_OUT = [ + 'Fwd: TR: OpenERP S.A' + 'fait un mois' +] diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 98f8869f85c..b287ded9d6d 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -175,6 +175,32 @@ 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): + 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) + else: + stop_idx = 0 + node.text = innertext + outertext[0:stop_idx] + # 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'}) + read_more_node.append(read_more_link_node) + # create outertext node + overtext_node = _create_node('span', outertext[stop_idx:]) + # tag node + overtext_node.set('in_overlength', '1') + # add newly created nodes in dom + node.append(read_more_node) + node.append(overtext_node) + if not html or not isinstance(html, basestring): return html html = ustr(html) @@ -206,7 +232,7 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): # 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}[^.]+)') + signature = re.compile(r'([-]{2,}[\s]?[\r\n]{1,2}[\s\S]+)') for node in root.getiterator(): _tag_matching_regex_in_text(quote_tags, node, 'span', {'text_quote': '1'}) _tag_matching_regex_in_text(signature, node, 'span', {'text_signature': '1'}) @@ -226,14 +252,16 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): if 'SkyDrivePlaceholder' in node.get('class', '') or 'SkyDrivePlaceholder' in node.get('id', ''): root.set('hotmail', '1') - # state of the parsing + # state of the parsing: flag quotes and tails to remove if quote_begin: node.set('in_quote', '1') node.set('tail_remove', '1') + # state of the parsing: flag when being in over-length content if overlength: node.set('in_overlength', '1') node.set('tail_remove', '1') + # find quote in msoffice / hotmail / blockquote / text quote and signatures if root.get('msoffice') and node.tag == 'div' and 'border-top:solid' in node.get('style', ''): quote_begin = True node.set('in_quote', '1') @@ -242,36 +270,28 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): quote_begin = True node.set('in_quote', '1') node.set('tail_remove', '1') + if node.tag == 'blockquote' or node.get('text_quote') or node.get('text_signature'): + node.set('in_quote', '1') # shorten: # 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: + 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 - # truncate text - innertext = node.text[0:(max_length - cur_char_nbr)] - outertext = node.text[(max_length - cur_char_nbr):] - stop_idx = outertext.find(' ') - if stop_idx == -1: - stop_idx = len(outertext) - node.text = innertext + outertext[0:stop_idx] - # 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'}) - read_more_node.append(read_more_link_node) - # create outertext node - new_node = _create_node('span', outertext[stop_idx:]) - # add newly created nodes in dom - node.addnext(new_node) - node.addnext(read_more_node) - # tag node - new_node.set('in_overlength', '1') + 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 '') - cur_char_nbr += len(node.text or '') + # Tree modification + # ------------------------------------------------------------ - if node.tag == 'blockquote' or node.get('text_quote') or node.get('text_signature'): - node.set('in_quote', '1') + for node in root.iter(): + if node.get('truncate'): + _truncate_node(node, int(node.get('truncate_position', '0'))) # Post processing # ------------------------------------------------------------