From 6acc5a157dc287adde7d31978e2ecc163a3ef9ef Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 15 Nov 2011 17:38:43 +0100 Subject: [PATCH 001/138] [IMP] Moved the tests directory inside the openerp module, added --run-tests command-line argument. bzr revid: vmt@openerp.com-20111115163843-djq6hybp24lk9f5e --- openerp-server | 14 +++++++++++ openerp/addons/base/test/test_ir_cron.py | 12 +++++----- openerp/tests/__init__.py | 17 +++++++++++++ .../tests}/addons/test_exceptions/__init__.py | 0 .../addons/test_exceptions/__openerp__.py | 0 .../tests}/addons/test_exceptions/models.py | 0 .../tests}/addons/test_exceptions/view.xml | 0 {tests => openerp/tests}/common.py | 24 ++++++++++--------- {tests => openerp/tests}/test_ir_sequence.py | 4 +++- {tests => openerp/tests}/test_orm.py | 8 +++---- {tests => openerp/tests}/test_xmlrpc.py | 5 +++- openerp/tools/config.py | 5 ++++ tests/__init__.py | 2 -- 13 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 openerp/tests/__init__.py rename {tests => openerp/tests}/addons/test_exceptions/__init__.py (100%) rename {tests => openerp/tests}/addons/test_exceptions/__openerp__.py (100%) rename {tests => openerp/tests}/addons/test_exceptions/models.py (100%) rename {tests => openerp/tests}/addons/test_exceptions/view.xml (100%) rename {tests => openerp/tests}/common.py (84%) rename {tests => openerp/tests}/test_ir_sequence.py (99%) rename {tests => openerp/tests}/test_orm.py (97%) rename {tests => openerp/tests}/test_xmlrpc.py (96%) delete mode 100644 tests/__init__.py diff --git a/openerp-server b/openerp-server index 71a52d5f06b..c108d9f9372 100755 --- a/openerp-server +++ b/openerp-server @@ -221,6 +221,20 @@ if __name__ == "__main__": setup_signal_handlers() + if config["run_tests_no_db"]: + import unittest2 + import openerp.tests + # This test suite creates a database. + unittest2.TextTestRunner(verbosity=2).run(openerp.tests.make_suite_no_db()) + sys.exit(0) + + if config["run_tests"]: + import unittest2 + import openerp.tests + # This test suite assumes a database. + unittest2.TextTestRunner(verbosity=2).run(openerp.tests.make_suite()) + sys.exit(0) + if config["test_file"]: run_test_file(config['db_name'], config['test_file']) sys.exit(0) diff --git a/openerp/addons/base/test/test_ir_cron.py b/openerp/addons/base/test/test_ir_cron.py index 6ef79ac3a78..2154c014f8c 100644 --- a/openerp/addons/base/test/test_ir_cron.py +++ b/openerp/addons/base/test/test_ir_cron.py @@ -40,7 +40,7 @@ JOB = { 'model': u'ir.cron' } -class test_ir_cron(openerp.osv.osv.osv): +class x_test_ir_cron(openerp.osv.osv.osv): """ Add a few handy methods to test cron jobs scheduling. """ _inherit = "ir.cron" @@ -57,7 +57,7 @@ class test_ir_cron(openerp.osv.osv.osv): time.sleep(80) print ">>> out _80_seconds" - def test_0(self, cr, uid): + def x_test_0(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') @@ -66,17 +66,17 @@ class test_ir_cron(openerp.osv.osv.osv): self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2)) self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3)) - def test_1(self, cr, uid): + def x_test_1(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3)) - def test_2(self, cr, uid): + def x_test_2(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2)) - def test_3(self, cr, uid): + def x_test_3(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') @@ -86,7 +86,7 @@ class test_ir_cron(openerp.osv.osv.osv): self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) # This test assumes 4 cron threads. - def test_00(self, cr, uid): + def x_test_00(self, cr, uid): self.test_00_set = set() now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py new file mode 100644 index 00000000000..306edcd68ce --- /dev/null +++ b/openerp/tests/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +import unittest2 + +import test_orm +import test_ir_sequence +import test_xmlrpc + +def make_suite(): + suite = unittest2.TestSuite() + suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_ir_sequence)) + suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_orm)) + return suite + +def make_suite_no_db(): + suite = unittest2.TestSuite() + suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_xmlrpc)) + return suite diff --git a/tests/addons/test_exceptions/__init__.py b/openerp/tests/addons/test_exceptions/__init__.py similarity index 100% rename from tests/addons/test_exceptions/__init__.py rename to openerp/tests/addons/test_exceptions/__init__.py diff --git a/tests/addons/test_exceptions/__openerp__.py b/openerp/tests/addons/test_exceptions/__openerp__.py similarity index 100% rename from tests/addons/test_exceptions/__openerp__.py rename to openerp/tests/addons/test_exceptions/__openerp__.py diff --git a/tests/addons/test_exceptions/models.py b/openerp/tests/addons/test_exceptions/models.py similarity index 100% rename from tests/addons/test_exceptions/models.py rename to openerp/tests/addons/test_exceptions/models.py diff --git a/tests/addons/test_exceptions/view.xml b/openerp/tests/addons/test_exceptions/view.xml similarity index 100% rename from tests/addons/test_exceptions/view.xml rename to openerp/tests/addons/test_exceptions/view.xml diff --git a/tests/common.py b/openerp/tests/common.py similarity index 84% rename from tests/common.py rename to openerp/tests/common.py index 116898dc7ef..618eafb21f7 100644 --- a/tests/common.py +++ b/openerp/tests/common.py @@ -6,9 +6,10 @@ import xmlrpclib import openerp -ADDONS_PATH = os.environ['OPENERP_ADDONS_PATH'] -PORT = int(os.environ['OPENERP_PORT']) -DB = os.environ['OPENERP_DATABASE'] +# The openerp library is supposed already configured. +ADDONS_PATH = openerp.tools.config['addons_path'] +PORT = openerp.tools.config['xmlrpc_port'] +DB = openerp.tools.config['db_name'] HOST = '127.0.0.1' @@ -25,15 +26,19 @@ db_proxy_61 = None model_proxy_61 = None model_uri_61 = None -def setUpModule(): +def start_openerp(): """ - Start the OpenERP server similary to the openerp-server script and - setup some xmlrpclib proxies. + Start the OpenERP server similary to the openerp-server script. """ - openerp.tools.config['addons_path'] = ADDONS_PATH - openerp.tools.config['xmlrpc_port'] = PORT openerp.service.start_services() + # Ugly way to ensure the server is listening. + time.sleep(2) + +def create_xmlrpc_proxies(): + """ + setup some xmlrpclib proxies. + """ global common_proxy_60 global db_proxy_60 global object_proxy_60 @@ -55,9 +60,6 @@ def setUpModule(): db_proxy_61 = xmlrpclib.ServerProxy(model_uri_61 + 'db') model_proxy_61 = xmlrpclib.ServerProxy(model_uri_61 + 'model/' + DB) - # Ugly way to ensure the server is listening. - time.sleep(2) - def tearDownModule(): """ Shutdown the OpenERP server similarly to a single ctrl-c. """ openerp.service.stop_services() diff --git a/tests/test_ir_sequence.py b/openerp/tests/test_ir_sequence.py similarity index 99% rename from tests/test_ir_sequence.py rename to openerp/tests/test_ir_sequence.py index 8a4731d524f..3bf84cf3404 100644 --- a/tests/test_ir_sequence.py +++ b/openerp/tests/test_ir_sequence.py @@ -19,7 +19,9 @@ import common DB = common.DB ADMIN_USER_ID = common.ADMIN_USER_ID -setUpModule = common.setUpModule +def setUpModule(): + common.create_xmlrpc_proxies() + tearDownModule = common.tearDownModule def registry(model): diff --git a/tests/test_orm.py b/openerp/tests/test_orm.py similarity index 97% rename from tests/test_orm.py rename to openerp/tests/test_orm.py index 61b574df5eb..79ac421b7ac 100644 --- a/tests/test_orm.py +++ b/openerp/tests/test_orm.py @@ -1,9 +1,10 @@ import os import unittest2 + import openerp UID = 1 -DB = os.environ['OPENERP_DATABASE'] +DB = openerp.tools.config['db_name'] CREATE = lambda values: (0, False, values) UPDATE = lambda id, values: (1, id, values) @@ -13,14 +14,13 @@ LINK_TO = lambda id: (4, id, False) DELETE_ALL = lambda: (5, False, False) REPLACE_WITH = lambda ids: (6, False, ids) -def setUpModule(): - openerp.tools.config['addons_path'] = os.environ['OPENERP_ADDONS_PATH'] - class TestO2MSerialization(unittest2.TestCase): + def setUp(self): self.cr = openerp.modules.registry.RegistryManager.get(DB).db.cursor() self.partner = openerp.modules.registry.RegistryManager.get(DB)['res.partner'] self.address = openerp.modules.registry.RegistryManager.get(DB)['res.partner.address'] + def tearDown(self): self.cr.rollback() self.cr.close() diff --git a/tests/test_xmlrpc.py b/openerp/tests/test_xmlrpc.py similarity index 96% rename from tests/test_xmlrpc.py rename to openerp/tests/test_xmlrpc.py index 4ad530cbeda..a08987cdd85 100644 --- a/tests/test_xmlrpc.py +++ b/openerp/tests/test_xmlrpc.py @@ -19,7 +19,10 @@ ADMIN_USER = common.ADMIN_USER ADMIN_USER_ID = common.ADMIN_USER_ID ADMIN_PASSWORD = common.ADMIN_PASSWORD -setUpModule = common.setUpModule +def setUpModule(): + common.start_openerp() + common.create_xmlrpc_proxies() + tearDownModule = common.tearDownModule class test_xmlrpc(unittest2.TestCase): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index f1c3931e81f..2b27844ceed 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -266,6 +266,10 @@ class configmanager(object): type="int") group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true", help="Use the unaccent function provided by the database when available.") + group.add_option("--run-tests", dest="run_tests", my_default=False, action="store_true", + help="Run a test suite.") + group.add_option("--run-tests-no-db", dest="run_tests_no_db", my_default=False, action="store_true", + help="Run a test suite (which does'nt assume an existing database).") parser.add_option_group(group) @@ -370,6 +374,7 @@ class configmanager(object): 'list_db', 'xmlrpcs', 'test_file', 'test_disable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', + 'run_tests', 'run_tests_no_db', ] for arg in keys: diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 396284efaf7..00000000000 --- a/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -import test_xmlrpc From 9e225c7c1e599624d419cd87f1705ad9a8dcb162 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 16 Nov 2011 13:11:42 +0100 Subject: [PATCH 002/138] [IMP] check the -d option is also provided when using --run-tests. bzr revid: vmt@openerp.com-20111116121142-bxahffcr7diseoiy --- openerp/tools/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 2b27844ceed..7d496d524b1 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -267,9 +267,9 @@ class configmanager(object): group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true", help="Use the unaccent function provided by the database when available.") group.add_option("--run-tests", dest="run_tests", my_default=False, action="store_true", - help="Run a test suite.") + help="Run a test suite. This creates a database (possibly created with --run-tests-no-db).") group.add_option("--run-tests-no-db", dest="run_tests_no_db", my_default=False, action="store_true", - help="Run a test suite (which does'nt assume an existing database).") + help="Run a test suite (which doesn't assume an existing database).") parser.add_option_group(group) @@ -327,6 +327,10 @@ class configmanager(object): "The config file '%s' selected with -c/--config doesn't exist, "\ "use -s/--save if you want to generate it"%(opt.config)) + die((opt.run_tests or opt.run_tests_no_db) and not opt.db_name, + "The --run-tests and --run-tests-no-db need a database name "\ + "(using the --database or -d options).") + # place/search the config file on Win32 near the server installation # (../etc from the server) # if the server is run by an unprivileged user, he has to specify location of a config file where he has the rights to write, From 330316672ff5490602a1eb4e7743514355c306e4 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 17 Nov 2011 11:06:08 +0100 Subject: [PATCH 003/138] [IMP] Clearer use of update_module arg. - update_module was defined as config[init] or config[update] - this means it is a mutable dictionary - the code testing update_module is actually mutating config[init] and config[update]... - now update_module is really a True or False value. bzr revid: vmt@openerp.com-20111117100608-0n8o99slgk42kiiv --- openerp-server | 6 ++++-- openerp/modules/loading.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openerp-server b/openerp-server index c108d9f9372..e47a8ea29cf 100755 --- a/openerp-server +++ b/openerp-server @@ -89,7 +89,8 @@ def setup_pid_file(): def preload_registry(dbname): """ Preload a registry, and start the cron.""" try: - db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) + update_module = True if config['init'] or config['update'] else False + db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=update_module, pooljobs=False) # jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services() registry.schedule_cron_jobs() @@ -99,7 +100,8 @@ def preload_registry(dbname): def run_test_file(dbname, test_file): """ Preload a registry, possibly run a test file, and start the cron.""" try: - db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) + update_module = True if config['init'] or config['update'] else False + db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=update_module, pooljobs=False) cr = db.cursor() logger = logging.getLogger('server') logger.info('loading test file %s', test_file) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 00948bc690e..6a2e653fd57 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -276,6 +276,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): tools.config['update']['all'] = 1 if not tools.config['without_demo']: tools.config["demo"]['all'] = 1 + update_module = True # This is a brand new pool, just created in pooler.get_db_and_pool() pool = pooler.get_pool(cr.dbname) @@ -293,7 +294,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # processed_modules: for cleanup step after install # loaded_modules: to avoid double loading - loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report) + loaded_modules, processed_modules = load_module_graph(cr, graph, status, report=report) if tools.config['load_language']: for lang in tools.config['load_language'].split(','): From 99f99b6d8317ddeb53339b24934d722c97d1dc61 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 17 Nov 2011 15:46:28 +0100 Subject: [PATCH 004/138] [REF] openerp.modules.loading: preparing to support the -i=all option: getting almost rid of the mess with tools.config[init|update]. bzr revid: vmt@openerp.com-20111117144628-pjpac87b5whg5cl0 --- openerp/modules/graph.py | 10 ------- openerp/modules/loading.py | 53 ++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 7b302e99f4c..62b706ad846 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -122,9 +122,6 @@ class Graph(dict): current.remove(package) node = self.add_node(package, info) node.data = info - for kind in ('init', 'demo', 'update'): - if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force: - setattr(node, kind, True) else: later.add(package) packages.append((package, info)) @@ -186,18 +183,11 @@ class Node(Singleton): node.depth = self.depth + 1 if node not in self.children: self.children.append(node) - for attr in ('init', 'update', 'demo'): - if hasattr(self, attr): - setattr(node, attr, True) self.children.sort(lambda x, y: cmp(x.name, y.name)) return node def __setattr__(self, name, value): super(Singleton, self).__setattr__(name, value) - if name in ('init', 'update', 'demo'): - tools.config[name][self.name] = 1 - for child in self.children: - setattr(child, name, value) if name == 'depth': for child in self.children: setattr(child, name, value + 1) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 6a2e653fd57..551947cf4a4 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -164,7 +164,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= register_module_classes(package.name) models = pool.load(cr, package) loaded_modules.append(package.name) - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + if package.state in ('to install', 'to upgrade'): init_module_models(cr, package.name, models) status['progress'] = float(index) / len(graph) @@ -178,18 +178,19 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= idref = {} - mode = 'update' - if hasattr(package, 'init') or package.state == 'to install': + if package.state == 'to install': mode = 'init' + else: + mode = 'update' - if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'): + if package.state in ('to install', 'to upgrade'): if package.state=='to upgrade': # upgrading the module information modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data)) load_init_xml(module_name, idref, mode) load_update_xml(module_name, idref, mode) load_data(module_name, idref, mode) - if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'): + if package.dbdemo and package.state != 'installed': status['progress'] = (index + 0.75) / len(graph) load_demo_xml(module_name, idref, mode) load_demo(module_name, idref, mode) @@ -212,9 +213,6 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= modobj.update_translations(cr, 1, [module_id], None) package.state = 'installed' - for kind in ('init', 'demo', 'update'): - if hasattr(package, kind): - delattr(package, kind) cr.commit() @@ -272,10 +270,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False): if not openerp.modules.db.is_initialized(cr): logger.notifyChannel("init", netsvc.LOG_INFO, "init db") openerp.modules.db.initialize(cr) - tools.config["init"]["all"] = 1 - tools.config['update']['all'] = 1 - if not tools.config['without_demo']: - tools.config["demo"]['all'] = 1 update_module = True # This is a brand new pool, just created in pooler.get_db_and_pool() @@ -294,6 +288,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # processed_modules: for cleanup step after install # loaded_modules: to avoid double loading + # After load_module_graph(), 'base' has been installed or updated and its state is 'installed'. loaded_modules, processed_modules = load_module_graph(cr, graph, status, report=report) if tools.config['load_language']: @@ -301,28 +296,33 @@ def load_modules(db, force_demo=False, status=None, update_module=False): tools.load_language(cr, lang) # STEP 2: Mark other modules to be loaded/updated + # This is a one-shot use of tools.config[init|update] from the command line + # arguments. It is directly cleared to not interfer with later create/update + # issued via RPC. if update_module: modobj = pool.get('ir.module.module') - if ('base' in tools.config['init']) or ('base' in tools.config['update']): + if ('base' in tools.config['init']) or ('base' in tools.config['update']) \ + or ('all' in tools.config['init']) or ('all' in tools.config['update']): logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list') modobj.update_list(cr, 1) + if 'all' in tools.config['init']: + pass + _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) - mods = [k for k in tools.config['init'] if tools.config['init'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) - if ids: - modobj.button_install(cr, 1, ids) + mods = [k for k in tools.config['init'] if tools.config['init'][k] and k not in ('base', 'all')] + ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)]) + if ids: + modobj.button_install(cr, 1, ids) # goes from 'uninstalled' to 'to install' - mods = [k for k in tools.config['update'] if tools.config['update'][k]] - if mods: - ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) - if ids: - modobj.button_upgrade(cr, 1, ids) - - cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base')) + mods = [k for k in tools.config['update'] if tools.config['update'][k] and k not in ('base', 'all')] + ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)]) + if ids: + modobj.button_upgrade(cr, 1, ids) # goes from 'installed' to 'to upgrade' + for kind in ('init', 'demo', 'update'): + tools.config[kind] = {} # STEP 3: Load marked modules (skipping base which was done in STEP 1) # IMPORTANT: this is done in two parts, first loading all installed or @@ -372,9 +372,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False): if report.get_report(): logger.notifyChannel('init', netsvc.LOG_INFO, report) - for kind in ('init', 'demo', 'update'): - tools.config[kind] = {} - cr.commit() if update_module: # Remove records referenced from ir_model_data for modules to be From b6b25ea70639bfe2eb5c1249e7b1d40216ab545b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 17 Nov 2011 16:07:20 +0100 Subject: [PATCH 005/138] [IMP] openerp.modules.loading: -i all seems to work as documented in --help. bzr revid: vmt@openerp.com-20111117150720-uaer043c2k55937n --- openerp/modules/loading.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 551947cf4a4..d0b18632e31 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -307,7 +307,8 @@ def load_modules(db, force_demo=False, status=None, update_module=False): modobj.update_list(cr, 1) if 'all' in tools.config['init']: - pass + ids = modobj.search(cr, 1, []) + tools.config['init'] = dict.fromkeys([m['name'] for m in modobj.read(cr, 1, ids, ['name'])], 1) _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys())) From e9c405c244249e536f0a97c5b6b9d9b4d7215e06 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 17 Nov 2011 17:28:24 +0100 Subject: [PATCH 006/138] [FIX] openerp.modules.loading: previous change disabled demo data altogether, this should be now fixed by making sure that --withou-demo flag (or its absence) is taken care of, and setting the base module demo field. Normally the demo state of a module should have a ripple effect on all modules depending on it. This might prove not enough in this case and require some more testing. bzr revid: vmt@openerp.com-20111117162824-yqswv6yk7bmiyj4s --- openerp-server | 6 ++++-- openerp/modules/graph.py | 14 +++++++++----- openerp/modules/loading.py | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/openerp-server b/openerp-server index e47a8ea29cf..c985579ef15 100755 --- a/openerp-server +++ b/openerp-server @@ -90,7 +90,8 @@ def preload_registry(dbname): """ Preload a registry, and start the cron.""" try: update_module = True if config['init'] or config['update'] else False - db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=update_module, pooljobs=False) + db, registry = openerp.pooler.get_db_and_pool( + dbname, update_module=update_module, pooljobs=False, force_demo=not config['without_demo']) # jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services() registry.schedule_cron_jobs() @@ -101,7 +102,8 @@ def run_test_file(dbname, test_file): """ Preload a registry, possibly run a test file, and start the cron.""" try: update_module = True if config['init'] or config['update'] else False - db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=update_module, pooljobs=False) + db, registry = openerp.pooler.get_db_and_pool( + dbname, update_module=update_module, pooljobs=False, force_demo=not config['without_demo']) cr = db.cursor() logger = logging.getLogger('server') logger.info('loading test file %s', test_file) diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 62b706ad846..5c6e5899ce4 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -88,15 +88,19 @@ class Graph(dict): for k, v in additional_data[package.name].items(): setattr(package, k, v) - def add_module(self, cr, module, force=None): - self.add_modules(cr, [module], force) + def add_module(self, cr, module, force_demo=False): + self.add_modules(cr, [module], force_demo) - def add_modules(self, cr, module_list, force=None): - if force is None: - force = [] + def add_modules(self, cr, module_list, force_demo=False): packages = [] len_graph = len(self) for module in module_list: + if force_demo: + cr.execute(""" + UPDATE ir_module_module + SET demo='t' + WHERE name = %s""", + (module,)) # This will raise an exception if no/unreadable descriptor file. # NOTE The call to load_information_from_description_file is already # done by db.initialize, so it is possible to not do it again here. diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index d0b18632e31..066b78f5e3f 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -281,7 +281,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) graph = openerp.modules.graph.Graph() - graph.add_module(cr, 'base', force) + graph.add_module(cr, 'base', force_demo) if not graph: logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)') raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)')) From 9212a7f81a4cc1fe74e81cdd4d58523119460a03 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 1 Dec 2011 13:18:56 +0100 Subject: [PATCH 007/138] [FIX] test_ir_cron: the names of the class and methods were changed by mistake. bzr revid: vmt@openerp.com-20111201121856-knw7ewbpj083zf12 --- openerp/addons/base/test/test_ir_cron.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/test/test_ir_cron.py b/openerp/addons/base/test/test_ir_cron.py index 2154c014f8c..6ef79ac3a78 100644 --- a/openerp/addons/base/test/test_ir_cron.py +++ b/openerp/addons/base/test/test_ir_cron.py @@ -40,7 +40,7 @@ JOB = { 'model': u'ir.cron' } -class x_test_ir_cron(openerp.osv.osv.osv): +class test_ir_cron(openerp.osv.osv.osv): """ Add a few handy methods to test cron jobs scheduling. """ _inherit = "ir.cron" @@ -57,7 +57,7 @@ class x_test_ir_cron(openerp.osv.osv.osv): time.sleep(80) print ">>> out _80_seconds" - def x_test_0(self, cr, uid): + def test_0(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') @@ -66,17 +66,17 @@ class x_test_ir_cron(openerp.osv.osv.osv): self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2)) self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3)) - def x_test_1(self, cr, uid): + def test_1(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3)) - def x_test_2(self, cr, uid): + def test_2(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2)) - def x_test_3(self, cr, uid): + def test_3(self, cr, uid): now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') @@ -86,7 +86,7 @@ class x_test_ir_cron(openerp.osv.osv.osv): self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) # This test assumes 4 cron threads. - def x_test_00(self, cr, uid): + def test_00(self, cr, uid): self.test_00_set = set() now = datetime.now() t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') From 6f4abdfbd519e8f8a416e24fdb541a88ef3104c6 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 6 Aug 2012 11:46:27 +0200 Subject: [PATCH 008/138] [IMP] move eval functions for domains, groupbys and contexts outside of corelib, as well as the standard evaluation context building (and related objects) bzr revid: xmo@openerp.com-20120806094627-8c9lwtfkbfws2bmj --- addons/web/__openerp__.py | 1 + addons/web/static/src/js/boot.js | 2 +- addons/web/static/src/js/corelib.js | 229 +------------------------- addons/web/static/src/js/pyeval.js | 244 ++++++++++++++++++++++++++++ addons/web/static/test/evals.js | 13 +- addons/web/static/test/test.html | 1 + addons/web/static/test/testing.js | 3 +- 7 files changed, 258 insertions(+), 235 deletions(-) create mode 100644 addons/web/static/src/js/pyeval.js diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 822d2fa3cbf..f64f853d0ea 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -39,6 +39,7 @@ This module provides the core of the OpenERP Web Client. "static/lib/cleditor/jquery.cleditor.js", "static/lib/py.js/lib/py.js", "static/src/js/boot.js", + "static/src/js/pyeval.js", "static/src/js/corelib.js", "static/src/js/coresetup.js", "static/src/js/dates.js", diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 844dd88a0ac..dd014846884 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -48,7 +48,7 @@ * OpenERP Web web module split *---------------------------------------------------------*/ openerp.web = function(session) { - var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; + var files = ["pyeval", "corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; for(var i=0; i 12) { - year += 1; - month -= 12; - } - } - - var lastMonthDay = new Date(year, month, 0).getDate(); - var day = asJS(this.ops.day) || asJS(other.day); - if (day > lastMonthDay) { day = lastMonthDay; } - var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); - if (days_offset) { - day = new Date(year, month-1, day + days_offset).getDate(); - } - // TODO: leapdays? - // TODO: hours, minutes, seconds? Not used in XML domains - // TODO: weekday? - return new datetime.date(year, month, day); - }, - __radd__: function (other) { - return this.__add__(other); - }, - - __sub__: function (other) { - if (!(other instanceof datetime.date)) { - return py.NotImplemented; - } - // TODO: test this whole mess - var year = asJS(this.ops.year) || asJS(other.year); - if (asJS(this.ops.years)) { - year -= asJS(this.ops.years); - } - - var month = asJS(this.ops.month) || asJS(other.month); - if (asJS(this.ops.months)) { - month -= asJS(this.ops.months); - // FIXME: no divmod in JS? - while (month < 1) { - year -= 1; - month += 12; - } - while (month > 12) { - year += 1; - month -= 12; - } - } - - var lastMonthDay = new Date(year, month, 0).getDate(); - var day = asJS(this.ops.day) || asJS(other.day); - if (day > lastMonthDay) { day = lastMonthDay; } - var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); - if (days_offset) { - day = new Date(year, month-1, day - days_offset).getDate(); - } - // TODO: leapdays? - // TODO: hours, minutes, seconds? Not used in XML domains - // TODO: weekday? - return new datetime.date(year, month, day); - }, - __rsub__: function (other) { - return this.__sub__(other); - } - }); - - return { - uid: new py.float(this.uid), - datetime: datetime, - time: time, - relativedelta: relativedelta, - current_date: date.today.__call__().strftime(['%Y-%m-%d']) - }; - }, /** * FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching */ @@ -1154,8 +1006,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ ''; try { // see Session.eval_context in Python - var ctx = this.test_eval_contexts( - ([this.context] || []).concat(source.contexts)); + var ctx = instance.web.pyeval.eval('contexts', + ([this.context] || []).concat(source.contexts)); if (!_.isEqual(ctx, expected.context)) { instance.webclient.notification.warn('Context mismatch, report to xmo', _.str.sprintf(match_template, { @@ -1173,7 +1025,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } try { - var dom = this.test_eval_domains(source.domains, this.test_eval_get_context()); + var dom = instance.web.pyeval.eval('domains', source.domains); if (!_.isEqual(dom, expected.domain)) { instance.webclient.notification.warn('Domains mismatch, report to xmo', _.str.sprintf(match_template, { @@ -1191,7 +1043,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } try { - var groups = this.test_eval_groupby(source.group_by_seq); + var groups = instance.web.pyeval.eval('groupbys', source.group_by_seq); if (!_.isEqual(groups, expected.group_by)) { instance.webclient.notification.warn('GroupBy mismatch, report to xmo', _.str.sprintf(match_template, { @@ -1208,79 +1060,6 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }), true); } }, - test_eval_contexts: function (contexts, evaluation_context) { - evaluation_context = evaluation_context || {}; - var self = this; - return _(contexts).reduce(function (result_context, ctx) { - // __eval_context evaluations can lead to some of `contexts`'s - // values being null, skip them as well as empty contexts - if (_.isEmpty(ctx)) { return result_context; } - var evaluated = ctx; - switch(ctx.__ref) { - case 'context': - evaluated = py.eval(ctx.__debug, evaluation_context); - break; - case 'compound_context': - var eval_context = self.test_eval_contexts([ctx.__eval_context]); - evaluated = self.test_eval_contexts( - ctx.__contexts, _.extend({}, evaluation_context, eval_context)); - break; - } - // add newly evaluated context to evaluation context for following - // siblings - _.extend(evaluation_context, evaluated); - return _.extend(result_context, evaluated); - }, _.extend({}, this.user_context)); - }, - test_eval_domains: function (domains, evaluation_context) { - var result_domain = [], self = this; - _(domains).each(function (dom) { - switch(dom.__ref) { - case 'domain': - result_domain.push.apply( - result_domain, py.eval(dom.__debug, evaluation_context)); - break; - case 'compound_domain': - var eval_context = self.test_eval_contexts([dom.__eval_context]); - result_domain.push.apply( - result_domain, self.test_eval_domains( - dom.__domains, _.extend( - {}, evaluation_context, eval_context))); - break; - default: - result_domain.push.apply( - result_domain, dom); - } - }); - return result_domain; - }, - test_eval_groupby: function (contexts) { - var result_group = [], self = this; - _(contexts).each(function (ctx) { - var group; - switch(ctx.__ref) { - case 'context': - group = py.eval(ctx.__debug).group_by; - break; - case 'compound_context': - group = self.test_eval_contexts( - ctx.__contexts, ctx.__eval_context).group_by; - break; - default: - group = ctx.group_by - } - if (!group) { return; } - if (typeof group === 'string') { - result_group.push(group); - } else if (group instanceof Array) { - result_group.push.apply(result_group, group); - } else { - throw new Error('Got invalid groupby {{' - + JSON.stringify(group) + '}}'); - } - }); - return result_group; - }, /** * Executes an RPC call, registering the provided callbacks. * diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js new file mode 100644 index 00000000000..8c498c3f885 --- /dev/null +++ b/addons/web/static/src/js/pyeval.js @@ -0,0 +1,244 @@ +/* + * py.js helpers and setup + */ +openerp.web.pyeval = function (instance) { + instance.web.pyeval = {}; + + var asJS = function (arg) { + if (arg instanceof py.object) { + return arg.toJSON(); + } + return arg; + }; + + var datetime = new py.object(); + datetime.datetime = new py.type(function datetime() { + throw new Error('datetime.datetime not implemented'); + }); + var date = datetime.date = new py.type(function date(y, m, d) { + if (y instanceof Array) { + d = y[2]; + m = y[1]; + y = y[0]; + } + this.year = asJS(y); + this.month = asJS(m); + this.day = asJS(d); + }, py.object, { + strftime: function (args) { + var f = asJS(args[0]), self = this; + return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) { + switch (c) { + case 'Y': return self.year; + case 'm': return _.str.sprintf('%02d', self.month); + case 'd': return _.str.sprintf('%02d', self.day); + } + throw new Error('ValueError: No known conversion for ' + m); + })); + } + }); + date.__getattribute__ = function (name) { + if (name === 'today') { + return date.today; + } + throw new Error("AttributeError: object 'date' has no attribute '" + name +"'"); + }; + date.today = new py.def(function () { + var d = new Date(); + return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()); + }); + datetime.time = new py.type(function time() { + throw new Error('datetime.time not implemented'); + }); + + var time = new py.object(); + time.strftime = new py.def(function (args) { + return date.today.__call__().strftime(args); + }); + + var relativedelta = new py.type(function relativedelta(args, kwargs) { + if (!_.isEmpty(args)) { + throw new Error('Extraction of relative deltas from existing datetimes not supported'); + } + this.ops = kwargs; + }, py.object, { + __add__: function (other) { + if (!(other instanceof datetime.date)) { + return py.NotImplemented; + } + // TODO: test this whole mess + var year = asJS(this.ops.year) || asJS(other.year); + if (asJS(this.ops.years)) { + year += asJS(this.ops.years); + } + + var month = asJS(this.ops.month) || asJS(other.month); + if (asJS(this.ops.months)) { + month += asJS(this.ops.months); + // FIXME: no divmod in JS? + while (month < 1) { + year -= 1; + month += 12; + } + while (month > 12) { + year += 1; + month -= 12; + } + } + + var lastMonthDay = new Date(year, month, 0).getDate(); + var day = asJS(this.ops.day) || asJS(other.day); + if (day > lastMonthDay) { day = lastMonthDay; } + var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); + if (days_offset) { + day = new Date(year, month-1, day + days_offset).getDate(); + } + // TODO: leapdays? + // TODO: hours, minutes, seconds? Not used in XML domains + // TODO: weekday? + return new datetime.date(year, month, day); + }, + __radd__: function (other) { + return this.__add__(other); + }, + + __sub__: function (other) { + if (!(other instanceof datetime.date)) { + return py.NotImplemented; + } + // TODO: test this whole mess + var year = asJS(this.ops.year) || asJS(other.year); + if (asJS(this.ops.years)) { + year -= asJS(this.ops.years); + } + + var month = asJS(this.ops.month) || asJS(other.month); + if (asJS(this.ops.months)) { + month -= asJS(this.ops.months); + // FIXME: no divmod in JS? + while (month < 1) { + year -= 1; + month += 12; + } + while (month > 12) { + year += 1; + month -= 12; + } + } + + var lastMonthDay = new Date(year, month, 0).getDate(); + var day = asJS(this.ops.day) || asJS(other.day); + if (day > lastMonthDay) { day = lastMonthDay; } + var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0); + if (days_offset) { + day = new Date(year, month-1, day - days_offset).getDate(); + } + // TODO: leapdays? + // TODO: hours, minutes, seconds? Not used in XML domains + // TODO: weekday? + return new datetime.date(year, month, day); + }, + __rsub__: function (other) { + return this.__sub__(other); + } + }); + + var eval_contexts = function (contexts, evaluation_context) { + evaluation_context = evaluation_context || {}; + return _(contexts).reduce(function (result_context, ctx) { + // __eval_context evaluations can lead to some of `contexts`'s + // values being null, skip them as well as empty contexts + if (_.isEmpty(ctx)) { return result_context; } + var evaluated = ctx; + switch(ctx.__ref) { + case 'context': + evaluated = py.eval(ctx.__debug, evaluation_context); + break; + case 'compound_context': + var eval_context = eval_contexts([ctx.__eval_context]); + evaluated = eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); + break; + } + // add newly evaluated context to evaluation context for following + // siblings + _.extend(evaluation_context, evaluated); + return _.extend(result_context, evaluated); + }, _.extend({}, instance.connection.user_context)); + }; + var eval_domains = function (domains, evaluation_context) { + var result_domain = []; + _(domains).each(function (domain) { + switch(domain.__ref) { + case 'domain': + result_domain.push.apply( + result_domain, py.eval(domain.__debug, evaluation_context)); + break; + case 'compound_domain': + var eval_context = eval_contexts([domain.__eval_context]); + result_domain.push.apply( + result_domain, eval_domains( + domain.__domains, _.extend( + {}, evaluation_context, eval_context))); + break; + default: + result_domain.push.apply(result_domain, domain); + } + }); + return result_domain; + }; + var eval_groupbys = function (contexts, evaluation_context) { + var result_group = []; + _(contexts).each(function (ctx) { + var group; + var evaluated = ctx; + switch(ctx.__ref) { + case 'context': + evaluated = py.eval(ctx.__debug), evaluation_context; + break; + case 'compound_context': + var eval_context = eval_contexts([ctx.__eval_context]); + evaluated = eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); + break; + } + group = evaluated.group_by; + if (!group) { return; } + if (typeof group === 'string') { + result_group.push(group); + } else if (group instanceof Array) { + result_group.push.apply(result_group, group); + } else { + throw new Error('Got invalid groupby {{' + + JSON.stringify(group) + '}}'); + } + _.extend(evaluation_context, evaluated); + }); + return result_group; + }; + + instance.web.pyeval.context = function () { + return { + uid: new py.float(instance.connection.uid), + datetime: datetime, + time: time, + relativedelta: relativedelta, + current_date: date.today.__call__().strftime(['%Y-%m-%d']), + }; + }; + + /** + * @param {String} type "domains", "contexts" or "groupbys" + * @param {Array} object domains or contexts to evaluate + * @param {Object} [context] evaluation context + */ + instance.web.pyeval.eval = function (type, object, context) { + if (!context) { context = instance.web.pyeval.context()} + switch(type) { + case 'contexts': return eval_contexts(object, context); + case 'domains': return eval_domains(object, context); + case 'groupbys': return eval_groupbys(object, context); + } + throw new Error("Unknow evaluation type " + type) + }; +}; diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 7bc6554e074..ee4aef4bf79 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -3,16 +3,14 @@ $(document).ready(function () { module("eval.contexts", { setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); + openerp = window.openerp.testing.instanceFor('coresetup'); } }); test('context_sequences', function () { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context var active_id = 4; - var result = openerp.connection.test_eval_contexts([ + var result = openerp.web.pyeval.eval('contexts', [ { "__contexts": [ { @@ -56,7 +54,7 @@ $(document).ready(function () { }); }); test('non-literal_eval_contexts', function () { - var result = openerp.connection.test_eval_contexts([{ + var result = openerp.web.pyeval.eval('contexts', [{ "__ref": "compound_context", "__contexts": [ {"__ref": "context", "__debug": "{'type':parent.type}", @@ -141,9 +139,8 @@ $(document).ready(function () { }); test('current_date', function () { var current_date = openerp.web.date_to_str(new Date()); - var result = openerp.connection.test_eval_domains( - [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], - openerp.connection.test_eval_get_context()); + var result = openerp.web.pyeval.eval('domains', + [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}]); deepEqual(result, [ ['name', '>=', current_date], ['name', '<=', current_date] diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html index 35d57d3c3a6..91b45c44bad 100644 --- a/addons/web/static/test/test.html +++ b/addons/web/static/test/test.html @@ -28,6 +28,7 @@ + diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js index e9525fc82a0..9a449a40493 100644 --- a/addons/web/static/test/testing.js +++ b/addons/web/static/test/testing.js @@ -6,7 +6,8 @@ openerp.testing = (function () { var doc = xhr.responseXML; var dependencies = { - corelib: [], + pyeval: [], + corelib: ['pyeval'], coresetup: ['corelib'], data: ['corelib', 'coresetup'], dates: [], From 6a23c24da13f5907d4c407e919bff187a3a0ae1d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 6 Aug 2012 13:06:38 +0200 Subject: [PATCH 009/138] [UP] py.js, old != operator and basic dict handling bzr revid: xmo@openerp.com-20120806110638-1m4rg205sb3vjvm5 --- addons/web/static/lib/py.js/.hg_archival.txt | 4 +-- addons/web/static/lib/py.js/README.rst | 26 ++++++++++------ addons/web/static/lib/py.js/lib/py.js | 32 ++++++++++++++++++-- addons/web/static/lib/py.js/test/test.js | 28 +++++++++++++++++ 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/addons/web/static/lib/py.js/.hg_archival.txt b/addons/web/static/lib/py.js/.hg_archival.txt index 35e98c8501d..2aa926eb56a 100644 --- a/addons/web/static/lib/py.js/.hg_archival.txt +++ b/addons/web/static/lib/py.js/.hg_archival.txt @@ -1,5 +1,5 @@ repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea -node: 1758bfec1ec1dcff95dcc4c72269cc0e3d000afd +node: 87e977311edbbb5f281b87390a9a304eb194ce89 branch: default latesttag: 0.5 -latesttagdistance: 11 +latesttagdistance: 15 diff --git a/addons/web/static/lib/py.js/README.rst b/addons/web/static/lib/py.js/README.rst index 046db7153d1..eea1cbbf963 100644 --- a/addons/web/static/lib/py.js/README.rst +++ b/addons/web/static/lib/py.js/README.rst @@ -60,7 +60,7 @@ Builtins Same as tuple (``list`` is currently an alias for ``tuple``) ``dict`` - Implements just about nothing + Implements trivial getting and setting, nothing beyond that. Note that most methods are probably missing from all of these. @@ -72,14 +72,14 @@ sub-protocols) of the `Python 2.7 data model `_: Rich comparisons - Roughly complete implementation but for two limits: ``__eq__`` and - ``__ne__`` can't return ``NotImplemented`` (well they can but it's - not going to work right), and the behavior is undefined if a - rich-comparison operation does not return a ``py.bool``. + Pretty much complete (including operator fallbacks), although the + behavior is currently undefined if an operation does not return + either a ``py.bool`` or ``NotImplemented``. - Also, a ``NotImplemented`` result does not try the reverse - operation, not sure if it's supposed to. It directly falls back to - comparing type names. + ``__hash__`` is supported (and used), but it should return **a + javascript string**. ``py.js``'s dict build on javascript objects, + reimplementing numeral hashing is worthless complexity at this + point. Boolean conversion Implementing ``__nonzero__`` should work. @@ -93,6 +93,12 @@ Descriptor protocol As with attributes, ``__delete__`` is not implemented. Callable objects + Work, although the handling of arguments isn't exactly nailed + down. For now, callables get two (javascript) arguments ``args`` + and ``kwargs``, holding (respectively) positional and keyword + arguments. + + Conflicts are *not* handled at this point. Collections Abstract Base Classes Container is the only implemented ABC protocol (ABCs themselves @@ -119,8 +125,8 @@ implementation: ``py.js`` types. When accessing instance methods, ``py.js`` automatically wraps - these in a variant of ``py.def`` automatically, to behave as - Python's (bound) methods. + these in a variant of ``py.def``, to behave as Python's (bound) + methods. Why === diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index a8c952679af..79a0ace1995 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -433,7 +433,8 @@ var py = {}; if (this._hash) { return this._hash; } - return this._hash = hash_counter++; + // tagged counter, to avoid collisions with e.g. number hashes + return this._hash = '\0\0\0' + String(hash_counter++); }, __eq__: function (other) { return (this === other) ? py.True : py.False; @@ -597,6 +598,9 @@ var py = {}; throw new Error('TypeError: __str__ returned non-string (type ' + v.constructor.name + ')'); }, py.object, { + __hash__: function () { + return '\1\0\1' + this._value; + }, __eq__: function (other) { if (other instanceof py.str && this._value === other._value) { return py.True; @@ -654,12 +658,33 @@ var py = {}; } }); py.list = py.tuple; - py.dict = py.type(function dict() { + py.dict = py.type(function dict(d) { this._store = {}; + for (var k in (d || {})) { + if (!d.hasOwnProperty(k)) { continue; } + var py_k = new py.str(k); + var val = PY_ensurepy(d[k]); + this._store[py_k.__hash__()] = [py_k, val]; + } }, py.object, { + __getitem__: function (key) { + var h = key.__hash__(); + if (!(h in this._store)) { + throw new Error("KeyError: '" + key.toJSON() + "'"); + } + return this._store[h][1]; + }, __setitem__: function (key, value) { this._store[key.__hash__()] = [key, value]; }, + get: function (args) { + var h = args[0].__hash__(); + var def = args.length > 1 ? args[1] : py.None; + if (!(h in this._store)) { + return def; + } + return this._store[h][1]; + }, toJSON: function () { var out = {}; for(var k in this._store) { @@ -700,6 +725,7 @@ var py = {}; var PY_operators = { '==': ['eq', 'eq', function (a, b) { return a === b; }], '!=': ['ne', 'ne', function (a, b) { return a !== b; }], + '<>': ['ne', 'ne', function (a, b) { return a !== b; }], '<': ['lt', 'gt', function (a, b) {return a.constructor.name < b.constructor.name;}], '<=': ['le', 'ge', function (a, b) {return a.constructor.name <= b.constructor.name;}], '>': ['gt', 'lt', function (a, b) {return a.constructor.name > b.constructor.name;}], @@ -782,7 +808,7 @@ var py = {}; return b.__contains__(a); case 'not in': return b.__contains__(a) === py.True ? py.False : py.True; - case '==': case '!=': + case '==': case '!=': case '<>': case '<': case '<=': case '>': case '>=': return PY_op(a, b, operator); diff --git a/addons/web/static/lib/py.js/test/test.js b/addons/web/static/lib/py.js/test/test.js index c1f92c44d81..f09245a38c8 100644 --- a/addons/web/static/lib/py.js/test/test.js +++ b/addons/web/static/lib/py.js/test/test.js @@ -117,6 +117,11 @@ describe('Comparisons', function () { expect(py.eval('foo != bar', {foo: 'qux', bar: 'quux'})) .to.be(true); }); + it('should accept deprecated form', function () { + expect(py.eval('1 <> 2')).to.be(true); + expect(py.eval('"foo" <> "foo"')).to.be(false); + expect(py.eval('"foo" <> "bar"')).to.be(true); + }); }); describe('rich comparisons', function () { it('should work with numbers', function () { @@ -377,6 +382,29 @@ describe('numerical protocols', function () { }); }); }); +describe('dicts', function () { + it('should be possible to retrieve their value', function () { + var d = new py.dict({foo: 3, bar: 4, baz: 5}); + expect(py.eval('d["foo"]', {d: d})).to.be(3); + expect(py.eval('d["baz"]', {d: d})).to.be(5); + }); + it('should raise KeyError if a key is missing', function () { + var d = new py.dict(); + expect(function () { + py.eval('d["foo"]', {d: d}); + }).to.throwException(/^KeyError/); + }); + it('should have a method to provide a default value', function () { + var d = new py.dict({foo: 3}); + expect(py.eval('d.get("foo")', {d: d})).to.be(3); + expect(py.eval('d.get("bar")', {d: d})).to.be(null); + expect(py.eval('d.get("bar", 42)', {d: d})).to.be(42); + + var e = new py.dict({foo: null}); + expect(py.eval('d.get("foo")', {d: e})).to.be(null); + expect(py.eval('d.get("bar")', {d: e})).to.be(null); + }); +}); describe('Type converter', function () { it('should convert bare objects to objects', function () { expect(py.eval('foo.bar', {foo: {bar: 3}})).to.be(3); From 620453c4a04822af52f25449433dcfd1a0c848e5 Mon Sep 17 00:00:00 2001 From: "Turkesh Patel (Open ERP)" Date: Wed, 5 Sep 2012 15:58:21 +0530 Subject: [PATCH 010/138] [FIX] Workflow function activities explode if they contain empty lines. bzr revid: tpa@tinyerp.com-20120905102821-8q6d42d5p0xudxmv --- openerp/workflow/wkf_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py index 608af34d118..bf9e99f00a4 100644 --- a/openerp/workflow/wkf_expr.py +++ b/openerp/workflow/wkf_expr.py @@ -44,7 +44,7 @@ class Env(dict): def _eval_expr(cr, ident, workitem, action): ret=False assert action, 'You used a NULL action in a workflow, use dummy node instead.' - for line in action.split('\n'): + for line in filter(lambda action: action != "", action.split('\n')): line = line.strip() uid=ident[0] model=ident[1] From 1b54fb39f23f8a6ba0d6e458723b12eb65bbaf94 Mon Sep 17 00:00:00 2001 From: "RGA(OpenERP)" <> Date: Fri, 14 Sep 2012 19:16:04 +0530 Subject: [PATCH 011/138] [FIX] Compute domain fail to eval when domain has array comparision bzr revid: rgaopenerp-20120914134604-agowp1k0lxgutk7s --- addons/web/static/src/js/view_form.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 7d328f012fe..43e5313eb7b 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1527,7 +1527,8 @@ instance.web.form.compute_domain = function(expr, fields) { switch (op.toLowerCase()) { case '=': case '==': - stack.push(field_value == val); + // ([] == []) ==> false + stack.push(_.isEqual(field_value, val)); break; case '!=': case '<>': @@ -1559,6 +1560,7 @@ instance.web.form.compute_domain = function(expr, fields) { op, JSON.stringify(expr)); } } + return _.all(stack, _.identity); }; From 0e101c63a3355e2aebce2d38f985e9ac6dc96376 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 5 Oct 2012 15:12:05 +0200 Subject: [PATCH 012/138] [FIX] evaluator: connection -> session bzr revid: xmo@openerp.com-20121005131205-ffk746ac5xrua62j --- addons/web/static/src/js/pyeval.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 8c498c3f885..007d73081ce 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -164,7 +164,7 @@ openerp.web.pyeval = function (instance) { // siblings _.extend(evaluation_context, evaluated); return _.extend(result_context, evaluated); - }, _.extend({}, instance.connection.user_context)); + }, _.extend({}, instance.session.user_context)); }; var eval_domains = function (domains, evaluation_context) { var result_domain = []; @@ -219,7 +219,7 @@ openerp.web.pyeval = function (instance) { instance.web.pyeval.context = function () { return { - uid: new py.float(instance.connection.uid), + uid: new py.float(instance.session.uid), datetime: datetime, time: time, relativedelta: relativedelta, From 10bc6ddfe4e12e792d856369b482ae74f73197d0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 5 Oct 2012 16:28:08 +0200 Subject: [PATCH 013/138] [ADD] groupby eval tests bzr revid: xmo@openerp.com-20121005142808-yx6o8nfnn0o4ms56 --- addons/web/static/src/js/pyeval.js | 2 +- addons/web/static/test/evals.js | 73 +++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 007d73081ce..34ddf0e119b 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -194,7 +194,7 @@ openerp.web.pyeval = function (instance) { var evaluated = ctx; switch(ctx.__ref) { case 'context': - evaluated = py.eval(ctx.__debug), evaluation_context; + evaluated = py.eval(ctx.__debug, evaluation_context); break; case 'compound_context': var eval_context = eval_contexts([ctx.__eval_context]); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index ee4aef4bf79..dc8ceb83daf 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -145,5 +145,76 @@ $(document).ready(function () { ['name', '>=', current_date], ['name', '<=', current_date] ]); - }) + }); + + module('eval.groupbys', { + setup: function () { + openerp = window.openerp.testing.instanceFor('coresetup'); + } + }); + test('groupbys_00', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + {group_by: ['bar', 'qux']}, + {group_by: null}, + {group_by: 'grault'} + ]); + deepEqual(result, ['foo', 'bar', 'qux', 'grault']); + }); + test('groupbys_01', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + { __ref: 'context', __debug: '{"group_by": "bar"}' }, + {group_by: 'grault'} + ]); + deepEqual(result, ['foo', 'bar', 'grault']); + }); + test('groupbys_02', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + { + __ref: 'compound_context', + __contexts: [ {group_by: 'bar'} ], + __eval_context: null + }, + {group_by: 'grault'} + ]); + deepEqual(result, ['foo', 'bar', 'grault']); + }); + test('groupbys_03', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + { + __ref: 'compound_context', + __contexts: [ + { __ref: 'context', __debug: '{"group_by": value}' } + ], + __eval_context: { value: 'bar' } + }, + {group_by: 'grault'} + ]); + deepEqual(result, ['foo', 'bar', 'grault']); + }); + test('groupbys_04', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + { + __ref: 'compound_context', + __contexts: [ + { __ref: 'context', __debug: '{"group_by": value}' } + ], + __eval_context: { value: 'bar' } + }, + {group_by: 'grault'} + ], { value: 'bar' }); + deepEqual(result, ['foo', 'bar', 'grault']); + }); + test('groupbys_05', function () { + var result = openerp.web.pyeval.eval('groupbys', [ + {group_by: 'foo'}, + { __ref: 'context', __debug: '{"group_by": value}' }, + {group_by: 'grault'} + ], { value: 'bar' }); + deepEqual(result, ['foo', 'bar', 'grault']); + }); }); From 7bcbadb099a71b731812ace299b98317edc49ae4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Oct 2012 14:06:19 +0200 Subject: [PATCH 014/138] [FIX] update py.js to 0.6, port existin classes to it, attempt to implement timedelta bzr revid: xmo@openerp.com-20121008120619-lhmexjnujjrigjn9 --- addons/web/static/lib/py.js/.hg_archival.txt | 6 +- addons/web/static/lib/py.js/README.rst | 9 +- addons/web/static/lib/py.js/doc/Makefile | 153 +++++ addons/web/static/lib/py.js/doc/builtins.rst | 53 ++ addons/web/static/lib/py.js/doc/conf.py | 247 ++++++++ .../web/static/lib/py.js/doc/differences.rst | 64 +++ addons/web/static/lib/py.js/doc/index.rst | 161 ++++++ addons/web/static/lib/py.js/doc/make.bat | 190 +++++++ addons/web/static/lib/py.js/doc/types.rst | 248 ++++++++ addons/web/static/lib/py.js/doc/utility.rst | 111 ++++ addons/web/static/lib/py.js/lib/py.js | 532 +++++++++++++----- addons/web/static/lib/py.js/test/parser.js | 132 ----- addons/web/static/lib/py.js/test/test.js | 416 -------------- addons/web/static/src/js/pyeval.js | 197 +++++-- addons/web/static/test/evals.js | 44 ++ 15 files changed, 1798 insertions(+), 765 deletions(-) create mode 100644 addons/web/static/lib/py.js/doc/Makefile create mode 100644 addons/web/static/lib/py.js/doc/builtins.rst create mode 100644 addons/web/static/lib/py.js/doc/conf.py create mode 100644 addons/web/static/lib/py.js/doc/differences.rst create mode 100644 addons/web/static/lib/py.js/doc/index.rst create mode 100644 addons/web/static/lib/py.js/doc/make.bat create mode 100644 addons/web/static/lib/py.js/doc/types.rst create mode 100644 addons/web/static/lib/py.js/doc/utility.rst delete mode 100644 addons/web/static/lib/py.js/test/parser.js delete mode 100644 addons/web/static/lib/py.js/test/test.js diff --git a/addons/web/static/lib/py.js/.hg_archival.txt b/addons/web/static/lib/py.js/.hg_archival.txt index 2aa926eb56a..e97bf6a334b 100644 --- a/addons/web/static/lib/py.js/.hg_archival.txt +++ b/addons/web/static/lib/py.js/.hg_archival.txt @@ -1,5 +1,5 @@ repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea -node: 87e977311edbbb5f281b87390a9a304eb194ce89 +node: e47d717cf47d165a5a9916abbb5ceb138661efc6 branch: default -latesttag: 0.5 -latesttagdistance: 15 +latesttag: 0.6 +latesttagdistance: 1 diff --git a/addons/web/static/lib/py.js/README.rst b/addons/web/static/lib/py.js/README.rst index eea1cbbf963..7601318df6d 100644 --- a/addons/web/static/lib/py.js/README.rst +++ b/addons/web/static/lib/py.js/README.rst @@ -1,14 +1,7 @@ What ==== -``py.js`` is a parser and evaluator of Python expressions, written in -pure javascript. -``py.js`` is not intended to implement a full Python interpreter -(although it could be used for such an effort later on), its -specification document is the `Python 2.7 Expressions spec -`_ (along with the -lexical analysis part). Syntax ------ @@ -69,7 +62,7 @@ Data model protocols ``py.js`` currently implements the following protocols (or sub-protocols) of the `Python 2.7 data model -`_: +<>`_: Rich comparisons Pretty much complete (including operator fallbacks), although the diff --git a/addons/web/static/lib/py.js/doc/Makefile b/addons/web/static/lib/py.js/doc/Makefile new file mode 100644 index 00000000000..cb7d658e796 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyjs.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyjs.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pyjs" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyjs" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/addons/web/static/lib/py.js/doc/builtins.rst b/addons/web/static/lib/py.js/doc/builtins.rst new file mode 100644 index 00000000000..e46926566d3 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/builtins.rst @@ -0,0 +1,53 @@ +.. default-domain: python + +.. _builtins: + +Supported Python builtins +========================= + +.. function:: py.type(object) + + Gets the class of a provided object, if possible. + + .. note:: currently doesn't work correctly when called on a class + object, will return the class itself (also, classes + don't currently have a type). + +.. js:function:: py.type(name, bases, dict) + + Not exactly a builtin as this form is solely javascript-level + (currently). Used to create new ``py.js`` types. See :doc:`types` + for its usage. + +.. data:: py.None + +.. data:: py.True + +.. data:: py.False + +.. data:: py.NotImplemented + +.. class:: py.object + + Base class for all types, even implicitly (if no bases are + provided to :js:func:`py.type`) + +.. class:: py.bool([object]) + +.. class:: py.float([object]) + +.. class:: py.str([object]) + +.. class:: py.unicode([object]) + +.. class:: py.tuple() + +.. class:: py.list() + +.. class:: py.dict() + +.. function:: py.isinstance(object, type) + +.. function:: py.issubclass(type, other_type) + +.. class:: py.classmethod diff --git a/addons/web/static/lib/py.js/doc/conf.py b/addons/web/static/lib/py.js/doc/conf.py new file mode 100644 index 00000000000..287f21af577 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/conf.py @@ -0,0 +1,247 @@ +# -*- coding: utf-8 -*- +# +# py.js documentation build configuration file, created by +# sphinx-quickstart on Sun Sep 9 19:36:23 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.todo'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'py.js' +copyright = u'2012, Xavier Morel' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.6' +# The full version, including alpha/beta/rc tags. +release = '0.6' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# Default sphinx domain +default_domain = 'js' + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' +# default code-block highlighting +highlight_language = 'javascript' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyjsdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pyjs.tex', u'py.js Documentation', + u'Xavier Morel', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pyjs', u'py.js Documentation', + [u'Xavier Morel'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'pyjs', u'py.js Documentation', + u'Xavier Morel', 'pyjs', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/addons/web/static/lib/py.js/doc/differences.rst b/addons/web/static/lib/py.js/doc/differences.rst new file mode 100644 index 00000000000..d81acd240a2 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/differences.rst @@ -0,0 +1,64 @@ +Differences with Python +======================= + +* ``py.js`` completely ignores old-style classes as well as their + lookup details. All ``py.js`` types should be considered matching + the behavior of new-style classes + +* New types can only have a single base. This is due to ``py.js`` + implementing its types on top of Javascript's, and javascript being + a single-inheritance language. + + This may change if ``py.js`` ever reimplements its object model from + scratch. + +* Piggybacking on javascript's object model also means metaclasses are + not available (:js:func:`py.type` is a function) + +* A python-level function (created through :js:class:`py.PY_def`) set + on a new type will not become a method, it'll remain a function. + +* :js:func:`py.PY_parseArgs` supports keyword-only arguments (though + it's a Python 3 feature) + +* Because the underlying type is a javascript ``String``, there + currently is no difference between :js:class:`py.str` and + :js:class:`py.unicode`. As a result, there also is no difference + between :js:func:`__str__` and :js:func:`__unicode__`. + +Unsupported features +-------------------- + +These are Python features which are not supported at all in ``py.js``, +usually because they don't make sense or there is no way to support them + +* The ``__delattr__``, ``__delete__`` and ``__delitem__``: as + ``py.js`` only handles expressions and these are accessed via the + ``del`` statement, there would be no way to call them. + +* ``__del__`` the lack of cross-platform GC hook means there is no way + to know when an object is deallocated. + +* ``__slots__`` are not handled + +* Dedicated (and deprecated) slicing special methods are unsupported + +Missing features +---------------- + +These are Python features which are missing because they haven't been +implemented yet: + +* Class-binding of descriptors doesn't currently work. + +* Instance and subclass checks can't be customized + +* "poor" comparison methods (``__cmp__`` and ``__rcmp__``) are not + supported and won't be falled-back to. + +* ``__coerce__`` is currently supported + +* Context managers are not currently supported + +* Unbound methods are not supported, instance methods can only be + accessed from instances. diff --git a/addons/web/static/lib/py.js/doc/index.rst b/addons/web/static/lib/py.js/doc/index.rst new file mode 100644 index 00000000000..1b9167529b8 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/index.rst @@ -0,0 +1,161 @@ +.. py.js documentation master file, created by + sphinx-quickstart on Sun Sep 9 19:36:23 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +py.js, a Python expressions parser and evaluator +================================================ + +``py.js`` is a parser and evaluator of Python expressions, written in +pure javascript. + +``py.js`` is not intended to implement a full Python interpreter, its +specification document is the `Python 2.7 Expressions spec +`_ (along with the +lexical analysis part) as well as the Python builtins. + + +.. toctree:: + :maxdepth: 2 + + builtins + types + utility + differences + +Usage +----- + +To evaluate a Python expression, simply call +:func:`py.eval`. :func:`py.eval` takes a mandatory Python expression +parameter, as a string, and an optional evaluation context (namespace +for the expression's free variables), and returns a javascript value:: + + > py.eval("t in ('a', 'b', 'c') and foo", {t: 'c', foo: true}); + true + +If the expression needs to be repeatedly evaluated, or the result of +the expression is needed in its "python" form without being converted +back to javascript, you can use the underlying triplet of functions +:func:`py.tokenize`, :func:`py.parse` and :func:`py.evaluate` +directly. + +API +--- + +Core functions +++++++++++++++ + +.. function:: py.eval(expr[, context]) + + "Do everything" function, to use for one-shot evaluation of Python + expressions. Chains tokenizing, parsing and evaluating the + expression then :ref:`converts the result back to javascript + ` + + :param expr: Python expression to evaluate + :type expr: String + :param context: evaluation context for the expression's free + variables + :type context: Object + :returns: the expression's result, converted back to javascript + +.. function:: py.tokenize(expr) + + Expression tokenizer + + :param expr: Python expression to tokenize + :type expr: String + :returns: token stream + +.. function:: py.parse(tokens) + + Parses a token stream and returns the corresponding parse tree. + + The parse tree is stateless and can be memoized and reused for + frequently evaluated expressions. + + :param tokens: token stream from :func:`py.tokenize` + :returns: parse tree + +.. function:: py.evaluate(tree[, context]) + + Evaluates the expression represented by the provided parse tree, + using the provided context for the exprssion's free variables. + + :param tree: parse tree returned by :func:`py.parse` + :param context: evaluation context + :returns: the "python object" resulting from the expression's + evaluation + :rtype: :class:`py.object` + +.. _convert-py: + +Conversions from Javascript to Python ++++++++++++++++++++++++++++++++++++++ + +``py.js`` will automatically attempt to convert non-:class:`py.object` +values into their ``py.js`` equivalent in the following situations: + +* Values passed through the context of :func:`py.eval` or + :func:`py.evaluate` + +* Attributes accessed directly on objects + +* Values of mappings passed to :class:`py.dict` + +Notably, ``py.js`` will *not* attempt an automatic conversion of +values returned by functions or methods, these must be +:class:`py.object` instances. + +The automatic conversions performed by ``py.js`` are the following: + +* ``null`` is converted to :data:`py.None` + +* ``true`` is converted to :data:`py.True` + +* ``false`` is converted to :data:`py.False` + +* numbers are converted to :class:`py.float` + +* strings are converted to :class:`py.str` + +* functions are wrapped into :class:`py.PY_dev` + +* ``Array`` instances are converted to :class:`py.list` + +The rest generates an error, except for ``undefined`` which +specifically generates a ``NameError``. + +.. _convert-js: + +Conversions from Python to Javascript ++++++++++++++++++++++++++++++++++++++ + +py.js types (extensions of :js:class:`py.object`) can be converted +back to javascript by calling their :js:func:`py.object.toJSON` +method. + +The default implementation raises an error, as arbitrary objects can +not be converted back to javascript. + +Most built-in objects provide a :js:func:`py.object.toJSON` +implementation out of the box. + +Javascript-level exceptions ++++++++++++++++++++++++++++ + +Javascript allows throwing arbitrary things, but runtimes don't seem +to provide any useful information (when they ever do) if what is +thrown isn't a direct instance of ``Error``. As a result, while +``py.js`` tries to match the exception-throwing semantics of Python it +only ever throws bare ``Error`` at the javascript-level. Instead, it +prefixes the error message with the name of the Python expression, a +colon, a space, and the actual message. + +For instance, where Python would throw ``KeyError("'foo'")`` when +accessing an invalid key on a ``dict``, ``py.js`` will throw +``Error("KeyError: 'foo'")``. + +.. _Python Data Model: http://docs.python.org/reference/datamodel.html + diff --git a/addons/web/static/lib/py.js/doc/make.bat b/addons/web/static/lib/py.js/doc/make.bat new file mode 100644 index 00000000000..c3db032e01a --- /dev/null +++ b/addons/web/static/lib/py.js/doc/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyjs.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyjs.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/addons/web/static/lib/py.js/doc/types.rst b/addons/web/static/lib/py.js/doc/types.rst new file mode 100644 index 00000000000..61d950c6ec6 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/types.rst @@ -0,0 +1,248 @@ +Implementing a custom type +========================== + +To implement a custom python-level type, one can use the +:func:`py.type` builtin. At the JS-level, it is a function with the +same signature as the :py:class:`type` builtin [#bases]_. It returns a +child type of its one base (or :py:class:`py.object` if no base is +provided). + +The ``dict`` parameter to :func:`py.type` can contain any +attribute, javascript-level or python-level: the default +``__getattribute__`` implementation will ensure they are converted to +Python-level attributes if needed. Most methods are also wrapped and +converted to :ref:`types-methods-python`, although there are a number +of special cases: + +* Most "magic methods" of the data model ("dunder" methods) remain + javascript-level. See :ref:`the listing of magic methods and their + signatures `. As a result, they do not respect + the :ref:`types-methods-python-call` + +* The ``toJSON`` and ``fromJSON`` methods are special-cased to remain + javascript-level and don't follow the + :ref:`types-methods-python-call` + +* Functions which have been wrapped explicitly (via + :class:`py.PY_def`, :py:class:`py.classmethod` or + :py:class:`py.staticmethod`) are associated to the class + untouched. But due to their wrapper, they will use the + :ref:`types-methods-python-call` anyway + +.. _types-methods-python: + +Python-level callable +--------------------- + +Wrapped javascript function *or* the :func:`__call__` method itself +follow the :ref:`types-methods-python-call`. As a result, they can't +(easily) be called directly from javascript code. Because +:func:`__new__` and :func:`__init__` follow from :func:`__call__`, +they also follow the :ref:`types-methods-python-call`. + +:func:`py.PY_call` should be used when interacting with them from +javascript is necessary. + +Because ``__call__`` follows the :ref:`types-methods-python-call`, +instantiating a ``py.js`` type from javascript requires using +:func:`py.PY_call`. + +.. _types-methods-python-call: + +Python calling conventions +++++++++++++++++++++++++++ + +The python-level arguments should be considered completely opaque, +they should be interacted with through :func:`py.PY_parseArgs` (to +extract python-level arguments to javascript implementation code) and +:func:`py.PY_call` (to call :ref:`types-methods-python` from +javascript code). + +A callable following the :ref:`types-methods-python-call` *must* +return a ``py.js`` object, an error will be generated when failing to +do so. + +.. todo:: arguments forwarding when e.g. overriding methods? + +.. _types-methods-dunder: + +Magic methods +------------- + +``py.js`` doesn't support calling magic ("dunder") methods of the +datamodel from Python code, and these methods remain javascript-level +(they don't follow the :ref:`types-methods-python-call`). + +Here is a list of the understood datamodel methods, refer to `the +relevant Python documentation +`_ +for their roles. + +Basic customization ++++++++++++++++++++ + +.. function:: __hash__() + + :returns: String + +.. function:: __eq__(other) + + The default implementation tests for identity + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + +.. function:: __ne__(other) + + The default implementation calls :func:`__eq__` and reverses + its result. + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + +.. function:: __lt__(other) + + The default implementation simply returns + :data:`py.NotImplemented`. + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + + +.. function:: __le__(other) + + The default implementation simply returns + :data:`py.NotImplemented`. + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + + +.. function:: __ge__(other) + + The default implementation simply returns + :data:`py.NotImplemented`. + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + + +.. function:: __gt__(other) + + The default implementation simply returns + :data:`py.NotImplemented`. + + :param other: :py:class:`py.object` to compare this object with + :returns: :py:class:`py.bool` + +.. function:: __str__() + + Simply calls :func:`__unicode__`. This method should not be + overridden, :func:`__unicode__` should be overridden instead. + + :returns: :py:class:`py.str` + +.. function:: __unicode__() + + :returns: :py:class:`py.unicode` + +.. function:: __nonzero__() + + The default implementation always returns :data:`py.True` + + :returns: :py:class:`py.bool` + +Customizing attribute access +++++++++++++++++++++++++++++ + +.. function:: __getattribute__(name) + + :param String name: name of the attribute, as a javascript string + :returns: :py:class:`py.object` + +.. function:: __getattr__(name) + + :param String name: name of the attribute, as a javascript string + :returns: :py:class:`py.object` + +.. function:: __setattr__(name, value) + + :param String name: name of the attribute, as a javascript string + :param value: :py:class:`py.object` + +Implementing descriptors +++++++++++++++++++++++++ + +.. function:: __get__(instance) + + .. note:: readable descriptors don't currently handle "owner + classes" + + :param instance: :py:class:`py.object` + :returns: :py:class:`py.object` + +.. function:: __set__(instance, value) + + :param instance: :py:class:`py.object` + :param value: :py:class:`py.object` + +Emulating Numeric Types ++++++++++++++++++++++++ + +* Non-in-place binary numeric methods (e.g. ``__add__``, ``__mul__``, + ...) should all be supported including reversed calls (in case the + primary call is not available or returns + :py:data:`py.NotImplemented`). They take a single + :py:class:`py.object` parameter and return a single + :py:class:`py.object` parameter. + +* Unary operator numeric methods are all supported: + + .. function:: __pos__() + + :returns: :py:class:`py.object` + + .. function:: __neg__() + + :returns: :py:class:`py.object` + + .. function:: __invert__() + + :returns: :py:class:`py.object` + +* For non-operator numeric methods, support is contingent on the + corresponding :ref:`builtins ` being implemented + +Emulating container types ++++++++++++++++++++++++++ + +.. function:: __len__() + + :returns: :py:class:`py.int` + +.. function:: __getitem__(name) + + :param name: :py:class:`py.object` + :returns: :py:class:`py.object` + +.. function:: __setitem__(name, value) + + :param name: :py:class:`py.object` + :param value: :py:class:`py.object` + +.. function:: __iter__() + + :returns: :py:class:`py.object` + +.. function:: __reversed__() + + :returns: :py:class:`py.object` + +.. function:: __contains__(other) + + :param other: :py:class:`py.object` + :returns: :py:class:`py.bool` + +.. [#bases] with the limitation that, because :ref:`py.js builds its + object model on top of javascript's + `, only one base is allowed. diff --git a/addons/web/static/lib/py.js/doc/utility.rst b/addons/web/static/lib/py.js/doc/utility.rst new file mode 100644 index 00000000000..90c05d448e1 --- /dev/null +++ b/addons/web/static/lib/py.js/doc/utility.rst @@ -0,0 +1,111 @@ +Utility functions for interacting with ``py.js`` objects +======================================================== + +Essentially the ``py.js`` version of the Python C API, these functions +are used to implement new ``py.js`` types or to interact with existing +ones. + +They are prefixed with ``PY_``. + +.. function:: py.PY_call(callable[, args][, kwargs]) + + Call an arbitrary python-level callable from javascript. + + :param callable: A ``py.js`` callable object (broadly speaking, + either a class or an object with a ``__call__`` + method) + + :param args: javascript Array of :class:`py.object`, used as + positional arguments to ``callable`` + + :param kwargs: javascript Object mapping names to + :class:`py.object`, used as named arguments to + ``callable`` + + :returns: nothing or :class:`py.object` + +.. function:: py.PY_parseArgs(arguments, format) + + Arguments parser converting from the :ref:`user-defined calling + conventions ` to a JS object mapping + argument names to values. It serves the same role as + `PyArg_ParseTupleAndKeywords`_. + + :: + + var args = py.PY_parseArgs( + arguments, ['foo', 'bar', ['baz', 3], ['qux', "foo"]]); + + roughly corresponds to the argument spec: + + .. code-block:: python + + def func(foo, bar, baz=3, qux="foo"): + pass + + .. note:: a significant difference is that "default values" will + be re-evaluated at each call, since they are within the + function. + + :param arguments: array-like objects holding the args and kwargs + passed to the callable, generally the + ``arguments`` of the caller. + + :param format: mapping declaration to the actual arguments of the + function. A javascript array composed of five + possible types of elements: + + * The literal string ``'*'`` marks all following + parameters as keyword-only, regardless of them + having a default value or not [#kwonly]_. Can + only be present once in the parameters list. + + * A string prefixed by ``*``, marks the positional + variadic parameter for the function: gathers all + provided positional arguments left and makes all + following parameters keyword-only + [#star-args]_. ``*args`` is incompatible with + ``*``. + + * A string prefixed with ``**``, marks the + positional keyword variadic parameter for the + function: gathers all provided keyword arguments + left and closes the argslist. If present, this + must be the last parameter of the format list. + + * A string defines a required parameter, accessible + positionally or through keyword + + * A pair of ``[String, py.object]`` defines an + optional parameter and its default value. + + For simplicity, when not using optional parameters + it is possible to use a simple string as the format + (using space-separated elements). The string will + be split on whitespace and processed as a normal + format array. + + :returns: a javascript object mapping argument names to values + + :raises: ``TypeError`` if the provided arguments don't match the + format + +.. class:: py.PY_def(fn) + + Type wrapping javascript functions into py.js callables. The + wrapped function follows :ref:`the py.js calling conventions + ` + + :param Function fn: the javascript function to wrap + :returns: a callable py.js object + +.. [#kwonly] Python 2, which py.js currently implements, does not + support Python-level keyword-only parameters (it can be + done through the C-API), but it seemed neat and easy + enough so there. + +.. [#star-args] due to this and contrary to Python 2, py.js allows + arguments other than ``**kwargs`` to follow ``*args``. + +.. _PyArg_ParseTupleAndKeywords: + http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index 79a0ace1995..d03de622689 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -369,24 +369,26 @@ var py = {}; return py.False; } - if (val instanceof py.object - || val === py.object - || py.issubclass.__call__([val, py.object]) === py.True) { + var fn = function () {} + fn.prototype = py.object; + if (py.PY_call(py.isinstance, [val, py.object]) === py.True + || py.PY_call(py.issubclass, [val, py.object]) === py.True) { return val; } switch (typeof val) { case 'number': - return new py.float(val); + return py.float.fromJSON(val); case 'string': - return new py.str(val); + return py.str.fromJSON(val); case 'function': - return new py.def(val); + return py.PY_def.fromJSON(val); } switch(val.constructor) { case Object: - var o = new py.object(); + // TODO: why py.object instead of py.dict? + var o = py.PY_call(py.object); for (var prop in val) { if (val.hasOwnProperty(prop)) { o[prop] = val[prop]; @@ -394,40 +396,170 @@ var py = {}; } return o; case Array: - var a = new py.list(); - a.values = val; + var a = py.PY_call(py.list); + a._values = val; return a; } throw new Error("Could not convert " + val + " to a pyval"); } - // Builtins - py.type = function type(constructor, base, dict) { - var proto; - if (!base) { - base = py.object; + + // JSAPI, JS-level utility functions for implementing new py.js + // types + py.py = {}; + + py.PY_parseArgs = function PY_parseArgs(argument, format) { + var out = {}; + var args = argument[0]; + var kwargs = {}; + for (var k in argument[1]) { + kwargs[k] = argument[1][k]; } - proto = constructor.prototype = create(base.prototype); - proto.constructor = constructor; - if (dict) { - for(var k in dict) { - if (!dict.hasOwnProperty(k)) { continue; } - proto[k] = dict[k]; + if (typeof format === 'string') { + format = format.split(/\s+/); + } + var name = function (spec) { + if (typeof spec === 'string') { + return spec; + } else if (spec instanceof Array && spec.length === 2) { + return spec[0]; + } + throw new Error( + "TypeError: unknown format specification " + + JSON.stringify(spec)); + }; + // TODO: ensure all format arg names are actual names? + for(var i=0; i 1) { + throw new Error("ValueError: can't provide multiple bases for a " + + "new type"); + } + var base = bases[0]; + var ClassObj = create(base); + if (dict) { + for (var k in dict) { + if (!dict.hasOwnProperty(k)) { continue; } + ClassObj[k] = dict[k]; + } + } + ClassObj.__class__ = ClassObj; + ClassObj.__name__ = name; + ClassObj.__bases__ = bases; + ClassObj.__is_type = true; + + return ClassObj; + }; + py.type.__call__ = function () { + var args = py.PY_parseArgs(arguments, ['object']); + return args.object.__class__; }; var hash_counter = 0; - py.object = py.type(function object() {}, {}, { + py.object = py.type('object', [{}], { + __new__: function () { + // If ``this`` isn't the class object, this is going to be + // beyond fucked up + var inst = create(this); + inst.__is_type = false; + return inst; + }, + __init__: function () {}, // Basic customization __hash__: function () { if (this._hash) { @@ -466,11 +598,11 @@ var py = {}; var val = this[name]; if (typeof val === 'object' && '__get__' in val) { // TODO: second argument should be class - return val.__get__(this); + return val.__get__(this, py.PY_call(py.type, [this])); } if (typeof val === 'function' && !this.hasOwnProperty(name)) { // val is a method from the class - return new PY_instancemethod(val, this); + return PY_instancemethod.fromJSON(val, this); } return PY_ensurepy(val); } @@ -492,140 +624,182 @@ var py = {}; throw new Error(this.constructor.name + ' can not be converted to JSON'); } }); - var NoneType = py.type(function NoneType() {}, py.object, { + var NoneType = py.type('NoneType', null, { __nonzero__: function () { return py.False; }, toJSON: function () { return null; } }); - py.None = new NoneType(); - var NotImplementedType = py.type(function NotImplementedType(){}); - py.NotImplemented = new NotImplementedType(); + py.None = py.PY_call(NoneType); + var NotImplementedType = py.type('NotImplementedType', null, {}); + py.NotImplemented = py.PY_call(NotImplementedType); var booleans_initialized = false; - py.bool = py.type(function bool(value) { - value = (value instanceof Array) ? value[0] : value; - // The only actual instance of py.bool should be py.True - // and py.False. Return the new instance of py.bool if we - // are initializing py.True and py.False, otherwise always - // return either py.True or py.False. - if (!booleans_initialized) { - return; - } - if (value === undefined) { return py.False; } - return value.__nonzero__() === py.True ? py.True : py.False; - }, py.object, { + py.bool = py.type('bool', null, { + __new__: function () { + if (!booleans_initialized) { + return py.object.__new__.apply(this); + } + + var ph = {}; + var args = py.PY_parseArgs(arguments, [['value', ph]]); + if (args.value === ph) { + return py.False; + } + return args.value.__nonzero__() === py.True ? py.True : py.False; + }, __nonzero__: function () { return this; }, toJSON: function () { return this === py.True; } }); - py.True = new py.bool(); - py.False = new py.bool(); + py.True = py.PY_call(py.bool); + py.False = py.PY_call(py.bool); booleans_initialized = true; - py.float = py.type(function float(value) { - value = (value instanceof Array) ? value[0] : value; - if (value === undefined) { this._value = 0; return; } - if (value instanceof py.float) { return value; } - if (typeof value === 'number' || value instanceof Number) { - this._value = value; - return; - } - if (typeof value === 'string' || value instanceof String) { - this._value = parseFloat(value); - return; - } - if (value instanceof py.object && '__float__' in value) { - var res = value.__float__(); - if (res instanceof py.float) { - return res; + py.float = py.type('float', null, { + __init__: function () { + var placeholder = {}; + var args = py.PY_parseArgs(arguments, [['value', placeholder]]); + var value = args.value; + if (value === placeholder) { + this._value = 0; return; } - throw new Error('TypeError: __float__ returned non-float (type ' + - res.constructor.name + ')'); - } - throw new Error('TypeError: float() argument must be a string or a number'); - }, py.object, { + if (py.PY_call(py.isinstance, [value, py.float]) === py.True) { + this._value = value._value; + } + if (py.PY_call(py.isinstance, [value, py.object]) === py.True + && '__float__' in value) { + var res = value.__float__(); + if (py.PY_call(py.isinstance, [res, py.float]) === py.True) { + this._value = res._value; + return; + } + throw new Error('TypeError: __float__ returned non-float (type ' + + res.__class__.__name__ + ')'); + } + throw new Error('TypeError: float() argument must be a string or a number'); + }, __eq__: function (other) { return this._value === other._value ? py.True : py.False; }, __lt__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } return this._value < other._value ? py.True : py.False; }, __le__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } return this._value <= other._value ? py.True : py.False; }, __gt__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } return this._value > other._value ? py.True : py.False; }, __ge__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } return this._value >= other._value ? py.True : py.False; }, __add__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } - return new py.float(this._value + other._value); + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + return py.float.fromJSON(this._value + other._value); }, __neg__: function () { - return new py.float(-this._value); + return py.float.fromJSON(-this._value); }, __sub__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } - return new py.float(this._value - other._value); + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + return py.float.fromJSON(this._value - other._value); }, __mul__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } - return new py.float(this._value * other._value); + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + return py.float.fromJSON(this._value * other._value); }, __div__: function (other) { - if (!(other instanceof py.float)) { return py.NotImplemented; } - return new py.float(this._value / other._value); + if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) { + return py.NotImplemented; + } + return py.float.fromJSON(this._value / other._value); }, __nonzero__: function () { return this._value ? py.True : py.False; }, + fromJSON: function (v) { + if (!(typeof v === 'number')) { + throw new Error('py.float.fromJSON can only take numbers '); + } + var instance = py.PY_call(py.float); + instance._value = v; + return instance; + }, toJSON: function () { return this._value; } }); - py.str = py.type(function str(s) { - s = (s instanceof Array) ? s[0] : s; - if (s === undefined) { this._value = ''; return; } - if (s instanceof py.str) { return s; } - if (typeof s === 'string' || s instanceof String) { - this._value = s; - return; - } - var v = s.__str__(); - if (v instanceof py.str) { return v; } - throw new Error('TypeError: __str__ returned non-string (type ' + - v.constructor.name + ')'); - }, py.object, { + py.str = py.type('str', null, { + __init__: function () { + var placeholder = {}; + var args = py.PY_parseArgs(arguments, [['value', placeholder]]); + var s = args.value; + if (s === placeholder) { this._value = ''; return; } + if (py.PY_call(py.isinstance, [s, py.str]) === py.True) { + this._value = s._value; + return; + } + var v = s.__str__(); + if (py.PY_call(py.isinstance, [v, py.str]) === py.True) { + this._value = v._value; + return; + } + throw new Error('TypeError: __str__ returned non-string (type ' + + v.__class__.__name__ + ')'); + }, __hash__: function () { return '\1\0\1' + this._value; }, __eq__: function (other) { - if (other instanceof py.str && this._value === other._value) { + if (py.PY_call(py.isinstance, [other, py.str]) === py.True + && this._value === other._value) { return py.True; } return py.False; }, __lt__: function (other) { - if (!(other instanceof py.str)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) { + return py.NotImplemented; + } return this._value < other._value ? py.True : py.False; }, __le__: function (other) { - if (!(other instanceof py.str)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) { + return py.NotImplemented; + } return this._value <= other._value ? py.True : py.False; }, __gt__: function (other) { - if (!(other instanceof py.str)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) { + return py.NotImplemented; + } return this._value > other._value ? py.True : py.False; }, __ge__: function (other) { - if (!(other instanceof py.str)) { return py.NotImplemented; } + if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) { + return py.NotImplemented; + } return this._value >= other._value ? py.True : py.False; }, __add__: function (other) { - if (!(other instanceof py.str)) { return py.NotImplemented; } - return new py.str(this._value + other._value); + if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) { + return py.NotImplemented; + } + return py.str.fromJSON(this._value + other._value); }, __nonzero__: function () { return this._value.length ? py.True : py.False; @@ -633,40 +807,46 @@ var py = {}; __contains__: function (s) { return (this._value.indexOf(s._value) !== -1) ? py.True : py.False; }, + fromJSON: function (s) { + if (typeof s === 'string') { + var instance = py.PY_call(py.str); + instance._value = s; + return instance; + }; + throw new Error("str.fromJSON can only take strings"); + }, toJSON: function () { return this._value; } }); - py.tuple = py.type(function tuple() {}, null, { + py.tuple = py.type('tuple', null, { + __init__: function () { + this._values = []; + }, __contains__: function (value) { - for(var i=0, len=this.values.length; i 1 ? args[1] : py.None; + get: function () { + var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]); + var h = args.k.__hash__(); if (!(h in this._store)) { - return def; + return args.d; } return this._store[h][1]; }, + fromJSON: function (d) { + var instance = py.PY_call(py.dict); + for (var k in (d || {})) { + if (!d.hasOwnProperty(k)) { continue; } + instance.__setitem__( + py.str.fromJSON(k), + PY_ensurepy(d[k])); + } + return instance; + }, toJSON: function () { var out = {}; for(var k in this._store) { @@ -694,30 +884,57 @@ var py = {}; return out; } }); - py.def = py.type(function def(nativefunc) { - this._inst = null; - this._func = nativefunc; - }, py.object, { - __call__: function (args, kwargs) { + py.PY_def = py.type('function', null, { + __call__: function () { // don't want to rewrite __call__ for instancemethod - return this._func.call(this._inst, args, kwargs); + return this._func.apply(this._inst, arguments); + }, + fromJSON: function (nativefunc) { + var instance = py.PY_call(py.PY_def); + instance._inst = null; + instance._func = nativefunc; + return instance; }, toJSON: function () { return this._func; } }); - var PY_instancemethod = py.type(function instancemethod(nativefunc, instance, _cls) { - // could also use bind? - this._inst = instance; - this._func = nativefunc; - }, py.def, {}); + py.classmethod = py.type('classmethod', null, { + __init__: function () { + var args = py.PY_parseArgs(arguments, 'function'); + this._func = args['function']; + }, + __get__: function (obj, type) { + return PY_instancemethod.fromJSON(this._func, type); + }, + fromJSON: function (func) { + return py.PY_call(py.classmethod, [func]); + } + }); + var PY_instancemethod = py.type('instancemethod', [py.PY_def], { + fromJSON: function (nativefunc, instance) { + var inst = py.PY_call(PY_instancemethod); + // could also use bind? + inst._inst = instance; + inst._func = nativefunc; + return inst; + } + }); - py.issubclass = new py.def(function issubclass(args) { - var derived = args[0], parent = args[1]; - // still hurts my brain that this can work - return derived.prototype instanceof py.object - ? py.True - : py.False; + py.isinstance = new py.PY_def.fromJSON(function isinstance() { + var args = py.PY_parseArgs(arguments, ['object', 'class']); + var fn = function () {}; + fn.prototype = args['class']; + return (args.object instanceof fn) ? py.True : py.False; + }); + py.issubclass = new py.PY_def.fromJSON(function issubclass() { + var args = py.PY_parseArgs(arguments, ['C', 'B']); + var fn = function () {}; + fn.prototype = args.B; + if (args.C === args.B || args.C instanceof fn) { + return py.True; + } + return py.False; }); @@ -726,10 +943,10 @@ var py = {}; '==': ['eq', 'eq', function (a, b) { return a === b; }], '!=': ['ne', 'ne', function (a, b) { return a !== b; }], '<>': ['ne', 'ne', function (a, b) { return a !== b; }], - '<': ['lt', 'gt', function (a, b) {return a.constructor.name < b.constructor.name;}], - '<=': ['le', 'ge', function (a, b) {return a.constructor.name <= b.constructor.name;}], - '>': ['gt', 'lt', function (a, b) {return a.constructor.name > b.constructor.name;}], - '>=': ['ge', 'le', function (a, b) {return a.constructor.name >= b.constructor.name;}], + '<': ['lt', 'gt', function (a, b) {return a.__class__.__name__ < b.__class__.__name__;}], + '<=': ['le', 'ge', function (a, b) {return a.__class__.__name__ <= b.__class__.__name__;}], + '>': ['gt', 'lt', function (a, b) {return a.__class__.__name__ > b.__class__.__name__;}], + '>=': ['ge', 'le', function (a, b) {return a.__class__.__name__ >= b.__class__.__name__;}], '+': ['add', 'radd'], '-': ['sub', 'rsub'], @@ -772,8 +989,8 @@ var py = {}; } throw new Error( "TypeError: unsupported operand type(s) for " + op + ": '" - + o1.constructor.name + "' and '" - + o2.constructor.name + "'"); + + o1.__class__.__name__ + "' and '" + + o2.__class__.__name__ + "'"); }; var PY_builtins = { @@ -787,10 +1004,15 @@ var py = {}; object: py.object, bool: py.bool, float: py.float, + str: py.str, + unicode: py.unicode, tuple: py.tuple, list: py.list, dict: py.dict, - issubclass: py.issubclass + + isinstance: py.isinstance, + issubclass: py.issubclass, + classmethod: py.classmethod, }; py.parse = function (toks) { @@ -825,9 +1047,9 @@ var py = {}; } return PY_ensurepy(val, expr.value); case '(string)': - return new py.str(expr.value); + return py.str.fromJSON(expr.value); case '(number)': - return new py.float(expr.value); + return py.float.fromJSON(expr.value); case '(constant)': switch (expr.value) { case 'None': return py.None; @@ -876,7 +1098,7 @@ var py = {}; py.evaluate(arg.second, context); } } - return callable.__call__(args, kwargs); + return py.PY_call(callable, args, kwargs); } var tuple_exprs = expr.first, tuple_values = []; @@ -884,8 +1106,8 @@ var py = {}; tuple_values.push(py.evaluate( tuple_exprs[j], context)); } - var t = new py.tuple(); - t.values = tuple_values; + var t = py.PY_call(py.tuple); + t._values = tuple_values; return t; case '[': if (expr.second) { @@ -897,11 +1119,11 @@ var py = {}; list_values.push(py.evaluate( list_exprs[k], context)); } - var l = new py.list(); - l.values = list_values; + var l = py.PY_call(py.list); + l._values = list_values; return l; case '{': - var dict_exprs = expr.first, dict = new py.dict; + var dict_exprs = expr.first, dict = py.PY_call(py.dict); for(var l=0; l 2')).to.be(true); - expect(py.eval('"foo" <> "foo"')).to.be(false); - expect(py.eval('"foo" <> "bar"')).to.be(true); - }); - }); - describe('rich comparisons', function () { - it('should work with numbers', function () { - expect(py.eval('3 < 5')).to.be(true); - expect(py.eval('5 >= 3')).to.be(true); - expect(py.eval('3 >= 3')).to.be(true); - expect(py.eval('3 > 5')).to.be(false); - }); - it('should support comparison chains', function () { - expect(py.eval('1 < 3 < 5')).to.be(true); - expect(py.eval('5 > 3 > 1')).to.be(true); - expect(py.eval('1 < 3 > 2 == 2 > -2')).to.be(true); - }); - it('should compare strings', function () { - expect(py.eval('date >= current', - {date: '2010-06-08', current: '2010-06-05'})) - .to.be(true); - expect(py.eval('state == "cancel"', {state: 'cancel'})) - .to.be(true); - expect(py.eval('state == "cancel"', {state: 'open'})) - .to.be(false); - }); - }); - describe('missing eq/neq', function () { - it('should fall back on identity', function () { - var typ = new py.type(function MyType() {}); - expect(py.eval('MyType() == MyType()', {MyType: typ})).to.be(false); - }); - }); - describe('un-comparable types', function () { - it('should default to type-name ordering', function () { - var t1 = new py.type(function Type1() {}); - var t2 = new py.type(function Type2() {}); - expect(py.eval('T1() < T2()', {T1: t1, T2: t2})).to.be(true); - expect(py.eval('T1() > T2()', {T1: t1, T2: t2})).to.be(false); - }); - it('should handle native stuff', function () { - expect(py.eval('None < 42')).to.be(true); - expect(py.eval('42 > None')).to.be(true); - expect(py.eval('None > 42')).to.be(false); - - expect(py.eval('None < False')).to.be(true); - expect(py.eval('None < True')).to.be(true); - expect(py.eval('False > None')).to.be(true); - expect(py.eval('True > None')).to.be(true); - expect(py.eval('None > False')).to.be(false); - expect(py.eval('None > True')).to.be(false); - - expect(py.eval('False < ""')).to.be(true); - expect(py.eval('"" > False')).to.be(true); - expect(py.eval('False > ""')).to.be(false); - }); - }); -}); -describe('Boolean operators', function () { - it('should work', function () { - expect(py.eval("foo == 'foo' or foo == 'bar'", - {foo: 'bar'})) - .to.be(true);; - expect(py.eval("foo == 'foo' and bar == 'bar'", - {foo: 'foo', bar: 'bar'})) - .to.be(true);; - }); - it('should be lazy', function () { - // second clause should nameerror if evaluated - expect(py.eval("foo == 'foo' or bar == 'bar'", - {foo: 'foo'})) - .to.be(true);; - expect(py.eval("foo == 'foo' and bar == 'bar'", - {foo: 'bar'})) - .to.be(false);; - }); - it('should return the actual object', function () { - expect(py.eval('"foo" or "bar"')).to.be('foo'); - expect(py.eval('None or "bar"')).to.be('bar'); - expect(py.eval('False or None')).to.be(null); - expect(py.eval('0 or 1')).to.be(1); - }); -}); -describe('Containment', function () { - describe('in sequences', function () { - it('should match collection items', function () { - expect(py.eval("'bar' in ('foo', 'bar')")) - .to.be(true); - expect(py.eval('1 in (1, 2, 3, 4)')) - .to.be(true);; - expect(py.eval('1 in (2, 3, 4)')) - .to.be(false);; - expect(py.eval('"url" in ("url",)')) - .to.be(true); - expect(py.eval('"foo" in ["foo", "bar"]')) - .to.be(true); - }); - it('should not be recursive', function () { - expect(py.eval('"ur" in ("url",)')) - .to.be(false);; - }); - it('should be negatable', function () { - expect(py.eval('1 not in (2, 3, 4)')).to.be(true); - expect(py.eval('"ur" not in ("url",)')).to.be(true); - expect(py.eval('-2 not in (1, 2, 3)')).to.be(true); - }); - }); - describe('in dict', function () { - // TODO - }); - describe('in strings', function () { - it('should match the whole string', function () { - expect(py.eval('"view" in "view"')).to.be(true); - expect(py.eval('"bob" in "view"')).to.be(false); - }); - it('should match substrings', function () { - expect(py.eval('"ur" in "url"')).to.be(true); - }); - }); -}); -describe('Conversions', function () { - describe('to bool', function () { - describe('strings', function () { - it('should be true if non-empty', function () { - expect(py.eval('bool(date_deadline)', - {date_deadline: '2008'})) - .to.be(true); - }); - it('should be false if empty', function () { - expect(py.eval('bool(s)', {s: ''})) .to.be(false); - }); - }); - }); -}); -describe('Attribute access', function () { - it("should return the attribute's value", function () { - var o = new py.object(); - o.bar = py.True; - expect(py.eval('foo.bar', {foo: o})).to.be(true); - o.bar = py.False; - expect(py.eval('foo.bar', {foo: o})).to.be(false); - }); - it("should work with functions", function () { - var o = new py.object(); - o.bar = new py.def(function () { - return new py.str("ok"); - }); - expect(py.eval('foo.bar()', {foo: o})).to.be('ok'); - }); - it('should not convert function attributes into methods', function () { - var o = new py.object(); - o.bar = new py.type(function bar() {}); - o.bar.__getattribute__ = function () { - return o.bar.baz; - } - o.bar.baz = py.True; - expect(py.eval('foo.bar.baz', {foo: o})).to.be(true); - }); - it('should work on instance attributes', function () { - var typ = py.type(function MyType() { - this.attr = new py.float(3); - }, py.object, {}); - expect(py.eval('MyType().attr', {MyType: typ})).to.be(3); - }); - it('should work on class attributes', function () { - var typ = py.type(function MyType() {}, py.object, { - attr: new py.float(3) - }); - expect(py.eval('MyType().attr', {MyType: typ})).to.be(3); - }); - it('should work with methods', function () { - var typ = py.type(function MyType() { - this.attr = new py.float(3); - }, py.object, { - some_method: function () { return new py.str('ok'); }, - get_attr: function () { return this.attr; } - }); - expect(py.eval('MyType().some_method()', {MyType: typ})).to.be('ok'); - expect(py.eval('MyType().get_attr()', {MyType: typ})).to.be(3); - }); -}); -describe('Callables', function () { - it('should wrap JS functions', function () { - expect(py.eval('foo()', {foo: function foo() { return new py.float(3); }})) - .to.be(3); - }); - it('should work on custom types', function () { - var typ = py.type(function MyType() {}, py.object, { - toJSON: function () { return true; } - }); - expect(py.eval('MyType()', {MyType: typ})).to.be(true); - }); - it('should accept kwargs', function () { - expect(py.eval('foo(ok=True)', { - foo: function foo() { return py.True; } - })).to.be(true); - }); - it('should be able to get its kwargs', function () { - expect(py.eval('foo(ok=True)', { - foo: function foo(args, kwargs) { return kwargs.ok; } - })).to.be(true); - }); - it('should be able to have both args and kwargs', function () { - expect(py.eval('foo(1, 2, 3, ok=True, nok=False)', { - foo: function (args, kwargs) { - expect(args).to.have.length(3); - expect(args[0].toJSON()).to.be(1); - expect(kwargs).to.only.have.keys('ok', 'nok') - expect(kwargs.nok.toJSON()).to.be(false); - return kwargs.ok; - } - })).to.be(true); - }); -}); -describe('issubclass', function () { - it('should say a type is its own subclass', function () { - expect(py.issubclass.__call__([py.dict, py.dict]).toJSON()) - .to.be(true); - expect(py.eval('issubclass(dict, dict)')) - .to.be(true); - }); - it('should work with subtypes', function () { - expect(py.issubclass.__call__([py.bool, py.object]).toJSON()) - .to.be(true); - }); -}); -describe('builtins', function () { - it('should aways be available', function () { - expect(py.eval('bool("foo")')).to.be(true); - }); -}); - -describe('numerical protocols', function () { - describe('True numbers (float)', function () { - describe('Basic arithmetic', function () { - it('can be added', function () { - expect(py.eval('1 + 1')).to.be(2); - expect(py.eval('1.5 + 2')).to.be(3.5); - expect(py.eval('1 + -1')).to.be(0); - }); - it('can be subtracted', function () { - expect(py.eval('1 - 1')).to.be(0); - expect(py.eval('1.5 - 2')).to.be(-0.5); - expect(py.eval('2 - 1.5')).to.be(0.5); - }); - it('can be multiplied', function () { - expect(py.eval('1 * 3')).to.be(3); - expect(py.eval('0 * 5')).to.be(0); - expect(py.eval('42 * -2')).to.be(-84); - }); - it('can be divided', function () { - expect(py.eval('1 / 2')).to.be(0.5); - expect(py.eval('2 / 1')).to.be(2); - }); - }); - }); - describe('Strings', function () { - describe('Basic arithmetics operators', function () { - it('can be added (concatenation)', function () { - expect(py.eval('"foo" + "bar"')).to.be('foobar'); - }); - }); - }); -}); -describe('dicts', function () { - it('should be possible to retrieve their value', function () { - var d = new py.dict({foo: 3, bar: 4, baz: 5}); - expect(py.eval('d["foo"]', {d: d})).to.be(3); - expect(py.eval('d["baz"]', {d: d})).to.be(5); - }); - it('should raise KeyError if a key is missing', function () { - var d = new py.dict(); - expect(function () { - py.eval('d["foo"]', {d: d}); - }).to.throwException(/^KeyError/); - }); - it('should have a method to provide a default value', function () { - var d = new py.dict({foo: 3}); - expect(py.eval('d.get("foo")', {d: d})).to.be(3); - expect(py.eval('d.get("bar")', {d: d})).to.be(null); - expect(py.eval('d.get("bar", 42)', {d: d})).to.be(42); - - var e = new py.dict({foo: null}); - expect(py.eval('d.get("foo")', {d: e})).to.be(null); - expect(py.eval('d.get("bar")', {d: e})).to.be(null); - }); -}); -describe('Type converter', function () { - it('should convert bare objects to objects', function () { - expect(py.eval('foo.bar', {foo: {bar: 3}})).to.be(3); - }); - it('should convert arrays to lists', function () { - expect(py.eval('foo[3]', {foo: [9, 8, 7, 6, 5]})) - .to.be(6); - }); -}); diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 34ddf0e119b..80aac87ecbe 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -4,66 +4,151 @@ openerp.web.pyeval = function (instance) { instance.web.pyeval = {}; + var obj = function () {}; + obj.prototype = py.object; var asJS = function (arg) { - if (arg instanceof py.object) { + if (arg instanceof obj) { return arg.toJSON(); } return arg; }; - var datetime = new py.object(); - datetime.datetime = new py.type(function datetime() { - throw new Error('datetime.datetime not implemented'); + var datetime = py.PY_call(py.object); + datetime.datetime = py.type('datetime', null, { + __init__: function () { + var zero = py.float.fromJSON(0); + var args = py.PY_parseArgs(arguments, [ + 'year', 'month', 'day', + ['hour', zero], ['minute', zero], ['second', zero], + ['microsecond', zero], ['tzinfo', py.None] + ]); + for(var key in args) { + if (!args.hasOwnProperty(key)) { continue; } + this[key] = asJS(args[key]); + } + }, + strftime: function () { + var self = this; + var args = py.PY_parseArgs(arguments, 'format'); + return py.str.fromJSON(args.format.toJSON() + .replace(/%([A-Za-z])/g, function (m, c) { + switch (c) { + case 'Y': return self.year; + case 'm': return _.str.sprintf('%02d', self.month); + case 'd': return _.str.sprintf('%02d', self.day); + case 'H': return _.str.sprintf('%02d', self.hour); + case 'M': return _.str.sprintf('%02d', self.minute); + case 'S': return _.str.sprintf('%02d', self.second); + } + throw new Error('ValueError: No known conversion for ' + m); + })); + }, + now: py.classmethod.fromJSON(function () { + var d = new Date(); + return py.PY_call(datetime.datetime, + [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), + d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), + d.getUTCMilliseconds() * 1000]); + }), + today: py.classmethod.fromJSON(function () { + var d = new Date(); + return py.PY_call(datetime.datetime, + [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]); + }), + combine: py.classmethod.fromJSON(function () { + var args = py.PY_parseArgs(arguments, 'date time'); + return py.PY_call(datetime.datetime, [ + // FIXME: should use getattr + args.date.year, + args.date.month, + args.date.day, + args.time.hour, + args.time.minute, + args.time.second + ]); + }) }); - var date = datetime.date = new py.type(function date(y, m, d) { - if (y instanceof Array) { - d = y[2]; - m = y[1]; - y = y[0]; - } - this.year = asJS(y); - this.month = asJS(m); - this.day = asJS(d); - }, py.object, { - strftime: function (args) { - var f = asJS(args[0]), self = this; - return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) { - switch (c) { - case 'Y': return self.year; - case 'm': return _.str.sprintf('%02d', self.month); - case 'd': return _.str.sprintf('%02d', self.day); - } - throw new Error('ValueError: No known conversion for ' + m); - })); - } + datetime.date = py.type('date', null, { + __init__: function () { + var args = py.PY_parseArgs(arguments, 'year month day'); + this.year = asJS(args.year); + this.month = asJS(args.month); + this.day = asJS(args.day); + }, + strftime: function () { + var self = this; + var args = py.PY_parseArgs(arguments, 'format'); + return py.str.fromJSON(args.format.toJSON() + .replace(/%([A-Za-z])/g, function (m, c) { + switch (c) { + case 'Y': return self.year; + case 'm': return _.str.sprintf('%02d', self.month); + case 'd': return _.str.sprintf('%02d', self.day); + } + throw new Error('ValueError: No known conversion for ' + m); + })); + }, + today: py.classmethod.fromJSON(function () { + var d = new Date(); + return py.PY_call( + datetime.date, [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]); + }) }); - date.__getattribute__ = function (name) { - if (name === 'today') { - return date.today; + datetime.time = py.type('time', null, { + __init__: function () { + var zero = py.float.fromJSON(0); + var args = py.PY_parseArgs(arguments, [ + ['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero], + ['tzinfo', py.None] + ]); + + for(var k in args) { + if (!args.hasOwnProperty(k)) { continue; } + this[k] = asJS(args[k]); + } } - throw new Error("AttributeError: object 'date' has no attribute '" + name +"'"); - }; - date.today = new py.def(function () { - var d = new Date(); - return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()); - }); - datetime.time = new py.type(function time() { - throw new Error('datetime.time not implemented'); }); - var time = new py.object(); - time.strftime = new py.def(function (args) { - return date.today.__call__().strftime(args); + datetime.timedelta = py.type('timedelta', null, { + __init__: function () { + var zero = py.float.fromJSON(0); + var args = py.PY_parseArgs(arguments, + [['days', zero], ['seconds', zero], ['microseconds', zero], + ['milliseconds', zero], ['minutes', zero], ['hours', zero], + ['weeks', zero]]); + var ms = args['microseconds'].toJSON() + + 1000 * args['milliseconds'].toJSON(); + this.milliseconds = ms % 1000000; + var sec = Math.floor(ms / 1000000) + + args['seconds'].toJSON() + + 60 * args['minutes'].toJSON() + + 3600 * args['hours'].toJSON(); + this.seconds = sec % 86400; + this.days = Math.floor(sec / 86400) + + args['days'].toJSON() + + 7 * args['weeks'].toJSON(); + } }); - var relativedelta = new py.type(function relativedelta(args, kwargs) { - if (!_.isEmpty(args)) { - throw new Error('Extraction of relative deltas from existing datetimes not supported'); - } - this.ops = kwargs; - }, py.object, { + var time = py.PY_call(py.object); + time.strftime = py.PY_def.fromJSON(function () { + // FIXME: needs PY_getattr + var d = py.PY_call(datetime.__getattribute__('datetime') + .__getattribute__('now')); + var args = [].slice.call(arguments); + return py.PY_call.apply( + null, [d.__getattribute__('strftime')].concat(args)); + }); + + var relativedelta = py.type('relativedelta', null, { + __init__: function () { + this.ops = py.PY_parseArgs(arguments, + '* year month day hour minute second microsecond ' + + 'years months weeks days hours minutes secondes microseconds ' + + 'weekday leakdays yearday nlyearday'); + }, __add__: function (other) { - if (!(other instanceof datetime.date)) { + if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) { return py.NotImplemented; } // TODO: test this whole mess @@ -96,14 +181,19 @@ openerp.web.pyeval = function (instance) { // TODO: leapdays? // TODO: hours, minutes, seconds? Not used in XML domains // TODO: weekday? - return new datetime.date(year, month, day); + // FIXME: use date.replace + return py.PY_call(datetime.date, [ + py.float.fromJSON(year), + py.float.fromJSON(month), + py.float.fromJSON(day) + ]); }, __radd__: function (other) { return this.__add__(other); }, __sub__: function (other) { - if (!(other instanceof datetime.date)) { + if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) { return py.NotImplemented; } // TODO: test this whole mess @@ -136,7 +226,11 @@ openerp.web.pyeval = function (instance) { // TODO: leapdays? // TODO: hours, minutes, seconds? Not used in XML domains // TODO: weekday? - return new datetime.date(year, month, day); + return py.PY_call(datetime.date, [ + py.float.fromJSON(year), + py.float.fromJSON(month), + py.float.fromJSON(day) + ]); }, __rsub__: function (other) { return this.__sub__(other); @@ -219,11 +313,12 @@ openerp.web.pyeval = function (instance) { instance.web.pyeval.context = function () { return { - uid: new py.float(instance.session.uid), + uid: py.float.fromJSON(instance.session.uid), datetime: datetime, time: time, relativedelta: relativedelta, - current_date: date.today.__call__().strftime(['%Y-%m-%d']), + current_date: py.PY_call( + time.strftime, [py.str.fromJSON('%Y-%m-%d')]), }; }; diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index dc8ceb83daf..3c3bface529 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -1,9 +1,51 @@ $(document).ready(function () { var openerp; + module("eval.types", { + setup: function () { + openerp = window.openerp.testing.instanceFor('coresetup'); + openerp.session.uid = 42; + } + }); + test('datetime.datetime', function () { + }); + test('datetime.date', function () { + + }); + test('datetime.timedelta', function () { + var d = new Date(); + d.setUTCDate(d.getUTCDate() - 15); + strictEqual( + py.eval("(datetime.date.today()" + + "- datetime.timedelta(days=15))" + + ".strftime('%Y-%m-%d')", openerp.web.pyeval.context()), + _.str.strftime('%04d-%02d-%02d', + d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())); + + }); + test('relativedelta', function () { + + }); + test('strftime', function () { + var d = new Date(); + var context = openerp.web.pyeval.context(); + strictEqual( + py.eval("time.strftime('%Y')", context), + String(d.getUTCFullYear())); + strictEqual( + py.eval("time.strftime('%Y')+'-01-30'", context), + String(d.getUTCFullYear()) + '-01-30'); + strictEqual( + py.eval("time.strftime('%Y-%m-%d %H:%M:%S')", context), + _.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d', + d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), + d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds())); + }); + module("eval.contexts", { setup: function () { openerp = window.openerp.testing.instanceFor('coresetup'); + openerp.session.uid = 42; } }); test('context_sequences', function () { @@ -135,6 +177,7 @@ $(document).ready(function () { setup: function () { openerp = window.openerp.testing.instanceFor('coresetup'); window.openerp.web.dates(openerp); + openerp.session.uid = 42; } }); test('current_date', function () { @@ -150,6 +193,7 @@ $(document).ready(function () { module('eval.groupbys', { setup: function () { openerp = window.openerp.testing.instanceFor('coresetup'); + openerp.session.uid = 42; } }); test('groupbys_00', function () { From 894bd34e6275fe838bb0de30e25de4b0e09d4636 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 8 Oct 2012 14:55:23 +0200 Subject: [PATCH 015/138] [REM] timedelta, because pain bzr revid: xmo@openerp.com-20121008125523-ed1g42vjxbcfo62r --- addons/web/static/src/js/pyeval.js | 21 --------------------- addons/web/static/test/evals.js | 19 ------------------- 2 files changed, 40 deletions(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 80aac87ecbe..8b56d17c14e 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -109,27 +109,6 @@ openerp.web.pyeval = function (instance) { } }); - datetime.timedelta = py.type('timedelta', null, { - __init__: function () { - var zero = py.float.fromJSON(0); - var args = py.PY_parseArgs(arguments, - [['days', zero], ['seconds', zero], ['microseconds', zero], - ['milliseconds', zero], ['minutes', zero], ['hours', zero], - ['weeks', zero]]); - var ms = args['microseconds'].toJSON() - + 1000 * args['milliseconds'].toJSON(); - this.milliseconds = ms % 1000000; - var sec = Math.floor(ms / 1000000) - + args['seconds'].toJSON() - + 60 * args['minutes'].toJSON() - + 3600 * args['hours'].toJSON(); - this.seconds = sec % 86400; - this.days = Math.floor(sec / 86400) - + args['days'].toJSON() - + 7 * args['weeks'].toJSON(); - } - }); - var time = py.PY_call(py.object); time.strftime = py.PY_def.fromJSON(function () { // FIXME: needs PY_getattr diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 3c3bface529..72c4cd7323a 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -6,25 +6,6 @@ $(document).ready(function () { openerp = window.openerp.testing.instanceFor('coresetup'); openerp.session.uid = 42; } - }); - test('datetime.datetime', function () { - }); - test('datetime.date', function () { - - }); - test('datetime.timedelta', function () { - var d = new Date(); - d.setUTCDate(d.getUTCDate() - 15); - strictEqual( - py.eval("(datetime.date.today()" + - "- datetime.timedelta(days=15))" + - ".strftime('%Y-%m-%d')", openerp.web.pyeval.context()), - _.str.strftime('%04d-%02d-%02d', - d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())); - - }); - test('relativedelta', function () { - }); test('strftime', function () { var d = new Date(); From 1fa00288b269e68a47849eaa3fefc090759a357d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 10 Oct 2012 16:59:45 +0200 Subject: [PATCH 016/138] [FIX] add 'context' free variable in evaluation contexts for domains and contexts bzr revid: xmo@openerp.com-20121010145945-16shdry3udum0mn1 --- addons/web/static/src/js/pyeval.js | 4 +++- addons/web/static/test/evals.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 8b56d17c14e..a00fd186d58 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -307,7 +307,9 @@ openerp.web.pyeval = function (instance) { * @param {Object} [context] evaluation context */ instance.web.pyeval.eval = function (type, object, context) { - if (!context) { context = instance.web.pyeval.context()} + context = _.extend(instance.web.pyeval.context(), context || {}); + context['context'] = py.dict.fromJSON(context); + switch(type) { case 'contexts': return eval_contexts(object, context); case 'domains': return eval_domains(object, context); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 72c4cd7323a..72503d5313b 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -29,6 +29,21 @@ $(document).ready(function () { openerp.session.uid = 42; } }); + test('context_recursive', function () { + var context_to_eval = [{ + __ref: 'context', + __debug: '{"foo": context.get("bar", "qux")}' + }]; + deepEqual( + openerp.web.pyeval.eval('contexts', context_to_eval, {bar: "ok"}), + {foo: 'ok'}); + deepEqual( + openerp.web.pyeval.eval('contexts', context_to_eval, {bar: false}), + {foo: false}); + deepEqual( + openerp.web.pyeval.eval('contexts', context_to_eval), + {foo: 'qux'}); + }); test('context_sequences', function () { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context @@ -170,6 +185,21 @@ $(document).ready(function () { ['name', '<=', current_date] ]); }); + test('context_freevar', function () { + var domains_to_eval = [{ + __ref: 'domain', + __debug: '[("foo", "=", context.get("bar", "qux"))]' + }, [['bar', '>=', 42]]]; + deepEqual( + openerp.web.pyeval.eval('domains', domains_to_eval, {bar: "ok"}), + [['foo', '=', 'ok'], ['bar', '>=', 42]]); + deepEqual( + openerp.web.pyeval.eval('domains', domains_to_eval, {bar: false}), + [['foo', '=', false], ['bar', '>=', 42]]); + deepEqual( + openerp.web.pyeval.eval('domains', domains_to_eval), + [['foo', '=', 'qux'], ['bar', '>=', 42]]); + }); module('eval.groupbys', { setup: function () { From b94e9c30a78e0047457977bf8ff5bd6bdda81372 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Oct 2012 09:57:59 +0200 Subject: [PATCH 017/138] [FIX] after discussions with odo, date/datetime/time for locally-evaluated contexts and domains should be local, not UTC bzr revid: xmo@openerp.com-20121011075759-qfwkfws5tu9yidab --- addons/web/static/src/js/pyeval.js | 10 +++++----- addons/web/static/test/evals.js | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index a00fd186d58..51fdc63e84c 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -46,14 +46,14 @@ openerp.web.pyeval = function (instance) { now: py.classmethod.fromJSON(function () { var d = new Date(); return py.PY_call(datetime.datetime, - [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), - d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), - d.getUTCMilliseconds() * 1000]); + [d.getFullYear(), d.getMonth() + 1, d.getDate(), + d.getHours(), d.getMinutes(), d.getSeconds(), + d.getMilliseconds() * 1000]); }), today: py.classmethod.fromJSON(function () { var d = new Date(); return py.PY_call(datetime.datetime, - [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]); + [d.getFullYear(), d.getMonth() + 1, d.getDate()]); }), combine: py.classmethod.fromJSON(function () { var args = py.PY_parseArgs(arguments, 'date time'); @@ -91,7 +91,7 @@ openerp.web.pyeval = function (instance) { today: py.classmethod.fromJSON(function () { var d = new Date(); return py.PY_call( - datetime.date, [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]); + datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]); }) }); datetime.time = py.type('time', null, { diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 72503d5313b..276636c1ed2 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -12,15 +12,15 @@ $(document).ready(function () { var context = openerp.web.pyeval.context(); strictEqual( py.eval("time.strftime('%Y')", context), - String(d.getUTCFullYear())); + String(d.getFullYear())); strictEqual( py.eval("time.strftime('%Y')+'-01-30'", context), - String(d.getUTCFullYear()) + '-01-30'); + String(d.getFullYear()) + '-01-30'); strictEqual( py.eval("time.strftime('%Y-%m-%d %H:%M:%S')", context), _.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d', - d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), - d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds())); + d.getFullYear(), d.getMonth() + 1, d.getDate(), + d.getHours(), d.getMinutes(), d.getSeconds())); }); module("eval.contexts", { From e6b77eb820a7670b416319f314808e4b90931a01 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 11 Oct 2012 12:05:32 +0200 Subject: [PATCH 018/138] [IMP] shortcut call to eval_domain_and_context to be evaluated on the JS side, also add some offline-evaluation of contexts and domains in rpc call methods bzr revid: xmo@openerp.com-20121011100532-5ihje0maslp37zpf --- addons/web/static/src/js/corelib.js | 83 ++++------------------------- addons/web/static/src/js/data.js | 14 +++-- addons/web/static/src/js/pyeval.js | 57 ++++++++++++++++++++ 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 228cee5e411..e26a839639c 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -997,75 +997,6 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ this.server = this.origin; // keep chs happy this.rpc_function = (this.origin == window_origin) ? this.rpc_json : this.rpc_jsonp; }, - /** - * FIXME: Huge testing hack, especially the evaluation context, rewrite + test for real before switching - */ - test_eval: function (source, expected) { - var match_template = '
    ' + - '
  • Source: %(source)s
  • ' + - '
  • Local: %(local)s
  • ' + - '
  • Remote: %(remote)s
  • ' + - '
', - fail_template = '
    ' + - '
  • Error: %(error)s
  • ' + - '
  • Source: %(source)s
  • ' + - '
'; - try { - // see Session.eval_context in Python - var ctx = instance.web.pyeval.eval('contexts', - ([this.context] || []).concat(source.contexts)); - if (!_.isEqual(ctx, expected.context)) { - instance.webclient.notification.warn('Context mismatch, report to xmo', - _.str.sprintf(match_template, { - source: JSON.stringify(source.contexts), - local: JSON.stringify(ctx), - remote: JSON.stringify(expected.context) - }), true); - } - } catch (e) { - instance.webclient.notification.warn('Context fail, report to xmo', - _.str.sprintf(fail_template, { - error: e.message, - source: JSON.stringify(source.contexts) - }), true); - } - - try { - var dom = instance.web.pyeval.eval('domains', source.domains); - if (!_.isEqual(dom, expected.domain)) { - instance.webclient.notification.warn('Domains mismatch, report to xmo', - _.str.sprintf(match_template, { - source: JSON.stringify(source.domains), - local: JSON.stringify(dom), - remote: JSON.stringify(expected.domain) - }), true); - } - } catch (e) { - instance.webclient.notification.warn('Domain fail, report to xmo', - _.str.sprintf(fail_template, { - error: e.message, - source: JSON.stringify(source.domains) - }), true); - } - - try { - var groups = instance.web.pyeval.eval('groupbys', source.group_by_seq); - if (!_.isEqual(groups, expected.group_by)) { - instance.webclient.notification.warn('GroupBy mismatch, report to xmo', - _.str.sprintf(match_template, { - source: JSON.stringify(source.group_by_seq), - local: JSON.stringify(groups), - remote: JSON.stringify(expected.group_by) - }), true); - } - } catch (e) { - instance.webclient.notification.warn('GroupBy fail, report to xmo', - _.str.sprintf(fail_template, { - error: e.message, - source: JSON.stringify(source.group_by_seq) - }), true); - } - }, /** * Executes an RPC call, registering the provided callbacks. * @@ -1098,13 +1029,17 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ this.trigger('request', url, payload); var aborter = params.aborter; delete params.aborter; - var request = this.rpc_function(url, payload).then( - function (response, textStatus, jqXHR) { + var request; + if (url.url === '/web/session/eval_domain_and_context') { + // intercept eval_domain_and_context + request = instance.web.pyeval.eval_domains_and_contexts( + params) + } else { + request = this.rpc_function(url, payload); + } + request.then(function (response, textStatus, jqXHR) { self.trigger('response', response); if (!response.error) { - if (url.url === '/web/session/eval_domain_and_context') { - self.test_eval(params, response.result); - } deferred.resolve(response["result"], textStatus, jqXHR); } else if (response.error.data.type === "session_invalid") { self.uid = false; diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 2b82e618203..af887a5152f 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -61,8 +61,10 @@ instance.web.Query = instance.web.Class.extend({ return instance.session.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, - domain: this._model.domain(this._filter), - context: this._model.context(this._context), + domain: instance.web.pyeval.eval('domains', + [this._model.domain(this._filter)]), + context: instance.web.pyeval.eval('contexts', + [this._model.context(this._context)]), offset: this._offset, limit: this._limit, sort: instance.web.serialize_sort(this._order_by) @@ -280,6 +282,7 @@ instance.web.Model = instance.web.Class.extend({ kwargs = args; args = []; } + instance.web.pyeval.ensure_evaluated(args, kwargs); return instance.session.rpc('/web/dataset/call_kw', { model: this.name, method: method, @@ -291,7 +294,7 @@ instance.web.Model = instance.web.Class.extend({ * Fetches a Query instance bound to this model, for searching * * @param {Array} [fields] fields to ultimately fetch during the search - * @returns {openerp.web.Query} + * @returns {instance.web.Query} */ query: function (fields) { return new instance.web.Query(this, fields); @@ -339,9 +342,11 @@ instance.web.Model = instance.web.Class.extend({ * FIXME: remove when evaluator integrated */ call_button: function (method, args) { + instance.web.pyeval.ensure_evaluated(args, {}); return instance.session.rpc('/web/dataset/call_button', { model: this.name, method: method, + // Should not be necessary anymore. Integrate remote in this? domain_id: null, context_id: args.length - 1, args: args || [] @@ -599,9 +604,12 @@ instance.web.DataSet = instance.web.CallbackEnabled.extend({ * @returns {$.Deferred} */ call_and_eval: function (method, args, domain_index, context_index) { + instance.web.pyeval.ensure_evaluated(args, {}); return instance.session.rpc('/web/dataset/call', { model: this.model, method: method, + // Should not be necessary anymore as ensure_evaluated traverses + // all of the args array domain_id: domain_index == undefined ? null : domain_index, context_id: context_index == undefined ? null : context_index, args: args || [] diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js index 51fdc63e84c..ca4d5a0a1e7 100644 --- a/addons/web/static/src/js/pyeval.js +++ b/addons/web/static/src/js/pyeval.js @@ -2,6 +2,7 @@ * py.js helpers and setup */ openerp.web.pyeval = function (instance) { + var _t = instance.web._t; instance.web.pyeval = {}; var obj = function () {}; @@ -317,4 +318,60 @@ openerp.web.pyeval = function (instance) { } throw new Error("Unknow evaluation type " + type) }; + + var eval_arg = function (arg) { + if (typeof arg !== 'object' || !arg.__ref) { return arg; } + switch(arg.__ref) { + case 'domain': case 'compound_domain': + return instance.web.pyeval.eval('domains', [arg]); + case 'context': case 'compound_context': + return instance.web.pyeval.eval('contexts', [arg]); + default: + throw new Error(_t("Unknown nonliteral type " + arg.__ref)); + } + }; + /** + * If args or kwargs are unevaluated contexts or domains (compound or not), + * evaluated them in-place. + * + * Potentially mutates both parameters. + * + * @param args + * @param kwargs + */ + instance.web.pyeval.ensure_evaluated = function (args, kwargs) { + for (var i=0; i Date: Tue, 23 Oct 2012 14:40:01 +0530 Subject: [PATCH 019/138] [IMP] improved code bzr revid: tpa@tinyerp.com-20121023091001-a5z5utrnxv3tcbsk --- openerp/workflow/wkf_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py index bf9e99f00a4..16b186c904d 100644 --- a/openerp/workflow/wkf_expr.py +++ b/openerp/workflow/wkf_expr.py @@ -44,7 +44,7 @@ class Env(dict): def _eval_expr(cr, ident, workitem, action): ret=False assert action, 'You used a NULL action in a workflow, use dummy node instead.' - for line in filter(lambda action: action != "", action.split('\n')): + for line in filter(None, action.split('\n')): line = line.strip() uid=ident[0] model=ident[1] From 72f59ff45e937fa24895069c0b68c6ad9cfbe228 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 25 Oct 2012 15:01:11 +0200 Subject: [PATCH 020/138] [FIX] modules: avoid to have the docutils' warnings rendered inline. The html rendering of the __openerp__.py description is done using docutils. This patch removes the warnings that docutils inserts in the resulting document and log them. Docutils still generates spurious output on stderr though. (An alternative is to use the `report_level` option but in that case it seems we can never witness the warnings being created.) bzr revid: vmt@openerp.com-20121025130111-7l157hexh6ew9w34 --- openerp/addons/base/module/module.py | 31 +++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 3484b40cee9..42b57b7c661 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -20,7 +20,10 @@ ############################################################################## import base64 +from docutils import io, nodes from docutils.core import publish_string +from docutils.transforms import Transform, writer_aux +from docutils.writers.html4css1 import Writer import imp import logging import re @@ -80,6 +83,32 @@ class module_category(osv.osv): 'visible' : 1, } +class MyFilterMessages(Transform): + """ + Custom docutils transform to remove `system message` for a document and + generate warnings. + + (The standard filter removes them based on some `report_level` passed in + the `settings_override` dictionary, but if we use it, we can't see them + and generate warnings.) + """ + + default_priority = 870 + + def apply(self): + for node in self.document.traverse(nodes.system_message): + _logger.warning("docutils' system message present: %s", str(node)) + node.parent.remove(node) + +class MyWriter(Writer): + """ + Custom docutils html4ccs1 writer that doesn't add the warnings to the + output document. + """ + + def get_transforms(self): + return [MyFilterMessages, writer_aux.Admonitions] + class module(osv.osv): _name = "ir.module.module" _rec_name = "shortdesc" @@ -100,7 +129,7 @@ class module(osv.osv): res = dict.fromkeys(ids, '') for module in self.browse(cr, uid, ids, context=context): overrides = dict(embed_stylesheet=False, doctitle_xform=False, output_encoding='unicode') - output = publish_string(source=module.description, writer_name='html', settings_overrides=overrides) + output = publish_string(source=module.description, settings_overrides=overrides, writer=MyWriter()) res[module.id] = output return res From 49dd95bf51ac8c6434bd42ea8620723b90cfe3c9 Mon Sep 17 00:00:00 2001 From: "ajay javiya (OpenERP)" Date: Fri, 2 Nov 2012 16:50:02 +0530 Subject: [PATCH 021/138] [FIX]: Password field should not allows to setdefault bzr revid: aja@tinyerp.com-20121102112002-awvp6juu2mvdndz2 --- addons/web/static/src/js/view_form.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index e0727b99259..4e6a8bcf437 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -990,7 +990,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM || field.get("readonly") || field.field.type === 'one2many' || field.field.type === 'many2many' - || field.field.type === 'binary') { + || field.field.type === 'binary' + || field.password) { return false; } From d6ba13a3945b5829972031c49a6ab623742e040d Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Fri, 16 Nov 2012 08:53:05 +0100 Subject: [PATCH 022/138] [IMP] Minor improvements in portal views, such as contact and employees. Improved menus and string on empty views. Send the password when a partner is invited to portal via sidebar menu. bzr revid: vta@openerp.com-20121116075305-4d0olu5zlvwsxeq8 --- addons/portal/portal_view.xml | 29 +++++--- addons/portal/wizard/portal_wizard.py | 5 +- addons/portal_crm/wizard/contact.py | 9 ++- addons/portal_crm/wizard/contact_view.xml | 60 +++++++--------- addons/portal_event/portal_event_view.xml | 4 +- .../portal_hr_employees/hr_employee_view.xml | 69 +++++++++++-------- .../portal_project_issue_view.xml | 4 +- addons/portal_sale/portal_sale_view.xml | 31 +++------ 8 files changed, 105 insertions(+), 106 deletions(-) diff --git a/addons/portal/portal_view.xml b/addons/portal/portal_view.xml index 5b39ea89525..45fa2a4dacb 100644 --- a/addons/portal/portal_view.xml +++ b/addons/portal/portal_view.xml @@ -8,18 +8,18 @@ groups="base.group_no_one,portal.group_portal" sequence="20"/> - - - + + + + - - - - - + + + + @@ -47,5 +47,12 @@ + + + + diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index bf8353c92c1..545b771e762 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -39,6 +39,7 @@ You have been given access to %(portal)s. Your login account data is: Database: %(db)s Username: %(login)s +Password: %(password)s In order to complete the signin process, click on the following url: %(url)s @@ -210,6 +211,7 @@ class wizard_user(osv.osv_memory): 'name': user.name, 'login': user.login, 'url': user.signup_url, + 'password': user.password, } mail_mail = self.pool.get('mail.mail') mail_values = { @@ -219,6 +221,7 @@ class wizard_user(osv.osv_memory): 'body_html': '
%s
' % (_(WELCOME_EMAIL_BODY) % data), 'state': 'outgoing', } - return mail_mail.create(cr, uid, mail_values, context=this_context) + mail_id = mail_mail.create(cr, uid, mail_values, context=this_context) + return mail_mail.send(cr, uid, [mail_id], context=this_context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/portal_crm/wizard/contact.py b/addons/portal_crm/wizard/contact.py index a0f0d952a66..65de0c96226 100644 --- a/addons/portal_crm/wizard/contact.py +++ b/addons/portal_crm/wizard/contact.py @@ -28,6 +28,8 @@ class crm_contact_us(osv.TransientModel): _description = 'Contact form for the portal' _inherit = 'crm.lead' _columns = { + 'subject' : fields.char('Subject', required=True), + 'body' : fields.text('Content', required=True), 'company_ids' : fields.many2many('res.company', string='Companies', readonly=True), } @@ -95,7 +97,10 @@ class crm_contact_us(osv.TransientModel): it is quite complicated to set proper rights for this object. Therefore, user SUPERUSER_ID will perform the creation. """ - values['contact_name'] = values['name'] + print values + values['name'] = values['subject'] + values['contact_name'] = values['partner_name'] + values['description'] = values['body'] crm_lead.create(cr, SUPERUSER_ID, dict(values,user_id=False), context) """ @@ -114,7 +119,7 @@ class crm_contact_us(osv.TransientModel): 'res_model': self._name, 'res_id': ids[0], 'view_id': self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal_crm', 'wizard_contact_form_view_thanks')[1], - 'target': 'inline', + 'target': 'new', } def _needaction_domain_get(self, cr, uid, context=None): diff --git a/addons/portal_crm/wizard/contact_view.xml b/addons/portal_crm/wizard/contact_view.xml index 778e7f9562e..70f87fe1ff8 100644 --- a/addons/portal_crm/wizard/contact_view.xml +++ b/addons/portal_crm/wizard/contact_view.xml @@ -2,31 +2,25 @@ - - + Wizard form view portal_crm.crm_contact_us
-
+

Contact us

- - - - - - - - -
-
- - - - - - + + + + + + + +
-
-
- + + + @@ -59,18 +53,13 @@ -
-
-
- -
-
-
+ + + + +
@@ -88,15 +77,14 @@ otherwise the orm will try to select all the model's records and this will result in a permission denied error --> - +
- - - Contact + + Contact Us portal_crm.crm_contact_us form inline @@ -104,7 +92,7 @@ + parent="portal.portal_company" action="action_contact_us" sequence="40"/>
diff --git a/addons/portal_event/portal_event_view.xml b/addons/portal_event/portal_event_view.xml index 5d2397f6148..0a0f59d7857 100755 --- a/addons/portal_event/portal_event_view.xml +++ b/addons/portal_event/portal_event_view.xml @@ -10,14 +10,14 @@ Events ir.actions.act_window event.event - kanban,calendar,tree,form,graph + kanban,calendar,tree,form {"search_default_upcoming":1} There are no public events. + action="action_event_view" sequence="30"/> diff --git a/addons/portal_hr_employees/hr_employee_view.xml b/addons/portal_hr_employees/hr_employee_view.xml index 4e133061da6..6af4abb7b7f 100644 --- a/addons/portal_hr_employees/hr_employee_view.xml +++ b/addons/portal_hr_employees/hr_employee_view.xml @@ -1,6 +1,6 @@ - + @@ -15,42 +15,51 @@ - - portal_hr_employees.employees_list + + portal_hr_employees_view portal_crm.crm_contact_us - - -

Meet the team

+
- - - - - -
-
- -
-
-

- () -

-
    -
  • -
  • Tel:
  • -
  • Mobile:
  • -
  • -
-
+ + + + +
+
+
- - - +
+

+ () +

+
    +
  • +
  • Tel:
  • +
  • Mobile:
  • +
  • +
+
+
+
+
+
- + + + Meet the team + portal_crm.crm_contact_us + + form + form + Here you can see our employees' public profile, if any + + + + diff --git a/addons/portal_project_issue/portal_project_issue_view.xml b/addons/portal_project_issue/portal_project_issue_view.xml index 6502d92b4dc..5a88e54a00b 100644 --- a/addons/portal_project_issue/portal_project_issue_view.xml +++ b/addons/portal_project_issue/portal_project_issue_view.xml @@ -75,8 +75,8 @@

Click to create an issue.

- OpenERP's kanban view will help you track easily your current - pipeline of issues to fix. + You can track your claims from this menu and the action we + will take.

diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index b46175f7feb..216cc828835 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -13,7 +13,7 @@ tree,form,calendar,graph {"search_default_draft":1} - You don't have any quotation. + We haven't sent you any quotation. @@ -23,7 +23,7 @@ tree,form,calendar,graph {"search_default_sales":1} - You don't have any sale order. + We haven't sent you any sale order. @@ -34,18 +34,7 @@ [('type','=','out')] {'default_type': 'out', 'contact_display': 'partner_address'} - You don't have any delivery order. - - - - Products - ir.actions.act_window - product.product - form - kanban,tree,form - - - There are no public products. + We haven't sent you any delivery order. @@ -55,7 +44,7 @@ [('type','=','out_invoice')] {'default_type':'out_invoice', 'type':'out_invoice', 'journal_type': 'sale'} - You don't have any invoice. + We haven't sent you any invoice. @@ -65,7 +54,7 @@ {'type':'receipt'} current - You don't have any refunds or payments. + We haven't sent you any credit note. - - - + + From e92a31e2bb365a1453d555bbf8d18069ed94299a Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Fri, 16 Nov 2012 08:57:44 +0100 Subject: [PATCH 023/138] [IMP] Now portal users can subscribe to public content. bzr revid: vta@openerp.com-20121116075744-tclpsjxi1ut6wvh8 --- addons/mail/mail_thread.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index ee1a015396b..4f7871ca961 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -796,11 +796,12 @@ class mail_thread(osv.AbstractModel): def message_subscribe(self, cr, uid, ids, partner_ids, subtype_ids=None, context=None): """ Add partners to the records followers. """ - self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) + self.check_access_rights(cr, uid, 'read') + self.write(cr, SUPERUSER_ID, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context) # if subtypes are not specified (and not set to a void list), fetch default ones if subtype_ids is None: subtype_obj = self.pool.get('mail.message.subtype') - subtype_ids = subtype_obj.search(cr, uid, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) + subtype_ids = subtype_obj.search(cr, SUPERUSER_ID, [('default', '=', True), '|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context) # update the subscriptions fol_obj = self.pool.get('mail.followers') fol_ids = fol_obj.search(cr, SUPERUSER_ID, [('res_model', '=', self._name), ('res_id', 'in', ids), ('partner_id', 'in', partner_ids)], context=context) From ed3c46aa6f973f65d6df0db295ec529789e927e4 Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Fri, 16 Nov 2012 11:30:48 +0100 Subject: [PATCH 024/138] [FIX] Move required to the view. bzr revid: vta@openerp.com-20121116103048-0ervln9ekqly89qx --- addons/portal_crm/wizard/contact.py | 4 ++-- addons/portal_crm/wizard/contact_view.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/portal_crm/wizard/contact.py b/addons/portal_crm/wizard/contact.py index 65de0c96226..09605257a5a 100644 --- a/addons/portal_crm/wizard/contact.py +++ b/addons/portal_crm/wizard/contact.py @@ -28,8 +28,8 @@ class crm_contact_us(osv.TransientModel): _description = 'Contact form for the portal' _inherit = 'crm.lead' _columns = { - 'subject' : fields.char('Subject', required=True), - 'body' : fields.text('Content', required=True), + 'subject' : fields.char('Subject'), + 'body' : fields.text('Content'), 'company_ids' : fields.many2many('res.company', string='Companies', readonly=True), } diff --git a/addons/portal_crm/wizard/contact_view.xml b/addons/portal_crm/wizard/contact_view.xml index 70f87fe1ff8..dce8e18a69b 100644 --- a/addons/portal_crm/wizard/contact_view.xml +++ b/addons/portal_crm/wizard/contact_view.xml @@ -15,8 +15,8 @@ - - + + -
-
- -
+ +
+ +
+
+ +
+
+
+

Followers

+ Invite others +
+
-
-

Followers

- Invite others -
-
- portal_hr_employees.employee.form + portal_hr_employees_form hr.employee @@ -17,7 +17,7 @@ portal_hr_employees_view - portal_crm.crm_contact_us + crm.team @@ -51,11 +51,11 @@ Meet the team - portal_crm.crm_contact_us + crm.team - form form - Here you can see our employees' public profile, if any + inline + Here you can see our employees' public profile, if any. Date: Tue, 20 Nov 2012 14:08:24 +0100 Subject: [PATCH 035/138] [IMP] General improvements to issues and claims. bzr revid: vta@openerp.com-20121120130824-zmx5yhaalwf3potd --- addons/base_status/base_stage.py | 6 +- addons/crm_claim/crm_claim_view.xml | 12 +-- addons/portal_claim/portal_claim_view.xml | 74 ++++++++++++++++++- .../portal_claim/security/ir.model.access.csv | 2 +- .../portal_claim/security/portal_security.xml | 2 +- addons/portal_project_issue/__init__.py | 1 - .../portal_project_issue_view.xml | 63 ++++++++-------- .../security/ir.model.access.csv | 2 +- .../portal_sale/security/ir.model.access.csv | 1 + addons/project/project_view.xml | 2 +- addons/project_issue/project_issue_view.xml | 14 ++-- 11 files changed, 122 insertions(+), 57 deletions(-) diff --git a/addons/base_status/base_stage.py b/addons/base_status/base_stage.py index 867b2207a99..98b32402788 100644 --- a/addons/base_status/base_stage.py +++ b/addons/base_status/base_stage.py @@ -41,9 +41,11 @@ class base_stage(object): """ if context is None: context = {} - if not context or not context.get('portal'): + if not context: return False user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + if context.get('portal'): + return user.partner_id.id if hasattr(user, 'partner_address_id') and user.partner_address_id: return user.partner_address_id return user.company_id.partner_id.id @@ -65,7 +67,7 @@ class base_stage(object): """ if context is None: context = {} - if not context or not context.get('portal'): + if not context or context.get('portal'): return False return uid diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml index 85507a32c0f..f56ee73f908 100644 --- a/addons/crm_claim/crm_claim_view.xml +++ b/addons/crm_claim/crm_claim_view.xml @@ -104,7 +104,7 @@
@@ -113,9 +113,9 @@ - + - + @@ -130,17 +130,17 @@ - + - + - + diff --git a/addons/portal_claim/portal_claim_view.xml b/addons/portal_claim/portal_claim_view.xml index e08b39e476b..0e3a2006bc3 100644 --- a/addons/portal_claim/portal_claim_view.xml +++ b/addons/portal_claim/portal_claim_view.xml @@ -1,14 +1,80 @@ - + + + + CRM Claim Kanban + crm.claim + 20 + + + + + + + + + + + +
+
+
+

+
+
+

+
+ +
+
+
+ Creation: + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+
+
+
+ + + + + Claims crm.claim form - tree,calendar,form - - {"search_default_user_id":'', "stage_type":'claim'} + tree,form,kanban,calendar + + {"search_default_user_id":'', "stage_type":'claim', "portal":'True'}

diff --git a/addons/portal_claim/security/ir.model.access.csv b/addons/portal_claim/security/ir.model.access.csv index ab2a2f66770..4fcfd0f947f 100644 --- a/addons/portal_claim/security/ir.model.access.csv +++ b/addons/portal_claim/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_crm_claim,crm.claim,crm_claim.model_crm_claim,portal.group_portal,1,0,0,0 +access_crm_claim,crm.claim,crm_claim.model_crm_claim,portal.group_portal,1,0,1,0 access_crm_claim_stage,crm.claim.stage,crm_claim.model_crm_claim_stage,portal.group_portal,1,0,0,0 diff --git a/addons/portal_claim/security/portal_security.xml b/addons/portal_claim/security/portal_security.xml index 3742765f795..7227e6511ac 100644 --- a/addons/portal_claim/security/portal_security.xml +++ b/addons/portal_claim/security/portal_security.xml @@ -10,7 +10,7 @@ - + diff --git a/addons/portal_project_issue/__init__.py b/addons/portal_project_issue/__init__.py index 26c654db9dd..45de899ed49 100644 --- a/addons/portal_project_issue/__init__.py +++ b/addons/portal_project_issue/__init__.py @@ -18,4 +18,3 @@ # along with this program. If not, see . # ############################################################################## - diff --git a/addons/portal_project_issue/portal_project_issue_view.xml b/addons/portal_project_issue/portal_project_issue_view.xml index 76ffbe3d1fd..9054445c8ad 100644 --- a/addons/portal_project_issue/portal_project_issue_view.xml +++ b/addons/portal_project_issue/portal_project_issue_view.xml @@ -20,42 +20,39 @@

-
- -
-
+
From e9e80c80e8b0512a774cd12de991a5f00d2aef78 Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Wed, 21 Nov 2012 08:47:58 +0100 Subject: [PATCH 039/138] [FIX] Hide message_followers widget in claims. bzr revid: vta@openerp.com-20121121074758-z0f7gy8jdlqqnv8f --- addons/crm_claim/crm_claim_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml index f56ee73f908..8b6217b97e1 100644 --- a/addons/crm_claim/crm_claim_view.xml +++ b/addons/crm_claim/crm_claim_view.xml @@ -167,7 +167,7 @@
- +
From 6dd29bcdff247fdb219861b40e575fae392878da Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Wed, 21 Nov 2012 10:05:21 +0100 Subject: [PATCH 040/138] [FIX] Fixed names and view modes. bzr revid: vta@openerp.com-20121121090521-z2a0e4mqo5k5ttl4 --- addons/portal_claim/security/portal_security.xml | 2 +- addons/portal_sale/portal_sale_view.xml | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/addons/portal_claim/security/portal_security.xml b/addons/portal_claim/security/portal_security.xml index 7227e6511ac..6a04ca9747e 100644 --- a/addons/portal_claim/security/portal_security.xml +++ b/addons/portal_claim/security/portal_security.xml @@ -2,7 +2,7 @@ - + Portal Personal Claims [('message_follower_ids','in', [user.partner_id.id])] diff --git a/addons/portal_sale/portal_sale_view.xml b/addons/portal_sale/portal_sale_view.xml index 4c93d03f43e..e989282a1a8 100644 --- a/addons/portal_sale/portal_sale_view.xml +++ b/addons/portal_sale/portal_sale_view.xml @@ -10,7 +10,7 @@ Quotations ir.actions.act_window sale.order - tree,form,calendar,graph + tree,form {"search_default_draft":1} We haven't sent you any quotation. @@ -20,7 +20,7 @@ Sales Orders ir.actions.act_window sale.order - tree,form,calendar,graph + tree,form {"search_default_sales":1} We haven't sent you any sale order. @@ -29,16 +29,17 @@ Customer Invoices account.invoice - tree,form,calendar,graph + tree,form [('type','=','out_invoice')] {'type':'out_invoice', 'journal_type': 'sale'} We haven't sent you any invoice. - + Customer Refunds account.invoice + tree,form [('type','=','out_refund')] {'type':'out_refund'} @@ -52,7 +53,7 @@ + action="action_invoice_tree2" sequence="40"/> From 132f052e178f53d2e7cffefebf7a3f8ec7196208 Mon Sep 17 00:00:00 2001 From: "vta vta@openerp.com" <> Date: Wed, 21 Nov 2012 13:33:46 +0100 Subject: [PATCH 041/138] [FIX] Security rules, and conflicts after merging trunk. bzr revid: vta@openerp.com-20121121123346-5wi4ko1e5ny57ki0 --- addons/mail/static/src/xml/mail_followers.xml | 34 +++++++++---------- addons/portal/portal_view.xml | 4 --- .../portal_claim/security/portal_security.xml | 2 +- .../security/portal_security.xml | 2 +- addons/portal_sale/portal_sale_view.xml | 6 ++-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/addons/mail/static/src/xml/mail_followers.xml b/addons/mail/static/src/xml/mail_followers.xml index 5fde087a660..11698c1fb62 100644 --- a/addons/mail/static/src/xml/mail_followers.xml +++ b/addons/mail/static/src/xml/mail_followers.xml @@ -5,25 +5,23 @@ followers main template Template used to display the followers, the actions and the subtypes in a record. --> -
- -
- -
-
- -
-
-
-

Followers

- Invite others -
-
+
+
+ +
+
+ +
+
+

Followers

+ Invite others +
+
- +