diff --git a/trytond/ir/__init__.py b/trytond/ir/__init__.py
index a2e2c67574..ff40f639a9 100644
--- a/trytond/ir/__init__.py
+++ b/trytond/ir/__init__.py
@@ -9,3 +9,4 @@ from translation import *
from exports import *
from rule import *
from property import *
+import module
diff --git a/trytond/ir/__terp__.py b/trytond/ir/__terp__.py
index 758cce8faa..2a93791783 100644
--- a/trytond/ir/__terp__.py
+++ b/trytond/ir/__terp__.py
@@ -16,5 +16,8 @@
'exports.xml',
'rule.xml',
'property.xml',
+ 'module/module.xml',
+ 'module/repository.xml',
+ 'module/category.xml',
],
}
diff --git a/trytond/ir/actions.py b/trytond/ir/actions.py
index bde6083c83..49323cace6 100644
--- a/trytond/ir/actions.py
+++ b/trytond/ir/actions.py
@@ -42,8 +42,7 @@ class ActionKeyword(OSV):
('form_action', 'Action form'),
('form_relate', 'Form relate'),
], string='Keyword', required=True),
- 'model': fields.Reference('Model', selection=_models_get, size=128,
- required=True),
+ 'model': fields.Reference('Model', selection=_models_get, size=128),
'action': fields.many2one('ir.actions.actions', 'Action',
ondelete='CASCADE'),
}
diff --git a/trytond/ir/module/__init__.py b/trytond/ir/module/__init__.py
new file mode 100644
index 0000000000..e8d8328283
--- /dev/null
+++ b/trytond/ir/module/__init__.py
@@ -0,0 +1,3 @@
+from category import *
+from repository import *
+from module import *
diff --git a/trytond/ir/module/category.py b/trytond/ir/module/category.py
new file mode 100644
index 0000000000..83cffcd271
--- /dev/null
+++ b/trytond/ir/module/category.py
@@ -0,0 +1,18 @@
+"Category"
+from trytond.osv import fields, OSV
+
+
+class Category(OSV):
+ "Module Category"
+ _name = "ir.module.category"
+ _description = __doc__
+ _columns = {
+ 'name': fields.Char("Name", size=128, required=True),
+ 'parent_id': fields.Many2One('ir.module.category',
+ 'Parent Category', select=1),
+ 'child_ids': fields.One2Many('ir.module.category',
+ 'parent_id', 'Parent Category'),
+ }
+ _order = 'name'
+
+Category()
diff --git a/trytond/ir/module/category.xml b/trytond/ir/module/category.xml
new file mode 100644
index 0000000000..d882908cc1
--- /dev/null
+++ b/trytond/ir/module/category.xml
@@ -0,0 +1,43 @@
+
+
+
+
+ ir.module.category.form
+ ir.module.category
+ form
+
+
+
+
+
+ ir.module.category.tree
+ ir.module.category
+ tree
+ child_ids
+
+
+
+
+
+
+
+ Categories
+ ir.actions.act_window
+ ir.module.category
+ tree
+ [('parent_id', '=', False)]
+
+
+
+ tree
+
+
+
+
+
+
diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py
new file mode 100644
index 0000000000..6c6ec5946d
--- /dev/null
+++ b/trytond/ir/module/module.py
@@ -0,0 +1,625 @@
+"Module"
+import os
+import re
+import zipfile
+import urllib
+from trytond.osv import fields, OSV
+import trytond.tools as tools
+from trytond.module import ADDONS_PATH
+from trytond.osv.orm import ExceptORM
+from trytond.wizard import Wizard
+
+VER_REGEXP = re.compile(
+ "^(\\d+)((\\.\\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?$")
+SUFFIX_VALUE = {"pre": -2, "p": 0, "alpha": -4, "beta": -3, "rc": -1}
+SUFFIX_REGEXP = re.compile("^(alpha|beta|rc|pre|p)(\\d*)$")
+
+
+def vercmp(ver1, ver2):
+ """
+ Compare two versions
+ Take from portage_versions.py
+ @param ver1: version to compare with
+ @type ver1: string (example "1.2-r3")
+ @param ver2: version to compare again
+ @type ver2: string (example "2.1-revision1")
+ @rtype: None or float
+ @return:
+ 1. position if ver1 is greater than ver2
+ 2. negative if ver1 is less than ver2
+ 3. 0 if ver1 equals ver2
+ 4. None if ver1 or ver2 are invalid
+ """
+
+ match1 = VER_REGEXP.match(ver1)
+ match2 = VER_REGEXP.match(ver2)
+
+ if not match1 or not match1.groups():
+ return None
+ if not match2 or not match2.groups():
+ return None
+
+ list1 = [int(match1.group(1))]
+ list2 = [int(match2.group(1))]
+
+ if len(match1.group(2)) or len(match2.group(2)):
+ vlist1 = match1.group(2)[1:].split(".")
+ vlist2 = match2.group(2)[1:].split(".")
+ for i in range(0, max(len(vlist1), len(vlist2))):
+ # Implicit .0 is given -1, so 1.0.0 > 1.0
+ # would be ambiguous if two versions that aren't literally equal
+ # are given the same value (in sorting, for example).
+ if len(vlist1) <= i or len(vlist1[i]) == 0:
+ list1.append(-1)
+ list2.append(int(vlist2[i]))
+ elif len(vlist2) <= i or len(vlist2[i]) == 0:
+ list1.append(int(vlist1[i]))
+ list2.append(-1)
+ # Let's make life easy and use integers
+ # unless we're forced to use floats
+ elif (vlist1[i][0] != "0" and vlist2[i][0] != "0"):
+ list1.append(int(vlist1[i]))
+ list2.append(int(vlist2[i]))
+ # now we have to use floats so 1.02 compares correctly against 1.1
+ else:
+ list1.append(float("0."+vlist1[i]))
+ list2.append(float("0."+vlist2[i]))
+ # and now the final letter
+ if len(match1.group(4)):
+ list1.append(ord(match1.group(4)))
+ if len(match2.group(4)):
+ list2.append(ord(match2.group(4)))
+
+ for i in range(0, max(len(list1), len(list2))):
+ if len(list1) <= i:
+ return -1
+ elif len(list2) <= i:
+ return 1
+ elif list1[i] != list2[i]:
+ return list1[i] - list2[i]
+
+ # main version is equal, so now compare the _suffix part
+ list1 = match1.group(5).split("_")[1:]
+ list2 = match2.group(5).split("_")[1:]
+
+ for i in range(0, max(len(list1), len(list2))):
+ # Implicit _p0 is given a value of -1, so that 1 < 1_p0
+ if len(list1) <= i:
+ suffix1 = ("p","-1")
+ else:
+ suffix1 = SUFFIX_REGEXP.match(list1[i]).groups()
+ if len(list2) <= i:
+ suffix2 = ("p","-1")
+ else:
+ suffix2 = SUFFIX_REGEXP.match(list2[i]).groups()
+ if suffix1[0] != suffix2[0]:
+ return SUFFIX_VALUE[suffix1[0]] - SUFFIX_VALUE[suffix2[0]]
+ if suffix1[1] != suffix2[1]:
+ # it's possible that the s(1|2)[1] == ''
+ # in such a case, fudge it.
+ try:
+ revision1 = int(suffix1[1])
+ except ValueError:
+ revision1 = 0
+ try:
+ revision2 = int(suffix2[1])
+ except ValueError:
+ revision2 = 0
+ if revision1 - revision2:
+ return revision1 - revision2
+
+ # the suffix part is equal to, so finally check the revision
+ if match1.group(9):
+ revision1 = int(match1.group(9))
+ else:
+ revision1 = 0
+ if match2.group(9):
+ revision2 = int(match2.group(9))
+ else:
+ revision2 = 0
+ return revision1 - revision2
+
+
+class Module(OSV):
+ "Module"
+ _name = "ir.module.module"
+ _description = __doc__
+
+ @staticmethod
+ def get_module_info(name):
+ "Return the content of the __terp__.py"
+ try:
+ file_p = tools.file_open(os.path.join(ADDONS_PATH, name,
+ '__terp__.py'))
+ data = file_p.read()
+ info = eval(data)
+ file_p.close()
+ except:
+ return {}
+ return info
+
+ def _get_installed_version(self, cursor, user, ids, name, arg,
+ context=None):
+ res = {}
+ for module in self.browse(cursor, user, ids, context=context):
+ if module.state in ('installed', 'to upgrade', 'to remove'):
+ res[module.id] = Module.get_module_info(
+ module.name).get('version', '')
+ else:
+ res[module.id] = ''
+ return res
+
+ _columns = {
+ 'name': fields.char("Name", size=128, readonly=True, required=True),
+ 'category_id': fields.many2one('ir.module.category', 'Category',
+ readonly=True),
+ 'shortdesc': fields.char('Short description', size=256, readonly=True),
+ 'description': fields.text("Description", readonly=True),
+ 'author': fields.char("Author", size=128, readonly=True),
+ 'website': fields.char("Website", size=256, readonly=True),
+ 'installed_version': fields.function(_get_installed_version,
+ method=True, string='Installed version', type='char'),
+ 'latest_version': fields.char('Latest version', size=64, readonly=True),
+ 'url': fields.char('URL', size=128),
+ 'dependencies_id': fields.one2many('ir.module.module.dependency',
+ 'module_id', 'Dependencies', readonly=True),
+ 'state': fields.selection([
+ ('uninstallable','Not Installable'),
+ ('uninstalled','Not Installed'),
+ ('installed','Installed'),
+ ('to upgrade','To be upgraded'),
+ ('to remove','To be removed'),
+ ('to install','To be installed')
+ ], string='State', readonly=True),
+ 'demo': fields.boolean('Demo data'),
+ 'license': fields.selection([('GPL-2', 'GPL-2'),
+ ('Other proprietary', 'Other proprietary')], string='License',
+ readonly=True),
+ }
+
+ _defaults = {
+ 'state': lambda *a: 'uninstalled',
+ 'demo': lambda *a: False,
+ 'license': lambda *a: 'GPL-2',
+ }
+ _order = 'name'
+
+ _sql_constraints = [
+ ('name_uniq', 'unique (name)', 'The name of the module must be unique!')
+ ]
+
+ def unlink(self, cursor, user, ids, context=None):
+ if not ids:
+ return True
+ if isinstance(ids, (int, long)):
+ ids = [ids]
+ for module in self.browse(cursor, user, ids, context=context):
+ if module.state in (
+ 'installed',
+ 'to upgrade',
+ 'to remove',
+ 'to install',
+ ):
+ raise ExceptORM('Error',
+ 'You try to remove a module that is installed ' \
+ 'or will be installed')
+ return super(Module, self).unlink(cursor, user, ids, context=context)
+
+ #TODO: remove the level check recursion
+ def state_change(self, cursor, user, ids, newstate, context=None, level=50):
+ if level < 1:
+ raise Exception, 'Recursion error in modules dependencies!'
+ demo = True
+ for module in self.browse(cursor, user, ids, context=context):
+ mdemo = True
+ for dep in module.dependencies_id:
+ ids2 = self.search(cursor, user, [('name', '=', dep.name)],
+ context=context)
+ mdemo = self.state_change(cursor, user, ids2, newstate,
+ context, level-1,) \
+ and mdemo
+ if not module.dependencies_id:
+ mdemo = module.demo
+ if module.state == 'uninstalled':
+ self.write(cursor, user, [module.id], {
+ 'state': newstate,
+ 'demo': mdemo,
+ }, context=context)
+ demo = demo and mdemo
+ return demo
+
+ def state_upgrade(self, cursor, user, ids, newstate, context=None,
+ level=50):
+ dependency_obj = self.pool.get('ir.module.module.dependency')
+ if level < 1:
+ raise Exception, 'Recursion error in modules dependencies!'
+ for module in self.browse(cursor, user, ids):
+ dep_ids = dependency_obj.search(cursor, user, [
+ ('name', '=', module.name),
+ ], context=context)
+ if dep_ids:
+ ids2 = []
+ for dep in dependency_obj.browse(cursor, user, dep_ids,
+ context=context):
+ if dep.module_id.state != 'to upgrade':
+ ids2.append(dep.module_id.id)
+ self.state_upgrade(cursor, user, ids2, newstate, context, level)
+ if module.state == 'installed':
+ self.write(cursor, user, module.id, {
+ 'state': newstate,
+ }, context=context)
+ return True
+
+ def button_install(self, cursor, user, ids, context=None):
+ return self.state_change(cursor, user, ids, 'to install',
+ context=context)
+
+ def button_install_cancel(self, cursor, user, ids, context=None):
+ self.write(cursor, user, ids, {
+ 'state': 'uninstalled',
+ 'demo': False,
+ }, context=context)
+ return True
+
+ def button_uninstall(self, cursor, user, ids, context=None):
+ for module in self.browse(cursor, user, ids, context=context):
+ cursor.execute('SELECT m.state, m.name ' \
+ 'FROM ir_module_module_dependency d ' \
+ 'JOIN ir_module_module m on (d.module_id=m.id) ' \
+ 'WHERE d.name = %s ' \
+ 'AND m.state not in ' \
+ '(\'uninstalled\',\'uninstallable\',\'to remove\')',
+ (module.name,))
+ res = cursor.fetchall()
+ if res:
+ raise ExceptORM('Error',
+ 'The modules you are trying to remove ' \
+ 'depends on installed modules :\n' + \
+ '\n'.join(['\t%s: %s' % (x[0], x[1]) for x in res]))
+ self.write(cursor, user, ids, {'state': 'to remove'})
+ return True
+
+ def button_uninstall_cancel(self, cursor, user, ids, context=None):
+ self.write(cursor, user, ids, {'state': 'installed'}, context=context)
+ return True
+
+ def button_upgrade(self, cursor, user, ids, context=None):
+ return self.state_upgrade(cursor, user, ids, 'to upgrade', context)
+
+ def button_upgrade_cancel(self, cursor, user, ids, context=None):
+ self.write(cursor, user, ids, {'state': 'installed'}, context=context)
+ return True
+
+ def button_update_translations(self, cursor, user, ids, context=None):
+ lang_obj = self.pool.get('res.lang')
+ lang_ids = lang_obj.search(cursor, user, [('translatable', '=', True)],
+ context=context)
+ langs = lang_obj.browse(cursor, user, lang_ids, context=context)
+ for module in self.browse(cursor, user, ids, context=context):
+ files = Module.get_module_info(module.name).get('translations', {})
+ for lang in langs:
+ if files.has_key(lang.code):
+ filepath = os.path.join(ADDONS_PATH, module.name,
+ files[lang.code])
+ tools.trans_load(filepath, lang.code)
+ return True
+
+ # update the list of available packages
+ def update_list(self, cursor, user, context=None):
+ robj = self.pool.get('ir.module.repository')
+ res = [0, 0] # [update, add]
+
+ # iterate through installed modules and mark them as being so
+ for name in os.listdir(ADDONS_PATH):
+ mod_name = name
+ if name[-4:] == '.zip':
+ mod_name = name[:-4]
+ ids = self.search(cursor, user, [('name', '=', mod_name)],
+ context=context)
+ if ids:
+ module_id = ids[0]
+ mod = self.browse(cursor, user, module_id)
+ terp = Module.get_module_info(mod_name)
+ if terp.get('installable', True) \
+ and mod.state == 'uninstallable':
+ self.write(cursor, user, module_id, {
+ 'state': 'uninstalled'}, context=context)
+ if vercmp(terp.get('version', ''),
+ mod.latest_version or '0') > 0:
+ self.write(cursor, user, module_id, {
+ 'latest_version': terp.get('version'),
+ 'url': '',
+ }, context=context)
+ res[0] += 1
+ self.write(cursor, user, module_id, {
+ 'description': terp.get('description', ''),
+ 'shortdesc': terp.get('name', ''),
+ 'author': terp.get('author', ''),
+ 'website': terp.get('website', ''),
+ 'license': terp.get('license', 'GPL-2'),
+ })
+ self._update_dependencies(cursor, user, module_id,
+ terp.get('depends', []))
+ self._update_category(cursor, user, module_id,
+ terp.get('category', 'None'))
+ continue
+ mod_path = os.path.join(ADDONS_PATH, name)
+ if os.path.isdir(mod_path) \
+ or os.path.islink(mod_path) \
+ or zipfile.is_zipfile(mod_path):
+ terp = Module.get_module_info(mod_name)
+ if not terp or not terp.get('installable', True):
+ continue
+ if not os.path.isfile(
+ os.path.join(ADDONS_PATH, mod_name+'.zip')):
+ import imp
+ # XXX must restrict to only addons paths
+ imp.load_module(name, *imp.find_module(mod_name))
+ else:
+ import zipimport
+ mod_path = os.path.join(ADDONS_PATH, mod_name+'.zip')
+ zimp = zipimport.zipimporter(mod_path)
+ zimp.load_module(mod_name)
+ new_id = self.create(cursor, user, {
+ 'name': mod_name,
+ 'state': 'uninstalled',
+ 'description': terp.get('description', ''),
+ 'shortdesc': terp.get('name', ''),
+ 'author': terp.get('author', 'Unknown'),
+ 'website': terp.get('website', ''),
+ 'latest_version': terp.get('version', ''),
+ 'license': terp.get('license', 'GPL-2'),
+ })
+ res[1] += 1
+ self._update_dependencies(cursor, user, new_id,
+ terp.get('depends', []))
+ self._update_category(cursor, user, new_id,
+ terp.get('category', 'None'))
+
+ import socket
+ socket.setdefaulttimeout(10)
+ for repository in robj.browse(cursor, user,
+ robj.search(cursor, user, []), context=context):
+ try:
+ index_page = urllib.urlopen(repository.url).read()
+ except IOError, exception:
+ if exception.errno == 21:
+ raise ExceptORM('Error',
+ 'This url \'%s\' must provide an html file '
+ 'with links to zip modules' % (repository.url))
+ else:
+ raise
+ modules = re.findall(repository.filter, index_page, re.I+re.M)
+ mod_sort = {}
+ for module in modules:
+ name = module[0]
+ version = module[1]
+ extension = module[-1]
+ if name in mod_sort:
+ if vercmp(version, mod_sort[name][0]) <= 0:
+ continue
+ mod_sort[name] = [version, extension]
+ for name in mod_sort.keys():
+ version, extension = mod_sort[name]
+ url = repository.url+'/'+name+'-'+version+extension
+ ids = self.search(cursor, user, [('name', '=', name)],
+ context=context)
+ if not ids:
+ self.create(cursor, user, {
+ 'name': name,
+ 'latest_version': version,
+ 'url': url,
+ 'state': 'uninstalled',
+ }, context=context)
+ res[1] += 1
+ else:
+ mod_id = ids[0]
+ latest_version = self.browse(cursor, user, mod_id,
+ context=context).latest_version
+ if vercmp(version, latest_version) > 0:
+ self.write(cursor, user, mod_id, {
+ 'latest_version': version,
+ 'url': url,
+ }, context=context)
+ res[0] += 1
+ return res
+
+ def download(self, cursor, user, ids, download=True, context=None):
+ res = []
+ for mod in self.browse(cursor, user, ids, context=context):
+ if not mod.url:
+ continue
+ match = re.search('-([a-zA-Z0-9\._-]+)(\.zip)', mod.url, re.I)
+ version = '0'
+ if match:
+ version = match.group(1)
+ if vercmp(mod.installed_version or '0', version) >= 0:
+ continue
+ res.append(mod.url)
+ if not download:
+ continue
+ zip_file = urllib.urlopen(mod.url).read()
+ fname = os.path.join(ADDONS_PATH, mod.name+'.zip')
+ try:
+ file_p = file(fname, 'wb')
+ file_p.write(zip_file)
+ file_p.close()
+ except IOError:
+ raise ExceptORM('Error', 'Can not create the module file: %s'
+ % (fname,))
+ terp = Module.get_module_info(mod.name)
+ self.write(cursor, user, mod.id, {
+ 'description': terp.get('description', ''),
+ 'shortdesc': terp.get('name', ''),
+ 'author': terp.get('author', 'Unknown'),
+ 'website': terp.get('website', ''),
+ 'license': terp.get('license', 'GPL-2'),
+ })
+ self._update_dependencies(cursor, user, mod.id, terp.get('depends',
+ []))
+ self._update_category(cursor, user, mod.id, terp.get('category',
+ 'Uncategorized'))
+ return res
+
+ def _update_dependencies(self, cursor, user, module_id, depends=None):
+ dependency_obj = self.pool.get('ir.module.module.dependency')
+ dependency_ids = dependency_obj.search(cursor, user, [
+ ('module_id', '=', module_id),
+ ])
+ dependency_obj.unlink(cursor, user, dependency_ids)
+ if depends is None:
+ depends = []
+ for depend in depends:
+ dependency_obj.create(cursor, user, {
+ 'module_id': module_id,
+ 'name': depend,
+ })
+
+ def _update_category(self, cursor, user, module_id, category='None'):
+ category_obj = self.pool.get('ir.module.category')
+ categs = category.split('/')
+ parent_id = None
+ while categs:
+ if parent_id is not None:
+ category_ids = category_obj.search(cursor, user, [
+ ('name', '=', categs[0]),
+ ('parent_id', '=', parent_id),
+ ])
+ else:
+ category_ids = category_obj.search(cursor, user, [
+ ('name', '=', categs[0]),
+ ('parent_id', '=', False),
+ ])
+ if not category_ids:
+ parent_id = category_obj.create(cursor, user, {
+ 'name': categs[0],
+ 'parent_id': parent_id,
+ })
+ else:
+ parent_id = category_ids[0]
+ categs = categs[1:]
+ self.write(cursor, user, module_id, {'category_id': parent_id})
+
+Module()
+
+class ModuleDependency(OSV):
+ "Module dependency"
+ _name = "ir.module.module.dependency"
+ _description = __doc__
+
+ def _state(self, cursor, user, ids, name, args, context=None):
+ result = {}
+ module_obj = self.pool.get('ir.module.module')
+ for dependency in self.browse(cursor, user, ids):
+ ids = module_obj.search(cursor, user, [
+ ('name', '=', dependency.name),
+ ], context=context)
+ if ids:
+ result[dependency.id] = module_obj.browse(cursor, user, ids[0],
+ context=context).state
+ else:
+ result[dependency.id] = 'unknown'
+ return result
+
+ _columns = {
+ 'name': fields.char('Name', size=128),
+ 'module_id': fields.many2one('ir.module.module', 'Module', select=1),
+ 'state': fields.function(_state, method=True, type='selection',
+ selection=[
+ ('uninstallable','Uninstallable'),
+ ('uninstalled','Not Installed'),
+ ('installed','Installed'),
+ ('to upgrade','To be upgraded'),
+ ('to remove','To be removed'),
+ ('to install','To be installed'),
+ ('unknown', 'Unknown'),
+ ], string='State', readonly=True),
+ }
+
+ModuleDependency()
+
+
+class ModuleUpdateList(Wizard):
+ "Update module list"
+ _name = 'ir.module.module.update_list'
+
+ arch = '''
+
'''
+ fields = {
+ 'repositories': {'type': 'text', 'string': 'Repositories', 'readonly': True},
+ }
+
+ arch_module = '''
+ '''
+
+ fields_module = {
+ 'update': {'type': 'integer', 'string': 'Number of modules updated', 'readonly': True},
+ 'add': {'type': 'integer', 'string': 'Number of modules added', 'readonly': True},
+ }
+
+ def _update_module(self, cr, uid, data, context):
+ module_obj = self.pool.get('ir.module.module')
+ update, add = module_obj.update_list(cr, uid)
+ return {'update': update, 'add': add}
+
+ def _action_module_open(self, cr, uid, data, context):
+ return {
+ 'domain': str([]),
+ 'name': 'Module List',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': 'ir.module.module',
+ 'view_id': False,
+ 'type': 'ir.actions.act_window',
+ }
+
+ def _get_repositories(self, cr, uid, data, context):
+ repository_obj = self.pool.get('ir.module.repository')
+ ids = repository_obj.search(cr, uid, [])
+ res = repository_obj.read(cr, uid, ids, ['name', 'url'], context)
+ return {
+ 'repositories': '\n'.join([x['name']+': '+x['url'] \
+ for x in res]),
+ }
+
+ states = {
+ 'init': {
+ 'actions': [_get_repositories],
+ 'result': {
+ 'type': 'form',
+ 'arch': arch,
+ 'fields': fields,
+ 'state': [
+ ('end', 'Cancel', 'gtk-cancel'),
+ ('update', 'Check new modules', 'gtk-ok', True),
+ ]
+ }
+ },
+ 'update': {
+ 'actions': [_update_module],
+ 'result': {
+ 'type': 'form',
+ 'arch': arch_module,
+ 'fields': fields_module,
+ 'state': [
+ ('open_window', 'Ok', 'gtk-ok', True),
+ ]
+ }
+ },
+ 'open_window': {
+ 'actions': [],
+ 'result': {
+ 'type': 'action',
+ 'action': _action_module_open,
+ 'state':'end',
+ }
+ },
+ }
+
+ModuleUpdateList()
diff --git a/trytond/ir/module/module.xml b/trytond/ir/module/module.xml
new file mode 100644
index 0000000000..465f874ec9
--- /dev/null
+++ b/trytond/ir/module/module.xml
@@ -0,0 +1,107 @@
+
+
+
+
+
+ ir.module.module.form
+ ir.module.module
+ form
+
+
+
+
+
+ ir.module.module.tree
+ ir.module.module
+ tree
+
+
+
+
+
+
+
+
+
+
+
+
+ Modules
+ ir.actions.act_window
+ ir.module.module
+ form
+
+
+
+ tree
+
+
+
+
+
+ form
+
+
+
+
+
+ tree_open
+ ir.module.category,0
+
+
+
+ Update Module List
+ ir.module.module.update_list
+
+
+
+
diff --git a/trytond/ir/module/repository.py b/trytond/ir/module/repository.py
new file mode 100644
index 0000000000..a6b7729d28
--- /dev/null
+++ b/trytond/ir/module/repository.py
@@ -0,0 +1,29 @@
+"Repository"
+from trytond.osv import OSV, fields
+
+
+class Repository(OSV):
+ "Module repository"
+ _name = "ir.module.repository"
+ _description = __doc__
+ _columns = {
+ 'name': fields.Char('Name', size=128),
+ 'url': fields.Char('Url', size=256, required=True),
+ 'sequence': fields.Integer('Sequence', required=True),
+ 'filter': fields.Char('Filter', size=128, required=True,
+ help='Regexp to search module on the repository webpage:\n'
+ '- The first parenthesis must match the name of the module.\n'
+ '- The second parenthesis must match all the version number.\n'
+ '- The last parenthesis must match the extension of the module.'),
+ 'active': fields.boolean('Active'),
+ }
+ _defaults = {
+ 'sequence': lambda *a: 5,
+ 'filter': lambda *a: 'href="([a-zA-Z0-9_]+)-' \
+ '((\\d+)((\\.\\d+)*)([a-z]?)' \
+ '((_(pre|p|beta|alpha|rc)\\d*)*)(-r(\\d+))?)(\.zip)"',
+ 'active': lambda *a: 1,
+ }
+ _order = "sequence"
+
+Repository()
diff --git a/trytond/ir/module/repository.xml b/trytond/ir/module/repository.xml
new file mode 100644
index 0000000000..3d777be825
--- /dev/null
+++ b/trytond/ir/module/repository.xml
@@ -0,0 +1,54 @@
+
+
+
+
+ ir.module.repository.form
+ ir.module.repository
+ form
+
+
+
+
+
+ ir.module.repository.tree
+ ir.module.repository
+ tree
+
+
+
+
+
+
+
+
+
+
+ Repositories
+ ir.actions.act_window
+ ir.module.repository
+ form
+
+
+
+ tree
+
+
+
+
+
+ form
+
+
+
+
+
+
diff --git a/trytond/ir/ui/view.py b/trytond/ir/ui/view.py
index d28f5807ee..1d7aae904f 100644
--- a/trytond/ir/ui/view.py
+++ b/trytond/ir/ui/view.py
@@ -42,6 +42,46 @@ class View(OSV):
(_check_xml, 'Invalid XML for View Architecture!', ['arch'])
]
+ def unlink(self, cursor, user, ids, context=None):
+ views = self.browse(cursor, user, ids, context=context)
+ for view in views:
+ # Restart the cache
+ try:
+ self.pool.get(view.model).fields_view_get()
+ except:
+ pass
+ res = super(View, self).unlink(cursor, user, ids, context=context)
+ return res
+
+ def create(self, cursor, user, vals, context=None):
+ res = super(View, self).create(cursor, user, vals, context=context)
+ if 'model' in vals:
+ model = vals['model']
+ # Restart the cache
+ try:
+ self.pool.get(model).fields_view_get()
+ except:
+ pass
+ return res
+
+ def write(self, cursor, user, ids, vals, context=None):
+ views = self.browse(cursor, user, ids)
+ for view in views:
+ # Restart the cache
+ try:
+ self.pool.get(view.model).fields_view_get()
+ except:
+ pass
+ res = super(View, self).write(cursor, user, ids, vals, context=context)
+ views = self.browse(cursor, user, ids)
+ for view in views:
+ # Restart the cache
+ try:
+ self.pool.get(view.model).fields_view_get()
+ except:
+ pass
+ return res
+
View()
diff --git a/trytond/module.py b/trytond/module.py
index 2fc5c86d08..c152e90638 100644
--- a/trytond/module.py
+++ b/trytond/module.py
@@ -168,6 +168,8 @@ def load_module_graph(cursor, graph, **kwargs):
sys.stdout.flush()
pool = pooler.get_pool(cursor.dbname)
modules = pool.instanciate(module)
+ pool_wizard = pooler.get_pool_wizard(cursor.dbname)
+ wizards = pool_wizard.instanciate(module, pool)
cursor.execute('SELECT state, demo FROM ir_module_module WHERE name = %s',
(module,))
(package_state, package_demo) = (cursor.rowcount and cursor.fetchone()) \
diff --git a/trytond/osv/fields.py b/trytond/osv/fields.py
index d3a2d5908d..06930ca842 100644
--- a/trytond/osv/fields.py
+++ b/trytond/osv/fields.py
@@ -479,8 +479,7 @@ class Function(_column):
_properties = True
def __init__(self, fnct, arg=None, fnct_inv=None, fnct_inv_arg=None,
- type='float', fnct_search=None, obj=None, method=False,
- store=False, **args):
+ type='float', fnct_search=None, obj=None, method=False, **args):
_column.__init__(self, **args)
self._obj = obj
self._method = method
@@ -494,7 +493,6 @@ class Function(_column):
self.readonly = 1
self._type = type
self._fnct_search = fnct_search
- self.store = store
if type == 'float':
self._symbol_c = '%f'
self._symbol_f = lambda x: __builtin__.float(x or 0.0)
diff --git a/trytond/osv/orm.py b/trytond/osv/orm.py
index 5f4e926d7c..84e7b8f4e5 100644
--- a/trytond/osv/orm.py
+++ b/trytond/osv/orm.py
@@ -1,8 +1,13 @@
# -*- coding: utf-8 -*-
-
from xml import dom
+from xml.dom import minidom
+from xml import xpath
from trytond.netsvc import Logger, LOG_ERROR, LOG_WARNING, LocalService
import fields
+from trytond.tools import Cache
+import md5
+
+ID_MAX = 1000
def intersect(i, j):
return [x for x in j if x in i]
@@ -427,11 +432,10 @@ class ORM(object):
"AND a.attname = %s " \
"AND c.oid = a.attrelid " \
"AND a.atttypid = t.oid",
- (self._table, k.lower()))
+ (self._table, k))
res = cursor.dictfetchall()
if not res:
- if not isinstance(field, fields.function) \
- or field.store:
+ if not isinstance(field, fields.function):
# add the missing field
cursor.execute("ALTER TABLE \"%s\" " \
"ADD COLUMN \"%s\" %s" % \
@@ -489,8 +493,7 @@ class ORM(object):
f_pg_type = f_pg_def['typname']
f_pg_size = f_pg_def['size']
f_pg_notnull = f_pg_def['attnotnull']
- if isinstance(field, fields.function) \
- and not field.store:
+ if isinstance(field, fields.function):
logger.notify_channel('init', LOG_WARNING,
'column %s (%s) in table %s was converted '\
'to a function !\n' \
@@ -954,29 +957,36 @@ class ORM(object):
and getattr(self._columns[x], '_classic_write')] + \
self._inherits.values()
+ res = []
if len(fields_pre) :
- fields_pre2 = map(lambda x: (x in ('create_date', 'write_date')) \
+ fields_pre2 = [(x in ('create_date', 'write_date')) \
and ('date_trunc(\'second\', ' + x + ') as ' + x) \
- or '"' + x + '"', fields_pre)
- if domain1:
- cursor.execute(('SELECT ' + ','.join(fields_pre2 + ['id']) + \
- ' FROM \"' + self._table +'\" ' \
- 'WHERE id in (' + ','.join([str(x) for x in ids]) + ')'\
- ' AND ' + domain1 + ' order by ' + self._order),
- domain2)
- if not cursor.rowcount == len({}.fromkeys(ids)):
- raise ExceptORM('AccessError',
- 'You try to bypass an access rule ' \
- '(Document type: %s).' % self._description)
- else:
- cursor.execute('SELECT ' + ','.join(fields_pre2 + ['id']) + \
- ' FROM \"' + self._table + '\" ' \
- 'WHERE id in (' + ','.join([str(x) for x in ids]) + ')'\
- ' ORDER BY ' + self._order)
-
- res = cursor.dictfetchall()
+ or '"' + x + '"' for x in fields_pre]
+ for i in range((len(ids) / ID_MAX) + ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i + 1)]
+ if domain1:
+ cursor.execute(('SELECT ' + \
+ ','.join(fields_pre2 + ['id']) + \
+ ' FROM \"' + self._table +'\" ' \
+ 'WHERE id IN ' \
+ '(' + ','.join([str(x) for x in sub_ids]) + ')'\
+ ' AND ' + domain1 + ' ORDER BY ' + self._order),
+ domain2)
+ if not cursor.rowcount == len({}.fromkeys(sub_ids)):
+ raise ExceptORM('AccessError',
+ 'You try to bypass an access rule ' \
+ '(Document type: %s).' % \
+ self._description)
+ else:
+ cursor.execute('SELECT ' + \
+ ','.join(fields_pre2 + ['id']) + \
+ ' FROM \"' + self._table + '\" ' \
+ 'WHERE id IN ' \
+ '(' + ','.join([str(x) for x in sub_ids]) + ')'\
+ ' ORDER BY ' + self._order)
+ res.extend(cursor.dictfetchall())
else:
- res = [{'id':x} for x in ids]
+ res = [{'id': x} for x in ids]
for field in fields_pre:
if self._columns[field].translate:
@@ -1111,14 +1121,18 @@ class ORM(object):
ids = [ids]
delta = context.get('read_delta', False)
if delta and self._log_access:
- cursor.execute(
- "SELECT (now() - min(write_date)) <= '%s'::interval " \
- "FROM \"%s\" WHERE id in (%s)" % \
- (delta, self._table, ",".join([str(x) for x in ids])))
- res = cursor.fetchone()
- if res and res[0]:
- raise ExceptORM('ConcurrencyException',
- 'This record was modified in the meanwhile')
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i + 1)]
+ cursor.execute(
+ "SELECT (now() - min(write_date)) <= '%s'::interval " \
+ "FROM \"%s\" WHERE id in (%s)" % \
+ (delta, self._table,
+ ",".join([str(x) for x in sub_ids])))
+ res = cursor.fetchone()
+ if res and res[0]:
+ raise ExceptORM('ConcurrencyException',
+ 'This record was modified in the meanwhile')
self.pool.get('ir.model.access').check(cursor, user, self._name,
'unlink')
@@ -1126,7 +1140,6 @@ class ORM(object):
wf_service = LocalService("workflow")
for obj_id in ids:
wf_service.trg_delete(user, self._name, obj_id, cursor)
- str_d = ','.join(('%d',) * len(ids))
#cursor.execute('select * from ' + self._table + \
# ' where id in ('+str_d+')', ids)
@@ -1139,19 +1152,31 @@ class ORM(object):
self._name)
if domain1:
domain1 = ' AND ' + domain1
- cursor.execute('SELECT id FROM "'+self._table+'" ' \
- 'WHERE id IN (' + str_d + ') ' + domain1, ids + domain2)
- if not cursor.rowcount == len({}.fromkeys(ids)):
- raise ExceptORM('AccessError',
- 'You try to bypass an access rule ' \
- '(Document type: %s).' % self._description)
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i + 1)]
+ str_d = string.join(('%d',) * len(sub_ids), ',')
+ if domain1:
+ cursor.execute('SELECT id FROM "'+self._table+'" ' \
+ 'WHERE id IN (' + str_d + ') ' + domain1,
+ sub_ids + domain2)
+ if not cursor.rowcount == len({}.fromkeys(ids)):
+ raise ExceptORM('AccessError',
+ 'You try to bypass an access rule ' \
+ '(Document type: %s).' % self._description)
- cursor.execute('DELETE FROM inherit ' \
- 'WHERE (obj_type = %s AND obj_id IN ('+str_d+')) ' \
- 'OR (inst_type = %s AND inst_id IN ('+str_d+'))',
- ((self._name,) + tuple(ids) + (self._name,) + tuple(ids)))
- cursor.execute('DELETE FROM "'+self._table+'" ' \
- 'WHERE id IN (' + str_d + ') ' + domain1, ids + domain2)
+ cursor.execute('DELETE FROM inherit ' \
+ 'WHERE (obj_type = %s AND obj_id IN ('+str_d+')) ' \
+ 'OR (inst_type = %s AND inst_id IN ('+str_d+'))',
+ ((self._name,) + tuple(sub_ids) + \
+ (self._name,) + tuple(sub_ids)))
+ if domain:
+ cursor.execute('DELETE FROM "'+self._table+'" ' \
+ 'WHERE id IN (' + str_d + ') ' + domain1,
+ sub_ids + domain2)
+ else:
+ cursor.execute('DELETE FROM "'+self._table+'" ' \
+ 'WHERE id IN (' + str_d + ')', sub_ids)
return True
# TODO: Validate
@@ -1164,23 +1189,27 @@ class ORM(object):
ids = [ids]
delta = context.get('read_delta', False)
if delta and self._log_access:
- cursor.execute("select (now() - min(write_date)) <= '%s'::interval"\
- " FROM %s WHERE id IN (%s)" % \
- (delta, self._table, ",".join([str(x) for x in ids])))
- res = cursor.fetchone()
- if res and res[0]:
- for field in vals:
- if field in self._columns \
- and self._columns[field]._classic_write:
- raise ExceptORM('ConcurrencyException',
- 'This record was modified in the meanwhile')
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i + 1)]
+ cursor.execute("SELECT " \
+ "(now() - min(write_date)) <= '%s'::interval"\
+ " FROM %s WHERE id IN (%s)" % \
+ (delta, self._table,
+ ",".join([str(x) for x in sub_ids])))
+ res = cursor.fetchone()
+ if res and res[0]:
+ for field in vals:
+ if field in self._columns \
+ and self._columns[field]._classic_write:
+ raise ExceptORM('ConcurrencyException',
+ 'This record was modified in the meanwhile')
self.pool.get('ir.model.access').check(cursor, user, self._name,
'write')
#for v in self._inherits.values():
# assert v not in vals, (v, vals)
- ids_str = ','.join([str(x) for x in ids])
upd0 = []
upd1 = []
upd_todo = []
@@ -1232,23 +1261,35 @@ class ORM(object):
user, self._name)
if domain1:
domain1 = ' and ' + domain1
- cursor.execute('SELECT id FROM "' + self._table + '" ' \
- 'WHERE id IN (' + ids_str + ') ' + domain1, domain2)
- if not cursor.rowcount == len({}.fromkeys(ids)):
- raise ExceptORM('AccessError',
- 'You try to bypass an access rule ' \
- '(Document type: %s).' % self._description)
- else:
- cursor.execute('SELECT id FROM "' + self._table + '" ' \
- 'WHERE id IN (' + ids_str + ')')
- if not cursor.rowcount == len({}.fromkeys(ids)):
- raise ExceptORM('AccessError',
- 'You try to write on an record ' \
- 'that doesn\'t exist (Document type: %s).' % \
- self._description)
- cursor.execute('UPDATE "' + self._table + '" ' \
- 'SET ' + ','.join(upd0) + ' ' \
- 'WHERE id IN (' + ids_str + ') ' + domain1, upd1 + domain2)
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i + 1)]
+ ids_str = ','.join([str(x) for x in sub_ids])
+ if domain1:
+ cursor.execute('SELECT id FROM "' + self._table + '" ' \
+ 'WHERE id IN (' + ids_str + ') ' + domain1, domain2)
+ if not cursor.rowcount == len({}.fromkeys(sub_ids)):
+ raise ExceptORM('AccessError',
+ 'You try to bypass an access rule ' \
+ '(Document type: %s).' % \
+ self._description)
+ else:
+ cursor.execute('SELECT id FROM "' + self._table + '" ' \
+ 'WHERE id IN (' + ids_str + ')')
+ if not cursor.rowcount == len({}.fromkeys(sub_ids)):
+ raise ExceptORM('AccessError',
+ 'You try to bypass an access rule ' \
+ '(Document type: %s).' % \
+ self._description)
+ if domain1:
+ cursor.execute('UPDATE "' + self._table + '" ' \
+ 'SET ' + ','.join(upd0) + ' ' \
+ 'WHERE id IN (' + ids_str + ') ' + domain1,
+ upd1 + domain2)
+ else:
+ cursor.execute('UPDATE "' + self._table + '" ' \
+ 'SET ' + ','.join(upd0) + ' ' \
+ 'WHERE id IN (' + ids_str + ') ', upd1)
if totranslate:
for field in direct:
@@ -1267,10 +1308,15 @@ class ORM(object):
for table in self._inherits:
col = self._inherits[table]
- cursor.execute('SELECT DISTINCT "' + col + '" ' \
- 'FROM "' + self._table + '" WHERE id IN (' + ids_str + ')',
- upd1)
- nids = [x[0] for x in cursor.fetchall()]
+ nids = []
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids = ids[ID_MAX * i:ID_MAX * (i +1)]
+ ids_str = ','.join([str(x) for x in sub_ids])
+ cursor.execute('SELECT DISTINCT "' + col + '" ' \
+ 'FROM "' + self._table + '" WHERE id IN (' + ids_str + ')',
+ upd1)
+ nids.extend([x[0] for x in cursor.fetchall()])
vals2 = {}
for val in updend:
@@ -1287,7 +1333,6 @@ class ORM(object):
wf_service = LocalService("workflow")
for obj_id in ids:
wf_service.trg_write(user, self._name, obj_id, cursor)
- self._update_function_stored(cursor, user, ids, context=context)
return True
def create(self, cursor, user, vals, context=None):
@@ -1385,32 +1430,8 @@ class ORM(object):
wf_service = LocalService("workflow")
wf_service.trg_create(user, self._name, id_new, cursor)
- self._update_function_stored(cursor, user, [id_new], context=context)
return id_new
- def _update_function_stored(self, cursor, user, ids, context=None):
- ffields = [x for x in self._columns if isinstance(self._columns[x],
- fields.function) and self._columns[x].store]
- if ffields:
- result = self.read(cursor, user, ids, fields_names=ffields,
- context=context)
- for res in result:
- upd0 = []
- upd1 = []
- for field in res:
- if field not in ffields:
- continue
- value = res[field]
- if self._columns[field]._type in ('many2one', 'one2one'):
- value = res[field][0]
- upd0.append('"' + field + '"=' + \
- self._columns[field]._symbol_set[0])
- upd1.append(self._columns[field]._symbol_set[1](value))
- upd1.append(res['id'])
- cursor.execute('update "' + self._table + '" set ' + \
- ','.join(upd0) + ' where id = %d', upd1)
- return True
-
def fields_get(self, cursor, user, fields_names=None, context=None):
"""
returns the definition of each field in the object
@@ -1599,28 +1620,15 @@ class ORM(object):
return arch, fields2
def fields_view_get(self, cursor, user, view_id=None, view_type='form',
- context=None, toolbar=False):
+ context=None, toolbar=False, hexmd5=None):
def _inherit_apply(src, inherit):
def _find(node, node2):
- if node.nodeType == node.ELEMENT_NODE \
- and node.localName == node2.localName:
- res = True
- for attr in node2.attributes.keys():
- if attr == 'position':
- continue
- if node.hasAttribute(attr):
- if node.getAttribute(attr) == \
- node2.getAttribute(attr):
- continue
- res = False
- if res:
- return node
- for child in node.childNodes:
- res = _find(child, node2)
- if res:
- return res
+ if node2.nodeType == node2.ELEMENT_NODE \
+ and node2.localName == 'xpath':
+ res = xpath.Evaluate(node2.getAttribute('expr'), node)
+ return res and res[0] or None
return None
doc_src = dom.minidom.parseString(src)
@@ -1782,8 +1790,13 @@ class ORM(object):
'action': resaction,
'relate': resrelate,
}
+ result['md5'] = md5.new(str(result)).hexdigest()
+ if hexmd5 == result['md5']:
+ return True
return result
+ fields_view_get = Cache()(fields_view_get)
+
_view_look_dom_arch = __view_look_dom_arch
def _where_calc(self, cursor, user, args, active_test=True, context=None):
@@ -1817,6 +1830,16 @@ class ORM(object):
fargs = args[i][0].split('.', 1)
field = table._columns.get(fargs[0], False)
if not field:
+ if args[i][0] == 'id' and args[i][1] == 'child_of':
+ ids2 = args[i][2]
+ def _rec_get(ids, table, parent):
+ if not ids:
+ return []
+ ids2 = table.search(cursor, user,
+ [(parent, 'in', ids)], context=context)
+ return ids2 + _rec_get(ids2, table, parent)
+ args[i] = (args[i][0], 'in', ids2 + \
+ _rec_get(ids2, table, table._parent_name), table)
i += 1
continue
if len(fargs) > 1:
@@ -1853,11 +1876,15 @@ class ORM(object):
if not ids2:
args[i] = ('id', '=', '0')
else:
- cursor.execute('SELECT "' + field._fields_id + \
- '" FROM "' + field_obj._table + '" ' \
- 'WHERE id IN (' + \
- ','.join([str(x) for x in ids2]) + ')')
- ids3 = [x[0] for x in cursor.fetchall()]
+ ids3 = []
+ for i in range((len(ids2) / ID_MAX) + \
+ (len(ids2) % ID_MAX)):
+ sub_ids = ids2[ID_MAX * i:ID_MAX * (i + 1)]
+ cursor.execute('SELECT "' + field._fields_id + \
+ '" FROM "' + field_obj._table + '" ' \
+ 'WHERE id IN (' + \
+ ','.join([str(x) for x in sub_ids2]) + ')')
+ ids3.extend([x[0] for x in cursor.fetchall()])
args[i] = ('id', 'in', ids3)
i += 1
@@ -2027,6 +2054,7 @@ class ORM(object):
todel.append(xitem)
for xitem in todel[::-1]:
del arg[2][xitem]
+ #TODO fix max_stack_depth
if arg[0] == 'id':
qu1.append('(%s.id in (%s))' % \
(table._table,
@@ -2201,10 +2229,17 @@ class ORM(object):
parent = self._parent_name
ids_parent = ids[:]
while len(ids_parent):
- cursor.execute('SELECT distinct "' + parent + '" ' +
- 'FROM "' + self._table + '" ' +
- 'WHERE id IN (' + ','.join([str(x) for x in ids_parent]) + ')')
- ids_parent = [x[0] for x in cursor.fetchall()]
+ ids_parent2 = []
+ for i in range((len(ids) / ID_MAX) + \
+ ((len(ids) % ID_MAX) and 1 or 0)):
+ sub_ids_parent = ids_parent[ID_MAX * i:ID_MAX * (i + 1)]
+ cursor.execute('SELECT distinct "' + parent + '" ' +
+ 'FROM "' + self._table + '" ' +
+ 'WHERE id IN ' \
+ '(' + ','.join([str(x) for x in sub_ids_parent]) + ')')
+ ids_parent2.extend(filter(None,
+ [x[0] for x in cursor.fetchall()]))
+ ids_parent = ids_parent2
for i in ids_parent:
if i in ids:
return False
diff --git a/trytond/pooler.py b/trytond/pooler.py
index 1b4300cd57..d43d6699a6 100644
--- a/trytond/pooler.py
+++ b/trytond/pooler.py
@@ -3,8 +3,10 @@ from netsvc import Logger, LOG_INFO
_DB = {}
_POOL = {}
+_POOL_WIZARD = {}
-def get_db_and_pool(db_name, force_demo=False, update_module=False):
+def get_db_and_pool(db_name, force_demo=False, update_module=False,
+ wizard=False):
if db_name in _DB:
database = _DB[db_name]
else:
@@ -15,12 +17,18 @@ def get_db_and_pool(db_name, force_demo=False, update_module=False):
if db_name in _POOL:
pool = _POOL[db_name]
+ pool_wizard = _POOL_WIZARD[db_name]
else:
Logger().notify_channel('pooler', LOG_INFO,
'Instanciate pooler for %s' % (db_name))
from osv.osv import OSVService
pool = OSVService()
_POOL[db_name] = pool
+
+ from wizard import WizardService
+ pool_wizard = WizardService()
+ _POOL_WIZARD[db_name] = pool_wizard
+
from module import load_modules
load_modules(database, force_demo, update_module)
@@ -28,6 +36,8 @@ def get_db_and_pool(db_name, force_demo=False, update_module=False):
import report
#report.interface.register_all(database)
pool.get('ir.cron').pool_jobs(database.dbname)
+ if wizard:
+ return database, pool_wizard
return database, pool
def restart_pool(db_name, force_demo=False, update_module=False):
@@ -57,3 +67,7 @@ def get_db(db_name):
def get_pool(db_name, force_demo=False, update_module=False):
pool = get_db_and_pool(db_name, force_demo, update_module)[1]
return pool
+
+def get_pool_wizard(db_name, force_demo=False, update_module=False):
+ pool = get_db_and_pool(db_name, force_demo, update_module, wizard=True)[1]
+ return pool
diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py
index 24b9693798..bb99d87088 100644
--- a/trytond/tools/misc.py
+++ b/trytond/tools/misc.py
@@ -350,7 +350,7 @@ class Cache(object):
kwargs.sort()
# Work out key as a tuple of ('argname', value) pairs
- key = (('dbname', cursor.dbname),) + tuple(kwargs)
+ key = (('dbname', cursor.dbname), str(kwargs))
# Check cache and return cached value if possible
if key in self.cache:
diff --git a/trytond/web_service/wizard.py b/trytond/web_service/wizard.py
index ad21bf9d02..d02532a41d 100644
--- a/trytond/web_service/wizard.py
+++ b/trytond/web_service/wizard.py
@@ -16,9 +16,9 @@ class Wizard(Service):
def _execute(self, database, user, wiz_id, datas, action, context):
self.wiz_datas[wiz_id].update(datas)
- wiz = LocalService('wizard.' + self.wiz_name[wiz_id])
- return wiz.execute(database, user, self.wiz_datas[wiz_id], action,
- context)
+ wiz = LocalService('wizard_proxy')
+ return wiz.execute(database, user, self.wiz_name[wiz_id],
+ self.wiz_datas[wiz_id], action, context)
def create(self, database, user, passwd, wiz_name, datas=None):
security.check(database, user, passwd)
diff --git a/trytond/wizard/wizard.py b/trytond/wizard/wizard.py
index 206ed7faf7..b646e5d8bf 100644
--- a/trytond/wizard/wizard.py
+++ b/trytond/wizard/wizard.py
@@ -6,6 +6,11 @@ from xml import dom
from trytond.osv import ExceptORM, ExceptOSV
import sys
+MODULE_LIST = []
+MODULE_CLASS_LIST = {}
+CLASS_POOL = {}
+
+
class ExceptWizard(Exception):
def __init__(self, name, value):
Exception.__init__(self)
@@ -16,107 +21,23 @@ class ExceptWizard(Exception):
except_wizard = ExceptWizard
class WizardService(Service):
- states = {}
- name = ""
def __init__(self):
- self.wiz_name = self.name
- assert not service_exist(self.wiz_name), \
- 'The wizard "%s" already exists!' % self.name
- Service.__init__(self, self.wiz_name)
+ self.object_name_pool = {}
+ self.module_obj_list = {}
+ Service.__init__(self, 'wizard_proxy')
+ Service.join_group(self, 'web-services')
Service.export_method(self, self.execute)
- def translate_view(self, cursor, node, state, lang):
- translation_obj = pooler.get_pool(cursor.dbname).get('ir.translation')
- if node.nodeType == node.ELEMENT_NODE:
- if node.hasAttribute('string') \
- and node.getAttribute('string'):
- res_trans = translation_obj._get_source(cursor,
- self.wiz_name + ',' + state, 'wizard_view', lang)
- if res_trans:
- node.setAttribute('string', res_trans.decode('utf8'))
- for child_node in node.childNodes:
- self.translate_view(cursor, child_node, state, lang)
-
- def execute_cr(self, cursor, user, data, state='init', context=None):
- if context is None:
- context = {}
- res = {}
- pool = pooler.get_pool(cursor.dbname)
- values_obj = pool.get('ir.values')
- translation_obj = pool.get('ir.translation')
+ def execute_cr(self, cursor, user, wizard_name, data, state='init',
+ context=None):
try:
- state_def = self.states[state]
- result_def = copy.copy(state_def.get('result', {}))
-
- # iterate through the list of actions defined for this state
- for action in state_def.get('actions', []):
- # execute them
- action_res = action(self, cursor, user, data, context)
- assert isinstance(action_res, dict), \
- 'The return value of wizard actions ' \
- 'should be a dictionary'
- result_def.update(action_res)
-
- res['datas'] = result_def.get('datas', {})
-
- lang = context.get('lang', 'en_US')
- if result_def['type'] == 'action':
- res['action'] = result_def['action'](self, cursor, user, data,
- context)
- elif result_def['type'] == 'form':
- fields = copy.copy(result_def['fields'])
- arch = copy.copy(result_def['arch'])
- button_list = copy.copy(result_def['state'])
-
- defaults = values_obj.get(cursor, user, 'default',
- False, [('wizard.'+self.wiz_name, False)])
- default_values = dict([(x[1], x[2]) for x in defaults])
- for val in fields.keys():
- if 'default' in fields[val]:
- # execute default method for this field
- if callable(fields[val]['default']):
- fields[val]['value'] = fields[val]['default']\
- (self, cursor, user, data, state, context)
- else:
- fields[val]['value'] = fields[val]['default']
- del fields[val]['default']
- if val in default_values:
- fields[val]['value'] = default_values[val]
- if 'selection' in fields[val]:
- if callable(fields[val]['selection']):
- fields[val] = copy.copy(fields[val])
- fields[val]['selection'] = fields[val]['selection']\
- (self, cursor, user, data, state, context)
-
- # translate fields
- for field in fields:
- res_trans = translation_obj._get_source(cursor, user,
- self.wiz_name + ',' + state + ',' + field,
- 'wizard_field', lang)
- if res_trans:
- fields[field]['string'] = res_trans
-
- # translate arch
- doc = dom.minidom.parseString(arch)
- self.translate_view(cursor, doc, state, lang)
- arch = doc.toxml()
-
- # translate buttons
- for i, button in enumerate(button_list):
- button_name = button[0]
- res_trans = translation_obj._get_source(cursor, user,
- self.wiz_name + ',' + state + ',' + button_name,
- 'wizard_button', lang)
- if res_trans:
- button = list(button)
- button[1] = res_trans
- button_list[i] = tuple(button)
-
- res['fields'] = fields
- res['arch'] = arch
- res['state'] = button_list
-
+ wizard = pooler.get_pool_wizard(cursor.dbname).get(wizard_name)
+ if not wizard:
+ self.abort_response('Wizard Error', 'warning',
+ 'Wizard %s doesn\'t exist' % str(wizard_name))
+ res = wizard.execute(cursor, user, data, state, context)
+ return res
except ExceptORM, inst:
self.abort_response(inst.name, 'warning', inst.value)
except ExceptOSV, inst:
@@ -131,17 +52,14 @@ class WizardService(Service):
'Exception in call: ' + tb_s)
raise
- if result_def['type'] == 'choice':
- next_state = result_def['next_state'](self, cursor, user,
- data, context)
- return self.execute_cr(cursor, user, data, next_state, context)
- return res
-
- def execute(self, dbname, user, data, state='init', context=None):
+ def execute(self, dbname, user, wizard_name, data, state='init',
+ context=None):
cursor = pooler.get_db(dbname).cursor()
+ pool = pooler.get_pool_wizard(dbname)
try:
try:
- res = self.execute_cr(cursor, user, data, state, context)
+ res = pool.execute_cr(cursor, user, wizard_name, data, state,
+ context)
cursor.commit()
except Exception:
cursor.rollback()
@@ -149,3 +67,171 @@ class WizardService(Service):
finally:
cursor.close()
return res
+
+ def add(self, name, object_name_inst):
+ """
+ adds a new obj instance to the obj pool.
+ if it already existed, the instance is replaced
+ """
+ if self.object_name_pool.has_key(name):
+ del self.object_name_pool[name]
+ self.object_name_pool[name] = object_name_inst
+
+ module = str(object_name_inst.__class__)[6:]
+ module = module[:len(module)-1]
+ module = module.split('.')[0][2:]
+ self.module_obj_list.setdefault(module, []).append(object_name_inst)
+
+ def get(self, name):
+ return self.object_name_pool.get(name, None)
+
+ def instanciate(self, module, pool_obj):
+ res = []
+ class_list = MODULE_CLASS_LIST.get(module, [])
+ for klass in class_list:
+ res.append(klass.create_instance(self, module, pool_obj))
+ return res
+
+
+class Wizard(object):
+ _name = ""
+ states = {}
+
+ def __new__(cls):
+ for module in cls.__module__.split('.'):
+ if module != 'trytond' and module != 'addons':
+ break
+ if not hasattr(cls, '_module'):
+ cls._module = module
+ MODULE_CLASS_LIST.setdefault(cls._module, []).append(cls)
+ CLASS_POOL[cls._name] = cls
+ if module not in MODULE_LIST:
+ MODULE_LIST.append(cls._module)
+ return None
+
+ def create_instance(cls, pool, module, pool_obj):
+ """
+ try to apply inheritancy at the instanciation level and
+ put objs in the pool var
+ """
+ if pool.get(cls._name):
+ parent_class = pool.get(cls._name).__class__
+ new_states = copy.copy(getattr(pool.get(cls._name), 'states'))
+ for i in (
+ 'actions',
+ 'result',
+ ):
+ new = copy.copy(getattr(new_states, i))
+ if hasattr(new, 'update'):
+ new.update(cls.__dict__.get(i, {}))
+ else:
+ new.extend(cls.__dict__.get(i, []))
+ new_states[i] = new
+ cls = type(cls._name, (cls, parent_class), {'states': new_states})
+
+ obj = object.__new__(cls)
+ obj.__init__(pool, pool_obj)
+ return obj
+
+ create_instance = classmethod(create_instance)
+
+ def __init__(self, pool, pool_obj):
+ pool.add(self._name, self)
+ self.pool = pool_obj
+ super(Wizard, self).__init__()
+
+ def translate_view(self, cursor, node, state, lang):
+ translation_obj = pooler.get_pool(cursor.dbname).get('ir.translation')
+ if node.nodeType == node.ELEMENT_NODE:
+ if node.hasAttribute('string') \
+ and node.getAttribute('string'):
+ res_trans = translation_obj._get_source(cursor,
+ self._name + ',' + state, 'wizard_view', lang)
+ if res_trans:
+ node.setAttribute('string', res_trans.decode('utf8'))
+ for child_node in node.childNodes:
+ self.translate_view(cursor, child_node, state, lang)
+
+ def execute(self, cursor, user, data, state='init', context=None):
+ if context is None:
+ context = {}
+ res = {}
+ values_obj = self.pool.get('ir.values')
+ translation_obj = self.pool.get('ir.translation')
+
+ state_def = self.states[state]
+ result_def = copy.copy(state_def.get('result', {}))
+
+ # iterate through the list of actions defined for this state
+ for action in state_def.get('actions', []):
+ # execute them
+ action_res = action(self, cursor, user, data, context)
+ assert isinstance(action_res, dict), \
+ 'The return value of wizard actions ' \
+ 'should be a dictionary'
+ result_def.update(action_res)
+
+ res = copy.copy(result_def)
+ res['datas'] = result_def.get('datas', {})
+
+ lang = context.get('lang', 'en_US')
+ if result_def['type'] == 'action':
+ res['action'] = result_def['action'](self, cursor, user, data,
+ context)
+ elif result_def['type'] == 'form':
+ fields = copy.copy(result_def['fields'])
+ arch = copy.copy(result_def['arch'])
+ button_list = copy.copy(result_def['state'])
+
+ defaults = values_obj.get(cursor, user, 'default',
+ False, [(self._name, False)])
+ default_values = dict([(x[1], x[2]) for x in defaults])
+ for val in fields.keys():
+ if 'default' in fields[val]:
+ # execute default method for this field
+ if callable(fields[val]['default']):
+ fields[val]['value'] = fields[val]['default']\
+ (self, cursor, user, data, state, context)
+ else:
+ fields[val]['value'] = fields[val]['default']
+ del fields[val]['default']
+ if val in default_values:
+ fields[val]['value'] = default_values[val]
+ if 'selection' in fields[val]:
+ if callable(fields[val]['selection']):
+ fields[val] = copy.copy(fields[val])
+ fields[val]['selection'] = fields[val]['selection']\
+ (self, cursor, user, data, state, context)
+
+ # translate fields
+ for field in fields:
+ res_trans = translation_obj._get_source(cursor, user,
+ self._name + ',' + state + ',' + field,
+ 'wizard_field', lang)
+ if res_trans:
+ fields[field]['string'] = res_trans
+
+ # translate arch
+ doc = dom.minidom.parseString(arch)
+ self.translate_view(cursor, doc, state, lang)
+ arch = doc.toxml()
+
+ # translate buttons
+ for i, button in enumerate(button_list):
+ button_name = button[0]
+ res_trans = translation_obj._get_source(cursor, user,
+ self._name + ',' + state + ',' + button_name,
+ 'wizard_button', lang)
+ if res_trans:
+ button = list(button)
+ button[1] = res_trans
+ button_list[i] = tuple(button)
+
+ res['fields'] = fields
+ res['arch'] = arch
+ res['state'] = button_list
+ if result_def['type'] == 'choice':
+ next_state = result_def['next_state'](self, cursor, user,
+ data, context)
+ return self.execute(cursor, user, data, next_state, context)
+ return res