From 96f86dd0e9fe6ede0b029a30189c6a583b815da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 24 Jan 2010 00:21:58 +0100 Subject: [PATCH 01/13] Update COPYRIGHT --- COPYRIGHT | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/COPYRIGHT b/COPYRIGHT index aa03494999..bab90c9e3e 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,7 +1,7 @@ Copyright (C) 2004-2008 Tiny SPRL. -Copyright (C) 2007-2009 Cédric Krier. -Copyright (C) 2007-2008 Bertrand Chenal. -Copyright (C) 2008-2009 B2CK SPRL. +Copyright (C) 2007-2010 Cédric Krier. +Copyright (C) 2007-2010 Bertrand Chenal. +Copyright (C) 2008-2010 B2CK SPRL. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From 07e893992585bc0f2fd2b488b6ec6b17c3cfcd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 26 Jan 2010 12:15:35 +0100 Subject: [PATCH 02/13] Raise exception when search function is missing on Function fields for issue1378 --- CHANGELOG | 1 + trytond/ir/translation.py | 1 + trytond/ir/translation.xml | 9 +++++++++ trytond/model/fields/function.py | 4 +++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 108181ad6e..c1cbe7fa65 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Raise exception when search function is missing on Function fields * Add LRU memoization, use it to cache compiled code for safe_eval. * Add PySON to replace python string evaluation on client side * Add JSON-RPC diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index f7d17d6123..a970ce7752 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -901,6 +901,7 @@ class TranslationClean(Wizard): 'required_field', 'foreign_model_missing', 'foreign_model_exist', + 'search_function_missing', ): continue if model_name in self.pool.object_name_list(): diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml index 9a95fd10ba..a639a7d206 100644 --- a/trytond/ir/translation.xml +++ b/trytond/ir/translation.xml @@ -193,6 +193,15 @@ this repository contains the full copyright notices and license terms. --> ir + + search_function_missing + en_US + error + Missing search function on field "%s". + Missing search function on field "%s". + ir + + ir.translation.set_report.init diff --git a/trytond/model/fields/function.py b/trytond/model/fields/function.py index 16b911f173..2d3320df25 100644 --- a/trytond/model/fields/function.py +++ b/trytond/model/fields/function.py @@ -92,12 +92,14 @@ class Function(Field): :param cursor: The database cursor. :param user: The user id. :param model: The model. + :param name: The name of the field. :param args: The search domain. See ModelStorage.search :param context: The context. :return: New list of domain. ''' if not self.fnct_search: - return [] + model.raise_user_error(cursor, 'search_function_missing', + name, context=context) return getattr(model, self.fnct_search)(cursor, user, name, args, context=context) From 8369b484f37c84d13ff4f930523ba7d63bd168a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 30 Jan 2010 00:17:49 +0100 Subject: [PATCH 03/13] Remove hardcoded True in SQL for issue1403 --- trytond/ir/ui/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py index b455d84cb9..508f1c9c65 100644 --- a/trytond/ir/ui/view.py +++ b/trytond/ir/ui/view.py @@ -154,9 +154,9 @@ class View(ModelSQL, ModelView): if seqmatch.ratio() > 0.6: cursor.execute('UPDATE ir_translation ' \ 'SET src = %s, ' \ - 'fuzzy = True ' \ + 'fuzzy = %s ' \ 'WHERE id = %s ', - (string, trans_views[string_trans]['id'])) + (string, True, trans_views[string_trans]['id'])) del trans_views[string_trans] done = True break From 0a9fae483ffd68011af3bc214f4fcee68de60c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 15:23:19 +0100 Subject: [PATCH 04/13] Return the same id when reading with default_get in Singleton --- trytond/model/modelsingleton.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/trytond/model/modelsingleton.py b/trytond/model/modelsingleton.py index 07da6d3656..4a9e79d394 100644 --- a/trytond/model/modelsingleton.py +++ b/trytond/model/modelsingleton.py @@ -29,9 +29,11 @@ class ModelSingleton(ModelStorage): if not singleton_id: res = self.default_get(cursor, user, fields_names, context=context, with_rec_name=False) - res['id'] = 1 if not isinstance(ids, (int, long)): + res['id'] = ids[0] res = [res] + else: + res['id'] = ids return res if isinstance(ids, (int, long)): ids2 = singleton_id From 6aec5c64abad6bfb5bbf54cb0b698583679f667f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 15:25:57 +0100 Subject: [PATCH 05/13] Don't put "OFFSET 0" for PostgreSQL backend "OFFSET 0" adds a limit operation in query plan --- trytond/backend/postgresql/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index dad35e6b2f..7be319b01f 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -389,7 +389,7 @@ class Cursor(CursorInterface): def limit_clause(self, select, limit=None, offset=None): if limit is not None: select += ' LIMIT %d' % limit - if offset is not None: + if offset is not None and offset != 0: select += ' OFFSET %d' % offset return select From 010ebcc33e7b5e90ac450353f16fb1295d6e9ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 15:31:21 +0100 Subject: [PATCH 06/13] Use generator expression to instanciate BrowseRecordList instead of list comprehensions --- trytond/model/modelstorage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index b17952b640..a54b58e1c9 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -509,8 +509,8 @@ class ModelStorage(Model): if isinstance(ids, (int, long)): return BrowseRecord(cursor, user, ids, self, context=context) local_cache = {} - return BrowseRecordList([BrowseRecord(cursor, user, x, self, - local_cache=local_cache, context=context) for x in ids], + return BrowseRecordList((BrowseRecord(cursor, user, x, self, + local_cache=local_cache, context=context) for x in ids), context=context) def __export_row(self, cursor, user, row, fields_names, context=None): From 4795d98d5d05c6e87645e387d3d0510ce467bd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 15:32:58 +0100 Subject: [PATCH 07/13] Use decorator for Cache --- trytond/model/modelview.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trytond/model/modelview.py b/trytond/model/modelview.py index 7b6bffa0cc..f1a03dc89d 100644 --- a/trytond/model/modelview.py +++ b/trytond/model/modelview.py @@ -91,6 +91,7 @@ class ModelView(Model): super(ModelView, self).__init__() self._rpc['fields_view_get'] = False + @Cache('modelview.fields_view_get') def fields_view_get(self, cursor, user, view_id=None, view_type='form', context=None, toolbar=False, hexmd5=None): ''' @@ -248,8 +249,6 @@ class ModelView(Model): return True return result - fields_view_get = Cache('modelview.fields_view_get')(fields_view_get) - def view_header_get(self, cursor, user, value, view_type='form', context=None): """ From a8c374617e066dcbf533fab36453d45855575dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 15:59:10 +0100 Subject: [PATCH 08/13] Refactor import_data of ModelStorage thanks to h.goebel - translate error message - handle numeric, date, datetime, reference fields - improve relation search, fail if too much is found - add unittest - add escapechar '\' for ',' in many2many list --- CHANGELOG | 1 + trytond/ir/translation.py | 5 + trytond/ir/translation.xml | 45 +++++ trytond/model/modelstorage.py | 332 ++++++++++++++++++++-------------- trytond/tests/__init__.py | 1 + trytond/tests/__tryton__.py | 3 +- trytond/tests/import_data.py | 185 +++++++++++++++++++ trytond/tests/import_data.xml | 63 +++++++ trytond/tests/test_tryton.py | 331 +++++++++++++++++++++++++++++++++ 9 files changed, 831 insertions(+), 135 deletions(-) create mode 100644 trytond/tests/import_data.py create mode 100644 trytond/tests/import_data.xml diff --git a/CHANGELOG b/CHANGELOG index c1cbe7fa65..c4635a41de 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Refactor import_data of ModelStorage * Raise exception when search function is missing on Function fields * Add LRU memoization, use it to cache compiled code for safe_eval. * Add PySON to replace python string evaluation on client side diff --git a/trytond/ir/translation.py b/trytond/ir/translation.py index a970ce7752..524e88aa0d 100644 --- a/trytond/ir/translation.py +++ b/trytond/ir/translation.py @@ -891,6 +891,11 @@ class TranslationClean(Wizard): 'delete_xml_record', 'xml_record_desc', 'write_xml_record', + 'not_found_in_selection', + 'relation_not_found', + 'too_many_relations_found', + 'xml_id_syntax_error', + 'reference_syntax_error', 'delete_workflow_record', 'domain_validation_record', 'required_validation_record', diff --git a/trytond/ir/translation.xml b/trytond/ir/translation.xml index a639a7d206..08afa35524 100644 --- a/trytond/ir/translation.xml +++ b/trytond/ir/translation.xml @@ -97,6 +97,51 @@ this repository contains the full copyright notices and license terms. --> ir + + not_found_in_selection + en_US + error + Key %r not found in selection field %r + Key %r not found in selection field %r + ir + + + + relation_not_found + en_US + error + Relation not found: %r in %s + Relation not found: %r in %s + ir + + + + too_many_relations_found + en_US + error + Too many relations found: %r in %s + Too many relations found: %r in %s + ir + + + + xml_id_syntax_error + en_US + error + Syntax error for XML id %r in %s + Syntax error for XML id %r in %s + ir + + + + reference_syntax_error + en_US + error + Syntax error for reference %r in %s + Syntax error for reference %r in %s + ir + + delete_workflow_record en_US diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index a54b58e1c9..810130a9e9 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -1,5 +1,6 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. + from trytond.model import Model from trytond.model import fields from trytond.model.browse import BrowseRecordList, BrowseRecord, BrowseRecordNull @@ -12,6 +13,11 @@ import time from decimal import Decimal import logging from itertools import chain +try: + import cStringIO as StringIO +except ImprotError: + import StringIO +import csv class ModelStorage(Model): @@ -584,186 +590,244 @@ class ModelStorage(Model): - the exception if failed - the warning if failed ''' - if context is None: - context = {} - fields_names = [x.split('/') for x in fields_names] - logger = logging.getLogger('import') + def process_lines(self, datas, prefix, fields_def, position=0): + + def warn(msgname, *args): + msg = self.raise_user_error(cursor, msgname, args, + raise_exception=False, context=context) + warnings.warn(msg) + + def get_selection(selection, value): + res = False + if not isinstance(selection, (tuple, list)): + selection = getattr(self, selection)(cursor, user, + context=context) + for key, val in selection: + if str(key) == value: + res = key + break + if value and not res: + warn('not_found_in_selection', value, '/'.join(field)) + return res + + def get_many2one(relation, value): + if not value: + return False + relation_obj = self.pool.get(relation) + res = relation_obj.search(cursor, user, [ + ('rec_name', '=', value), + ], limit=2, context=context) + if len(res) < 1: + warn('relation_not_found', value, relation) + res = False + elif len(res) > 1: + warn('too_many_relations_found', value, relation) + res = False + else: + res = res[0] + return res + + def get_many2many(relation, value): + if not value: + return False + res = [] + relation_obj = self.pool.get(relation) + for word in csv.reader(StringIO.StringIO(value), delimiter=',', + quoting=csv.QUOTE_NONE, escapechar='\\').next(): + res2 = relation_obj.search(cursor, user, [ + ('rec_name', '=', word), + ], limit=2, context=context) + if len(res2) < 1: + warn('relation_not_found', word, relation) + elif len(res2) > 1: + warn('too_many_relations_found', word, relation) + else: + res.extend(res2) + if len(res): + res = [('set', res)] + return res + + def get_reference(value): + if not value: + return False + try: + relation, value = value.split(',', 1) + except: + warn('reference_syntax_error', value, '/'.join(field)) + return False + relation_obj = self.pool.get(relation) + res = relation_obj.search(cursor, user, [ + ('rec_name', '=', value), + ], limit=2, context=context) + if len(res) < 1: + warn('relation_not_found', value, relation) + res = False + elif len(res) > 1: + warn('too_many_relations_found', value, relation) + res = False + else: + res = '%s,%s' % (relation, str(res[0])) + return res + + def get_by_id(value): + if not value: + return False + relation = None + type = fields_def[field[-1][:-3]]['type'] + if type == 'many2many': + value = csv.reader(StringIO.StringIO(value), delimiter=',', + quoting=csv.QUOTE_NONE, escapechar='\\').next() + elif type == 'reference': + try: + relation, value = value.split(',', 1) + except: + warn('reference_syntax_error', value, '/'.join(field)) + return False + value = [value] + else: + value = [value] + res_ids = [] + for word in value: + try: + module, xml_id = word.rsplit('.', 1) + except: + warn('xml_id_syntax_error', word, '/'.join(field)) + continue + db_id = ir_model_data_obj.get_id(cursor, user, + module, xml_id) + res_ids.append(db_id) + if type == 'many2many' and res_ids: + return [('set', res_ids)] + elif type == 'reference' and res_ids: + return '%s,%s' % (relation, str(res_ids[0])) + return res_ids and res_ids[0] or False - def process_liness(self, datas, prefix, fields_def, position=0): line = datas[position] row = {} translate = {} - todo = [] - warning = '' - + todo = set() + prefix_len = len(prefix) # Import normal fields_names - for i in range(len(fields_names)): + for i, field in enumerate(fields_names): if i >= len(line): raise Exception('ImportError', 'Please check that all your lines have %d cols.' % \ (len(fields_names),)) - field = fields_names[i] - if (len(field) == len(prefix) + 1) \ - and field[len(prefix)].endswith(':id'): - res_id = False - if line[i]: - if fields_def[field[len(prefix)][:-3]]['type'] \ - == 'many2many': - res_id = [] - for word in line[i].split(','): - module, xml_id = word.rsplit('.', 1) - ir_model_data_obj = \ - self.pool.get('ir.model.data') - new_id = ir_model_data_obj.get_id(cursor, - user, module, xml_id) - res_id2 = ir_model_data_obj.read(cursor, user, - [new_id], ['res_id'])[0]['res_id'] - if res_id2: - res_id.append(res_id2) - if len(res_id): - res_id = [('set', res_id)] + is_prefix_len = (len(field) == (prefix_len + 1)) + value = line[i] + if is_prefix_len and field[-1].endswith(':id'): + row[field[0][:-3]] = get_by_id(value) + elif is_prefix_len and ':lang=' in field[-1]: + field_name, lang = field[-1].split(':lang=') + translate.setdefault(lang, {})[field_name] = value or False + elif is_prefix_len and prefix == field[:-1]: + this_field_def = fields_def[field[-1]] + field_type = this_field_def['type'] + res = False + if field_type == 'boolean': + if value.lower() == 'true': + res = True + elif value.lower() == 'false': + res = False else: - module, xml_id = line[i].rsplit('.', 1) - ir_model_data_obj = self.pool.get('ir.model.data') - new_id = ir_model_data_obj.get_id(cursor, user, - module, xml_id) - res_id = ir_model_data_obj.read(cursor, user, - [new_id], ['res_id'])[0]['res_id'] - row[field[0][:-3]] = res_id or False - continue - if (len(field) == len(prefix)+1) and \ - len(field[len(prefix)].split(':lang=')) == 2: - field, lang = field[len(prefix)].split(':lang=') - translate.setdefault(lang, {})[field]=line[i] or False - continue - if (len(field) == len(prefix)+1) and \ - (prefix == field[0:len(prefix)]): - if fields_def[field[len(prefix)]]['type'] == 'integer': - res = line[i] and int(line[i]) - elif fields_def[field[len(prefix)]]['type'] == 'float': - res = line[i] and float(line[i]) - elif fields_def[field[len(prefix)]]['type'] == 'selection': - res = False - if isinstance( - fields_def[field[len(prefix)]]['selection'], - (tuple, list)): - sel = fields_def[field[len(prefix)]]['selection'] - else: - sel = getattr(self, fields_def[field[len(prefix)]]\ - ['selection'])(cursor, user, context) - for key, val in sel: - if str(key) == line[i]: - res = key - if line[i] and not res: - logger.warning("key '%s' not found " \ - "in selection field '%s'" % \ - (line[i], field[len(prefix)])) - elif fields_def[field[len(prefix)]]['type'] == 'many2one': - res = False - if line[i]: - relation = \ - fields_def[field[len(prefix)]]['relation'] - relation_obj = self.pool.get(relation) - res = relation_obj.search(cursor, user, [ - ('rec_name', '=', line[i]), - ], limit=1, context=context) - if not res: - warning += ('Relation not found: ' + line[i] + \ - ' on ' + relation + ' !\n') - logger.warning( - 'Relation not found: ' + line[i] + \ - ' on ' + relation + ' !\n') - res = False - else: - res = res[0] - elif fields_def[field[len(prefix)]]['type'] == 'many2many': - res = [] - if line[i]: - relation = \ - fields_def[field[len(prefix)]]['relation'] - for word in line[i].split(','): - relation_obj = self.pool.get(relation) - res2 = relation_obj.search(cursor, user, [ - ('rec_name', '=', word), - ], limit=1, context=context) - if not res2: - warning += ('Relation not found: ' + \ - word + ' on '+relation + ' !\n') - logger.warning( - 'Relation not found: ' + word + \ - ' on '+relation + ' !\n') - else: - res.extend(res2) - if len(res): - res = [('set', res)] + res = value and bool(int(value)) + elif field_type == 'integer': + res = value and int(value) + elif field_type == 'float': + res = value and float(value) + elif field_type == 'numeric': + res = value and Decimal(value) + elif field_type == 'date': + res = value and datetime.date(*time.strptime(value, + '%Y-%m-%d')[:3]) + elif field_type == 'datetime': + res = value and datetime.datetime(*time.strptime(value, + '%Y-%m-%d %H:%M:%S')[:6]) + elif field_type == 'selection': + res = get_selection(this_field_def['selection'], value) + elif field_type == 'many2one': + res = get_many2one(this_field_def['relation'], value) + elif field_type == 'many2many': + res = get_many2many(this_field_def['relation'], value) + elif field_type == 'reference': + res = get_reference(value) else: - res = line[i] or False - row[field[len(prefix)]] = res - elif (prefix==field[0:len(prefix)]): - if field[0] not in todo: - todo.append(field[len(prefix)]) - + res = value or False + row[field[-1]] = res + elif prefix == field[0:prefix_len]: + todo.add(field[prefix_len]) # Import one2many fields nbrmax = 1 for field in todo: newfd = self.pool.get(fields_def[field]['relation']).fields_get( cursor, user, context=context) - res = process_liness(self, datas, prefix + [field], newfd, + res = process_lines(self, datas, prefix + [field], newfd, position) - (newrow, max2, warning2, translate2) = res + (newrow, max2, translate2) = res nbrmax = max(nbrmax, max2) - warning = warning + warning2 reduce(lambda x, y: x and y, newrow) - row[field] = (reduce(lambda x, y: x or y, newrow.values()) and \ - [('create', newrow)]) or [] + row[field] = (reduce(lambda x, y: x or y, newrow.values()) and + [('create', newrow)]) or [] i = max2 - while (position+i) + + + + + + Test + + + + Duplicate + + + + Duplicate + + + + Test 1 + + + + Test 2 + + + + Test, comma + + + + Duplicate + + + + Duplicate + + + + Test + + + + Duplicate + + + + Duplicate + + + + diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py index e4e0c8d9ba..549157d153 100644 --- a/trytond/tests/test_tryton.py +++ b/trytond/tests/test_tryton.py @@ -2913,6 +2913,335 @@ class MPTTTestCase(unittest.TestCase): self.CheckTree() +class ImportDataTestCase(unittest.TestCase): + ''' + Test import_data. + ''' + + def setUp(self): + install_module('tests') + self.boolean = RPCProxy('tests.import_data.boolean') + self.integer = RPCProxy('tests.import_data.integer') + self.float = RPCProxy('tests.import_data.float') + self.numeric = RPCProxy('tests.import_data.numeric') + self.char = RPCProxy('tests.import_data.char') + self.text = RPCProxy('tests.import_data.text') + self.sha = RPCProxy('tests.import_data.sha') + self.date = RPCProxy('tests.import_data.date') + self.datetime = RPCProxy('tests.import_data.datetime') + self.selection = RPCProxy('tests.import_data.selection') + self.many2one = RPCProxy('tests.import_data.many2one') + self.many2many = RPCProxy('tests.import_data.many2many') + self.one2many = RPCProxy('tests.import_data.one2many') + self.reference = RPCProxy('tests.import_data.reference') + + def test0010boolean(self): + ''' + Test boolean. + ''' + self.assert_(self.boolean.import_data(['boolean'], [['True']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], [['1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], [['False']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], [['0']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], + [['True'], ['False']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.boolean.import_data(['boolean'], [['foo']], + CONTEXT)[0] == -1) + + def test0020integer(self): + ''' + Test integer. + ''' + self.assert_(self.integer.import_data(['integer'], [['1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.integer.import_data(['integer'], [['-1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.integer.import_data(['integer'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.integer.import_data(['integer'], + [['1'], ['2']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.integer.import_data(['integer'], [['1.1']], + CONTEXT)[0] == -1) + + self.assert_(self.integer.import_data(['integer'], [['-1.1']], + CONTEXT)[0] == -1) + + self.assert_(self.integer.import_data(['integer'], [['foo']], + CONTEXT)[0] == -1) + + def test0030float(self): + ''' + Test float. + ''' + self.assert_(self.float.import_data(['float'], [['1.1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.float.import_data(['float'], [['-1.1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.float.import_data(['float'], [['1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.float.import_data(['float'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.float.import_data(['float'], + [['1.1'], ['2.2']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.float.import_data(['float'], [['foo']], CONTEXT)[0] + == -1) + + def test0040numeric(self): + ''' + Test numeric. + ''' + self.assert_(self.numeric.import_data(['numeric'], [['1.1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.numeric.import_data(['numeric'], [['-1.1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.numeric.import_data(['numeric'], [['1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.numeric.import_data(['numeric'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.numeric.import_data(['numeric'], + [['1.1'], ['2.2']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.numeric.import_data(['numeric'], [['foo']], + CONTEXT)[0] == -1) + + def test0050char(self): + ''' + Test char. + ''' + self.assert_(self.char.import_data(['char'], [['test']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.char.import_data(['char'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.char.import_data(['char'], + [['test'], ['foo'], ['bar']], CONTEXT) == (3, 0, 0, 0)) + + def test0060text(self): + ''' + Test text. + ''' + self.assert_(self.text.import_data(['text'], [['test']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.text.import_data(['text'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.text.import_data(['text'], + [['test'], ['foo'], ['bar']], CONTEXT) == (3, 0, 0, 0)) + + def test0070sha(self): + ''' + Test sha. + ''' + self.assert_(self.sha.import_data(['sha'], [['test']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.sha.import_data(['sha'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.sha.import_data(['sha'], + [['test'], ['foo']], CONTEXT) == (2, 0, 0, 0)) + + def test0080date(self): + ''' + Test date. + ''' + self.assert_(self.date.import_data(['date'], [['2010-01-01']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.date.import_data(['date'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.date.import_data(['date'], [ + ['2010-01-01'], ['2010-02-01']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.date.import_data(['date'], [['foo']], CONTEXT)[0] + == -1) + + def test0090datetime(self): + ''' + Test datetime. + ''' + self.assert_(self.datetime.import_data(['datetime'], + [['2010-01-01 12:00:00']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.datetime.import_data(['datetime'], [['']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.datetime.import_data(['datetime'], + [['2010-01-01 12:00:00'], ['2010-01-01 13:30:00']], CONTEXT) + == (2, 0, 0, 0)) + + self.assert_(self.datetime.import_data(['datetime'], [['foo']], + CONTEXT)[0] == -1) + + def test0100selection(self): + ''' + Test selection. + ''' + self.assert_(self.selection.import_data(['selection'], [['select1']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.selection.import_data(['selection'], [['']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.selection.import_data(['selection'], + [['select1'], ['select2']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.selection.import_data(['selection'], [['foo']], + CONTEXT)[0] == -1) + + def test0110many2one(self): + ''' + Test many2one. + ''' + self.assert_(self.many2one.import_data(['many2one'], [['Test']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2one.import_data(['many2one:id'], + [['tests.import_data_many2one_target_test']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.many2one.import_data(['many2one'], [['']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2one.import_data(['many2one'], + [['Test'], ['Test']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.many2one.import_data(['many2one'], [['foo']], + CONTEXT)[0] == -1) + + self.assert_(self.many2one.import_data(['many2one'], [['Duplicate']], + CONTEXT)[0] == -1) + + self.assert_(self.many2one.import_data(['many2one:id'], [['foo']], + CONTEXT)[0] == -1) + + self.assert_(self.many2one.import_data(['many2one:id'], + [['tests.foo']], CONTEXT)[0] == -1) + + def test0120many2many(self): + ''' + Test many2many. + ''' + self.assert_(self.many2many.import_data(['many2many'], [['Test 1']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many:id'], + [['tests.import_data_many2many_target_test1']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], + [['Test 1,Test 2']], CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many:id'], + [['tests.import_data_many2many_target_test1,' \ + 'tests.import_data_many2many_target_test2']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], + [['Test\, comma']], CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], + [['Test\, comma,Test 1']], CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], [['']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], + [['Test 1'], ['Test 2']], CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.many2many.import_data(['many2many'], [['foo']], + CONTEXT)[0] == -1) + + self.assert_(self.many2many.import_data(['many2many'], [['Test 1,foo']], + CONTEXT)[0] == -1) + + self.assert_(self.many2many.import_data(['many2many'], [['Duplicate']], + CONTEXT)[0] == -1) + + self.assert_(self.many2many.import_data(['many2many'], + [['Test 1,Duplicate']], CONTEXT)[0] == -1) + + def test0130one2many(self): + ''' + Test one2many. + ''' + self.assert_(self.one2many.import_data(['name', 'one2many/name'], + [['Test', 'Test 1']], CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.one2many.import_data(['name', 'one2many/name'], + [['Test', 'Test 1'], ['', 'Test 2']], CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.one2many.import_data(['name', 'one2many/name'], + [['Test 1', 'Test 1'], ['', 'Test 2'], + ['Test 2', 'Test 1']], CONTEXT) == (2, 0, 0, 0)) + + def test0140reference(self): + ''' + Test reference. + ''' + self.assert_(self.reference.import_data(['reference'], + [['tests.import_data.reference.selection,Test']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.reference.import_data(['reference:id'], + [['tests.import_data.reference.selection,'\ + 'tests.import_data_reference_selection_test']], CONTEXT) + == (1, 0, 0, 0)) + + self.assert_(self.reference.import_data(['reference'], [['']], + CONTEXT) == (1, 0, 0, 0)) + + self.assert_(self.reference.import_data(['reference'], + [['tests.import_data.reference.selection,Test'], + ['tests.import_data.reference.selection,Test']], + CONTEXT) == (2, 0, 0, 0)) + + self.assert_(self.reference.import_data(['reference'], + [['tests.import_data.reference.selection,foo']], + CONTEXT)[0] == -1) + + self.assert_(self.reference.import_data(['reference'], + [['tests.import_data.reference.selection,Duplicate']], + CONTEXT)[0] == -1) + + self.assert_(self.reference.import_data(['reference:id'], + [['tests.import_data.reference.selection,foo']], + CONTEXT)[0] == -1) + + self.assert_(self.reference.import_data(['reference:id'], + [['tests.import_data.reference.selection,tests.foo']], + CONTEXT)[0] == -1) + + class RPCProxy(object): def __init__(self, name): @@ -3008,5 +3337,7 @@ if __name__ == '__main__': suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelSingletonTestCase)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ModelViewTestCase)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(MPTTTestCase)) + suite.addTests(unittest.TestLoader().loadTestsFromTestCase( + ImportDataTestCase)) unittest.TextTestRunner(verbosity=2).run(suite) SOCK.disconnect() From dbd73db28b3c401e217f479b14db92cb6f9eb188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 3 Feb 2010 17:36:44 +0100 Subject: [PATCH 09/13] Fix exec_pg_command_pipe to work on windows for issue1405 --- trytond/tools/misc.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py index 21ecac11d6..50bff0c056 100644 --- a/trytond/tools/misc.py +++ b/trytond/tools/misc.py @@ -67,10 +67,8 @@ def exec_pg_command_pipe(name, *args): child_env = dict(os.environ) if CONFIG['db_password']: child_env['PGPASSWORD'] = CONFIG['db_password'] - pipe = subprocess.Popen(cmd, shell=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - close_fds=True, - env=child_env) + pipe = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, env=child_env) return pipe def exec_command_pipe(name, *args): From ac9c532f4192a2f0e7b582ccea4eb2c80c32ba08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 8 Feb 2010 09:36:10 +0100 Subject: [PATCH 10/13] Fix wrong initialisation type of _indexes in TableHandler of PostgreSQL backend --- trytond/backend/postgresql/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py index d05f5cdcbb..69aa738845 100644 --- a/trytond/backend/postgresql/table.py +++ b/trytond/backend/postgresql/table.py @@ -14,7 +14,7 @@ class TableHandler(TableHandlerInterface): self._columns = {} self._constraints = [] self._fk_deltypes = {} - self._indexes = {} + self._indexes = [] self._field2module = {} # Create sequence if necessary From 6961c5c925efb2f99b89172f1f8986fce77cc83c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 8 Feb 2010 09:36:32 +0100 Subject: [PATCH 11/13] Fix typo --- trytond/backend/postgresql/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trytond/backend/postgresql/table.py b/trytond/backend/postgresql/table.py index 69aa738845..a2dbdc7433 100644 --- a/trytond/backend/postgresql/table.py +++ b/trytond/backend/postgresql/table.py @@ -136,7 +136,7 @@ class TableHandler(TableHandlerInterface): "size": size, "typname": typname} - # fetch constrains for the table + # fetch constraints for the table self.cursor.execute("SELECT co.contype, co.confdeltype, at.attname, "\ "cl2.relname, co.conname "\ "FROM pg_constraint co "\ From f042e768811c8459a101ba95a0910ae4819b6d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 9 Feb 2010 10:13:17 +0100 Subject: [PATCH 12/13] Modify ondelete attribute of Many2One according to required attribute value "SET NULL" will be changed into "RESTRICT" if required is set. --- CHANGELOG | 1 + trytond/model/fields/many2one.py | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c4635a41de..382c4dfd30 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Modify ondelete attribute of Many2One according to required attribute value * Refactor import_data of ModelStorage * Raise exception when search function is missing on Function fields * Add LRU memoization, use it to cache compiled code for safe_eval. diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index 8787fe40be..8e04f94f63 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -23,9 +23,14 @@ class Many2One(Field): :param right: The name of the field to store the right value. See left :param ondelete: Define the behavior of the record when the target record is deleted. (``CASCADE``, ``RESTRICT``, ``SET NULL``) + ``SET NULL`` will be changed into ``RESTRICT`` if required is set. :param datetime_field: The name of the field that contains the datetime value to read the target record. ''' + self.__required = required + if ondelete not in ('CASCADE', 'RESTRICT', 'SET NULL'): + raise Exception('Bad arguments') + self.ondelete = ondelete if datetime_field: if depends: depends.append(datetime_field) @@ -40,8 +45,16 @@ class Many2One(Field): self.model_name = model_name self.left = left self.right = right - if ondelete not in ('CASCADE', 'RESTRICT', 'SET NULL'): - raise Exception('Bad arguments') - self.ondelete = ondelete self.datetime_field = datetime_field + + def __get_required(self): + return self.__required + + def __set_required(self, value): + self.__required = value + if value and self.ondelete == 'SET NULL': + self.ondelete = 'RESTRICT' + + required = property(__get_required, __set_required) + __init__.__doc__ += Field.__init__.__doc__ From 6997fd60b2996f46ef082bc1e071af5b50e6b80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Wed, 10 Feb 2010 18:43:39 +0100 Subject: [PATCH 13/13] Fix typo in sqlite database functions --- trytond/backend/sqlite/database.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index d522a599ce..6d11388fe3 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -54,7 +54,7 @@ def extract(lookup_type, date): elif lookup_type.lower() == 'millennium': return date.year / 1000 + (date.year % 1000 and 1 or 0) elif lookup_type.lower() == 'milliseconds': - return a.microsecond / 1000 + return date.microsecond / 1000 elif lookup_type.lower() == 'quarter': return date.month / 4 + 1 elif lookup_type.lower() == 'week': @@ -65,15 +65,16 @@ def date_trunc(_type, date): if _type == 'second': return date try: - time.strptime(date, '%Y-%m-%d %H:%M:%S') + tm_tuple = time.strptime(date, '%Y-%m-%d %H:%M:%S') except: return None if _type == 'year': - return "%i-01-01 00:00:00" % dt.year + return "%i-01-01 00:00:00" % tm_tuple.tm_year elif _type == 'month': - return "%i-%02i-01 00:00:00" % (dt.year, dt.month) + return "%i-%02i-01 00:00:00" % (tm_tuple.tm_year, tm_tuple.tm_mon) elif _type == 'day': - return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day) + return "%i-%02i-%02i 00:00:00" % (tm_tuple.tm_year, tm_tuple.tm_mon, + tm_tuple.tm_mday) def split_part(text, delimiter, count): return (text.split(delimiter) + [''] * (count - 1))[count - 1]