From 5c24a193f74a6a12c393877041fc083b1f315911 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 14:02:04 +0200 Subject: [PATCH 01/47] [REF] web_services: removed unused class. bzr revid: vmt@openerp.com-20110728120204-ezzhvkbvlw134n2b --- openerp/service/web_services.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 40e7ccf1d69..f89329d41b8 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -357,23 +357,9 @@ class db(netsvc.ExportService): raise return True -class _ObjectService(netsvc.ExportService): - "A common base class for those who have fn(db, uid, password,...) " - - def common_dispatch(self, method, auth, params): - (db, uid, passwd ) = params[0:3] - params = params[3:] - security.check(db,uid,passwd) - cr = pooler.get_db(db).cursor() - fn = getattr(self, 'exp_'+method) - res = fn(cr, uid, *params) - cr.commit() - cr.close() - return res - -class common(_ObjectService): +class common(netsvc.ExportService): def __init__(self,name="common"): - _ObjectService.__init__(self,name) + netsvc.ExportService.__init__(self,name) self.joinGroup("web-services") def dispatch(self, method, auth, params): From d8db9e6b1d61daf0a326158d911d6da31761910c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 14:07:54 +0200 Subject: [PATCH 02/47] [REF] web_services: removed unused new_dispatch method. bzr revid: vmt@openerp.com-20110728120754-deq2a4jiyhne1ynv --- openerp/netsvc.py | 3 --- openerp/service/web_services.py | 17 ----------------- 2 files changed, 20 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 3a2add26421..1037960c851 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -144,9 +144,6 @@ class ExportService(object): def dispatch(self, method, auth, params): raise Exception("stub dispatch at %s" % self.__name) - def new_dispatch(self,method,auth,params): - raise Exception("stub dispatch at %s" % self.__name) - def abortResponse(self, error, description, origin, details): if not tools.config['debug_mode']: raise Exception("%s -- %s\n\n%s"%(origin, description, details)) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index f89329d41b8..0264150c678 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -73,8 +73,6 @@ class db(netsvc.ExportService): fn = getattr(self, 'exp_'+method) return fn(*params) - def new_dispatch(self,method,auth,params): - pass def _create_empty_database(self, name): db = sql_db.db_connect('template1') cr = db.cursor() @@ -390,10 +388,6 @@ class common(netsvc.ExportService): fn = getattr(self, 'exp_'+method) return fn(*params) - - def new_dispatch(self,method,auth,params): - pass - def exp_about(self, extended=False): """Return information about the OpenERP Server. @@ -575,10 +569,6 @@ class objects_proxy(netsvc.ExportService): return res - def new_dispatch(self,method,auth,params): - pass - - # # Wizard ID: 1 # - None = end of wizard @@ -609,9 +599,6 @@ class wizard(netsvc.ExportService): res = fn(db, uid, *params) return res - def new_dispatch(self,method,auth,params): - pass - def _execute(self, db, uid, wiz_id, datas, action, context): self.wiz_datas[wiz_id].update(datas) wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id]) @@ -670,10 +657,6 @@ class report_spool(netsvc.ExportService): res = fn(db, uid, *params) return res - - def new_dispatch(self,method,auth,params): - pass - def exp_report(self, db, uid, object, ids, datas=None, context=None): if not datas: datas={} From df555e35c15f1c7f93940f7ed45a79c841a738c7 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 14:28:56 +0200 Subject: [PATCH 03/47] [REF] netsvc: LocalService() can be a function. bzr revid: vmt@openerp.com-20110728122856-ne9299uqh21xpabs --- openerp/netsvc.py | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 1037960c851..81a97f124d1 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -94,25 +94,8 @@ class Service(object): else: raise -class LocalService(object): - """ Proxy for local services. - - Any instance of this class will behave like the single instance - of Service(name) - """ - __logger = logging.getLogger('service') - def __init__(self, name): - self.__name = name - try: - self._service = Service._services[name] - for method_name, method_definition in self._service._methods.items(): - setattr(self, method_name, method_definition) - except KeyError, keyError: - self.__logger.error('This service does not exist: %s' % (str(keyError),) ) - raise - - def __call__(self, method, *params): - return getattr(self, method)(*params) +def LocalService(name): + return Service._services[name] class ExportService(object): """ Proxy for exported services. From e1939127175e0003c6f9bff6157f7eb67807f6f6 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 14:39:16 +0200 Subject: [PATCH 04/47] [REF] netsvc: removed exportMethod that was only used with the previously removed class LocalService. bzr revid: vmt@openerp.com-20110728123916-cobk8vk2oktkidke --- openerp/netsvc.py | 5 ----- openerp/osv/osv.py | 2 -- openerp/report/interface.py | 1 - openerp/wizard/__init__.py | 1 - openerp/workflow/wkf_service.py | 7 ------- 5 files changed, 16 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 81a97f124d1..7fdf927fe64 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -69,7 +69,6 @@ class Service(object): def __init__(self, name, audience=''): Service._services[name] = self self.__name = name - self._methods = {} def joinGroup(self, name): raise Exception("No group for local services") @@ -84,10 +83,6 @@ class Service(object): if cls.exists(name): cls._services.pop(name) - def exportMethod(self, method): - if callable(method): - self._methods[method.__name__] = method - def abortResponse(self, error, description, origin, details): if not tools.config['debug_mode']: raise Exception("%s -- %s\n\n%s"%(origin, description, details)) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 1327efe5731..852e6b7c7ef 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -44,8 +44,6 @@ class object_proxy(netsvc.Service): def __init__(self): self.logger = logging.getLogger('web-services') netsvc.Service.__init__(self, 'object_proxy', audience='') - self.exportMethod(self.exec_workflow) - self.exportMethod(self.execute) def check(f): @wraps(f) diff --git a/openerp/report/interface.py b/openerp/report/interface.py index 178c015ba34..de2a3bba967 100644 --- a/openerp/report/interface.py +++ b/openerp/report/interface.py @@ -52,7 +52,6 @@ class report_int(netsvc.Service): # TODO the reports have methods with a 'title' kwarg that is redundant with this attribute self.title = None #self.joinGroup('report') - self.exportMethod(self.create) def create(self, cr, uid, ids, datas, context=None): return False diff --git a/openerp/wizard/__init__.py b/openerp/wizard/__init__.py index df0b942d0df..536d9303028 100644 --- a/openerp/wizard/__init__.py +++ b/openerp/wizard/__init__.py @@ -44,7 +44,6 @@ class interface(netsvc.Service): def __init__(self, name): assert not self.exists('wizard.'+name), 'The wizard "%s" already exists!' % (name,) super(interface, self).__init__('wizard.'+name) - self.exportMethod(self.execute) self.wiz_name = name def translate_view(self, cr, node, state, lang): diff --git a/openerp/workflow/wkf_service.py b/openerp/workflow/wkf_service.py index 6f5059e6817..3684ac05694 100644 --- a/openerp/workflow/wkf_service.py +++ b/openerp/workflow/wkf_service.py @@ -29,13 +29,6 @@ import openerp.pooler as pooler class workflow_service(netsvc.Service): def __init__(self, name='workflow', audience='*'): netsvc.Service.__init__(self, name, audience) - self.exportMethod(self.trg_write) - self.exportMethod(self.trg_delete) - self.exportMethod(self.trg_create) - self.exportMethod(self.trg_validate) - self.exportMethod(self.trg_redirect) - self.exportMethod(self.trg_trigger) - self.exportMethod(self.clear_cache) self.wkf_on_create_cache={} def clear_cache(self, cr, uid): From 5f3f9e820eca3cd5092e8f8716aaa4428e5466c1 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 15:10:34 +0200 Subject: [PATCH 05/47] [REF] netsvc: Service._groups and joinGroup are unused. bzr revid: vmt@openerp.com-20110728131034-q5wpiwqh7f860l5s --- openerp/netsvc.py | 8 -------- openerp/report/interface.py | 1 - openerp/service/web_services.py | 5 ----- 3 files changed, 14 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 7fdf927fe64..21dfb7d6877 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -70,10 +70,6 @@ class Service(object): Service._services[name] = self self.__name = name - def joinGroup(self, name): - raise Exception("No group for local services") - #GROUPS.setdefault(name, {})[self.__name] = self - @classmethod def exists(cls, name): return name in cls._services @@ -104,7 +100,6 @@ class ExportService(object): """ _services = {} - _groups = {} _logger = logging.getLogger('web-services') def __init__(self, name, audience=''): @@ -112,9 +107,6 @@ class ExportService(object): self.__name = name self._logger.debug("Registered an exported service: %s" % name) - def joinGroup(self, name): - ExportService._groups.setdefault(name, {})[self.__name] = self - @classmethod def getService(cls,name): return cls._services[name] diff --git a/openerp/report/interface.py b/openerp/report/interface.py index de2a3bba967..d940505a342 100644 --- a/openerp/report/interface.py +++ b/openerp/report/interface.py @@ -51,7 +51,6 @@ class report_int(netsvc.Service): self.name2 = '.'.join(name.split('.')[1:]) # TODO the reports have methods with a 'title' kwarg that is redundant with this attribute self.title = None - #self.joinGroup('report') def create(self, cr, uid, ids, datas, context=None): return False diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 0264150c678..f40cf2009ec 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -50,7 +50,6 @@ from cStringIO import StringIO class db(netsvc.ExportService): def __init__(self, name="db"): netsvc.ExportService.__init__(self, name) - self.joinGroup("web-services") self.actions = {} self.id = 0 self.id_protect = threading.Semaphore() @@ -358,7 +357,6 @@ class db(netsvc.ExportService): class common(netsvc.ExportService): def __init__(self,name="common"): netsvc.ExportService.__init__(self,name) - self.joinGroup("web-services") def dispatch(self, method, auth, params): logger = netsvc.Logger() @@ -553,7 +551,6 @@ GNU Public Licence. class objects_proxy(netsvc.ExportService): def __init__(self, name="object"): netsvc.ExportService.__init__(self,name) - self.joinGroup('web-services') def dispatch(self, method, auth, params): (db, uid, passwd ) = params[0:3] @@ -583,7 +580,6 @@ class objects_proxy(netsvc.ExportService): class wizard(netsvc.ExportService): def __init__(self, name='wizard'): netsvc.ExportService.__init__(self,name) - self.joinGroup('web-services') self.id = 0 self.wiz_datas = {} self.wiz_name = {} @@ -642,7 +638,6 @@ class ExceptionWithTraceback(Exception): class report_spool(netsvc.ExportService): def __init__(self, name='report'): netsvc.ExportService.__init__(self, name) - self.joinGroup('web-services') self._reports = {} self.id = 0 self.id_protect = threading.Semaphore() From 64490ad5ac1497f9dfba85131bfcbbbb242338a3 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 28 Jul 2011 16:19:10 +0200 Subject: [PATCH 06/47] [REF] netsvc: removed unused kwarg. bzr revid: vmt@openerp.com-20110728141910-r0z95l3mf1zeh8us --- openerp/netsvc.py | 4 ++-- openerp/osv/osv.py | 2 +- openerp/report/interface.py | 4 ++-- openerp/workflow/wkf_service.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 21dfb7d6877..09f2048bc03 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -66,7 +66,7 @@ class Service(object): Functionality here is trusted, no authentication. """ _services = {} - def __init__(self, name, audience=''): + def __init__(self, name): Service._services[name] = self self.__name = name @@ -102,7 +102,7 @@ class ExportService(object): _services = {} _logger = logging.getLogger('web-services') - def __init__(self, name, audience=''): + def __init__(self, name): ExportService._services[name] = self self.__name = name self._logger.debug("Registered an exported service: %s" % name) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 852e6b7c7ef..5cffec62794 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -43,7 +43,7 @@ class except_osv(Exception): class object_proxy(netsvc.Service): def __init__(self): self.logger = logging.getLogger('web-services') - netsvc.Service.__init__(self, 'object_proxy', audience='') + netsvc.Service.__init__(self, 'object_proxy') def check(f): @wraps(f) diff --git a/openerp/report/interface.py b/openerp/report/interface.py index d940505a342..110110fb1a9 100644 --- a/openerp/report/interface.py +++ b/openerp/report/interface.py @@ -41,9 +41,9 @@ def toxml(value): return unicode_value.replace('&', '&').replace('<','<').replace('>','>') class report_int(netsvc.Service): - def __init__(self, name, audience='*'): + def __init__(self, name): assert not self.exists(name), 'The report "%s" already exists!' % name - super(report_int, self).__init__(name, audience) + super(report_int, self).__init__(name) if name[0:7]<>'report.': raise Exception, 'ConceptionError, bad report name, should start with "report."' self.name = name diff --git a/openerp/workflow/wkf_service.py b/openerp/workflow/wkf_service.py index 3684ac05694..c74a0ad25c1 100644 --- a/openerp/workflow/wkf_service.py +++ b/openerp/workflow/wkf_service.py @@ -27,8 +27,8 @@ import openerp.netsvc as netsvc import openerp.pooler as pooler class workflow_service(netsvc.Service): - def __init__(self, name='workflow', audience='*'): - netsvc.Service.__init__(self, name, audience) + def __init__(self, name='workflow'): + netsvc.Service.__init__(self, name) self.wkf_on_create_cache={} def clear_cache(self, cr, uid): From c4d7dde6456832d4e4278f7bc357670a33eb458b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 29 Jul 2011 10:38:24 +0200 Subject: [PATCH 07/47] [REF] netsvc: object_proxy is no longer in netsvc. bzr revid: vmt@openerp.com-20110729083824-onx5kklbgzb9pok2 --- openerp/netsvc.py | 11 +++++++++++ openerp/osv/osv.py | 16 +++++++++------- openerp/report/custom.py | 21 +++++++++------------ openerp/report/print_xml.py | 5 ++--- openerp/service/web_services.py | 14 +++++++------- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 09f2048bc03..db828babd5e 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -60,6 +60,12 @@ def close_socket(sock): #.apidoc title: Common Services: netsvc #.apidoc module-mods: member-order: bysource +def abort_response(error, description, origin, details): + if not tools.config['debug_mode']: + raise Exception("%s -- %s\n\n%s"%(origin, description, details)) + else: + raise + class Service(object): """ Base class for *Local* services @@ -86,6 +92,11 @@ class Service(object): raise def LocalService(name): + # Special case for addons support, will be removed in a few days when addons + # are updated to directly use openerp.osv.osv.service. + if name == 'object_proxy': + return openerp.osv.osv.service + return Service._services[name] class ExportService(object): diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 5cffec62794..02c6fbe3c5d 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -39,11 +39,13 @@ class except_osv(Exception): self.value = value self.args = (exc_type, name) +service = None -class object_proxy(netsvc.Service): +class object_proxy(): def __init__(self): self.logger = logging.getLogger('web-services') - netsvc.Service.__init__(self, 'object_proxy') + global service + service = self def check(f): @wraps(f) @@ -117,14 +119,14 @@ class object_proxy(netsvc.Service): except orm.except_orm, inst: if inst.name == 'AccessError': self.logger.debug("AccessError", exc_info=True) - self.abortResponse(1, inst.name, 'warning', inst.value) + netsvc.abort_response(1, inst.name, 'warning', inst.value) except except_osv, inst: - self.abortResponse(1, inst.name, inst.exc_type, inst.value) + netsvc.abort_response(1, inst.name, inst.exc_type, inst.value) except IntegrityError, inst: osv_pool = pooler.get_pool(dbname) for key in osv_pool._sql_error.keys(): if key in inst[0]: - self.abortResponse(1, _('Constraint Error'), 'warning', + netsvc.abort_response(1, _('Constraint Error'), 'warning', tr(osv_pool._sql_error[key], 'sql_constraint') or inst[0]) if inst.pgcode in (errorcodes.NOT_NULL_VIOLATION, errorcodes.FOREIGN_KEY_VIOLATION, errorcodes.RESTRICT_VIOLATION): msg = _('The operation cannot be completed, probably due to the following:\n- deletion: you may be trying to delete a record while other records still reference it\n- creation/update: a mandatory field is not correctly set') @@ -145,9 +147,9 @@ class object_proxy(netsvc.Service): msg += _('\n\n[object with reference: %s - %s]') % (model_name, model) except Exception: pass - self.abortResponse(1, _('Integrity Error'), 'warning', msg) + netsvc.abort_response(1, _('Integrity Error'), 'warning', msg) else: - self.abortResponse(1, _('Integrity Error'), 'warning', inst[0]) + netsvc.abort_response(1, _('Integrity Error'), 'warning', inst[0]) except Exception: self.logger.exception("Uncaught exception") raise diff --git a/openerp/report/custom.py b/openerp/report/custom.py index f3d6cfbc074..051d1f29862 100644 --- a/openerp/report/custom.py +++ b/openerp/report/custom.py @@ -136,16 +136,15 @@ class report_custom(report_int): ids = self.pool.get(report.model_id.model).search(cr, uid, []) datas['ids'] = ids - service = netsvc.LocalService("object_proxy") report_id = datas['report_id'] - report = service.execute(cr.dbname, uid, 'ir.report.custom', 'read', [report_id], context=context)[0] - fields = service.execute(cr.dbname, uid, 'ir.report.custom.fields', 'read', report['fields_child0'], context=context) + report = self.pool.get('ir.report.custom').read(cr, uid, [report_id], context=context)[0] + fields = self.pool.get('ir.report.custom.fields').read(cr, uid, report['fields_child0'], context=context) fields.sort(lambda x,y : x['sequence'] - y['sequence']) if report['field_parent']: - parent_field = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [report['field_parent'][0]],['model']) - model_name = service.execute(cr.dbname, uid, 'ir.model', 'read', [report['model_id'][0]], ['model'],context=context)[0]['model'] + parent_field = self.pool.get('ir.model.fields').read(cr, uid, [report['field_parent'][0]], ['model']) + model_name = self.pool.get('ir.model').read(cr, uid, [report['model_id'][0]], ['model'], context=context)[0]['model'] fct = {} fct['id'] = lambda x : x @@ -160,9 +159,7 @@ class report_custom(report_int): field_child = f['field_child'+str(i)] if field_child: row.append( - service.execute(cr.dbname, uid, - 'ir.model.fields', 'read', [field_child[0]], - ['name'], context=context)[0]['name'] + self.pool.get('ir.model.fields').read(cr, uid, [field_child[0]], ['name'], context=context)[0]['name'] ) if f['fc'+str(i)+'_operande']: fct_name = 'id' @@ -346,7 +343,7 @@ class report_custom(report_int): def _create_lines(self, cr, uid, ids, report, fields, results, context): - service = netsvc.LocalService("object_proxy") + pool = pooler.get_pool(cr.dbname) pdf_string = cStringIO.StringIO() can = canvas.init(fname=pdf_string, format='pdf') @@ -376,7 +373,7 @@ class report_custom(report_int): for f in fields: field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0]) if field_id: - type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype']) + type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype']) if type[0]['ttype'] == 'date': date_idx = idx fct[idx] = process_date[report['frequency']] @@ -449,7 +446,7 @@ class report_custom(report_int): def _create_bars(self, cr, uid, ids, report, fields, results, context): - service = netsvc.LocalService("object_proxy") + pool = pooler.get_pool(cr.dbname) pdf_string = cStringIO.StringIO() can = canvas.init(fname=pdf_string, format='pdf') @@ -475,7 +472,7 @@ class report_custom(report_int): for f in fields: field_id = (f['field_child3'] and f['field_child3'][0]) or (f['field_child2'] and f['field_child2'][0]) or (f['field_child1'] and f['field_child1'][0]) or (f['field_child0'] and f['field_child0'][0]) if field_id: - type = service.execute(cr.dbname, uid, 'ir.model.fields', 'read', [field_id],['ttype']) + type = pool.get('ir.model.fields').read(cr, uid, [field_id],['ttype']) if type[0]['ttype'] == 'date': date_idx = idx fct[idx] = process_date[report['frequency']] diff --git a/openerp/report/print_xml.py b/openerp/report/print_xml.py index ad2b0528d99..b7074a2d0c3 100644 --- a/openerp/report/print_xml.py +++ b/openerp/report/print_xml.py @@ -137,9 +137,8 @@ class document(object): value = self.get_value(browser, attrs['name']) - service = netsvc.LocalService("object_proxy") - ids = service.execute(self.cr.dbname, self.uid, 'ir.attachment', 'search', [('res_model','=',model),('res_id','=',int(value))]) - datas = service.execute(self.cr.dbname, self.uid, 'ir.attachment', 'read', ids) + ids = self.pool.get('ir.attachment').search(self.cr, self.uid, [('res_model','=',model),('res_id','=',int(value))]) + datas = self.pool.get('ir.attachment').read(self.cr, self.uid, ids) if len(datas): # if there are several, pick first diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index f40cf2009ec..fb347c38970 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -344,9 +344,9 @@ class db(netsvc.ExportService): tools.config['update']['base'] = True pooler.restart_pool(db, force_demo=False, update_module=True) except except_orm, inst: - self.abortResponse(1, inst.name, 'warning', inst.value) + netsvc.abort_response(1, inst.name, 'warning', inst.value) except except_osv, inst: - self.abortResponse(1, inst.name, inst.exc_type, inst.value) + netsvc.abort_response(1, inst.name, inst.exc_type, inst.value) except Exception: import traceback tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback)) @@ -419,7 +419,7 @@ GNU Public Licence. return rc.get_available_updates(rc.id, openerp.modules.get_modules_with_version()) except tm.RemoteContractException, e: - self.abortResponse(1, 'Migration Error', 'warning', str(e)) + netsvc.abort_response(1, 'Migration Error', 'warning', str(e)) def exp_get_migration_scripts(self, contract_id, contract_password): @@ -487,7 +487,7 @@ GNU Public Licence. return True except tm.RemoteContractException, e: - self.abortResponse(1, 'Migration Error', 'warning', str(e)) + netsvc.abort_response(1, 'Migration Error', 'warning', str(e)) except Exception, e: import traceback tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback)) @@ -560,8 +560,8 @@ class objects_proxy(netsvc.ExportService): if method not in ['execute','exec_workflow']: raise NameError("Method not available %s" % method) security.check(db,uid,passwd) - ls = netsvc.LocalService('object_proxy') - fn = getattr(ls, method) + assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy." + fn = getattr(openerp.osv.osv.service, method) res = fn(db, uid, *params) return res @@ -701,7 +701,7 @@ class report_spool(netsvc.ExportService): result = self._reports[report_id] exc = result['exception'] if exc: - self.abortResponse(exc, exc.message, 'warning', exc.traceback) + netsvc.abort_response(exc, exc.message, 'warning', exc.traceback) res = {'state': result['state']} if res['state']: if tools.config['reportgz']: From 9ea28d59700a2cc44cfb89744a3fea88f6007a81 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 29 Jul 2011 11:08:20 +0200 Subject: [PATCH 08/47] [REF] netsvc: netsvc wont provide any longer LocalService("object_service"), better use the model directly. bzr revid: vmt@openerp.com-20110729090820-6kvt9a2swqeepmhn --- addons/auction/report/artists_lots.py | 5 +++-- addons/auction/report/seller_address.py | 8 ++++---- addons/auction/report/total.py | 8 +++----- addons/auction/wizard/auction_aie_send.py | 6 ++---- addons/auction/wizard/auction_aie_send_result.py | 3 +-- addons/auction/wizard/auction_lots_invoice.py | 10 ++++------ 6 files changed, 17 insertions(+), 23 deletions(-) diff --git a/addons/auction/report/artists_lots.py b/addons/auction/report/artists_lots.py index 981440dac53..6c9344c88db 100644 --- a/addons/auction/report/artists_lots.py +++ b/addons/auction/report/artists_lots.py @@ -21,14 +21,15 @@ from report.interface import report_int import netsvc +import openerp.pooler class report_artistlot(report_int): def __init__(self, name): report_int.__init__(self, name) def create(self, cr, uid, ids, datas, context): - service = netsvc.LocalService("object_proxy") - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', ids, ['artist_id']) + pool = pooler.get_pool(cr.dbname) + lots = pool.get('auction.lots').read(cr, uid, ids, ['artist_id']) artists = [] for lot in lots: if lot['artist_id'] and lot['artist_id'] not in artists: diff --git a/addons/auction/report/seller_address.py b/addons/auction/report/seller_address.py index 315533e99f4..7a7c10cb4aa 100644 --- a/addons/auction/report/seller_address.py +++ b/addons/auction/report/seller_address.py @@ -20,15 +20,15 @@ ############################################################################## from report.interface import report_int -import netsvc +import openerp.pooler class auction_seller(report_int): def __init__(self, name): report_int.__init__(self, name) def create(self, cr, uid, ids, datas, context): - service = netsvc.LocalService("object_proxy") - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', ids, ['bord_vnd_id']) + pool = openerp.pooler.get_pool(cr.dbname) + lots = pool.get('auction.lots').read(cr, uid, ids, ['bord_vnd_id']) bords = {} for l in lots: @@ -37,7 +37,7 @@ class auction_seller(report_int): new_ids = bords.keys() parts = {} - partners = service.execute(cr.dbname, uid, 'auction.deposit', 'read', new_ids, ['partner_id']) + partners = pool.get('auction.deposit').read(cr, uid, new_ids, ['partner_id']) for l in partners: if l['partner_id']: parts[l['partner_id'][0]]=True diff --git a/addons/auction/report/total.py b/addons/auction/report/total.py index e878001c2e4..609a1ad3a48 100644 --- a/addons/auction/report/total.py +++ b/addons/auction/report/total.py @@ -32,10 +32,8 @@ class report_custom(report_rml): report_rml.__init__(self, name, table, tmpl, xsl) def create_xml(self, cr, uid, ids, datas, context=None): - service = netsvc.LocalService("object_proxy") - - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', ids, ['obj_price','ach_login','obj_comm','lot_est1','lot_est2','bord_vnd_id','ach_emp','auction_id']) - auction = service.execute(cr.dbname, uid, 'auction.dates', 'read', [lots[0]['auction_id'][0]])[0] + lots = self.pool.get('auction.lots').read(cr, uid , ids, ['obj_price','ach_login','obj_comm','lot_est1','lot_est2','bord_vnd_id','ach_emp','auction_id']) + auction = self.pool.get('auction.dates').read(cr, uid, [lots[0]['auction_id'][0]])[0] unsold = comm = emp = paid = unpaid = 0 est1 = est2 = adj = 0 @@ -75,7 +73,7 @@ class report_custom(report_rml): debit = adj - costs = service.execute(cr.dbname, uid, 'auction.lots', 'compute_seller_costs', ids) + costs = self.pool.get('auction.lots').compute_seller_costs(cr, uid, ids) for cost in costs: debit += cost['amount'] diff --git a/addons/auction/wizard/auction_aie_send.py b/addons/auction/wizard/auction_aie_send.py index 1caec13b852..563230c0d1f 100644 --- a/addons/auction/wizard/auction_aie_send.py +++ b/addons/auction/wizard/auction_aie_send.py @@ -141,9 +141,8 @@ class auction_lots_send_aie(osv.osv_memory): def _photos_send(cr, uid, uname, passwd, did, ids): - service = netsvc.LocalService("object_proxy") for (ref,id) in ids: - datas = service.execute(cr.db_name, uid, 'auction.lots', 'read', [id], ['name','image']) + datas = self.pool.get('auction.lots').read(cr, uid, [id], ['name','image']) if len(datas): bin = base64.decodestring(datas[0]['image']) fname = datas[0]['name'] @@ -186,8 +185,7 @@ class auction_lots_send_aie(osv.osv_memory): if context is None: context = {} - service = netsvc.LocalService("object_proxy") - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', context.get('active_ids',[]), ['obj_num','lot_num','obj_desc','bord_vnd_id','lot_est1','lot_est2','artist_id','lot_type','aie_categ']) + lots = self.pool.get('auction.lots').read(cr, uid, context.get('active_ids',[]), ['obj_num','lot_num','obj_desc','bord_vnd_id','lot_est1','lot_est2','artist_id','lot_type','aie_categ']) lots_ids = [] datas = self.read(cr, uid, ids[0],['uname','login','lang','numerotation','dates']) for l in lots: diff --git a/addons/auction/wizard/auction_aie_send_result.py b/addons/auction/wizard/auction_aie_send_result.py index d6102dd0522..24489fda441 100644 --- a/addons/auction/wizard/auction_aie_send_result.py +++ b/addons/auction/wizard/auction_aie_send_result.py @@ -132,9 +132,8 @@ class auction_lots_pay(osv.osv_memory): if context is None: context = {} import pickle - service = netsvc.LocalService("object_proxy") datas = self.read(cr, uid, ids[0],['uname','password','dates']) - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', context['active_ids'], ['obj_num','obj_price']) + lots = self.pool.get('auction.lots').read(cr, uid, context['active_ids'], ['obj_num','obj_price']) args = pickle.dumps(lots) self._catalog_send(datas['uname'], datas['password'], datas['dates'], args) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/auction/wizard/auction_lots_invoice.py b/addons/auction/wizard/auction_lots_invoice.py index 66bb5e42f2b..3994aa6ff91 100644 --- a/addons/auction/wizard/auction_lots_invoice.py +++ b/addons/auction/wizard/auction_lots_invoice.py @@ -49,9 +49,8 @@ class auction_lots_invoice(osv.osv_memory): if context is None: context = {} res = super(auction_lots_invoice, self).default_get(cr, uid, fields, context=context) - service = netsvc.LocalService("object_proxy") - lots = service.execute(cr.dbname, uid, 'auction.lots', 'read', context.get('active_ids', [])) - auction = service.execute(cr.dbname, uid, 'auction.dates', 'read', [lots[0]['auction_id'][0]])[0] + lots = self.pool.get('auction.lots').read(cr, uid, context.get('active_ids', [])) + auction = self.pool.get('auction.dates').read(cr, uid, [lots[0]['auction_id'][0]])[0] price = 0.0 price_topay = 0.0 @@ -59,7 +58,7 @@ class auction_lots_invoice(osv.osv_memory): for lot in lots: price_lot = lot['obj_price'] or 0.0 - costs = service.execute(cr.dbname, uid, 'auction.lots', 'compute_buyer_costs', [lot['id']]) + costs = self.pool.get('auction.lots').compute_buyer_costs(cr, uid, [lot['id']]) price_lot += costs['amount'] price += price_lot @@ -68,7 +67,7 @@ class auction_lots_invoice(osv.osv_memory): raise osv.except_osv(_('UserError'), _('Two different buyers for the same invoice !\nPlease correct this problem before invoicing')) uid = lot['ach_uid'][0] elif lot['ach_login']: - refs = service.execute(uid, 'res.partner', 'search', [('ref','=',lot['ach_login'])]) + refs = self.pool.get('res.partner').search(cr, uid, [('ref','=',lot['ach_login'])]) if len(refs): uid = refs[-1] if 'ach_pay_id' in lot and lot['ach_pay_id']: @@ -105,7 +104,6 @@ class auction_lots_invoice(osv.osv_memory): """ if context is None: context = {} - service = netsvc.LocalService("object_proxy") datas = {'ids' : context.get('active_ids',[])} res = self.read(cr, uid, ids, ['number','ach_uid']) res = res and res[0] or {} From 40bde558c5df3061b631899aee84585a02b092f2 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 29 Jul 2011 11:41:22 +0200 Subject: [PATCH 09/47] [REF] netsvc: removed abortResponse method. bzr revid: vmt@openerp.com-20110729094122-8c2q2c1nx0x7jdmf --- openerp/netsvc.py | 6 ------ openerp/wizard/__init__.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index db828babd5e..f3c13535d6f 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -85,12 +85,6 @@ class Service(object): if cls.exists(name): cls._services.pop(name) - def abortResponse(self, error, description, origin, details): - if not tools.config['debug_mode']: - raise Exception("%s -- %s\n\n%s"%(origin, description, details)) - else: - raise - def LocalService(name): # Special case for addons support, will be removed in a few days when addons # are updated to directly use openerp.osv.osv.service. diff --git a/openerp/wizard/__init__.py b/openerp/wizard/__init__.py index 536d9303028..2ed01921afa 100644 --- a/openerp/wizard/__init__.py +++ b/openerp/wizard/__init__.py @@ -155,7 +155,7 @@ class interface(netsvc.Service): if isinstance(e, except_wizard) \ or isinstance(e, except_osv) \ or isinstance(e, except_orm): - self.abortResponse(2, e.name, 'warning', e.value) + netsvc.abort_response(2, e.name, 'warning', e.value) else: import traceback tb_s = reduce(lambda x, y: x+y, traceback.format_exception( From ee12a9c9c54984b3e9b09bdb86007e8d87adb778 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 29 Jul 2011 16:56:09 +0200 Subject: [PATCH 10/47] [REF] netsvc: comments (kind of notes to self). bzr revid: vmt@openerp.com-20110729145609-cbzlre0rsfxld3xq --- openerp/netsvc.py | 7 +++++++ openerp/service/http_server.py | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index f3c13535d6f..6125f2218b3 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -116,6 +116,8 @@ class ExportService(object): def getService(cls,name): return cls._services[name] + # Dispatch a RPC call w.r.t. the method name. The dispatching + # w.r.t. the service (this class) is done by OpenERPDispatcher. def dispatch(self, method, auth, params): raise Exception("stub dispatch at %s" % self.__name) @@ -397,6 +399,11 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): logger.log(channel, indent+line) indent=indent_after +# This class is used to dispatch a RPC to a service. So it is used +# for both XMLRPC (with a SimpleXMLRPCRequestHandler), and NETRPC. +# The service (ExportService) will then dispatch on the method name. +# This can be re-written as a single function +# def dispatch(self, service_name, method, params, auth_provider). class OpenERPDispatcher: def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): log(title, msg, channel=channel, depth=depth, fn=fn) diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 61713e9f2dd..d22d32aabeb 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -213,7 +213,8 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server): return ret - +# No need for these two classes: init_server() below can initialize correctly +# directly the BaseHttpDaemon class. class HttpDaemon(BaseHttpDaemon): _RealProto = 'HTTP' def __init__(self, interface, port): @@ -270,6 +271,8 @@ def list_http_services(protocol=None): raise Exception("Incorrect protocol or no http services") import SimpleXMLRPCServer +# Basically, this class extends SimpleXMLRPCRequestHandler to use +# OpenERPDispatcher as the dispatcher (to select the correct ExportService). class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): rpc_paths = [] protocol_version = 'HTTP/1.1' From ef542bf9a70bdb355533212ebb803b010655d56c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 18 Aug 2011 10:07:09 +0200 Subject: [PATCH 11/47] [REF] netsvc: removed no longer used method (the method was changed to a function in a previous commit). bzr revid: vmt@openerp.com-20110818080709-8htdrn4ixl3gu8sf --- openerp/netsvc.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 6125f2218b3..3548cf3046c 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -121,11 +121,6 @@ class ExportService(object): def dispatch(self, method, auth, params): raise Exception("stub dispatch at %s" % self.__name) - def abortResponse(self, error, description, origin, details): - if not tools.config['debug_mode']: - raise Exception("%s -- %s\n\n%s"%(origin, description, details)) - else: - raise BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10) #The background is set with 40 plus the number of the color, and the foreground with 30 From 94b86a7bf60eb828cb2dd024a6f0cd0402ca2c20 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 19 Aug 2011 15:02:48 +0200 Subject: [PATCH 12/47] [IMP] wsgi: (proof of concept) serving xml-rpc requests behind gunicorn: > gunicorn openerp:wsgi.application -c gunicorn.conf.py If you want to try, you should know an addons_path is hard coded in openerp.wsgi.on_starting(). bzr revid: vmt@openerp.com-20110819130248-tf81ttu42t2uhkuo --- gunicorn.conf.py | 7 ++++ openerp/__init__.py | 1 + openerp/wsgi.py | 86 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 gunicorn.conf.py create mode 100644 openerp/wsgi.py diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 00000000000..427c76764de --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,7 @@ +import openerp +# Standard OpenERP XML-RPC port. +bind = '127.0.0.1:8069' +# Some application-wide initialization is needed. +on_starting = openerp.wsgi.on_starting +# This is the big TODO: safely use more than a single worker. +workers = 1 diff --git a/openerp/__init__.py b/openerp/__init__.py index b7b5fc1b533..9026c7be90f 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -41,6 +41,7 @@ import tiny_socket import tools import wizard import workflow +import wsgi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/wsgi.py b/openerp/wsgi.py new file mode 100644 index 00000000000..7c52dd3db46 --- /dev/null +++ b/openerp/wsgi.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" WSGI stuffs (proof of concept for now) + +This module offers an WSGI interface to OpenERP. + +""" + +from wsgiref.simple_server import make_server +from SimpleXMLRPCServer import SimpleXMLRPCDispatcher +import xmlrpclib + +import sys + +import openerp +import openerp.tools.config as config + +def wsgi_xmlrpc(environ, start_response): + if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'): + length = int(environ['CONTENT_LENGTH']) + data = environ['wsgi.input'].read(length) + path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ... TODO should we expect a possible trailing '/' ? + + # TODO see SimpleXMLRPCDispatcher._marshaled_dispatch() for some necessary handling. + # TODO see OpenERPDispatcher for some othe handling (in particular, auth things). + params, method = xmlrpclib.loads(data) + result = openerp.netsvc.ExportService.getService(path).dispatch(method, None, params) + response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) + + start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) + return [response] + +def wsgi_jsonrpc(environ, start_response): + pass + +def application(environ, start_response): + + # Try all handlers until one returns some result (i.e. not None). + wsgi_handlers = [wsgi_xmlrpc, wsgi_jsonrpc] + for handler in wsgi_handlers: + result = handler(environ, start_response) + if result is None: + continue + return result + + # We never returned from the loop. + else: + response = 'No handler found.\n' + start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) + return [response] + +# Serve XMLRPC via wsgiref's simple_server. +# Blocking, should probably be called in its own process. +def serve(): + httpd = make_server('localhost', config['xmlrpc_port'], application) + httpd.serve_forever() + +# Application setup be fore we can spawn any worker process. +# This is suitable for e.g. gunicorn's on_starting hook. +def on_starting(server): + config = openerp.tools.config + config['addons_path'] = '/home/openerp/repos/addons/trunk/' # need a config file + openerp.netsvc.init_logger() + openerp.osv.osv.start_object_proxy() + openerp.service.web_services.start_web_services() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From a1b433fac8252ea89d8dec2f68162d8111b1ef4d Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 24 Aug 2011 15:22:20 +0200 Subject: [PATCH 13/47] [IMP] wsgi: make it possible to kill siblings. bzr revid: vmt@openerp.com-20110824132220-htd1c4ssewwozl35 --- gunicorn.conf.py | 7 +++++-- openerp/wsgi.py | 45 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 427c76764de..0c1709dbed5 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -1,7 +1,10 @@ import openerp # Standard OpenERP XML-RPC port. bind = '127.0.0.1:8069' -# Some application-wide initialization is needed. -on_starting = openerp.wsgi.on_starting +pidfile = '.gunicorn.pid' # This is the big TODO: safely use more than a single worker. workers = 1 +# Some application-wide initialization is needed. +on_starting = openerp.wsgi.on_starting +when_ready = openerp.wsgi.when_ready +timeout = 240 # openerp request-response cycle can be quite long diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 7c52dd3db46..f1ddb04cf31 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -21,7 +21,7 @@ """ WSGI stuffs (proof of concept for now) -This module offers an WSGI interface to OpenERP. +This module offers a WSGI interface to OpenERP. """ @@ -29,7 +29,10 @@ from wsgiref.simple_server import make_server from SimpleXMLRPCServer import SimpleXMLRPCDispatcher import xmlrpclib +import os +import signal import sys +import time import openerp import openerp.tools.config as config @@ -63,10 +66,9 @@ def application(environ, start_response): return result # We never returned from the loop. - else: - response = 'No handler found.\n' - start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) - return [response] + response = 'No handler found.\n' + start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) + return [response] # Serve XMLRPC via wsgiref's simple_server. # Blocking, should probably be called in its own process. @@ -74,13 +76,44 @@ def serve(): httpd = make_server('localhost', config['xmlrpc_port'], application) httpd.serve_forever() -# Application setup be fore we can spawn any worker process. +arbiter_pid = None + +# Application setup before we can spawn any worker process. # This is suitable for e.g. gunicorn's on_starting hook. def on_starting(server): + global arbiter_pid + arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable config = openerp.tools.config config['addons_path'] = '/home/openerp/repos/addons/trunk/' # need a config file + openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() openerp.service.web_services.start_web_services() +# Install our own signal handler on the master process. +def when_ready(server): + # Hijack gunicorn's SIGWINCH handling; we can choose another one. + signal.signal(signal.SIGWINCH, make_winch_handler(server)) + +# Our signal handler will signal a SGIQUIT to all workers. +def make_winch_handler(server): + def handle_winch(sig, fram): + server.kill_workers(signal.SIGQUIT) # This is gunicorn specific. + return handle_winch + +# Kill gracefuly the workers (e.g. because we want to clear their cache). +# This is done by signaling a SIGWINCH to the master process, so it can be +# called by the workers themselves. +def kill_workers(): + try: + os.kill(arbiter_pid, signal.SIGWINCH) + except OSError, e: + if e.errno == errno.ESRCH: # no such pid + return + raise + +class kill_workers_cache(openerp.tools.real_cache): + def clear(self, dbname, *args, **kwargs): + kill_workers() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 3c8571ead03dff644ab15818aec54d38f128eeed Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 1 Sep 2011 15:04:50 +0200 Subject: [PATCH 14/47] [IMP] wsgi: added new (still to be refined) routes for XMLRPC dispatching. bzr revid: vmt@openerp.com-20110901130450-ub41ey4s0060vucg --- gunicorn.conf.py | 2 +- openerp/service/web_services.py | 61 +++++++++++++++++++++++++++- openerp/wsgi.py | 70 +++++++++++++++++++++++++++------ 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 0c1709dbed5..4ab6aa85748 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -3,7 +3,7 @@ import openerp bind = '127.0.0.1:8069' pidfile = '.gunicorn.pid' # This is the big TODO: safely use more than a single worker. -workers = 1 +workers = 2 # Some application-wide initialization is needed. on_starting = openerp.wsgi.on_starting when_ready = openerp.wsgi.when_ready diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index df848b94e67..8d05b5b776d 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -96,7 +96,8 @@ class db(netsvc.ExportService): def dispatch(self, method, auth, params): if method in [ 'create', 'get_progress', 'drop', 'dump', 'restore', 'rename', - 'change_admin_password', 'migrate_databases' ]: + 'change_admin_password', 'migrate_databases', + 'create_database' ]: passwd = params[0] params = params[1:] security.check_super(passwd) @@ -135,6 +136,20 @@ class db(netsvc.ExportService): self.actions[id]['thread'] = create_thread return id + def exp_create_database(self, db_name, demo, lang, user_password='admin'): + """ Similar to exp_create but blocking.""" + self.id_protect.acquire() + self.id += 1 + id = self.id + self.id_protect.release() + + self.actions[id] = {'clean': False} + + logging.getLogger('db.create').info('CREATE DATABASE %s', db_name.lower()) + self._create_empty_database(db_name) + _initialize_db(self, id, db_name, demo, lang, user_password) + return True + def exp_get_progress(self, id): if self.actions[id]['thread'].isAlive(): # return openerp.modules.init_progress[db_name] @@ -640,13 +655,55 @@ class report_spool(netsvc.ExportService): def dispatch(self, method, auth, params): (db, uid, passwd ) = params[0:3] params = params[3:] - if method not in ['report','report_get']: + if method not in ['report', 'report_get', 'render_report']: raise KeyError("Method not supported %s" % method) security.check(db,uid,passwd) fn = getattr(self, 'exp_' + method) res = fn(db, uid, *params) return res + def exp_render_report(self, db, uid, object, ids, datas=None, context=None): + if not datas: + datas={} + if not context: + context={} + + self.id_protect.acquire() + self.id += 1 + id = self.id + self.id_protect.release() + + self._reports[id] = {'uid': uid, 'result': False, 'state': False, 'exception': None} + + cr = pooler.get_db(db).cursor() + import traceback + import sys + try: + obj = netsvc.LocalService('report.'+object) + (result, format) = obj.create(cr, uid, ids, datas, context) + if not result: + tb = sys.exc_info() + self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb) + self._reports[id]['result'] = result + self._reports[id]['format'] = format + self._reports[id]['state'] = True + except Exception, exception: + + tb = sys.exc_info() + tb_s = "".join(traceback.format_exception(*tb)) + logger = netsvc.Logger() + logger.notifyChannel('web-services', netsvc.LOG_ERROR, + 'Exception: %s\n%s' % (str(exception), tb_s)) + if hasattr(exception, 'name') and hasattr(exception, 'value'): + self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value)) + else: + self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb) + self._reports[id]['state'] = True + cr.commit() + cr.close() + + return self._check_report(id) + def exp_report(self, db, uid, object, ids, datas=None, context=None): if not datas: datas={} diff --git a/openerp/wsgi.py b/openerp/wsgi.py index f1ddb04cf31..93bf7182785 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -37,45 +37,89 @@ import time import openerp import openerp.tools.config as config +def xmlrpc_return(start_response, service, method, params): + """ Helper to call a service's method with some params, using a + wsgi-supplied ``start_response`` callback.""" + result = openerp.netsvc.ExportService.getService(service).dispatch(method, None, params) + response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) + start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) + return [response] + def wsgi_xmlrpc(environ, start_response): - if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'): + """ The main OpenERP WSGI handler.""" + if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'): length = int(environ['CONTENT_LENGTH']) data = environ['wsgi.input'].read(length) - path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ... TODO should we expect a possible trailing '/' ? # TODO see SimpleXMLRPCDispatcher._marshaled_dispatch() for some necessary handling. # TODO see OpenERPDispatcher for some othe handling (in particular, auth things). params, method = xmlrpclib.loads(data) - result = openerp.netsvc.ExportService.getService(path).dispatch(method, None, params) - response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) - start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) - return [response] + path = environ['PATH_INFO'][len('/openerp/xmlrpc'):] + if path.startswith('/'): path = path[1:] + if path.endswith('/'): p = path[:-1] + path = path.split('/') + + # All routes are hard-coded. Need a way to register addons-supplied handlers. + + # No need for a db segment. + if len(path) == 1: + service = path[0] + + if service == 'common': + if method in ('create_database', 'list', 'server_version'): + return xmlrpc_return(start_response, 'db', method, params) + else: + return xmlrpc_return(start_response, 'common', method, params) + # A db segment must be given. + elif len(path) == 2: + service, db_name = path + params = (db_name,) + params + + if service == 'model': + return xmlrpc_return(start_response, 'object', method, params) + elif service == 'report': + return xmlrpc_return(start_response, 'report', method, params) + +def legacy_wsgi_xmlrpc(environ, start_response): + if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'): + length = int(environ['CONTENT_LENGTH']) + data = environ['wsgi.input'].read(length) + path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ... + + # TODO see SimpleXMLRPCDispatcher._marshaled_dispatch() for some necessary handling. + # TODO see OpenERPDispatcher for some othe handling (in particular, auth things). + params, method = xmlrpclib.loads(data) + return xmlrpc_return(start_response, path, method, params) def wsgi_jsonrpc(environ, start_response): pass def application(environ, start_response): + """ WSGI entry point.""" # Try all handlers until one returns some result (i.e. not None). - wsgi_handlers = [wsgi_xmlrpc, wsgi_jsonrpc] + wsgi_handlers = [wsgi_xmlrpc, wsgi_jsonrpc, legacy_wsgi_xmlrpc] for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: continue return result - # We never returned from the loop. + # We never returned from the loop. Needs something else than 200 OK. response = 'No handler found.\n' start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) return [response] -# Serve XMLRPC via wsgiref's simple_server. -# Blocking, should probably be called in its own process. def serve(): + """ Serve XMLRPC requests via wsgiref's simple_server. + + Blocking, should probably be called in its own process. + """ httpd = make_server('localhost', config['xmlrpc_port'], application) httpd.serve_forever() +# Master process id, can be used for signaling. arbiter_pid = None # Application setup before we can spawn any worker process. @@ -84,8 +128,8 @@ def on_starting(server): global arbiter_pid arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable config = openerp.tools.config - config['addons_path'] = '/home/openerp/repos/addons/trunk/' # need a config file - openerp.tools.cache = kill_workers_cache + config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc-no-osv-memory' # need a config file + #openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() openerp.service.web_services.start_web_services() @@ -112,7 +156,7 @@ def kill_workers(): return raise -class kill_workers_cache(openerp.tools.real_cache): +class kill_workers_cache(openerp.tools.ormcache): def clear(self, dbname, *args, **kwargs): kill_workers() From 79d0e39c4c0924dbee8ddc884752fc73c3812fe5 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 2 Sep 2011 15:27:12 +0200 Subject: [PATCH 15/47] [FIX] loading: load base only once. bzr revid: vmt@openerp.com-20110902132712-j9bgfnzbmddarr7h --- openerp/modules/loading.py | 10 +++++----- openerp/modules/registry.py | 13 ++++++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 45a466e5462..4bc8e440b62 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -158,7 +158,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= logger.info('module %s: loading objects', package.name) migrations.migrate_module(package, 'pre') register_module_classes(package.name) - models = pool.instanciate(package.name, cr) + 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'): init_module_models(cr, package.name, models) @@ -273,8 +273,6 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # This is a brand new pool, just created in pooler.get_db_and_pool() pool = pooler.get_pool(cr.dbname) - processed_modules = [] # for cleanup step after install - loaded_modules = [] # to avoid double loading report = tools.assertion_report() if 'base' in tools.config['update'] or 'all' in tools.config['update']: cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed')) @@ -285,8 +283,10 @@ def load_modules(db, force_demo=False, status=None, update_module=False): 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)')) - loaded, processed = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report) - processed_modules.extend(processed) + + # 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) if tools.config['load_language']: for lang in tools.config['load_language'].split(','): diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 072082db000..e88852a8c18 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -65,14 +65,21 @@ class Registry(object): """ Return a model for a given name or raise KeyError if it doesn't exist.""" return self.models[model_name] - def instanciate(self, module, cr): - """ Instanciate all the classes of a given module for a particular db.""" + def load(self, cr, module): + """ Load a given module in the registry. + + At the Python level, the modules are already loaded, but not yet on a + per-registry level. This method populates a registry with the given + modules, i.e. it instanciates all the classes of a the given module + and registers them in the registry. + + """ res = [] # Instantiate registered classes (via metamodel discovery or via explicit # constructor call), and add them to the pool. - for cls in openerp.osv.orm.MetaModel.module_to_models.get(module, []): + for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []): res.append(cls.createInstance(self, cr)) return res From 8ca1a87201fd2ab7b56399af27d2da0f22bc08b8 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 2 Sep 2011 15:31:36 +0200 Subject: [PATCH 16/47] [IMP] wsgi: modules can be pre-loaded and expose a WSGI handler. An option --load is added to list the modules to pre-load. sys.path and sys.modules are initialized when the config is parsed. bzr revid: vmt@openerp.com-20110902133136-4v7fgptyd0g1kc5s --- openerp-server | 10 +++++++++ openerp/addons/__init__.py | 13 +++++++---- openerp/addons/base/__init__.py | 5 +++++ openerp/addons/base/__openerp__.py | 1 + openerp/conf/__init__.py | 13 +++++++++++ openerp/modules/graph.py | 23 ++++++++++--------- openerp/modules/module.py | 32 ++++++++++++++------------ openerp/tools/config.py | 36 ++++++++++++++++++++++++++++-- openerp/wsgi.py | 22 +++++++++++++++++- 9 files changed, 124 insertions(+), 31 deletions(-) diff --git a/openerp-server b/openerp-server index 2698307ff42..bc4a661b08a 100755 --- a/openerp-server +++ b/openerp-server @@ -259,6 +259,16 @@ if __name__ == "__main__": if config["stop_after_init"]: sys.exit(0) + for m in openerp.conf.server_wide_modules: + __import__(m) + # Register a WSGI entry point if any. + info = openerp.modules.module.load_information_from_description_file(m) + if info['wsgi']: + openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) + + openerp.wsgi.serve() + + setup_pid_file() setup_signal_handlers() start_services() diff --git a/openerp/addons/__init__.py b/openerp/addons/__init__.py index 88a0d113151..1f863eca509 100644 --- a/openerp/addons/__init__.py +++ b/openerp/addons/__init__.py @@ -22,10 +22,15 @@ """ Addons module. -This module only serves to contain OpenERP addons. For the code to -manage those addons, see openerp.modules. This module conveniently -reexports some symbols from openerp.modules. Importing them from here -is deprecated. +This module serves to contain all OpenERP addons, across all configured addons +paths. For the code to manage those addons, see openerp.modules. + +Addons are made available here (i.e. under openerp.addons) after +openerp.tools.config.parse_config() is called (so that the addons paths +are known). + +This module also conveniently reexports some symbols from openerp.modules. +Importing them from here is deprecated. """ diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index 847bef71f8c..457c46f7887 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -25,5 +25,10 @@ import res import publisher_warranty import report +def YEAH(environ, start_response): + response = 'YEAH.\n' + start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) + return [response] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 37f26edc177..f19c5bc7832 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -93,6 +93,7 @@ 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_ir_values.yml', ], + 'wsgi': 'YEAH', 'installable': True, 'active': True, 'certificate': '0076807797149', diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py index 7de8070f9e2..0a975c5d4e2 100644 --- a/openerp/conf/__init__.py +++ b/openerp/conf/__init__.py @@ -28,8 +28,21 @@ parsing, configuration file loading and saving, ...) in this module and provide real Python variables, e.g. addons_paths is really a list of paths. +To initialize properly this module, openerp.tools.config.parse_config() +must be used. + """ import deprecation +# Paths to search for OpenERP addons. +addons_paths = [] + +# List of server-wide modules to load. Those modules are supposed to provide +# features not necessarily tied to a particular database. This is in contrast +# to modules that are always bound to a specific database when they are +# installed (i.e. the majority of OpenERP addons). This is set with the --load +# command-line option. +server_wide_modules = [] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/modules/graph.py b/openerp/modules/graph.py index 078d23c235b..539dda25b50 100644 --- a/openerp/modules/graph.py +++ b/openerp/modules/graph.py @@ -58,16 +58,16 @@ class Graph(dict): """ - def add_node(self, name, deps): + def add_node(self, name, info): max_depth, father = 0, None - for n in [Node(x, self) for x in deps]: + for n in [Node(x, self, None) for x in info['depends']]: if n.depth >= max_depth: father = n max_depth = n.depth if father: - return father.add_child(name) + return father.add_child(name, info) else: - return Node(name, self) + return Node(name, self, info) def update_from_db(self, cr): if not len(self): @@ -120,7 +120,7 @@ class Graph(dict): continue later.clear() current.remove(package) - node = self.add_node(package, deps) + 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: @@ -154,12 +154,13 @@ class Graph(dict): class Singleton(object): - def __new__(cls, name, graph): + def __new__(cls, name, graph, info): if name in graph: inst = graph[name] else: inst = object.__new__(cls) inst.name = name + inst.info = info graph[name] = inst return inst @@ -167,19 +168,21 @@ class Singleton(object): class Node(Singleton): """ One module in the modules dependency graph. - Node acts as a per-module singleton. + Node acts as a per-module singleton. A node is constructed via + Graph.add_module() or Graph.add_modules(). Some of its fields are from + ir_module_module (setted by Graph.update_from_db()). """ - def __init__(self, name, graph): + def __init__(self, name, graph, info): self.graph = graph if not hasattr(self, 'children'): self.children = [] if not hasattr(self, 'depth'): self.depth = 0 - def add_child(self, name): - node = Node(name, self.graph) + def add_child(self, name, info): + node = Node(name, self.graph, info) node.depth = self.depth + 1 if node not in self.children: self.children.append(node) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 1514e836dd8..cd5e6537a6d 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -31,7 +31,6 @@ import openerp.osv as osv import openerp.tools as tools import openerp.tools.osutil as osutil from openerp.tools.safe_eval import safe_eval as eval -import openerp.pooler as pooler from openerp.tools.translate import _ import openerp.netsvc as netsvc @@ -58,6 +57,11 @@ loaded = [] logger = netsvc.Logger() def initialize_sys_path(): + """ Add all addons paths in sys.path. + + This ensures something like ``import crm`` works even if the addons are + not in the PYTHONPATH. + """ global ad_paths if ad_paths: @@ -250,6 +254,7 @@ def load_information_from_description_file(module): info['license'] = info.get('license') or 'AGPL-3' info.setdefault('installable', True) info.setdefault('active', False) + info.setdefault('wsgi', None) # WSGI entry point, given as a string for kind in ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']: info.setdefault(kind, []) @@ -290,23 +295,22 @@ def init_module_models(cr, module_name, obj_list): t[1](cr, *t[2]) cr.commit() +# Import hook to write a addon m in both sys.modules['m'] and +# sys.modules['openerp.addons.m']. Otherwise it could be loaded twice +# if imported twice using different names. +#class MyImportHook(object): +# def find_module(self, module_name, package_path): +# print ">>>", module_name, package_path +# def load_module(self, module_name): +# raise ImportError("Restricted") -def load_module(module_name): - """ Load a Python module found on the addons paths.""" - fm = imp.find_module(module_name, ad_paths) - try: - imp.load_module(module_name, *fm) - finally: - if fm[0]: - fm[0].close() - +#sys.meta_path.append(MyImportHook()) def register_module_classes(m): """ Register module named m, if not already registered. - This will load the module and register all of its models. (Actually, the - explicit constructor call of each of the models inside the module will - register them.) + This loads the module and register all of its models, thanks to either + the MetaModel metaclass, or the explicit instantiation of the model. """ @@ -326,7 +330,7 @@ def register_module_classes(m): try: zip_mod_path = mod_path + '.zip' if not os.path.isfile(zip_mod_path): - load_module(m) + __import__(m) else: zimp = zipimport.zipimporter(zip_mod_path) zimp.load_module(m) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 18f40e0011f..f5a45dd67a0 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -24,6 +24,7 @@ import optparse import os import sys import openerp +import openerp.conf import openerp.loglevels as loglevels import logging import openerp.release as release @@ -101,8 +102,10 @@ class configmanager(object): group.add_option("-P", "--import-partial", dest="import_partial", my_default='', help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.") group.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored") + group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules") parser.add_option_group(group) + # XML-RPC / HTTP group = optparse.OptionGroup(parser, "XML-RPC Configuration") group.add_option("--xmlrpc-interface", dest="xmlrpc_interface", my_default='', help="Specify the TCP IP address for the XML-RPC protocol. The empty string binds to all interfaces.") @@ -112,6 +115,7 @@ class configmanager(object): help="disable the XML-RPC protocol") parser.add_option_group(group) + # XML-RPC / HTTPS title = "XML-RPC Secure Configuration" if not self.has_ssl: title += " (disabled as ssl is unavailable)" @@ -258,9 +262,26 @@ class configmanager(object): self.options[option.dest] = option.my_default self.casts[option.dest] = option - self.parse_config() + self.parse_config(None, False) - def parse_config(self, args=None): + def parse_config(self, args=None, complete=True): + """ Parse the configuration file (if any) and the command-line + arguments. + + This method initializes openerp.tools.config and openerp.conf (the + former should be removed in the furture) with library-wide + configuration values. + + This method must be called before proper usage of this library can be + made. + + Typical usage of this method: + + openerp.tools.config.parse_config(sys.argv[1:]) + + :param complete: this is a hack used in __init__(), leave it to True. + + """ if args is None: args = [] opt, args = self.parser.parse_args(args) @@ -417,6 +438,17 @@ class configmanager(object): if opt.save: self.save() + openerp.conf.addons_paths = self.options['addons_path'].split(',') + openerp.conf.server_wide_modules = \ + map(lambda m: m.strip(), opt.server_wide_modules.split(',')) if \ + opt.server_wide_modules else [] + if complete: + openerp.modules.module.initialize_sys_path() + openerp.modules.loading.open_openerp_namespace() + # openerp.addons.__path__.extend(openerp.conf.addons_paths) # This + # is not compatible with initialize_sys_path(): import crm and + # import openerp.addons.crm load twice the module. + def _generate_pgpassfile(self): """ Generate the pgpass file with the parameters from the command line (db_host, db_user, diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 93bf7182785..ef3d6d033c6 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -95,11 +95,31 @@ def legacy_wsgi_xmlrpc(environ, start_response): def wsgi_jsonrpc(environ, start_response): pass +def wsgi_modules(environ, start_response): + """ WSGI handler dispatching to addons-provided entry points.""" + pass + +# WSGI handlers provided by modules loaded with the --load command-line option. +module_handlers = [] + +def register_wsgi_handler(handler): + """ Register a WSGI handler. + + Handlers are tried in the order they are added. We might provide a way to + register a handler for specific routes later. + """ + module_handlers.append(handler) + def application(environ, start_response): """ WSGI entry point.""" # Try all handlers until one returns some result (i.e. not None). - wsgi_handlers = [wsgi_xmlrpc, wsgi_jsonrpc, legacy_wsgi_xmlrpc] + wsgi_handlers = [ + wsgi_xmlrpc, + wsgi_jsonrpc, + legacy_wsgi_xmlrpc, + wsgi_modules, + ] + module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: From 9aeba4fece4f03c64cc9c2b12104aed3cf1bff11 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 2 Sep 2011 16:12:32 +0200 Subject: [PATCH 17/47] [REF] netsvc.OpenERPDispatcher: that class is gone, replaced by a simple function. bzr revid: vmt@openerp.com-20110902141232-umpscnz2cot468hy --- openerp-server | 2 +- openerp/netsvc.py | 62 ++++++++++++++++---------------- openerp/service/http_server.py | 7 ++-- openerp/service/netrpc_server.py | 5 +-- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/openerp-server b/openerp-server index bc4a661b08a..30d08f2b3da 100755 --- a/openerp-server +++ b/openerp-server @@ -266,7 +266,7 @@ if __name__ == "__main__": if info['wsgi']: openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) - openerp.wsgi.serve() + #openerp.wsgi.serve() setup_pid_file() diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 0892d47c646..0a95d1c824b 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -393,38 +393,36 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): logger.log(channel, indent+line) indent=indent_after -# This class is used to dispatch a RPC to a service. So it is used -# for both XMLRPC (with a SimpleXMLRPCRequestHandler), and NETRPC. -# The service (ExportService) will then dispatch on the method name. -# This can be re-written as a single function -# def dispatch(self, service_name, method, params, auth_provider). -class OpenERPDispatcher: - def log(self, title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): +def dispatch_rpc(service_name, method, params, auth): + """ Handle a RPC call. + + This is pure Python code, the actual marshalling (from/to XML-RPC or + NET-RPC) is done in a upper laye. + """ + def _log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): log(title, msg, channel=channel, depth=depth, fn=fn) - def dispatch(self, service_name, method, params): - try: - auth = getattr(self, 'auth_provider', None) - logger = logging.getLogger('result') - start_time = end_time = 0 - if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER): - self.log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method)) - if logger.isEnabledFor(logging.DEBUG_RPC): - start_time = time.time() - result = ExportService.getService(service_name).dispatch(method, auth, params) - if logger.isEnabledFor(logging.DEBUG_RPC): - end_time = time.time() - if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER): - self.log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method)) - self.log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER) - self.log('result', result, channel=logging.DEBUG_RPC_ANSWER) - return result - except Exception, e: - self.log('exception', tools.exception_to_unicode(e)) - tb = getattr(e, 'traceback', sys.exc_info()) - tb_s = "".join(traceback.format_exception(*tb)) - if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType): - import pdb - pdb.post_mortem(tb[2]) - raise OpenERPDispatcherException(e, tb_s) + try: + logger = logging.getLogger('result') + start_time = end_time = 0 + if logger.isEnabledFor(logging.DEBUG_RPC_ANSWER): + _log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method)) + if logger.isEnabledFor(logging.DEBUG_RPC): + start_time = time.time() + result = ExportService.getService(service_name).dispatch(method, auth, params) + if logger.isEnabledFor(logging.DEBUG_RPC): + end_time = time.time() + if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER): + _log('service (%.3fs)' % (end_time - start_time), tuple(replace_request_password(params)), depth=1, fn='%s.%s'%(service_name,method)) + _log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER) + _log('result', result, channel=logging.DEBUG_RPC_ANSWER) + return result + except Exception, e: + _log('exception', tools.exception_to_unicode(e)) + tb = getattr(e, 'traceback', sys.exc_info()) + tb_s = "".join(traceback.format_exception(*tb)) + if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType): + import pdb + pdb.post_mortem(tb[2]) + raise OpenERPDispatcherException(e, tb_s) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index d22d32aabeb..3e76c3810ef 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -271,9 +271,7 @@ def list_http_services(protocol=None): raise Exception("Incorrect protocol or no http services") import SimpleXMLRPCServer -# Basically, this class extends SimpleXMLRPCRequestHandler to use -# OpenERPDispatcher as the dispatcher (to select the correct ExportService). -class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): +class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): rpc_paths = [] protocol_version = 'HTTP/1.1' _logger = logging.getLogger('xmlrpc') @@ -281,7 +279,8 @@ class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler, def _dispatch(self, method, params): try: service_name = self.path.split("/")[-1] - return self.dispatch(service_name, method, params) + auth = getattr(self, 'auth_provider', None) + return netsvc.dispatch_rpc(service_name, method, params, auth) except netsvc.OpenERPDispatcherException, e: raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback) diff --git a/openerp/service/netrpc_server.py b/openerp/service/netrpc_server.py index 70855a018e5..f99253a1e76 100644 --- a/openerp/service/netrpc_server.py +++ b/openerp/service/netrpc_server.py @@ -36,7 +36,7 @@ import openerp.netsvc as netsvc import openerp.tiny_socket as tiny_socket import openerp.tools as tools -class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher): +class TinySocketClientThread(threading.Thread): def __init__(self, sock, threads): spn = sock and sock.getpeername() spn = 'netrpc-client-%s:%s' % spn[0:2] @@ -59,7 +59,8 @@ class TinySocketClientThread(threading.Thread, netsvc.OpenERPDispatcher): while self.running: try: msg = ts.myreceive() - result = self.dispatch(msg[0], msg[1], msg[2:]) + auth = getattr(self, 'auth_provider', None) + result = netsvc.dispatch_rpc(msg[0], msg[1], msg[2:], auth) ts.mysend(result) except socket.timeout: #terminate this channel because other endpoint is gone From 0a6fe4d1f706272e154c93722d0873415e88ca1b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 2 Sep 2011 17:02:26 +0200 Subject: [PATCH 18/47] [IMP] wsgi: added exception handling. bzr revid: vmt@openerp.com-20110902150226-223gkl4efb8o8jhs --- openerp-server | 4 +++- openerp/netsvc.py | 2 +- openerp/wsgi.py | 18 ++++++++++++------ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/openerp-server b/openerp-server index 30d08f2b3da..d2478d00ff6 100755 --- a/openerp-server +++ b/openerp-server @@ -266,7 +266,9 @@ if __name__ == "__main__": if info['wsgi']: openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) - #openerp.wsgi.serve() + openerp.osv.osv.start_object_proxy() + openerp.service.web_services.start_web_services() + openerp.wsgi.serve() setup_pid_file() diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 0a95d1c824b..8d11891640e 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -397,7 +397,7 @@ def dispatch_rpc(service_name, method, params, auth): """ Handle a RPC call. This is pure Python code, the actual marshalling (from/to XML-RPC or - NET-RPC) is done in a upper laye. + NET-RPC) is done in a upper layer. """ def _log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): log(title, msg, channel=channel, depth=depth, fn=fn) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index ef3d6d033c6..cdd28e9c8b7 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -40,8 +40,18 @@ import openerp.tools.config as config def xmlrpc_return(start_response, service, method, params): """ Helper to call a service's method with some params, using a wsgi-supplied ``start_response`` callback.""" - result = openerp.netsvc.ExportService.getService(service).dispatch(method, None, params) - response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) + # This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception + # handling. + try: + result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth + response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) + except openerp.netsvc.OpenERPDispatcherException, e: + fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except: + exc_type, exc_value, exc_tb = sys.exc_info() + fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)) + response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) return [response] @@ -51,8 +61,6 @@ def wsgi_xmlrpc(environ, start_response): length = int(environ['CONTENT_LENGTH']) data = environ['wsgi.input'].read(length) - # TODO see SimpleXMLRPCDispatcher._marshaled_dispatch() for some necessary handling. - # TODO see OpenERPDispatcher for some othe handling (in particular, auth things). params, method = xmlrpclib.loads(data) path = environ['PATH_INFO'][len('/openerp/xmlrpc'):] @@ -87,8 +95,6 @@ def legacy_wsgi_xmlrpc(environ, start_response): data = environ['wsgi.input'].read(length) path = environ['PATH_INFO'][len('/xmlrpc/'):] # expected to be one of db, object, ... - # TODO see SimpleXMLRPCDispatcher._marshaled_dispatch() for some necessary handling. - # TODO see OpenERPDispatcher for some othe handling (in particular, auth things). params, method = xmlrpclib.loads(data) return xmlrpc_return(start_response, path, method, params) From 14a82cdf9f4154ba4066968ac240e3f3b800b009 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 7 Sep 2011 16:13:18 +0200 Subject: [PATCH 19/47] [IMP] wsgi: WSGI-to-BaseHTTPRequestHandler, there are probably some shortcomings but the document_webdav/test/webdav_test1.yml tests pass. bzr revid: vmt@openerp.com-20110907141318-4gxmyztv1zp6sk7q --- gunicorn.conf.py | 2 +- openerp/wsgi.py | 185 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 180 insertions(+), 7 deletions(-) diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 4ab6aa85748..0c1709dbed5 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -3,7 +3,7 @@ import openerp bind = '127.0.0.1:8069' pidfile = '.gunicorn.pid' # This is the big TODO: safely use more than a single worker. -workers = 2 +workers = 1 # Some application-wide initialization is needed. on_starting = openerp.wsgi.on_starting when_ready = openerp.wsgi.when_ready diff --git a/openerp/wsgi.py b/openerp/wsgi.py index cdd28e9c8b7..422d4a7f9c3 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -27,7 +27,10 @@ This module offers a WSGI interface to OpenERP. from wsgiref.simple_server import make_server from SimpleXMLRPCServer import SimpleXMLRPCDispatcher +import httplib +import urllib import xmlrpclib +import StringIO import os import signal @@ -89,6 +92,8 @@ def wsgi_xmlrpc(environ, start_response): elif service == 'report': return xmlrpc_return(start_response, 'report', method, params) + # TODO the body has been read, need to raise an exception (not return None). + def legacy_wsgi_xmlrpc(environ, start_response): if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/xmlrpc/'): length = int(environ['CONTENT_LENGTH']) @@ -105,6 +110,157 @@ def wsgi_modules(environ, start_response): """ WSGI handler dispatching to addons-provided entry points.""" pass +def wsgi_webdav(environ, start_response): + if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*': + return return_options(start_response) + if environ['PATH_INFO'].startswith('/webdav'): # TODO depends on config + environ['PATH_INFO'] = '/' + environ['PATH_INFO'][len('/webdav'):] + return wsgi_to_http(environ, start_response) + +def return_options(start_response): + # TODO Microsoft specifi header, see websrv_lib do_OPTIONS + options = [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')] + start_response("200 OK", [('Content-Length', str(0))] + options) + return [] + +webdav = None + +def wsgi_to_http(environ, start_response): + """ + Forward a WSGI request to a BaseHTTPRequestHandler. + + This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign(). + It is a temporary solution: the HTTP sub-handlers (in particular the + document_webdav addon) have to be WSGIfied. + """ + global webdav + # Make sure the addons are loaded in the registry, so they have a chance + # to register themselves in the 'service' layer. + openerp.pooler.get_db_and_pool('xx', update_module=[], pooljobs=False) + + scheme = environ['wsgi.url_scheme'] + + headers = {} + for key, value in environ.items(): + if key.startswith('HTTP_'): + key = key[5:].replace('_', '-').title() + headers[key] = value + if key == 'CONTENT_LENGTH': + key = key.replace('_', '-').title() + headers[key] = value + if environ.get('Content-Type'): + headers['Content-Type'] = environ['Content-Type'] + + path = urllib.quote(environ.get('PATH_INFO', '')) + if environ.get('QUERY_STRING'): + path += '?' + environ['QUERY_STRING'] + + class Dummy(): + pass + server = Dummy() + server.server_name = environ['SERVER_NAME'] + server.server_port = int(environ['SERVER_PORT']) + con = openerp.service.websrv_lib.noconnection(environ['gunicorn.socket']) # None + fore = webdav.handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) + + # let's pretend we are a Multi handler + class M(): + def __init__(self): + self.sec_realms = {} + self.shared_headers = [] + self.shared_response = '' + self.shared_body = '' + def send_error(self, code, msg): + self.shared_response = str(code) + ' ' + msg + def send_response(self, code, msg): + self.shared_response = str(code) + ' ' + msg + def send_header(self, a, b): + self.shared_headers.append((a, b)) + def end_headers(self, *args, **kwargs): + pass + + multi = M() + + webdav.auth_provider.setupAuth(multi, fore) + + request_version = 'HTTP/1.1' # TODO + fore.wfile = StringIO.StringIO() + fore.rfile = environ['wsgi.input'] + fore.headers = headers + fore.command = environ['REQUEST_METHOD'] + fore.path = path + fore.request_version = request_version + fore.close_connection = 1 + + fore.raw_requestline = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version) + fore.requestline = fore.raw_requestline + + from openerp.service.websrv_lib import AuthRequiredExc, AuthRejectedExc + + def go(): + auth_provider = webdav.auth_provider + if auth_provider and auth_provider.realm: + try: + multi.sec_realms[auth_provider.realm].checkRequest(fore, path) + except AuthRequiredExc, ae: + # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the + # authorisation features of HTTP/1.1 + if request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')): + print 'self.log_error("Cannot require auth at %s", self.request_version)' + multi.send_error(403) + return + #self._get_ignore_body(fore) # consume any body that came, not loose sync with input + multi.send_response(401,'Authorization required') + multi.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm)) + multi.send_header('Connection', 'keep-alive') + multi.send_header('Content-Type','text/html') + multi.send_header('Content-Length', 4) # len(self.auth_required_msg)) + multi.end_headers() + #self.wfile.write(self.auth_required_msg) + multi.shared_body = 'Blah' + return + except AuthRejectedExc,e: + print '("Rejected auth: %s" % e.args[0])' + multi.send_error(403,e.args[0]) + return + mname = 'do_' + fore.command + if not hasattr(fore, mname): + if fore.command == 'OPTIONS': + return return_options(start_response) + multi.send_error(501, "Unsupported method (%r)" % fore.command) + return + method = getattr(fore, mname) + try: + method() + if hasattr(fore, '_flush'): + fore._flush() + response = fore.wfile.getvalue() + class DummySocket(StringIO.StringIO): + """ + This is used to provide a StringIO to httplib.HTTPResponse + which, instead of taking a file object, expects a socket and + uses its makefile() method. + """ + def makefile(self, *args, **kw): + return self + response = httplib.HTTPResponse(DummySocket(response)) + response.begin() + response_headers = response.getheaders() + body = response.read() + start_response(str(response.status) + ' ' + response.reason, response_headers) + return [body] + except (AuthRejectedExc, AuthRequiredExc): + raise + except Exception, e: + multi.send_error(500, "Internal error") + return + res = go() + if res is None: + start_response(multi.shared_response, multi.shared_headers) + return [multi.shared_body] + else: + return res + # WSGI handlers provided by modules loaded with the --load command-line option. module_handlers = [] @@ -121,11 +277,12 @@ def application(environ, start_response): # Try all handlers until one returns some result (i.e. not None). wsgi_handlers = [ - wsgi_xmlrpc, - wsgi_jsonrpc, - legacy_wsgi_xmlrpc, - wsgi_modules, - ] + module_handlers + #wsgi_xmlrpc, + #wsgi_jsonrpc, + #legacy_wsgi_xmlrpc, + #wsgi_modules, + wsgi_webdav + ] #+ module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: @@ -154,11 +311,27 @@ def on_starting(server): global arbiter_pid arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable config = openerp.tools.config - config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc-no-osv-memory' # need a config file + config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' # need a config file + #config['log_level'] = 10 # debug #openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() openerp.service.web_services.start_web_services() + test_in_thread() + +def test_in_thread(): + def f(): + import time + time.sleep(2) + print ">>>> test thread" + cr = openerp.sql_db.db_connect('xx').cursor() + module_name = 'document_webdav' + fp = openerp.tools.file_open('/home/openerp/repos/addons/trunk-xmlrpc/document_webdav/test/webdav_test1.yml') + openerp.tools.convert_yaml_import(cr, module_name, fp, {}, 'update', True) + cr.close() + print "<<<< test thread" + import threading + threading.Thread(target=f).start() # Install our own signal handler on the master process. def when_ready(server): From 1cd7804374f5fc0ee29541ab98fdafffc4c786aa Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 7 Sep 2011 17:42:17 +0200 Subject: [PATCH 20/47] [REF] websrv_lib: moved vdirs mangement code to websrv_lib (from service.http_server). The vdirs list is now a global variable, containing both secure-only and non-necessarily-secure handlers. This makes it possible to register handlers without being too tied to the multi http request handler. The goal is obviously to make it possible to serve those handlers by other means than the multi handler. bzr revid: vmt@openerp.com-20110907154217-f30nrzgvw45ga2q8 --- openerp/service/http_server.py | 50 +----------------------------- openerp/service/websrv_lib.py | 56 ++++++++++++++++++++++++++++------ 2 files changed, 47 insertions(+), 59 deletions(-) diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 3e76c3810ef..8bf40024282 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -154,7 +154,6 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server): try: self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto) - self.server.vdirs = [] self.server.logRequests = True self.server.timeout = self._busywait_timeout logging.getLogger("web-services").info( @@ -191,28 +190,6 @@ class BaseHttpDaemon(threading.Thread, netsvc.Server): res += ", %d threads" % (self.server.numThreads,) return res - def append_svc(self, service): - if not isinstance(service, HTTPDir): - raise Exception("Wrong class for http service") - - pos = len(self.server.vdirs) - lastpos = pos - while pos > 0: - pos -= 1 - if self.server.vdirs[pos].matches(service.path): - lastpos = pos - # we won't break here, but search all way to the top, to - # ensure there is no lesser entry that will shadow the one - # we are inserting. - self.server.vdirs.insert(lastpos, service) - - def list_services(self): - ret = [] - for svc in self.server.vdirs: - ret.append( ( svc.path, str(svc.handler)) ) - - return ret - # No need for these two classes: init_server() below can initialize correctly # directly the BaseHttpDaemon class. class HttpDaemon(BaseHttpDaemon): @@ -245,31 +222,6 @@ def init_servers(): httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''), int(tools.config.get('xmlrpcs_port', 8071))) -def reg_http_service(hts, secure_only = False): - """ Register some handler to httpd. - hts must be an HTTPDir - """ - global httpd, httpsd - - if httpd and not secure_only: - httpd.append_svc(hts) - - if httpsd: - httpsd.append_svc(hts) - - if (not httpd) and (not httpsd): - logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path) - return - -def list_http_services(protocol=None): - global httpd, httpsd - if httpd and (protocol == 'http' or protocol == None): - return httpd.list_services() - elif httpsd and (protocol == 'https' or protocol == None): - return httpsd.list_services() - else: - raise Exception("Incorrect protocol or no http services") - import SimpleXMLRPCServer class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): rpc_paths = [] @@ -305,7 +257,7 @@ def init_xmlrpc(): if tools.config.get('xmlrpcs', False) \ and not tools.config.get('xmlrpc', False): # only register at the secure server - reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler), True) + reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler, secure_only=True)) logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only") class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): diff --git a/openerp/service/websrv_lib.py b/openerp/service/websrv_lib.py index 2e098169bae..5b98714e80e 100644 --- a/openerp/service/websrv_lib.py +++ b/openerp/service/websrv_lib.py @@ -125,13 +125,17 @@ class HTTPHandler(SimpleHTTPRequestHandler): def setup(self): pass +# A list of HTTPDir. +handlers = [] + class HTTPDir: """ A dispatcher class, like a virtual folder in httpd """ - def __init__(self,path,handler, auth_provider = None): + def __init__(self, path, handler, auth_provider=None, secure_only=False): self.path = path self.handler = handler self.auth_provider = auth_provider + self.secure_only = secure_only def matches(self, request): """ Test if some request matches us. If so, return @@ -140,6 +144,42 @@ class HTTPDir: return self.path return False +def reg_http_service(service): + """ Register some handler to httpd. + hts must be an HTTPDir + """ + assert isinstance(service, HTTPDir), "Wrong class for http service" + + global handlers + pos = len(handlers) + lastpos = pos + while pos > 0: + pos -= 1 + if handlers[pos].matches(service.path): + lastpos = pos + # we won't break here, but search all way to the top, to + # ensure there is no lesser entry that will shadow the one + # we are inserting. + handlers.insert(lastpos, service) + +def list_http_services(protocol=None): + global handlers + ret = [] + for svc in handlers: + if protocol is None or protocol == 'http' or svc.secure_only: + ret.append((svc.path, str(svc.handler))) + + return ret + +def find_http_service(path, secure=False): + global handlers + for vdir in handlers: + p = vdir.matches(path) + if p == False or (vdir.secure_only and not secure): + continue + return vdir + return None + class noconnection(object): """ a class to use instead of the real connection """ @@ -408,11 +448,9 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): return self.do_OPTIONS() return - - for vdir in self.server.vdirs: - p = vdir.matches(self.path) - if p == False: - continue + vdir = find_http_service(self.path, self.server.proto == 'HTTPS') + if vdir: + p = vdir.path npath = self.path[len(p):] if not npath.startswith('/'): npath = '/' + npath @@ -433,10 +471,8 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): "client closed connection", self.rlpath.rstrip()) else: raise - return - # if no match: - self.send_error(404, "Path not found: %s" % self.path) - return + else: # no match: + self.send_error(404, "Path not found: %s" % self.path) def _get_ignore_body(self,fore): if not fore.headers.has_key("content-length"): From be827a813a53d0be5d521975ebcb06513b04e6a1 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 8 Sep 2011 11:44:38 +0200 Subject: [PATCH 21/47] [FIX] document_webdav: make sure the headers is a dict. bzr revid: vmt@openerp.com-20110908094438-rpqz2k5xn0pmhigx --- addons/document_webdav/webdav_server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index e23daab9f66..e92c7f1994c 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -174,11 +174,11 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): pass elif self.close_connection == 1: # close header already sent pass - else: - if headers is None: - headers = {} - if self.headers.get('Connection',False) == 'Keep-Alive': - headers['Connection'] = 'keep-alive' + elif headers and self.headers.get('Connection',False) == 'Keep-Alive': + headers['Connection'] = 'keep-alive' + + if headers is None: + headers = {} DAVRequestHandler.send_body(self, DATA, code=code, msg=msg, desc=desc, ctype=ctype, headers=headers) From 17c54e1217ec190bb145a32d7fddc2e53ca1f212 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 8 Sep 2011 11:48:30 +0200 Subject: [PATCH 22/47] [IMP] wsgi: cleaner code, moar comments. bzr revid: vmt@openerp.com-20110908094830-kls32ewruywepjmm --- openerp/wsgi.py | 219 ++++++++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 108 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 422d4a7f9c3..ad36eb14c26 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -39,6 +39,7 @@ import time import openerp import openerp.tools.config as config +import openerp.service.websrv_lib as websrv_lib def xmlrpc_return(start_response, service, method, params): """ Helper to call a service's method with some params, using a @@ -113,9 +114,15 @@ def wsgi_modules(environ, start_response): def wsgi_webdav(environ, start_response): if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*': return return_options(start_response) - if environ['PATH_INFO'].startswith('/webdav'): # TODO depends on config - environ['PATH_INFO'] = '/' + environ['PATH_INFO'][len('/webdav'):] - return wsgi_to_http(environ, start_response) + + # Make sure the addons are loaded in the registry, so they have a chance + # to register themselves in the 'service' layer. + openerp.pooler.get_db_and_pool('xx', update_module=[], pooljobs=False) + + http_dir = websrv_lib.find_http_service(environ['PATH_INFO']) + if http_dir: + environ['PATH_INFO'] = '/' + environ['PATH_INFO'][len(http_dir.path):] + return http_to_wsgi(http_dir)(environ, start_response) def return_options(start_response): # TODO Microsoft specifi header, see websrv_lib do_OPTIONS @@ -123,143 +130,139 @@ def return_options(start_response): start_response("200 OK", [('Content-Length', str(0))] + options) return [] -webdav = None - -def wsgi_to_http(environ, start_response): +def http_to_wsgi(http_dir): """ - Forward a WSGI request to a BaseHTTPRequestHandler. + Turn BaseHTTPRequestHandler into a WSGI entry point. + + Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped + (as a class, so it needs to be instanciated) in a HTTPDir. This code is adapted from wbsrv_lib.MultiHTTPHandler._handle_one_foreign(). It is a temporary solution: the HTTP sub-handlers (in particular the document_webdav addon) have to be WSGIfied. """ - global webdav - # Make sure the addons are loaded in the registry, so they have a chance - # to register themselves in the 'service' layer. - openerp.pooler.get_db_and_pool('xx', update_module=[], pooljobs=False) + def wsgi_handler(environ, start_response): - scheme = environ['wsgi.url_scheme'] + # Extract from the WSGI environment the necessary data. + scheme = environ['wsgi.url_scheme'] - headers = {} - for key, value in environ.items(): - if key.startswith('HTTP_'): - key = key[5:].replace('_', '-').title() - headers[key] = value - if key == 'CONTENT_LENGTH': - key = key.replace('_', '-').title() - headers[key] = value - if environ.get('Content-Type'): - headers['Content-Type'] = environ['Content-Type'] + headers = {} + for key, value in environ.items(): + if key.startswith('HTTP_'): + key = key[5:].replace('_', '-').title() + headers[key] = value + if key == 'CONTENT_LENGTH': + key = key.replace('_', '-').title() + headers[key] = value + if environ.get('Content-Type'): + headers['Content-Type'] = environ['Content-Type'] - path = urllib.quote(environ.get('PATH_INFO', '')) - if environ.get('QUERY_STRING'): - path += '?' + environ['QUERY_STRING'] + path = urllib.quote(environ.get('PATH_INFO', '')) + if environ.get('QUERY_STRING'): + path += '?' + environ['QUERY_STRING'] - class Dummy(): - pass - server = Dummy() - server.server_name = environ['SERVER_NAME'] - server.server_port = int(environ['SERVER_PORT']) - con = openerp.service.websrv_lib.noconnection(environ['gunicorn.socket']) # None - fore = webdav.handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) + request_version = 'HTTP/1.1' # TODO + request_line = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version) - # let's pretend we are a Multi handler - class M(): - def __init__(self): - self.sec_realms = {} - self.shared_headers = [] - self.shared_response = '' - self.shared_body = '' - def send_error(self, code, msg): - self.shared_response = str(code) + ' ' + msg - def send_response(self, code, msg): - self.shared_response = str(code) + ' ' + msg - def send_header(self, a, b): - self.shared_headers.append((a, b)) - def end_headers(self, *args, **kwargs): + class Dummy(object): pass - multi = M() + # Let's pretend we have a server to hand to the handler. + server = Dummy() + server.server_name = environ['SERVER_NAME'] + server.server_port = int(environ['SERVER_PORT']) + con = openerp.service.websrv_lib.noconnection(environ['gunicorn.socket']) # None TODO - webdav.auth_provider.setupAuth(multi, fore) + # Let's pretend we have a Multi handler (i.e. the + # websrv_lib.MultiHTTPHandler class) to hand to the auth. provider. + multi = Dummy() + multi.sec_realms = {} - request_version = 'HTTP/1.1' # TODO - fore.wfile = StringIO.StringIO() - fore.rfile = environ['wsgi.input'] - fore.headers = headers - fore.command = environ['REQUEST_METHOD'] - fore.path = path - fore.request_version = request_version - fore.close_connection = 1 + # Initialize the underlying handler and associated auth. provider. + handler = http_dir.handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) + auth_provider = http_dir.auth_provider + auth_provider.setupAuth(multi, handler) - fore.raw_requestline = "%s %s %s\n" % (environ['REQUEST_METHOD'], path, request_version) - fore.requestline = fore.raw_requestline + # Populate the handler as if it is called by a regular HTTP server + # and the request is already parsed. + handler.wfile = StringIO.StringIO() + handler.rfile = environ['wsgi.input'] + handler.headers = headers + handler.command = environ['REQUEST_METHOD'] + handler.path = path + handler.request_version = request_version + handler.close_connection = 1 + handler.raw_requestline = request_line + handler.requestline = request_line - from openerp.service.websrv_lib import AuthRequiredExc, AuthRejectedExc - - def go(): - auth_provider = webdav.auth_provider + # Handle authentication if there is an auth. provider associated to + # the handler. if auth_provider and auth_provider.realm: try: - multi.sec_realms[auth_provider.realm].checkRequest(fore, path) - except AuthRequiredExc, ae: + multi.sec_realms[auth_provider.realm].checkRequest(handler, path) + except websrv_lib.AuthRequiredExc, ae: # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the # authorisation features of HTTP/1.1 - if request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')): + if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')): print 'self.log_error("Cannot require auth at %s", self.request_version)' - multi.send_error(403) - return - #self._get_ignore_body(fore) # consume any body that came, not loose sync with input - multi.send_response(401,'Authorization required') - multi.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm)) - multi.send_header('Connection', 'keep-alive') - multi.send_header('Content-Type','text/html') - multi.send_header('Content-Length', 4) # len(self.auth_required_msg)) - multi.end_headers() - #self.wfile.write(self.auth_required_msg) - multi.shared_body = 'Blah' - return - except AuthRejectedExc,e: + start_response("403 Forbidden", []) + return [] + start_response("401 Authorization required", [ + ('WWW-Authenticate', '%s realm="%s"' % (ae.atype,ae.realm)), + # ('Connection', 'keep-alive'), + ('Content-Type', 'text/html'), + ('Content-Length', 4), # len(self.auth_required_msg) + ]) + return ['Blah'] # self.auth_required_msg + except websrv_lib.AuthRejectedExc,e: print '("Rejected auth: %s" % e.args[0])' - multi.send_error(403,e.args[0]) - return - mname = 'do_' + fore.command - if not hasattr(fore, mname): - if fore.command == 'OPTIONS': + start_response("403 %s" % (e.args[0],)) + return [] + + method_name = 'do_' + handler.command + + # Support the OPTIONS method even when not provided directly by the + # handler. TODO I would prefer to remove it and fix the handler if + # needed. + if not hasattr(handler, method_name): + if handler.command == 'OPTIONS': return return_options(start_response) - multi.send_error(501, "Unsupported method (%r)" % fore.command) - return - method = getattr(fore, mname) + start_response("501 Unsupported method (%r)" % handler.command) + return [] + + # Finally, call the handler's method. try: + method = getattr(handler, method_name) method() - if hasattr(fore, '_flush'): - fore._flush() - response = fore.wfile.getvalue() - class DummySocket(StringIO.StringIO): - """ - This is used to provide a StringIO to httplib.HTTPResponse - which, instead of taking a file object, expects a socket and - uses its makefile() method. - """ - def makefile(self, *args, **kw): - return self - response = httplib.HTTPResponse(DummySocket(response)) - response.begin() + # The DAV handler buffers its output and provides a _flush() + # method. + getattr(handler, '_flush', lambda: None)() + response = parse_http_response(handler.wfile.getvalue()) response_headers = response.getheaders() body = response.read() start_response(str(response.status) + ' ' + response.reason, response_headers) return [body] - except (AuthRejectedExc, AuthRequiredExc): + except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc): raise except Exception, e: - multi.send_error(500, "Internal error") - return - res = go() - if res is None: - start_response(multi.shared_response, multi.shared_headers) - return [multi.shared_body] - else: - return res + start_response("500 Internal error") + return [] + + return wsgi_handler + +def parse_http_response(s): + """ Turn a HTTP response string into a httplib.HTTPResponse object.""" + class DummySocket(StringIO.StringIO): + """ + This is used to provide a StringIO to httplib.HTTPResponse + which, instead of taking a file object, expects a socket and + uses its makefile() method. + """ + def makefile(self, *args, **kw): + return self + response = httplib.HTTPResponse(DummySocket(s)) + response.begin() + return response # WSGI handlers provided by modules loaded with the --load command-line option. module_handlers = [] From 1a40e512944c8562bc3e2ecb4d742e990b6e8720 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 8 Sep 2011 14:31:15 +0200 Subject: [PATCH 23/47] [REF] document_webdav: reg_http_service accepts the HTTPDir arguments, renamed auth_proxy to auth_provider. bzr revid: vmt@openerp.com-20110908123115-meorb9rnc2j0rb40 --- addons/document_webdav/dav_fs.py | 8 ++++---- addons/document_webdav/webdav_server.py | 13 +++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/addons/document_webdav/dav_fs.py b/addons/document_webdav/dav_fs.py index 5e01bf6a66c..3447da9befd 100644 --- a/addons/document_webdav/dav_fs.py +++ b/addons/document_webdav/dav_fs.py @@ -454,7 +454,7 @@ class openerp_dav_handler(dav_interface): def get_cr(self, uri, allow_last=False): """ Split the uri, grab a cursor for that db """ - pdb = self.parent.auth_proxy.last_auth + pdb = self.parent.auth_provider.last_auth dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last) uri2 = (uri2 and uri2.split('/')) or [] if not dbname: @@ -462,10 +462,10 @@ class openerp_dav_handler(dav_interface): # if dbname was in our uri, we should have authenticated # against that. assert pdb == dbname, " %s != %s" %(pdb, dbname) - res = self.parent.auth_proxy.auth_creds.get(dbname, False) + res = self.parent.auth_provider.auth_creds.get(dbname, False) if not res: - self.parent.auth_proxy.checkRequest(self.parent, uri, dbname) - res = self.parent.auth_proxy.auth_creds[dbname] + self.parent.auth_provider.checkRequest(self.parent, uri, dbname) + res = self.parent.auth_provider.auth_creds[dbname] user, passwd, dbn2, uid = res db,pool = pooler.get_db_and_pool(dbname) cr = db.cursor() diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index e92c7f1994c..eb4ab86fc1e 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -40,7 +40,7 @@ from dav_fs import openerp_dav_handler from tools.config import config from DAV.WebDAVServer import DAVRequestHandler from service import http_server -from service.websrv_lib import HTTPDir, FixSendError, HttpOptions +from service.websrv_lib import FixSendError, HttpOptions from BaseHTTPServer import BaseHTTPRequestHandler import urlparse import urllib @@ -572,7 +572,7 @@ try: conf = OpenDAVConfig(**_dc) handler._config = conf - reg_http_service(HTTPDir(directory,DAVHandler,DAVAuthProvider())) + reg_http_service(directory, DAVHandler, DAVAuthProvider()) logging.getLogger('webdav').info("WebDAV service registered at path: %s/ "% directory) if not (config.get_misc('webdav', 'no_root_hack', False)): @@ -592,9 +592,7 @@ try: # the StaticHttpHandler can find its dir_path. config.misc.setdefault('static-http',{})['dir_path'] = dir_path - if reg_http_service(HTTPDir('/', DAVStaticHandler)): - logging.getLogger("web-services").info("WebDAV registered HTTP dir %s for /" % \ - (dir_path)) + reg_http_service('/', DAVStaticHandler) except Exception, e: logging.getLogger('webdav').error('Cannot launch webdav: %s' % e) @@ -613,8 +611,7 @@ def init_well_known(): reps['/'+uri] = path if int(num_svcs): - if http_server.reg_http_service(HTTPDir('/.well-known', RedirectHTTPHandler)): - logging.getLogger("web-services").info("Registered HTTP redirect handler at /.well-known" ) + reg_http_service('/.well-known', RedirectHTTPHandler) init_well_known() @@ -641,7 +638,7 @@ def init_principals_redirect(): dbname = config.get('db_name', False) if dbname: PrincipalsRedirect.redirect_paths[''] = '/webdav/%s/principals' % dbname - reg_http_service(HTTPDir('/principals', PrincipalsRedirect)) + reg_http_service('/principals', PrincipalsRedirect) logging.getLogger("web-services").info( "Registered HTTP redirect handler for /principals to the %s db.", dbname) From e643ffcc32485723cf0aaaee990bf7985cf9fce1 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 8 Sep 2011 14:38:18 +0200 Subject: [PATCH 24/47] [REF] websrv_lib: - grouped together the concept of auth proxy and auth provider - removed the sec_realms dictionary: it was redundant as the auth proxy was set (in one of the implementation, not all) on the handler by setupAuth. Now the auth proxy is always set on the handler. - reg_http_service accepts the argument of the HTTPDir constructor. bzr revid: vmt@openerp.com-20110908123818-suc53zebgv9kcy3c --- openerp/service/http_server.py | 94 ++++++++++++++------------------- openerp/service/web_services.py | 2 +- openerp/service/websrv_lib.py | 73 ++++++------------------- openerp/wsgi.py | 15 ++---- 4 files changed, 62 insertions(+), 122 deletions(-) diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 8bf40024282..9bfb9397d19 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -250,14 +250,14 @@ class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.Simple def init_xmlrpc(): if tools.config.get('xmlrpc', False): # Example of http file serving: - # reg_http_service(HTTPDir('/test/',HTTPHandler)) - reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler)) + # reg_http_service('/test/', HTTPHandler) + reg_http_service('/xmlrpc/', XMLRPCRequestHandler) logging.getLogger("web-services").info("Registered XML-RPC over HTTP") if tools.config.get('xmlrpcs', False) \ and not tools.config.get('xmlrpc', False): # only register at the secure server - reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler, secure_only=True)) + reg_http_service('/xmlrpc/', XMLRPCRequestHandler, secure_only=True) logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only") class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): @@ -299,65 +299,21 @@ def init_static_http(): base_path = tools.config.get('static_http_url_prefix', '/') - reg_http_service(HTTPDir(base_path,StaticHTTPHandler)) + reg_http_service(base_path, StaticHTTPHandler) logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \ (document_root, base_path)) -class OerpAuthProxy(AuthProxy): - """ Require basic authentication.. +import security - This is a copy of the BasicAuthProxy, which however checks/caches the db - as well. - """ - def __init__(self,provider): - AuthProxy.__init__(self,provider) +class OpenERPAuthProvider(AuthProvider): + """ Require basic authentication.""" + def __init__(self,realm='OpenERP User'): + self.realm = realm self.auth_creds = {} self.auth_tries = 0 self.last_auth = None - def checkRequest(self,handler,path, db=False): - auth_str = handler.headers.get('Authorization',False) - try: - if not db: - db = handler.get_db_from_path(path) - except Exception: - if path.startswith('/'): - path = path[1:] - psp= path.split('/') - if len(psp)>1: - db = psp[0] - else: - #FIXME! - self.provider.log("Wrong path: %s, failing auth" %path) - raise AuthRejectedExc("Authorization failed. Wrong sub-path.") - if self.auth_creds.get(db): - return True - if auth_str and auth_str.startswith('Basic '): - auth_str=auth_str[len('Basic '):] - (user,passwd) = base64.decodestring(auth_str).split(':') - self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db)) - acd = self.provider.authenticate(db,user,passwd,handler.client_address) - if acd != False: - self.auth_creds[db] = acd - self.last_auth = db - return True - if self.auth_tries > 5: - self.provider.log("Failing authorization after 5 requests w/o password") - raise AuthRejectedExc("Authorization failed.") - self.auth_tries += 1 - raise AuthRequiredExc(atype='Basic', realm=self.provider.realm) - -import security -class OpenERPAuthProvider(AuthProvider): - def __init__(self,realm='OpenERP User'): - self.realm = realm - - def setupAuth(self, multi, handler): - if not multi.sec_realms.has_key(self.realm): - multi.sec_realms[self.realm] = OerpAuthProxy(self) - handler.auth_proxy = multi.sec_realms[self.realm] - def authenticate(self, db, user, passwd, client_address): try: uid = security.login(db,user,passwd) @@ -371,4 +327,36 @@ class OpenERPAuthProvider(AuthProvider): def log(self, msg, lvl=logging.INFO): logging.getLogger("auth").log(lvl,msg) + def checkRequest(self,handler,path, db=False): + auth_str = handler.headers.get('Authorization',False) + try: + if not db: + db = handler.get_db_from_path(path) + except Exception: + if path.startswith('/'): + path = path[1:] + psp= path.split('/') + if len(psp)>1: + db = psp[0] + else: + #FIXME! + self.log("Wrong path: %s, failing auth" %path) + raise AuthRejectedExc("Authorization failed. Wrong sub-path.") + if self.auth_creds.get(db): + return True + if auth_str and auth_str.startswith('Basic '): + auth_str=auth_str[len('Basic '):] + (user,passwd) = base64.decodestring(auth_str).split(':') + self.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db)) + acd = self.authenticate(db,user,passwd,handler.client_address) + if acd != False: + self.auth_creds[db] = acd + self.last_auth = db + return True + if self.auth_tries > 5: + self.log("Failing authorization after 5 requests w/o password") + raise AuthRejectedExc("Authorization failed.") + self.auth_tries += 1 + raise AuthRequiredExc(atype='Basic', realm=self.realm) + #eof diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 8d05b5b776d..c941d3a809f 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -379,7 +379,7 @@ class common(netsvc.ExportService): return res or False elif method == 'logout': if auth: - auth.logout(params[1]) + auth.logout(params[1]) # TODO I didn't see any AuthProxy implementing this method. logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db)) return True elif method in ['about', 'timezone_get', 'get_server_environment', diff --git a/openerp/service/websrv_lib.py b/openerp/service/websrv_lib.py index 5b98714e80e..ac83cb893c0 100644 --- a/openerp/service/websrv_lib.py +++ b/openerp/service/websrv_lib.py @@ -52,61 +52,17 @@ class AuthProvider: def __init__(self,realm): self.realm = realm - def setupAuth(self, multi,handler): - """ Attach an AuthProxy object to handler - """ - pass - def authenticate(self, user, passwd, client_address): return False def log(self, msg): print msg -class BasicAuthProvider(AuthProvider): - def setupAuth(self, multi, handler): - if not multi.sec_realms.has_key(self.realm): - multi.sec_realms[self.realm] = BasicAuthProxy(self) - - -class AuthProxy: - """ This class will hold authentication information for a handler, - i.e. a connection - """ - def __init__(self, provider): - self.provider = provider - def checkRequest(self,handler,path = '/'): """ Check if we are allowed to process that request """ pass -class BasicAuthProxy(AuthProxy): - """ Require basic authentication.. - """ - def __init__(self,provider): - AuthProxy.__init__(self,provider) - self.auth_creds = None - self.auth_tries = 0 - - def checkRequest(self,handler,path = '/'): - if self.auth_creds: - return True - auth_str = handler.headers.get('Authorization',False) - if auth_str and auth_str.startswith('Basic '): - auth_str=auth_str[len('Basic '):] - (user,passwd) = base64.decodestring(auth_str).split(':') - self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd)) - self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address) - if self.auth_creds: - return True - if self.auth_tries > 5: - self.provider.log("Failing authorization after 5 requests w/o password") - raise AuthRejectedExc("Authorization failed.") - self.auth_tries += 1 - raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm) - - class HTTPHandler(SimpleHTTPRequestHandler): def __init__(self,request, client_address, server): SimpleHTTPRequestHandler.__init__(self,request,client_address,server) @@ -144,13 +100,19 @@ class HTTPDir: return self.path return False -def reg_http_service(service): - """ Register some handler to httpd. - hts must be an HTTPDir + def instanciate_handler(self, request, client_address, server): + handler = self.handler(noconnection(request), client_address, server) + if self.auth_provider: + handler.auth_provider = self.auth_provider + return handler + +def reg_http_service(path, handler, auth_provider=None, secure_only=False): + """ Register a HTTP handler at a given path. + + The auth_provider will be set on the handler instances. """ - assert isinstance(service, HTTPDir), "Wrong class for http service" - global handlers + service = HTTPDir(path, handler, auth_provider, secure_only) pos = len(handlers) lastpos = pos while pos > 0: @@ -286,11 +248,10 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): def __init__(self, request, client_address, server): self.in_handlers = {} - self.sec_realms = {} SocketServer.StreamRequestHandler.__init__(self,request,client_address,server) self.log_message("MultiHttpHandler init for %s" %(str(client_address))) - def _handle_one_foreign(self,fore, path, auth_provider): + def _handle_one_foreign(self, fore, path): """ This method overrides the handle_one_request for *children* handlers. It is required, since the first line should not be read again.. @@ -306,9 +267,9 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): return self.request_version = fore.request_version - if auth_provider and auth_provider.realm: + if hasattr(fore, 'auth_provider'): try: - self.sec_realms[auth_provider.realm].checkRequest(fore,path) + fore.auth_provider.checkRequest(fore,path) except AuthRequiredExc,ae: # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the # authorisation features of HTTP/1.1 @@ -456,15 +417,13 @@ class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): npath = '/' + npath if not self.in_handlers.has_key(p): - self.in_handlers[p] = vdir.handler(noconnection(self.request),self.client_address,self.server) - if vdir.auth_provider: - vdir.auth_provider.setupAuth(self, self.in_handlers[p]) + self.in_handlers[p] = vdir.instanciate_handler(noconnection(self.request),self.client_address,self.server) hnd = self.in_handlers[p] hnd.rfile = self.rfile hnd.wfile = self.wfile self.rlpath = self.raw_requestline try: - self._handle_one_foreign(hnd,npath, vdir.auth_provider) + self._handle_one_foreign(hnd, npath) except IOError, e: if e.errno == errno.EPIPE: self.log_message("Could not complete request %s," \ diff --git a/openerp/wsgi.py b/openerp/wsgi.py index ad36eb14c26..3519cdf4090 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -173,15 +173,8 @@ def http_to_wsgi(http_dir): server.server_port = int(environ['SERVER_PORT']) con = openerp.service.websrv_lib.noconnection(environ['gunicorn.socket']) # None TODO - # Let's pretend we have a Multi handler (i.e. the - # websrv_lib.MultiHTTPHandler class) to hand to the auth. provider. - multi = Dummy() - multi.sec_realms = {} - # Initialize the underlying handler and associated auth. provider. - handler = http_dir.handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) - auth_provider = http_dir.auth_provider - auth_provider.setupAuth(multi, handler) + handler = http_dir.instanciate_handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) # Populate the handler as if it is called by a regular HTTP server # and the request is already parsed. @@ -197,9 +190,9 @@ def http_to_wsgi(http_dir): # Handle authentication if there is an auth. provider associated to # the handler. - if auth_provider and auth_provider.realm: + if hasattr(handler, 'auth_provider'): try: - multi.sec_realms[auth_provider.realm].checkRequest(handler, path) + handler.auth_provider.checkRequest(handler, path) except websrv_lib.AuthRequiredExc, ae: # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the # authorisation features of HTTP/1.1 @@ -245,7 +238,7 @@ def http_to_wsgi(http_dir): except (websrv_lib.AuthRejectedExc, websrv_lib.AuthRequiredExc): raise except Exception, e: - start_response("500 Internal error") + start_response("500 Internal error", []) return [] return wsgi_handler From 5b20065d7833f58e0cbf05c2c4573ec45a9f77bc Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 8 Sep 2011 17:37:00 +0200 Subject: [PATCH 25/47] [FIX] wsgi: missing argument to start_response, add "/" prefix only when needed. bzr revid: vmt@openerp.com-20110908153700-ppfqhf7v5l9s3m65 --- openerp/wsgi.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 3519cdf4090..6e9dd5e222f 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -121,7 +121,11 @@ def wsgi_webdav(environ, start_response): http_dir = websrv_lib.find_http_service(environ['PATH_INFO']) if http_dir: - environ['PATH_INFO'] = '/' + environ['PATH_INFO'][len(http_dir.path):] + path = environ['PATH_INFO'][len(http_dir.path):] + if path.startswith('/'): + environ['PATH_INFO'] = path + else: + environ['PATH_INFO'] = '/' + npath return http_to_wsgi(http_dir)(environ, start_response) def return_options(start_response): @@ -209,7 +213,7 @@ def http_to_wsgi(http_dir): return ['Blah'] # self.auth_required_msg except websrv_lib.AuthRejectedExc,e: print '("Rejected auth: %s" % e.args[0])' - start_response("403 %s" % (e.args[0],)) + start_response("403 %s" % (e.args[0],), []) return [] method_name = 'do_' + handler.command @@ -220,7 +224,7 @@ def http_to_wsgi(http_dir): if not hasattr(handler, method_name): if handler.command == 'OPTIONS': return return_options(start_response) - start_response("501 Unsupported method (%r)" % handler.command) + start_response("501 Unsupported method (%r)" % handler.command, []) return [] # Finally, call the handler's method. From 4a1597ab16924e613b721a305f8b6d3df51cb673 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 9 Sep 2011 09:31:45 +0200 Subject: [PATCH 26/47] [FIX] typo, removed wsg_modules entry point (redundant with "module_handlers"). bzr revid: vmt@openerp.com-20110909073145-vqnjhdu0xwb8wurt --- openerp/wsgi.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 6e9dd5e222f..5e4e6288388 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -107,10 +107,6 @@ def legacy_wsgi_xmlrpc(environ, start_response): def wsgi_jsonrpc(environ, start_response): pass -def wsgi_modules(environ, start_response): - """ WSGI handler dispatching to addons-provided entry points.""" - pass - def wsgi_webdav(environ, start_response): if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*': return return_options(start_response) @@ -125,7 +121,7 @@ def wsgi_webdav(environ, start_response): if path.startswith('/'): environ['PATH_INFO'] = path else: - environ['PATH_INFO'] = '/' + npath + environ['PATH_INFO'] = '/' + path return http_to_wsgi(http_dir)(environ, start_response) def return_options(start_response): @@ -280,7 +276,6 @@ def application(environ, start_response): #wsgi_xmlrpc, #wsgi_jsonrpc, #legacy_wsgi_xmlrpc, - #wsgi_modules, wsgi_webdav ] #+ module_handlers for handler in wsgi_handlers: @@ -312,12 +307,13 @@ def on_starting(server): arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable config = openerp.tools.config config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' # need a config file - #config['log_level'] = 10 # debug + config['static_http_document_root'] = '/tmp' + config['log_level'] = 10 # debug #openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() openerp.service.web_services.start_web_services() - test_in_thread() + #test_in_thread() def test_in_thread(): def f(): From 77fbcb3c79743e151656cf2ad2e93d3d61b5b655 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 9 Sep 2011 14:28:56 +0200 Subject: [PATCH 27/47] [FIX] websrv_lib: the auth provider was instanciated once and the auth proxy was instanciated multiple times (once per handler instcance). The previous refactoring merged the proxy and the provider in one class which once instanciated once. Now it is instanciated once per handler, which is better. bzr revid: vmt@openerp.com-20110909122856-29uhsjfe193fqdi8 --- openerp/service/websrv_lib.py | 4 ++-- openerp/wsgi.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/service/websrv_lib.py b/openerp/service/websrv_lib.py index ac83cb893c0..1efab672eb3 100644 --- a/openerp/service/websrv_lib.py +++ b/openerp/service/websrv_lib.py @@ -103,13 +103,13 @@ class HTTPDir: def instanciate_handler(self, request, client_address, server): handler = self.handler(noconnection(request), client_address, server) if self.auth_provider: - handler.auth_provider = self.auth_provider + handler.auth_provider = self.auth_provider() return handler def reg_http_service(path, handler, auth_provider=None, secure_only=False): """ Register a HTTP handler at a given path. - The auth_provider will be set on the handler instances. + The auth_provider will be instanciated and set on the handler instances. """ global handlers service = HTTPDir(path, handler, auth_provider, secure_only) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 5e4e6288388..71694dee594 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -308,7 +308,7 @@ def on_starting(server): config = openerp.tools.config config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' # need a config file config['static_http_document_root'] = '/tmp' - config['log_level'] = 10 # debug + #config['log_level'] = 10 # debug #openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() From 6ba60770b84671d63cac494909032a361f17dd5b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 9 Sep 2011 14:30:05 +0200 Subject: [PATCH 28/47] [FIX] websrv_lib: commit associated to a change in the server: revno 3548, revid vmt@openerp.com-20110909122856-29uhsjfe193fqdi8 bzr revid: vmt@openerp.com-20110909123005-aqbixbe7xj0nwlc9 --- addons/document_webdav/webdav_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index eb4ab86fc1e..aeff0a23f25 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -572,7 +572,7 @@ try: conf = OpenDAVConfig(**_dc) handler._config = conf - reg_http_service(directory, DAVHandler, DAVAuthProvider()) + reg_http_service(directory, DAVHandler, DAVAuthProvider) logging.getLogger('webdav').info("WebDAV service registered at path: %s/ "% directory) if not (config.get_misc('webdav', 'no_root_hack', False)): From 995a037f2d218a9443f8db226b7b135979f9d3e2 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 12 Sep 2011 12:21:47 +0200 Subject: [PATCH 29/47] [IMP] wsgi: set the config via gunicorn config file. bzr revid: vmt@openerp.com-20110912102147-yxgv9hpm4fvoxqn6 --- gunicorn.conf.py | 7 +++++++ openerp-server | 6 +++--- openerp/wsgi.py | 11 ++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 0c1709dbed5..7b117c040f6 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -8,3 +8,10 @@ workers = 1 on_starting = openerp.wsgi.on_starting when_ready = openerp.wsgi.when_ready timeout = 240 # openerp request-response cycle can be quite long + +# Setting openerp.conf.xxx will be better than setting +# openerp.tools.config['xxx'] +conf = openerp.tools.config +conf['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' +conf['static_http_document_root'] = '/tmp' +#conf['log_level'] = 10 # 10 is DEBUG diff --git a/openerp-server b/openerp-server index 7965a0c073d..7b734e2ef1a 100755 --- a/openerp-server +++ b/openerp-server @@ -271,9 +271,9 @@ if __name__ == "__main__": if info['wsgi']: openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) - openerp.osv.osv.start_object_proxy() - openerp.service.web_services.start_web_services() - openerp.wsgi.serve() + #openerp.osv.osv.start_object_proxy() + #openerp.service.web_services.start_web_services() + #openerp.wsgi.serve() setup_pid_file() diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 71694dee594..2bfb793f58b 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -273,11 +273,11 @@ def application(environ, start_response): # Try all handlers until one returns some result (i.e. not None). wsgi_handlers = [ - #wsgi_xmlrpc, - #wsgi_jsonrpc, - #legacy_wsgi_xmlrpc, + wsgi_xmlrpc, + wsgi_jsonrpc, + legacy_wsgi_xmlrpc, wsgi_webdav - ] #+ module_handlers + ] + module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: @@ -306,9 +306,6 @@ def on_starting(server): global arbiter_pid arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable config = openerp.tools.config - config['addons_path'] = '/home/openerp/repos/addons/trunk-xmlrpc' # need a config file - config['static_http_document_root'] = '/tmp' - #config['log_level'] = 10 # debug #openerp.tools.cache = kill_workers_cache openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() From 46ad605226d79bf3a5dd09d2c344ec1fa284d1a3 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 14 Sep 2011 12:43:00 +0200 Subject: [PATCH 30/47] [IMP] wsgi: - added Microsoft specific header for webdav. - serve WSGI handlers with werkzeug when available. - effectively use WSGI instead of netsvc HTTP stack. bzr revid: vmt@openerp.com-20110914104300-n0l3dnmdu3jau7w2 --- openerp-server | 19 ++++------- openerp/modules/module.py | 3 +- openerp/tools/yaml_import.py | 17 ++++++++++ openerp/wsgi.py | 66 ++++++++++++++++++------------------ 4 files changed, 59 insertions(+), 46 deletions(-) diff --git a/openerp-server b/openerp-server index b9404c7361e..e01f0f385c2 100755 --- a/openerp-server +++ b/openerp-server @@ -145,10 +145,7 @@ def start_services(): # Export (for RPC) services. openerp.service.web_services.start_web_services() - # Initialize the HTTP stack. - http_server.init_servers() - http_server.init_xmlrpc() - http_server.init_static_http() + # Start the NET-RPC server. netrpc_server.init_servers() # Start the main cron thread. @@ -157,6 +154,9 @@ def start_services(): # Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC). openerp.netsvc.Server.startAll() + # Start the WSGI server. + threading.Thread(target=openerp.wsgi.serve).start() + # Variable keeping track of the number of calls to the signal handler defined # below. This variable is monitored by ``quit_on_signals()``. quit_signals_received = 0 @@ -268,15 +268,10 @@ if __name__ == "__main__": for m in openerp.conf.server_wide_modules: __import__(m) - # Register a WSGI entry point if any. + # Call any post_load hook. info = openerp.modules.module.load_information_from_description_file(m) - if info['wsgi']: - openerp.wsgi.register_wsgi_handler(getattr(sys.modules[m], info['wsgi'])) - - #openerp.osv.osv.start_object_proxy() - #openerp.service.web_services.start_web_services() - #openerp.wsgi.serve() - + if info['post_load']: + getattr(sys.modules[m], info['post_load'])() setup_pid_file() logger = logging.getLogger('server') diff --git a/openerp/modules/module.py b/openerp/modules/module.py index cd5e6537a6d..a33c2651a85 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -254,7 +254,8 @@ def load_information_from_description_file(module): info['license'] = info.get('license') or 'AGPL-3' info.setdefault('installable', True) info.setdefault('active', False) - info.setdefault('wsgi', None) # WSGI entry point, given as a string + # If the following is provided, it is called after the module is --loaded. + info.setdefault('post_load', None) for kind in ['data', 'demo', 'test', 'init_xml', 'update_xml', 'demo_xml']: info.setdefault(kind, []) diff --git a/openerp/tools/yaml_import.py b/openerp/tools/yaml_import.py index 00bb79f0499..81fec089fe1 100644 --- a/openerp/tools/yaml_import.py +++ b/openerp/tools/yaml_import.py @@ -1,10 +1,12 @@ # -*- encoding: utf-8 -*- +import threading import types import time # used to eval time.strftime expressions from datetime import datetime, timedelta import logging import openerp.pooler as pooler +import openerp.sql_db as sql_db import misc from config import config import yaml_tag @@ -800,4 +802,19 @@ def yaml_import(cr, module, yamlfile, idref=None, mode='init', noupdate=False): # keeps convention of convert.py convert_yaml_import = yaml_import +def threaded_yaml_import(db_name, module_name, file_name, delay=0): + def f(): + time.sleep(delay) + cr = None + fp = None + try: + cr = sql_db.db_connect(db_name).cursor() + fp = misc.file_open(file_name) + convert_yaml_import(cr, module_name, fp, {}, 'update', True) + finally: + if cr: cr.close() + if fp: fp.close() + threading.Thread(target=f).start() + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 2bfb793f58b..9f374acb374 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -25,13 +25,12 @@ This module offers a WSGI interface to OpenERP. """ -from wsgiref.simple_server import make_server -from SimpleXMLRPCServer import SimpleXMLRPCDispatcher import httplib import urllib import xmlrpclib import StringIO +import logging import os import signal import sys @@ -109,11 +108,7 @@ def wsgi_jsonrpc(environ, start_response): def wsgi_webdav(environ, start_response): if environ['REQUEST_METHOD'] == 'OPTIONS' and environ['PATH_INFO'] == '*': - return return_options(start_response) - - # Make sure the addons are loaded in the registry, so they have a chance - # to register themselves in the 'service' layer. - openerp.pooler.get_db_and_pool('xx', update_module=[], pooljobs=False) + return return_options(environ, start_response) http_dir = websrv_lib.find_http_service(environ['PATH_INFO']) if http_dir: @@ -124,15 +119,20 @@ def wsgi_webdav(environ, start_response): environ['PATH_INFO'] = '/' + path return http_to_wsgi(http_dir)(environ, start_response) -def return_options(start_response): - # TODO Microsoft specifi header, see websrv_lib do_OPTIONS - options = [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')] +def return_options(environ, start_response): + # Microsoft specific header, see + # http://www.ibm.com/developerworks/rational/library/2089.html + if 'Microsoft' in environ.get('User-Agent', ''): + option = [('MS-Author-Via', 'DAV')] + else: + option = [] + options += [('DAV', '1 2'), ('Allow', 'GET HEAD PROPFIND OPTIONS REPORT')] start_response("200 OK", [('Content-Length', str(0))] + options) return [] def http_to_wsgi(http_dir): """ - Turn BaseHTTPRequestHandler into a WSGI entry point. + Turn a BaseHTTPRequestHandler into a WSGI entry point. Actually the argument is not a bare BaseHTTPRequestHandler but is wrapped (as a class, so it needs to be instanciated) in a HTTPDir. @@ -219,7 +219,7 @@ def http_to_wsgi(http_dir): # needed. if not hasattr(handler, method_name): if handler.command == 'OPTIONS': - return return_options(start_response) + return return_options(environ, start_response) start_response("501 Unsupported method (%r)" % handler.command, []) return [] @@ -257,7 +257,7 @@ def parse_http_response(s): response.begin() return response -# WSGI handlers provided by modules loaded with the --load command-line option. +# WSGI handlers registered through the register_wsgi_handler() function below. module_handlers = [] def register_wsgi_handler(handler): @@ -284,17 +284,32 @@ def application(environ, start_response): continue return result - # We never returned from the loop. Needs something else than 200 OK. + # We never returned from the loop. response = 'No handler found.\n' - start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) + start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) return [response] def serve(): - """ Serve XMLRPC requests via wsgiref's simple_server. + """ Serve XMLRPC requests via werkzeug development server. - Blocking, should probably be called in its own process. + If werkzeug can not be imported, we fall back to wsgiref's simple_server. + + Calling this function is blocking, you might want to call it in its own + thread. """ - httpd = make_server('localhost', config['xmlrpc_port'], application) + + # TODO Change the xmlrpc_port option to http_port. + try: + import werkzeug.serving + httpd = werkzeug.serving.make_server('localhost', + config['xmlrpc_port'], application, threaded=True) + except ImportError, e: + import wsgiref.simple_server + logging.getLogger('wsgi').warn('Can not import werkzeug, ' + 'falling back to wsgiref.') + httpd = wsgiref.simple_server.make_server('localhost', + config['xmlrpc_port'], application) + httpd.serve_forever() # Master process id, can be used for signaling. @@ -310,21 +325,6 @@ def on_starting(server): openerp.netsvc.init_logger() openerp.osv.osv.start_object_proxy() openerp.service.web_services.start_web_services() - #test_in_thread() - -def test_in_thread(): - def f(): - import time - time.sleep(2) - print ">>>> test thread" - cr = openerp.sql_db.db_connect('xx').cursor() - module_name = 'document_webdav' - fp = openerp.tools.file_open('/home/openerp/repos/addons/trunk-xmlrpc/document_webdav/test/webdav_test1.yml') - openerp.tools.convert_yaml_import(cr, module_name, fp, {}, 'update', True) - cr.close() - print "<<<< test thread" - import threading - threading.Thread(target=f).start() # Install our own signal handler on the master process. def when_ready(server): From d743353e1681b8400cca3e02d3964117eeb16ab5 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 14 Sep 2011 17:06:03 +0200 Subject: [PATCH 31/47] [IMP] wsgi: provide the start- and stop-server functions. bzr revid: vmt@openerp.com-20110914150603-zu6zh4v7pmnchvf3 --- openerp-server | 3 ++- openerp/wsgi.py | 29 ++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/openerp-server b/openerp-server index e01f0f385c2..277451df74e 100755 --- a/openerp-server +++ b/openerp-server @@ -155,7 +155,7 @@ def start_services(): openerp.netsvc.Server.startAll() # Start the WSGI server. - threading.Thread(target=openerp.wsgi.serve).start() + openerp.wsgi.start_server() # Variable keeping track of the number of calls to the signal handler defined # below. This variable is monitored by ``quit_on_signals()``. @@ -210,6 +210,7 @@ def quit_on_signals(): openerp.netsvc.Agent.quit() openerp.netsvc.Server.quitAll() + openerp.wsgi.stop_server() config = openerp.tools.config if config['pidfile']: os.unlink(config['pidfile']) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 9f374acb374..693dc1d53e1 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -34,6 +34,7 @@ import logging import os import signal import sys +import threading import time import openerp @@ -171,10 +172,10 @@ def http_to_wsgi(http_dir): server = Dummy() server.server_name = environ['SERVER_NAME'] server.server_port = int(environ['SERVER_PORT']) - con = openerp.service.websrv_lib.noconnection(environ['gunicorn.socket']) # None TODO # Initialize the underlying handler and associated auth. provider. - handler = http_dir.instanciate_handler(openerp.service.websrv_lib.noconnection(con), environ['REMOTE_ADDR'], server) + con = openerp.service.websrv_lib.noconnection(environ['wsgi.input']) + handler = http_dir.instanciate_handler(con, environ['REMOTE_ADDR'], server) # Populate the handler as if it is called by a regular HTTP server # and the request is already parsed. @@ -197,7 +198,6 @@ def http_to_wsgi(http_dir): # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the # authorisation features of HTTP/1.1 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')): - print 'self.log_error("Cannot require auth at %s", self.request_version)' start_response("403 Forbidden", []) return [] start_response("401 Authorization required", [ @@ -208,7 +208,6 @@ def http_to_wsgi(http_dir): ]) return ['Blah'] # self.auth_required_msg except websrv_lib.AuthRejectedExc,e: - print '("Rejected auth: %s" % e.args[0])' start_response("403 %s" % (e.args[0],), []) return [] @@ -289,8 +288,11 @@ def application(environ, start_response): start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) return [response] +# The WSGI server, started by start_server(), stopped by stop_server(). +httpd = None + def serve(): - """ Serve XMLRPC requests via werkzeug development server. + """ Serve HTTP requests via werkzeug development server. If werkzeug can not be imported, we fall back to wsgiref's simple_server. @@ -298,6 +300,8 @@ def serve(): thread. """ + global httpd + # TODO Change the xmlrpc_port option to http_port. try: import werkzeug.serving @@ -312,6 +316,21 @@ def serve(): httpd.serve_forever() +def start_server(): + """ Call serve() in its own thread. + + The WSGI server can be shutdown with stop_server() below. + """ + threading.Thread(target=openerp.wsgi.serve).start() + +def stop_server(): + """ Initiate the shutdown of the WSGI server. + + The server is supposed to have been started by start_server() above. + """ + if httpd: + httpd.shutdown() + # Master process id, can be used for signaling. arbiter_pid = None From cc26f2b5d2fb7e1046989541ba292190901b6276 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 23 Sep 2011 15:34:08 +0200 Subject: [PATCH 32/47] [IMP] tests: added a simple test case to create a database via XML-RPC. The test setup starts an OpenERP server similarly to the openerp-server script. For this to be possible, some of its code is moved to openerp.services. bzr revid: vmt@openerp.com-20110923133408-qqf4i5l86pkk7xn5 --- openerp-server | 41 +----------------- openerp/service/__init__.py | 47 ++++++++++++++++++++ tests/__init__.py | 2 + tests/test_xmlrpc.py | 85 +++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_xmlrpc.py diff --git a/openerp-server b/openerp-server index c42651dc419..c86400be6d6 100755 --- a/openerp-server +++ b/openerp-server @@ -136,27 +136,6 @@ def import_translation(): cr.commit() cr.close() -def start_services(): - http_server = openerp.service.http_server - netrpc_server = openerp.service.netrpc_server - - # Instantiate local services (this is a legacy design). - openerp.osv.osv.start_object_proxy() - # Export (for RPC) services. - openerp.service.web_services.start_web_services() - - # Initialize the HTTP stack. - http_server.init_servers() - http_server.init_xmlrpc() - http_server.init_static_http() - netrpc_server.init_servers() - - # Start the main cron thread. - openerp.netsvc.start_agent() - - # Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC). - openerp.netsvc.Server.startAll() - # Variable keeping track of the number of calls to the signal handler defined # below. This variable is monitored by ``quit_on_signals()``. quit_signals_received = 0 @@ -208,26 +187,10 @@ def quit_on_signals(): while quit_signals_received == 0: time.sleep(60) - openerp.netsvc.Agent.quit() - openerp.netsvc.Server.quitAll() - config = openerp.tools.config if config['pidfile']: os.unlink(config['pidfile']) - logger = logging.getLogger('server') - logger.info("Initiating shutdown") - logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") - logging.shutdown() - # manually join() all threads before calling sys.exit() to allow a second signal - # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. - # threading.Thread.join() should not mask signals (at least in python 2.5) - for thread in threading.enumerate(): - if thread != threading.currentThread() and not thread.isDaemon(): - while thread.isAlive(): - # need a busyloop here as thread.join() masks signals - # and would present the forced shutdown - thread.join(0.05) - time.sleep(0.05) + openerp.service.stop_services() sys.exit(0) if __name__ == "__main__": @@ -259,7 +222,7 @@ if __name__ == "__main__": if not config["stop_after_init"]: # Some module register themselves when they are loaded so we need the # services to be running before loading any registry. - start_services() + openerp.service.start_services() if config['db_name']: for dbname in config['db_name'].split(','): diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index b9ee98fcab0..2a4c03a0892 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -34,5 +34,52 @@ import web_services low-level behavior of the wire. """ +def start_services(): + import openerp + http_server = openerp.service.http_server + netrpc_server = openerp.service.netrpc_server + + # Instantiate local services (this is a legacy design). + openerp.osv.osv.start_object_proxy() + # Export (for RPC) services. + openerp.service.web_services.start_web_services() + + # Initialize the HTTP stack. + http_server.init_servers() + http_server.init_xmlrpc() + http_server.init_static_http() + netrpc_server.init_servers() + + # Start the main cron thread. + openerp.netsvc.start_agent() + + # Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC). + openerp.netsvc.Server.startAll() + +def stop_services(): + import openerp + import logging + import threading + import time + openerp.netsvc.Agent.quit() + openerp.netsvc.Server.quitAll() + config = openerp.tools.config + logger = logging.getLogger('server') + logger.info("Initiating shutdown") + logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") + logging.shutdown() + + # manually join() all threads before calling sys.exit() to allow a second signal + # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. + # threading.Thread.join() should not mask signals (at least in python 2.5) + for thread in threading.enumerate(): + if thread != threading.currentThread() and not thread.isDaemon(): + while thread.isAlive(): + # need a busyloop here as thread.join() masks signals + # and would present the forced shutdown + thread.join(0.05) + time.sleep(0.05) + + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000000..396284efaf7 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +import test_xmlrpc diff --git a/tests/test_xmlrpc.py b/tests/test_xmlrpc.py new file mode 100644 index 00000000000..30af863f12d --- /dev/null +++ b/tests/test_xmlrpc.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Run with one of these commands: +# > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \ +# OPENERP_DATABASE=yy PYTHONPATH=. python tests/test_xmlrpc.py +# > OPENERP_ADDONS_PATH='../../addons/trunk' OPENERP_PORT=8069 \ +# OPENERP_DATABASE=yy nosetests tests/test_xmlrpc.py +# > OPENERP_ADDONS_PATH='../../../addons/trunk' OPENERP_PORT=8069 \ +# OPENERP_DATABASE=yy PYTHONPATH=../:. unit2 test_xmlrpc +import os +import time +import unittest2 +import xmlrpclib + +import openerp + +ADDONS_PATH = os.environ['OPENERP_ADDONS_PATH'] +PORT = int(os.environ['OPENERP_PORT']) +DB = os.environ['OPENERP_DATABASE'] + +HOST = '127.0.0.1' + +ADMIN_USER = 'admin' +ADMIN_USER_ID = 1 +ADMIN_PASSWORD = 'admin' + +common_proxy_60 = None +db_proxy_60 = None +object_proxy_60 = None + +def setUpModule(): + """ + Start the OpenERP server similary to the openerp-server script and + setup some xmlrpclib proxies. + """ + openerp.tools.config['addons_path'] = ADDONS_PATH + openerp.tools.config['xmlrpc_port'] = PORT + openerp.service.start_services() + + global common_proxy_60 + global db_proxy_60 + global object_proxy_60 + + # Use the old (pre 6.1) API. + url = 'http://%s:%d/xmlrpc/' % (HOST, PORT) + common_proxy_60 = xmlrpclib.ServerProxy(url + 'common') + db_proxy_60 = xmlrpclib.ServerProxy(url + 'db') + object_proxy_60 = xmlrpclib.ServerProxy(url + 'object') + +def tearDownModule(): + """ Shutdown the OpenERP server similarly to a single ctrl-c. """ + openerp.service.stop_services() + +class test_xmlrpc(unittest2.TestCase): + + def test_xmlrpc_create_database_polling(self): + """ + Simulate a OpenERP client requesting the creation of a database and + polling the server until the creation is complete. + """ + progress_id = db_proxy_60.create(ADMIN_PASSWORD, DB, True, False, + ADMIN_PASSWORD) + while True: + time.sleep(1) + progress, users = db_proxy_60.get_progress(ADMIN_PASSWORD, + progress_id) + if progress == 1.0: + break + + def test_xmlrpc_login(self): + """ Try to login on the common service. """ + uid = common_proxy_60.login(DB, ADMIN_USER, ADMIN_PASSWORD) + assert uid == ADMIN_USER_ID + + def test_xmlrpc_ir_model_search(self): + """ Try a search on the object service. """ + ids = object_proxy_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, + 'ir.model', 'search', []) + assert ids + ids = object_proxy_60.execute(DB, ADMIN_USER_ID, ADMIN_PASSWORD, + 'ir.model', 'search', [], {}) + assert ids + +if __name__ == '__main__': + unittest2.main() + From 73cdf6159ebe70a92839993af3bd2babf06ad0af Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 24 Sep 2011 17:18:31 +0200 Subject: [PATCH 33/47] [FIX] wsgi server listen to ipv4 0.0.0.0 bzr revid: al@openerp.com-20110924151831-7nra57wpqq6oqose --- openerp/wsgi.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 693dc1d53e1..ff17b057f3a 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -305,14 +305,11 @@ def serve(): # TODO Change the xmlrpc_port option to http_port. try: import werkzeug.serving - httpd = werkzeug.serving.make_server('localhost', - config['xmlrpc_port'], application, threaded=True) + httpd = werkzeug.serving.make_server('0.0.0.0', config['xmlrpc_port'], application, threaded=True) except ImportError, e: import wsgiref.simple_server - logging.getLogger('wsgi').warn('Can not import werkzeug, ' - 'falling back to wsgiref.') - httpd = wsgiref.simple_server.make_server('localhost', - config['xmlrpc_port'], application) + logging.getLogger('wsgi').warn('Can not import werkzeug, ' 'falling back to wsgiref.') + httpd = wsgiref.simple_server.make_server('0.0.0.0', config['xmlrpc_port'], application) httpd.serve_forever() From 02ee5ee64cf847a67375a2c3100d67285d03033b Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 24 Sep 2011 17:24:01 +0200 Subject: [PATCH 34/47] [FIX] remove old-style test wsgi hook bzr revid: al@openerp.com-20110924152401-5ilujqvg6gnhdrbz --- openerp/addons/base/__init__.py | 5 ----- openerp/addons/base/__openerp__.py | 1 - 2 files changed, 6 deletions(-) diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index 457c46f7887..847bef71f8c 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -25,10 +25,5 @@ import res import publisher_warranty import report -def YEAH(environ, start_response): - response = 'YEAH.\n' - start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) - return [response] - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 4857ff0bedf..474cb384049 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -93,7 +93,6 @@ 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_ir_values.yml', ], - 'wsgi': 'YEAH', 'installable': True, 'active': True, 'certificate': '0076807797149', From 94393cc074c3536260104a5565aeef72e339c5ce Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 24 Sep 2011 17:34:18 +0200 Subject: [PATCH 35/47] [REM] python25-compat bzr revid: al@openerp.com-20110924153418-kgkrqg0s2mtniofm --- python25-compat/BaseHTTPServer.py | 587 ---------------------- python25-compat/SimpleXMLRPCServer.py | 611 ----------------------- python25-compat/SocketServer.py | 681 -------------------------- 3 files changed, 1879 deletions(-) delete mode 100644 python25-compat/BaseHTTPServer.py delete mode 100644 python25-compat/SimpleXMLRPCServer.py delete mode 100644 python25-compat/SocketServer.py diff --git a/python25-compat/BaseHTTPServer.py b/python25-compat/BaseHTTPServer.py deleted file mode 100644 index 5f2d558b689..00000000000 --- a/python25-compat/BaseHTTPServer.py +++ /dev/null @@ -1,587 +0,0 @@ -"""HTTP server base class. - -Note: the class in this module doesn't implement any HTTP request; see -SimpleHTTPServer for simple implementations of GET, HEAD and POST -(including CGI scripts). It does, however, optionally implement HTTP/1.1 -persistent connections, as of version 0.3. - -Contents: - -- BaseHTTPRequestHandler: HTTP request handler base class -- test: test function - -XXX To do: - -- log requests even later (to capture byte count) -- log user-agent header and other interesting goodies -- send error log to separate file -""" - - -# See also: -# -# HTTP Working Group T. Berners-Lee -# INTERNET-DRAFT R. T. Fielding -# H. Frystyk Nielsen -# Expires September 8, 1995 March 8, 1995 -# -# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt -# -# and -# -# Network Working Group R. Fielding -# Request for Comments: 2616 et al -# Obsoletes: 2068 June 1999 -# Category: Standards Track -# -# URL: http://www.faqs.org/rfcs/rfc2616.html - -# Log files -# --------- -# -# Here's a quote from the NCSA httpd docs about log file format. -# -# | The logfile format is as follows. Each line consists of: -# | -# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb -# | -# | host: Either the DNS name or the IP number of the remote client -# | rfc931: Any information returned by identd for this person, -# | - otherwise. -# | authuser: If user sent a userid for authentication, the user name, -# | - otherwise. -# | DD: Day -# | Mon: Month (calendar name) -# | YYYY: Year -# | hh: hour (24-hour format, the machine's timezone) -# | mm: minutes -# | ss: seconds -# | request: The first line of the HTTP request as sent by the client. -# | ddd: the status code returned by the server, - if not available. -# | bbbb: the total number of bytes sent, -# | *not including the HTTP/1.0 header*, - if not available -# | -# | You can determine the name of the file accessed through request. -# -# (Actually, the latter is only true if you know the server configuration -# at the time the request was made!) - -__version__ = "0.3" - -__all__ = ["HTTPServer", "BaseHTTPRequestHandler"] - -import sys -import time -import socket # For gethostbyaddr() -import mimetools -import SocketServer - -# Default error message template -DEFAULT_ERROR_MESSAGE = """\ - -Error response - - -

Error response

-

Error code %(code)d. -

Message: %(message)s. -

Error code explanation: %(code)s = %(explain)s. - -""" - -DEFAULT_ERROR_CONTENT_TYPE = "text/html" - -def _quote_html(html): - return html.replace("&", "&").replace("<", "<").replace(">", ">") - -class HTTPServer(SocketServer.TCPServer): - - allow_reuse_address = 1 # Seems to make sense in testing environment - - def server_bind(self): - """Override server_bind to store the server name.""" - SocketServer.TCPServer.server_bind(self) - host, port = self.socket.getsockname()[:2] - self.server_name = socket.getfqdn(host) - self.server_port = port - - -class BaseHTTPRequestHandler(SocketServer.StreamRequestHandler): - - """HTTP request handler base class. - - The following explanation of HTTP serves to guide you through the - code as well as to expose any misunderstandings I may have about - HTTP (so you don't need to read the code to figure out I'm wrong - :-). - - HTTP (HyperText Transfer Protocol) is an extensible protocol on - top of a reliable stream transport (e.g. TCP/IP). The protocol - recognizes three parts to a request: - - 1. One line identifying the request type and path - 2. An optional set of RFC-822-style headers - 3. An optional data part - - The headers and data are separated by a blank line. - - The first line of the request has the form - - - - where is a (case-sensitive) keyword such as GET or POST, - is a string containing path information for the request, - and should be the string "HTTP/1.0" or "HTTP/1.1". - is encoded using the URL encoding scheme (using %xx to signify - the ASCII character with hex code xx). - - The specification specifies that lines are separated by CRLF but - for compatibility with the widest range of clients recommends - servers also handle LF. Similarly, whitespace in the request line - is treated sensibly (allowing multiple spaces between components - and allowing trailing whitespace). - - Similarly, for output, lines ought to be separated by CRLF pairs - but most clients grok LF characters just fine. - - If the first line of the request has the form - - - - (i.e. is left out) then this is assumed to be an HTTP - 0.9 request; this form has no optional headers and data part and - the reply consists of just the data. - - The reply form of the HTTP 1.x protocol again has three parts: - - 1. One line giving the response code - 2. An optional set of RFC-822-style headers - 3. The data - - Again, the headers and data are separated by a blank line. - - The response code line has the form - - - - where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), - is a 3-digit response code indicating success or - failure of the request, and is an optional - human-readable string explaining what the response code means. - - This server parses the request and the headers, and then calls a - function specific to the request type (). Specifically, - a request SPAM will be handled by a method do_SPAM(). If no - such method exists the server sends an error response to the - client. If it exists, it is called with no arguments: - - do_SPAM() - - Note that the request name is case sensitive (i.e. SPAM and spam - are different requests). - - The various request details are stored in instance variables: - - - client_address is the client IP address in the form (host, - port); - - - command, path and version are the broken-down request line; - - - headers is an instance of mimetools.Message (or a derived - class) containing the header information; - - - rfile is a file object open for reading positioned at the - start of the optional input data part; - - - wfile is a file object open for writing. - - IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! - - The first thing to be written must be the response line. Then - follow 0 or more header lines, then a blank line, and then the - actual data (if any). The meaning of the header lines depends on - the command executed by the server; in most cases, when data is - returned, there should be at least one header line of the form - - Content-type: / - - where and should be registered MIME types, - e.g. "text/html" or "text/plain". - - """ - - # The Python system version, truncated to its first component. - sys_version = "Python/" + sys.version.split()[0] - - # The server software version. You may want to override this. - # The format is multiple whitespace-separated strings, - # where each string is of the form name[/version]. - server_version = "BaseHTTP/" + __version__ - - # The default request version. This only affects responses up until - # the point where the request line is parsed, so it mainly decides what - # the client gets back when sending a malformed request line. - # Most web servers default to HTTP 0.9, i.e. don't send a status line. - default_request_version = "HTTP/0.9" - - def parse_request(self): - """Parse a request (internal). - - The request should be stored in self.raw_requestline; the results - are in self.command, self.path, self.request_version and - self.headers. - - Return True for success, False for failure; on failure, an - error is sent back. - - """ - self.command = None # set in case of error on the first line - self.request_version = version = self.default_request_version - self.close_connection = 1 - requestline = self.raw_requestline - if requestline[-2:] == '\r\n': - requestline = requestline[:-2] - elif requestline[-1:] == '\n': - requestline = requestline[:-1] - self.requestline = requestline - words = requestline.split() - if len(words) == 3: - [command, path, version] = words - if version[:5] != 'HTTP/': - self.send_error(400, "Bad request version (%r)" % version) - return False - try: - base_version_number = version.split('/', 1)[1] - version_number = base_version_number.split(".") - # RFC 2145 section 3.1 says there can be only one "." and - # - major and minor numbers MUST be treated as - # separate integers; - # - HTTP/2.4 is a lower version than HTTP/2.13, which in - # turn is lower than HTTP/12.3; - # - Leading zeros MUST be ignored by recipients. - if len(version_number) != 2: - raise ValueError - version_number = int(version_number[0]), int(version_number[1]) - except (ValueError, IndexError): - self.send_error(400, "Bad request version (%r)" % version) - return False - if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": - self.close_connection = 0 - if version_number >= (2, 0): - self.send_error(505, - "Invalid HTTP Version (%s)" % base_version_number) - return False - elif len(words) == 2: - [command, path] = words - self.close_connection = 1 - if command != 'GET': - self.send_error(400, - "Bad HTTP/0.9 request type (%r)" % command) - return False - elif not words: - return False - else: - self.send_error(400, "Bad request syntax (%r)" % requestline) - return False - self.command, self.path, self.request_version = command, path, version - - # Examine the headers and look for a Connection directive - self.headers = self.MessageClass(self.rfile, 0) - - conntype = self.headers.get('Connection', "") - if conntype.lower() == 'close': - self.close_connection = 1 - elif (conntype.lower() == 'keep-alive' and - self.protocol_version >= "HTTP/1.1"): - self.close_connection = 0 - return True - - def handle_one_request(self): - """Handle a single HTTP request. - - You normally don't need to override this method; see the class - __doc__ string for information on how to handle specific HTTP - commands such as GET and POST. - - """ - self.raw_requestline = self.rfile.readline() - if not self.raw_requestline: - self.close_connection = 1 - return - if not self.parse_request(): # An error code has been sent, just exit - return - mname = 'do_' + self.command - if not hasattr(self, mname): - self.send_error(501, "Unsupported method (%r)" % self.command) - return - method = getattr(self, mname) - method() - - def handle(self): - """Handle multiple requests if necessary.""" - self.close_connection = 1 - - self.handle_one_request() - while not self.close_connection: - self.handle_one_request() - - def send_error(self, code, message=None): - """Send and log an error reply. - - Arguments are the error code, and a detailed message. - The detailed message defaults to the short entry matching the - response code. - - This sends an error response (so it must be called before any - output has been generated), logs the error, and finally sends - a piece of HTML explaining the error to the user. - - """ - - try: - short, long = self.responses[code] - except KeyError: - short, long = '???', '???' - if message is None: - message = short - explain = long - self.log_error("code %d, message %s", code, message) - # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201) - content = (self.error_message_format % - {'code': code, 'message': _quote_html(message), 'explain': explain}) - self.send_response(code, message) - self.send_header("Content-Type", self.error_content_type) - self.send_header('Connection', 'close') - self.end_headers() - if self.command != 'HEAD' and code >= 200 and code not in (204, 304): - self.wfile.write(content) - - error_message_format = DEFAULT_ERROR_MESSAGE - error_content_type = DEFAULT_ERROR_CONTENT_TYPE - - def send_response(self, code, message=None): - """Send the response header and log the response code. - - Also send two standard headers with the server software - version and the current date. - - """ - self.log_request(code) - if message is None: - if code in self.responses: - message = self.responses[code][0] - else: - message = '' - if self.request_version != 'HTTP/0.9': - self.wfile.write("%s %d %s\r\n" % - (self.protocol_version, code, message)) - # print (self.protocol_version, code, message) - self.send_header('Server', self.version_string()) - self.send_header('Date', self.date_time_string()) - - def send_header(self, keyword, value): - """Send a MIME header.""" - if self.request_version != 'HTTP/0.9': - self.wfile.write("%s: %s\r\n" % (keyword, value)) - - if keyword.lower() == 'connection': - if value.lower() == 'close': - self.close_connection = 1 - elif value.lower() == 'keep-alive': - self.close_connection = 0 - - def end_headers(self): - """Send the blank line ending the MIME headers.""" - if self.request_version != 'HTTP/0.9': - self.wfile.write("\r\n") - - def log_request(self, code='-', size='-'): - """Log an accepted request. - - This is called by send_response(). - - """ - - self.log_message('"%s" %s %s', - self.requestline, str(code), str(size)) - - def log_error(self, format, *args): - """Log an error. - - This is called when a request cannot be fulfilled. By - default it passes the message on to log_message(). - - Arguments are the same as for log_message(). - - XXX This should go to the separate error log. - - """ - - self.log_message(format, *args) - - def log_message(self, format, *args): - """Log an arbitrary message. - - This is used by all other logging functions. Override - it if you have specific logging wishes. - - The first argument, FORMAT, is a format string for the - message to be logged. If the format string contains - any % escapes requiring parameters, they should be - specified as subsequent arguments (it's just like - printf!). - - The client host and current date/time are prefixed to - every message. - - """ - - sys.stderr.write("%s - - [%s] %s\n" % - (self.address_string(), - self.log_date_time_string(), - format%args)) - - def version_string(self): - """Return the server software version string.""" - return self.server_version + ' ' + self.sys_version - - def date_time_string(self, timestamp=None): - """Return the current date and time formatted for a message header.""" - if timestamp is None: - timestamp = time.time() - year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) - s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( - self.weekdayname[wd], - day, self.monthname[month], year, - hh, mm, ss) - return s - - def log_date_time_string(self): - """Return the current time formatted for logging.""" - now = time.time() - year, month, day, hh, mm, ss, x, y, z = time.localtime(now) - s = "%02d/%3s/%04d %02d:%02d:%02d" % ( - day, self.monthname[month], year, hh, mm, ss) - return s - - weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - - monthname = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - def address_string(self): - """Return the client address formatted for logging. - - This version looks up the full hostname using gethostbyaddr(), - and tries to find a name that contains at least one dot. - - """ - - host, port = self.client_address[:2] - return socket.getfqdn(host) - - # Essentially static class variables - - # The version of the HTTP protocol we support. - # Set this to HTTP/1.1 to enable automatic keepalive - protocol_version = "HTTP/1.0" - - # The Message-like class used to parse headers - MessageClass = mimetools.Message - - # Table mapping response codes to messages; entries have the - # form {code: (shortmessage, longmessage)}. - # See RFC 2616. - responses = { - 100: ('Continue', 'Request received, please continue'), - 101: ('Switching Protocols', - 'Switching to new protocol; obey Upgrade header'), - - 200: ('OK', 'Request fulfilled, document follows'), - 201: ('Created', 'Document created, URL follows'), - 202: ('Accepted', - 'Request accepted, processing continues off-line'), - 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), - 204: ('No Content', 'Request fulfilled, nothing follows'), - 205: ('Reset Content', 'Clear input form for further input.'), - 206: ('Partial Content', 'Partial content follows.'), - - 300: ('Multiple Choices', - 'Object has several resources -- see URI list'), - 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), - 302: ('Found', 'Object moved temporarily -- see URI list'), - 303: ('See Other', 'Object moved -- see Method and URL list'), - 304: ('Not Modified', - 'Document has not changed since given time'), - 305: ('Use Proxy', - 'You must use proxy specified in Location to access this ' - 'resource.'), - 307: ('Temporary Redirect', - 'Object moved temporarily -- see URI list'), - - 400: ('Bad Request', - 'Bad request syntax or unsupported method'), - 401: ('Unauthorized', - 'No permission -- see authorization schemes'), - 402: ('Payment Required', - 'No payment -- see charging schemes'), - 403: ('Forbidden', - 'Request forbidden -- authorization will not help'), - 404: ('Not Found', 'Nothing matches the given URI'), - 405: ('Method Not Allowed', - 'Specified method is invalid for this server.'), - 406: ('Not Acceptable', 'URI not available in preferred format.'), - 407: ('Proxy Authentication Required', 'You must authenticate with ' - 'this proxy before proceeding.'), - 408: ('Request Timeout', 'Request timed out; try again later.'), - 409: ('Conflict', 'Request conflict.'), - 410: ('Gone', - 'URI no longer exists and has been permanently removed.'), - 411: ('Length Required', 'Client must specify Content-Length.'), - 412: ('Precondition Failed', 'Precondition in headers is false.'), - 413: ('Request Entity Too Large', 'Entity is too large.'), - 414: ('Request-URI Too Long', 'URI is too long.'), - 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), - 416: ('Requested Range Not Satisfiable', - 'Cannot satisfy request range.'), - 417: ('Expectation Failed', - 'Expect condition could not be satisfied.'), - - 500: ('Internal Server Error', 'Server got itself in trouble'), - 501: ('Not Implemented', - 'Server does not support this operation'), - 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), - 503: ('Service Unavailable', - 'The server cannot process the request due to a high load'), - 504: ('Gateway Timeout', - 'The gateway server did not receive a timely response'), - 505: ('HTTP Version Not Supported', 'Cannot fulfill request.'), - } - - -def test(HandlerClass = BaseHTTPRequestHandler, - ServerClass = HTTPServer, protocol="HTTP/1.0"): - """Test the HTTP request handler class. - - This runs an HTTP server on port 8000 (or the first command line - argument). - - """ - - if sys.argv[1:]: - port = int(sys.argv[1]) - else: - port = 8000 - server_address = ('', port) - - HandlerClass.protocol_version = protocol - httpd = ServerClass(server_address, HandlerClass) - - sa = httpd.socket.getsockname() - print "Serving HTTP on", sa[0], "port", sa[1], "..." - httpd.serve_forever() - - -if __name__ == '__main__': - test() diff --git a/python25-compat/SimpleXMLRPCServer.py b/python25-compat/SimpleXMLRPCServer.py deleted file mode 100644 index 43757a03dda..00000000000 --- a/python25-compat/SimpleXMLRPCServer.py +++ /dev/null @@ -1,611 +0,0 @@ -"""Simple XML-RPC Server. - -This module can be used to create simple XML-RPC servers -by creating a server and either installing functions, a -class instance, or by extending the SimpleXMLRPCServer -class. - -It can also be used to handle XML-RPC requests in a CGI -environment using CGIXMLRPCRequestHandler. - -A list of possible usage patterns follows: - -1. Install functions: - -server = SimpleXMLRPCServer(("localhost", 8000)) -server.register_function(pow) -server.register_function(lambda x,y: x+y, 'add') -server.serve_forever() - -2. Install an instance: - -class MyFuncs: - def __init__(self): - # make all of the string functions available through - # string.func_name - import string - self.string = string - def _listMethods(self): - # implement this method so that system.listMethods - # knows to advertise the strings methods - return list_public_methods(self) + \ - ['string.' + method for method in list_public_methods(self.string)] - def pow(self, x, y): return pow(x, y) - def add(self, x, y) : return x + y - -server = SimpleXMLRPCServer(("localhost", 8000)) -server.register_introspection_functions() -server.register_instance(MyFuncs()) -server.serve_forever() - -3. Install an instance with custom dispatch method: - -class Math: - def _listMethods(self): - # this method must be present for system.listMethods - # to work - return ['add', 'pow'] - def _methodHelp(self, method): - # this method must be present for system.methodHelp - # to work - if method == 'add': - return "add(2,3) => 5" - elif method == 'pow': - return "pow(x, y[, z]) => number" - else: - # By convention, return empty - # string if no help is available - return "" - def _dispatch(self, method, params): - if method == 'pow': - return pow(*params) - elif method == 'add': - return params[0] + params[1] - else: - raise 'bad method' - -server = SimpleXMLRPCServer(("localhost", 8000)) -server.register_introspection_functions() -server.register_instance(Math()) -server.serve_forever() - -4. Subclass SimpleXMLRPCServer: - -class MathServer(SimpleXMLRPCServer): - def _dispatch(self, method, params): - try: - # We are forcing the 'export_' prefix on methods that are - # callable through XML-RPC to prevent potential security - # problems - func = getattr(self, 'export_' + method) - except AttributeError: - raise Exception('method "%s" is not supported' % method) - else: - return func(*params) - - def export_add(self, x, y): - return x + y - -server = MathServer(("localhost", 8000)) -server.serve_forever() - -5. CGI script: - -server = CGIXMLRPCRequestHandler() -server.register_function(pow) -server.handle_request() -""" - -# Written by Brian Quinlan (brian@sweetapp.com). -# Based on code written by Fredrik Lundh. - -import xmlrpclib -from xmlrpclib import Fault -import SocketServer -import BaseHTTPServer -import sys -import os -import traceback -try: - import fcntl -except ImportError: - fcntl = None - -def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): - """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d - - Resolves a dotted attribute name to an object. Raises - an AttributeError if any attribute in the chain starts with a '_'. - - If the optional allow_dotted_names argument is false, dots are not - supported and this function operates similar to getattr(obj, attr). - """ - - if allow_dotted_names: - attrs = attr.split('.') - else: - attrs = [attr] - - for i in attrs: - if i.startswith('_'): - raise AttributeError( - 'attempt to access private attribute "%s"' % i - ) - else: - obj = getattr(obj,i) - return obj - -def list_public_methods(obj): - """Returns a list of attribute strings, found in the specified - object, which represent callable attributes""" - - return [member for member in dir(obj) - if not member.startswith('_') and - hasattr(getattr(obj, member), '__call__')] - -def remove_duplicates(lst): - """remove_duplicates([2,2,2,1,3,3]) => [3,1,2] - - Returns a copy of a list without duplicates. Every list - item must be hashable and the order of the items in the - resulting list is not defined. - """ - u = {} - for x in lst: - u[x] = 1 - - return u.keys() - -class SimpleXMLRPCDispatcher: - """Mix-in class that dispatches XML-RPC requests. - - This class is used to register XML-RPC method handlers - and then to dispatch them. There should never be any - reason to instantiate this class directly. - """ - - def __init__(self, allow_none, encoding): - self.funcs = {} - self.instance = None - self.allow_none = allow_none - self.encoding = encoding - - def register_instance(self, instance, allow_dotted_names=False): - """Registers an instance to respond to XML-RPC requests. - - Only one instance can be installed at a time. - - If the registered instance has a _dispatch method then that - method will be called with the name of the XML-RPC method and - its parameters as a tuple - e.g. instance._dispatch('add',(2,3)) - - If the registered instance does not have a _dispatch method - then the instance will be searched to find a matching method - and, if found, will be called. Methods beginning with an '_' - are considered private and will not be called by - SimpleXMLRPCServer. - - If a registered function matches a XML-RPC request, then it - will be called instead of the registered instance. - - If the optional allow_dotted_names argument is true and the - instance does not have a _dispatch method, method names - containing dots are supported and resolved, as long as none of - the name segments start with an '_'. - - *** SECURITY WARNING: *** - - Enabling the allow_dotted_names options allows intruders - to access your module's global variables and may allow - intruders to execute arbitrary code on your machine. Only - use this option on a secure, closed network. - - """ - - self.instance = instance - self.allow_dotted_names = allow_dotted_names - - def register_function(self, function, name = None): - """Registers a function to respond to XML-RPC requests. - - The optional name argument can be used to set a Unicode name - for the function. - """ - - if name is None: - name = function.__name__ - self.funcs[name] = function - - def register_introspection_functions(self): - """Registers the XML-RPC introspection methods in the system - namespace. - - see http://xmlrpc.usefulinc.com/doc/reserved.html - """ - - self.funcs.update({'system.listMethods' : self.system_listMethods, - 'system.methodSignature' : self.system_methodSignature, - 'system.methodHelp' : self.system_methodHelp}) - - def register_multicall_functions(self): - """Registers the XML-RPC multicall method in the system - namespace. - - see http://www.xmlrpc.com/discuss/msgReader$1208""" - - self.funcs.update({'system.multicall' : self.system_multicall}) - - def _marshaled_dispatch(self, data, dispatch_method = None): - """Dispatches an XML-RPC method from marshalled (XML) data. - - XML-RPC methods are dispatched from the marshalled (XML) data - using the _dispatch method and the result is returned as - marshalled data. For backwards compatibility, a dispatch - function can be provided as an argument (see comment in - SimpleXMLRPCRequestHandler.do_POST) but overriding the - existing method through subclassing is the prefered means - of changing method dispatch behavior. - """ - - try: - params, method = xmlrpclib.loads(data) - - # generate response - if dispatch_method is not None: - response = dispatch_method(method, params) - else: - response = self._dispatch(method, params) - # wrap response in a singleton tuple - response = (response,) - response = xmlrpclib.dumps(response, methodresponse=1, - allow_none=self.allow_none, encoding=self.encoding) - except Fault, fault: - response = xmlrpclib.dumps(fault, allow_none=self.allow_none, - encoding=self.encoding) - except: - # report exception back to server - exc_type, exc_value, exc_tb = sys.exc_info() - response = xmlrpclib.dumps( - xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)), - encoding=self.encoding, allow_none=self.allow_none, - ) - - return response - - def system_listMethods(self): - """system.listMethods() => ['add', 'subtract', 'multiple'] - - Returns a list of the methods supported by the server.""" - - methods = self.funcs.keys() - if self.instance is not None: - # Instance can implement _listMethod to return a list of - # methods - if hasattr(self.instance, '_listMethods'): - methods = remove_duplicates( - methods + self.instance._listMethods() - ) - # if the instance has a _dispatch method then we - # don't have enough information to provide a list - # of methods - elif not hasattr(self.instance, '_dispatch'): - methods = remove_duplicates( - methods + list_public_methods(self.instance) - ) - methods.sort() - return methods - - def system_methodSignature(self, method_name): - """system.methodSignature('add') => [double, int, int] - - Returns a list describing the signature of the method. In the - above example, the add method takes two integers as arguments - and returns a double result. - - This server does NOT support system.methodSignature.""" - - # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html - - return 'signatures not supported' - - def system_methodHelp(self, method_name): - """system.methodHelp('add') => "Adds two integers together" - - Returns a string containing documentation for the specified method.""" - - method = None - if method_name in self.funcs: - method = self.funcs[method_name] - elif self.instance is not None: - # Instance can implement _methodHelp to return help for a method - if hasattr(self.instance, '_methodHelp'): - return self.instance._methodHelp(method_name) - # if the instance has a _dispatch method then we - # don't have enough information to provide help - elif not hasattr(self.instance, '_dispatch'): - try: - method = resolve_dotted_attribute( - self.instance, - method_name, - self.allow_dotted_names - ) - except AttributeError: - pass - - # Note that we aren't checking that the method actually - # be a callable object of some kind - if method is None: - return "" - else: - import pydoc - return pydoc.getdoc(method) - - def system_multicall(self, call_list): - """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ -[[4], ...] - - Allows the caller to package multiple XML-RPC calls into a single - request. - - See http://www.xmlrpc.com/discuss/msgReader$1208 - """ - - results = [] - for call in call_list: - method_name = call['methodName'] - params = call['params'] - - try: - # XXX A marshalling error in any response will fail the entire - # multicall. If someone cares they should fix this. - results.append([self._dispatch(method_name, params)]) - except Fault, fault: - results.append( - {'faultCode' : fault.faultCode, - 'faultString' : fault.faultString} - ) - except: - exc_type, exc_value, exc_tb = sys.exc_info() - results.append( - {'faultCode' : 1, - 'faultString' : "%s:%s" % (exc_type, exc_value)} - ) - return results - - def _dispatch(self, method, params): - """Dispatches the XML-RPC method. - - XML-RPC calls are forwarded to a registered function that - matches the called XML-RPC method name. If no such function - exists then the call is forwarded to the registered instance, - if available. - - If the registered instance has a _dispatch method then that - method will be called with the name of the XML-RPC method and - its parameters as a tuple - e.g. instance._dispatch('add',(2,3)) - - If the registered instance does not have a _dispatch method - then the instance will be searched to find a matching method - and, if found, will be called. - - Methods beginning with an '_' are considered private and will - not be called. - """ - - func = None - try: - # check to see if a matching function has been registered - func = self.funcs[method] - except KeyError: - if self.instance is not None: - # check for a _dispatch method - if hasattr(self.instance, '_dispatch'): - return self.instance._dispatch(method, params) - else: - # call instance method directly - try: - func = resolve_dotted_attribute( - self.instance, - method, - self.allow_dotted_names - ) - except AttributeError: - pass - - if func is not None: - return func(*params) - else: - raise Exception('method "%s" is not supported' % method) - -class SimpleXMLRPCRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """Simple XML-RPC request handler class. - - Handles all HTTP POST requests and attempts to decode them as - XML-RPC requests. - """ - - # Class attribute listing the accessible path components; - # paths not on this list will result in a 404 error. - rpc_paths = ('/', '/RPC2') - - def is_rpc_path_valid(self): - if self.rpc_paths: - return self.path in self.rpc_paths - else: - # If .rpc_paths is empty, just assume all paths are legal - return True - - def do_POST(self): - """Handles the HTTP POST request. - - Attempts to interpret all HTTP POST requests as XML-RPC calls, - which are forwarded to the server's _dispatch method for handling. - """ - - # Check that the path is legal - if not self.is_rpc_path_valid(): - self.report_404() - return - - try: - # Get arguments by reading body of request. - # We read this in chunks to avoid straining - # socket.read(); around the 10 or 15Mb mark, some platforms - # begin to have problems (bug #792570). - max_chunk_size = 10*1024*1024 - size_remaining = int(self.headers["content-length"]) - L = [] - while size_remaining: - chunk_size = min(size_remaining, max_chunk_size) - L.append(self.rfile.read(chunk_size)) - size_remaining -= len(L[-1]) - data = ''.join(L) - - # In previous versions of SimpleXMLRPCServer, _dispatch - # could be overridden in this class, instead of in - # SimpleXMLRPCDispatcher. To maintain backwards compatibility, - # check to see if a subclass implements _dispatch and dispatch - # using that method if present. - response = self.server._marshaled_dispatch( - data, getattr(self, '_dispatch', None) - ) - except Exception, e: # This should only happen if the module is buggy - # internal error, report as HTTP server error - self.send_response(500) - - # Send information about the exception if requested - if hasattr(self.server, '_send_traceback_header') and \ - self.server._send_traceback_header: - self.send_header("X-exception", str(e)) - self.send_header("X-traceback", traceback.format_exc()) - - self.end_headers() - else: - # got a valid XML RPC response - self.send_response(200) - self.send_header("Content-type", "text/xml") - self.send_header("Content-length", str(len(response))) - self.end_headers() - self.wfile.write(response) - - # shut down the connection - self.wfile.flush() - self.connection.shutdown(1) - - def report_404 (self): - # Report a 404 error - self.send_response(404) - response = 'No such page' - self.send_header("Content-type", "text/plain") - self.send_header("Content-length", str(len(response))) - self.end_headers() - self.wfile.write(response) - # shut down the connection - self.wfile.flush() - self.connection.shutdown(1) - - def log_request(self, code='-', size='-'): - """Selectively log an accepted request.""" - - if self.server.logRequests: - BaseHTTPServer.BaseHTTPRequestHandler.log_request(self, code, size) - -class SimpleXMLRPCServer(SocketServer.TCPServer, - SimpleXMLRPCDispatcher): - """Simple XML-RPC server. - - Simple XML-RPC server that allows functions and a single instance - to be installed to handle requests. The default implementation - attempts to dispatch XML-RPC calls to the functions or instance - installed in the server. Override the _dispatch method inhereted - from SimpleXMLRPCDispatcher to change this behavior. - """ - - allow_reuse_address = True - - # Warning: this is for debugging purposes only! Never set this to True in - # production code, as will be sending out sensitive information (exception - # and stack trace details) when exceptions are raised inside - # SimpleXMLRPCRequestHandler.do_POST - _send_traceback_header = False - - def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, - logRequests=True, allow_none=False, encoding=None, bind_and_activate=True): - self.logRequests = logRequests - - SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) - SocketServer.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) - - # [Bug #1222790] If possible, set close-on-exec flag; if a - # method spawns a subprocess, the subprocess shouldn't have - # the listening socket open. - if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): - flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) - flags |= fcntl.FD_CLOEXEC - fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) - -class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): - """Simple handler for XML-RPC data passed through CGI.""" - - def __init__(self, allow_none=False, encoding=None): - SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) - - def handle_xmlrpc(self, request_text): - """Handle a single XML-RPC request""" - - response = self._marshaled_dispatch(request_text) - - print 'Content-Type: text/xml' - print 'Content-Length: %d' % len(response) - print - sys.stdout.write(response) - - def handle_get(self): - """Handle a single HTTP GET request. - - Default implementation indicates an error because - XML-RPC uses the POST method. - """ - - code = 400 - message, explain = \ - BaseHTTPServer.BaseHTTPRequestHandler.responses[code] - - response = BaseHTTPServer.DEFAULT_ERROR_MESSAGE % \ - { - 'code' : code, - 'message' : message, - 'explain' : explain - } - print 'Status: %d %s' % (code, message) - print 'Content-Type: text/html' - print 'Content-Length: %d' % len(response) - print - sys.stdout.write(response) - - def handle_request(self, request_text = None): - """Handle a single XML-RPC request passed through a CGI post method. - - If no XML data is given then it is read from stdin. The resulting - XML-RPC response is printed to stdout along with the correct HTTP - headers. - """ - - if request_text is None and \ - os.environ.get('REQUEST_METHOD', None) == 'GET': - self.handle_get() - else: - # POST data is normally available through stdin - if request_text is None: - request_text = sys.stdin.read() - - self.handle_xmlrpc(request_text) - -if __name__ == '__main__': - print 'Running XML-RPC server on port 8000' - server = SimpleXMLRPCServer(("localhost", 8000)) - server.register_function(pow) - server.register_function(lambda x,y: x+y, 'add') - server.serve_forever() diff --git a/python25-compat/SocketServer.py b/python25-compat/SocketServer.py deleted file mode 100644 index 2c41fbb6dc0..00000000000 --- a/python25-compat/SocketServer.py +++ /dev/null @@ -1,681 +0,0 @@ -"""Generic socket server classes. - -This module tries to capture the various aspects of defining a server: - -For socket-based servers: - -- address family: - - AF_INET{,6}: IP (Internet Protocol) sockets (default) - - AF_UNIX: Unix domain sockets - - others, e.g. AF_DECNET are conceivable (see -- socket type: - - SOCK_STREAM (reliable stream, e.g. TCP) - - SOCK_DGRAM (datagrams, e.g. UDP) - -For request-based servers (including socket-based): - -- client address verification before further looking at the request - (This is actually a hook for any processing that needs to look - at the request before anything else, e.g. logging) -- how to handle multiple requests: - - synchronous (one request is handled at a time) - - forking (each request is handled by a new process) - - threading (each request is handled by a new thread) - -The classes in this module favor the server type that is simplest to -write: a synchronous TCP/IP server. This is bad class design, but -save some typing. (There's also the issue that a deep class hierarchy -slows down method lookups.) - -There are five classes in an inheritance diagram, four of which represent -synchronous servers of four types: - - +------------+ - | BaseServer | - +------------+ - | - v - +-----------+ +------------------+ - | TCPServer |------->| UnixStreamServer | - +-----------+ +------------------+ - | - v - +-----------+ +--------------------+ - | UDPServer |------->| UnixDatagramServer | - +-----------+ +--------------------+ - -Note that UnixDatagramServer derives from UDPServer, not from -UnixStreamServer -- the only difference between an IP and a Unix -stream server is the address family, which is simply repeated in both -unix server classes. - -Forking and threading versions of each type of server can be created -using the ForkingMixIn and ThreadingMixIn mix-in classes. For -instance, a threading UDP server class is created as follows: - - class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass - -The Mix-in class must come first, since it overrides a method defined -in UDPServer! Setting the various member variables also changes -the behavior of the underlying server mechanism. - -To implement a service, you must derive a class from -BaseRequestHandler and redefine its handle() method. You can then run -various versions of the service by combining one of the server classes -with your request handler class. - -The request handler class must be different for datagram or stream -services. This can be hidden by using the request handler -subclasses StreamRequestHandler or DatagramRequestHandler. - -Of course, you still have to use your head! - -For instance, it makes no sense to use a forking server if the service -contains state in memory that can be modified by requests (since the -modifications in the child process would never reach the initial state -kept in the parent process and passed to each child). In this case, -you can use a threading server, but you will probably have to use -locks to avoid two requests that come in nearly simultaneous to apply -conflicting changes to the server state. - -On the other hand, if you are building e.g. an HTTP server, where all -data is stored externally (e.g. in the file system), a synchronous -class will essentially render the service "deaf" while one request is -being handled -- which may be for a very long time if a client is slow -to reqd all the data it has requested. Here a threading or forking -server is appropriate. - -In some cases, it may be appropriate to process part of a request -synchronously, but to finish processing in a forked child depending on -the request data. This can be implemented by using a synchronous -server and doing an explicit fork in the request handler class -handle() method. - -Another approach to handling multiple simultaneous requests in an -environment that supports neither threads nor fork (or where these are -too expensive or inappropriate for the service) is to maintain an -explicit table of partially finished requests and to use select() to -decide which request to work on next (or whether to handle a new -incoming request). This is particularly important for stream services -where each client can potentially be connected for a long time (if -threads or subprocesses cannot be used). - -Future work: -- Standard classes for Sun RPC (which uses either UDP or TCP) -- Standard mix-in classes to implement various authentication - and encryption schemes -- Standard framework for select-based multiplexing - -XXX Open problems: -- What to do with out-of-band data? - -BaseServer: -- split generic "request" functionality out into BaseServer class. - Copyright (C) 2000 Luke Kenneth Casson Leighton - - example: read entries from a SQL database (requires overriding - get_request() to return a table entry from the database). - entry is processed by a RequestHandlerClass. - -""" - -# Author of the BaseServer patch: Luke Kenneth Casson Leighton - -# XXX Warning! -# There is a test suite for this module, but it cannot be run by the -# standard regression test. -# To run it manually, run Lib/test/test_socketserver.py. - -__version__ = "0.4" - - -import socket -import select -import sys -import os -try: - import threading -except ImportError: - import dummy_threading as threading - -__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer", - "ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler", - "StreamRequestHandler","DatagramRequestHandler", - "ThreadingMixIn", "ForkingMixIn"] -if hasattr(socket, "AF_UNIX"): - __all__.extend(["UnixStreamServer","UnixDatagramServer", - "ThreadingUnixStreamServer", - "ThreadingUnixDatagramServer"]) - -class BaseServer: - - """Base class for server classes. - - Methods for the caller: - - - __init__(server_address, RequestHandlerClass) - - serve_forever(poll_interval=0.5) - - shutdown() - - handle_request() # if you do not use serve_forever() - - fileno() -> int # for select() - - Methods that may be overridden: - - - server_bind() - - server_activate() - - get_request() -> request, client_address - - handle_timeout() - - verify_request(request, client_address) - - server_close() - - process_request(request, client_address) - - close_request(request) - - handle_error() - - Methods for derived classes: - - - finish_request(request, client_address) - - Class variables that may be overridden by derived classes or - instances: - - - timeout - - address_family - - socket_type - - allow_reuse_address - - Instance variables: - - - RequestHandlerClass - - socket - - """ - - timeout = None - - def __init__(self, server_address, RequestHandlerClass): - """Constructor. May be extended, do not override.""" - self.server_address = server_address - self.RequestHandlerClass = RequestHandlerClass - self.__is_shut_down = threading.Event() - self.__serving = False - - def server_activate(self): - """Called by constructor to activate the server. - - May be overridden. - - """ - pass - - def serve_forever(self, poll_interval=0.5): - """Handle one request at a time until shutdown. - - Polls for shutdown every poll_interval seconds. Ignores - self.timeout. If you need to do periodic tasks, do them in - another thread. - """ - self.__serving = True - self.__is_shut_down.clear() - while self.__serving: - # XXX: Consider using another file descriptor or - # connecting to the socket to wake this up instead of - # polling. Polling reduces our responsiveness to a - # shutdown request and wastes cpu at all other times. - r, w, e = select.select([self], [], [], poll_interval) - if r: - self._handle_request_noblock() - self.__is_shut_down.set() - - def shutdown(self): - """Stops the serve_forever loop. - - Blocks until the loop has finished. This must be called while - serve_forever() is running in another thread, or it will - deadlock. - """ - self.__serving = False - self.__is_shut_down.wait() - - # The distinction between handling, getting, processing and - # finishing a request is fairly arbitrary. Remember: - # - # - handle_request() is the top-level call. It calls - # select, get_request(), verify_request() and process_request() - # - get_request() is different for stream or datagram sockets - # - process_request() is the place that may fork a new process - # or create a new thread to finish the request - # - finish_request() instantiates the request handler class; - # this constructor will handle the request all by itself - - def handle_request(self): - """Handle one request, possibly blocking. - - Respects self.timeout. - """ - # Support people who used socket.settimeout() to escape - # handle_request before self.timeout was available. - timeout = self.socket.gettimeout() - if timeout is None: - timeout = self.timeout - elif self.timeout is not None: - timeout = min(timeout, self.timeout) - fd_sets = select.select([self], [], [], timeout) - if not fd_sets[0]: - self.handle_timeout() - return - self._handle_request_noblock() - - def _handle_request_noblock(self): - """Handle one request, without blocking. - - I assume that select.select has returned that the socket is - readable before this function was called, so there should be - no risk of blocking in get_request(). - """ - try: - request, client_address = self.get_request() - except socket.error: - return - if self.verify_request(request, client_address): - try: - self.process_request(request, client_address) - except: - self.handle_error(request, client_address) - self.close_request(request) - - def handle_timeout(self): - """Called if no new request arrives within self.timeout. - - Overridden by ForkingMixIn. - """ - pass - - def verify_request(self, request, client_address): - """Verify the request. May be overridden. - - Return True if we should proceed with this request. - - """ - return True - - def process_request(self, request, client_address): - """Call finish_request. - - Overridden by ForkingMixIn and ThreadingMixIn. - - """ - self.finish_request(request, client_address) - self.close_request(request) - - def server_close(self): - """Called to clean-up the server. - - May be overridden. - - """ - pass - - def finish_request(self, request, client_address): - """Finish one request by instantiating RequestHandlerClass.""" - self.RequestHandlerClass(request, client_address, self) - - def close_request(self, request): - """Called to clean up an individual request.""" - pass - - def handle_error(self, request, client_address): - """Handle an error gracefully. May be overridden. - - The default is to print a traceback and continue. - - """ - print '-'*40 - print 'Exception happened during processing of request from', - print client_address - import traceback - traceback.print_exc() # XXX But this goes to stderr! - print '-'*40 - - -class TCPServer(BaseServer): - - """Base class for various socket-based server classes. - - Defaults to synchronous IP stream (i.e., TCP). - - Methods for the caller: - - - __init__(server_address, RequestHandlerClass, bind_and_activate=True) - - serve_forever(poll_interval=0.5) - - shutdown() - - handle_request() # if you don't use serve_forever() - - fileno() -> int # for select() - - Methods that may be overridden: - - - server_bind() - - server_activate() - - get_request() -> request, client_address - - handle_timeout() - - verify_request(request, client_address) - - process_request(request, client_address) - - close_request(request) - - handle_error() - - Methods for derived classes: - - - finish_request(request, client_address) - - Class variables that may be overridden by derived classes or - instances: - - - timeout - - address_family - - socket_type - - request_queue_size (only for stream sockets) - - allow_reuse_address - - Instance variables: - - - server_address - - RequestHandlerClass - - socket - - """ - - address_family = socket.AF_INET - - socket_type = socket.SOCK_STREAM - - request_queue_size = 5 - - allow_reuse_address = False - - def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True): - """Constructor. May be extended, do not override.""" - BaseServer.__init__(self, server_address, RequestHandlerClass) - self.socket = socket.socket(self.address_family, - self.socket_type) - if bind_and_activate: - self.server_bind() - self.server_activate() - - def server_bind(self): - """Called by constructor to bind the socket. - - May be overridden. - - """ - if self.allow_reuse_address: - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind(self.server_address) - self.server_address = self.socket.getsockname() - - def server_activate(self): - """Called by constructor to activate the server. - - May be overridden. - - """ - self.socket.listen(self.request_queue_size) - - def server_close(self): - """Called to clean-up the server. - - May be overridden. - - """ - self.socket.close() - - def fileno(self): - """Return socket file number. - - Interface required by select(). - - """ - return self.socket.fileno() - - def get_request(self): - """Get the request and client address from the socket. - - May be overridden. - - """ - return self.socket.accept() - - def close_request(self, request): - """Called to clean up an individual request.""" - request.close() - - -class UDPServer(TCPServer): - - """UDP server class.""" - - allow_reuse_address = False - - socket_type = socket.SOCK_DGRAM - - max_packet_size = 8192 - - def get_request(self): - data, client_addr = self.socket.recvfrom(self.max_packet_size) - return (data, self.socket), client_addr - - def server_activate(self): - # No need to call listen() for UDP. - pass - - def close_request(self, request): - # No need to close anything. - pass - -class ForkingMixIn: - - """Mix-in class to handle each request in a new process.""" - - timeout = 300 - active_children = None - max_children = 40 - - def collect_children(self): - """Internal routine to wait for children that have exited.""" - if self.active_children is None: return - while len(self.active_children) >= self.max_children: - # XXX: This will wait for any child process, not just ones - # spawned by this library. This could confuse other - # libraries that expect to be able to wait for their own - # children. - try: - pid, status = os.waitpid(0, options=0) - except os.error: - pid = None - if pid not in self.active_children: continue - self.active_children.remove(pid) - - # XXX: This loop runs more system calls than it ought - # to. There should be a way to put the active_children into a - # process group and then use os.waitpid(-pgid) to wait for any - # of that set, but I couldn't find a way to allocate pgids - # that couldn't collide. - for child in self.active_children: - try: - pid, status = os.waitpid(child, os.WNOHANG) - except os.error: - pid = None - if not pid: continue - try: - self.active_children.remove(pid) - except ValueError, e: - raise ValueError('%s. x=%d and list=%r' % (e.message, pid, - self.active_children)) - - def handle_timeout(self): - """Wait for zombies after self.timeout seconds of inactivity. - - May be extended, do not override. - """ - self.collect_children() - - def process_request(self, request, client_address): - """Fork a new subprocess to process the request.""" - self.collect_children() - pid = os.fork() - if pid: - # Parent process - if self.active_children is None: - self.active_children = [] - self.active_children.append(pid) - self.close_request(request) - return - else: - # Child process. - # This must never return, hence os._exit()! - try: - self.finish_request(request, client_address) - os._exit(0) - except: - try: - self.handle_error(request, client_address) - finally: - os._exit(1) - - -class ThreadingMixIn: - """Mix-in class to handle each request in a new thread.""" - - # Decides how threads will act upon termination of the - # main process - daemon_threads = False - - def process_request_thread(self, request, client_address): - """Same as in BaseServer but as a thread. - - In addition, exception handling is done here. - - """ - try: - self.finish_request(request, client_address) - self.close_request(request) - except: - self.handle_error(request, client_address) - self.close_request(request) - - def process_request(self, request, client_address): - """Start a new thread to process the request.""" - t = threading.Thread(target = self.process_request_thread, - args = (request, client_address)) - if self.daemon_threads: - t.setDaemon (1) - t.start() - - -class ForkingUDPServer(ForkingMixIn, UDPServer): pass -class ForkingTCPServer(ForkingMixIn, TCPServer): pass - -class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass -class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass - -if hasattr(socket, 'AF_UNIX'): - - class UnixStreamServer(TCPServer): - address_family = socket.AF_UNIX - - class UnixDatagramServer(UDPServer): - address_family = socket.AF_UNIX - - class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass - - class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass - -class BaseRequestHandler: - - """Base class for request handler classes. - - This class is instantiated for each request to be handled. The - constructor sets the instance variables request, client_address - and server, and then calls the handle() method. To implement a - specific service, all you need to do is to derive a class which - defines a handle() method. - - The handle() method can find the request as self.request, the - client address as self.client_address, and the server (in case it - needs access to per-server information) as self.server. Since a - separate instance is created for each request, the handle() method - can define arbitrary other instance variariables. - - """ - - def __init__(self, request, client_address, server): - self.request = request - self.client_address = client_address - self.server = server - try: - self.setup() - self.handle() - self.finish() - finally: - sys.exc_traceback = None # Help garbage collection - - def setup(self): - pass - - def handle(self): - pass - - def finish(self): - pass - - -# The following two classes make it possible to use the same service -# class for stream or datagram servers. -# Each class sets up these instance variables: -# - rfile: a file object from which receives the request is read -# - wfile: a file object to which the reply is written -# When the handle() method returns, wfile is flushed properly - - -class StreamRequestHandler(BaseRequestHandler): - - """Define self.rfile and self.wfile for stream sockets.""" - - # Default buffer sizes for rfile, wfile. - # We default rfile to buffered because otherwise it could be - # really slow for large data (a getc() call per byte); we make - # wfile unbuffered because (a) often after a write() we want to - # read and we need to flush the line; (b) big writes to unbuffered - # files are typically optimized by stdio even when big reads - # aren't. - rbufsize = -1 - wbufsize = 0 - - def setup(self): - self.connection = self.request - self.rfile = self.connection.makefile('rb', self.rbufsize) - self.wfile = self.connection.makefile('wb', self.wbufsize) - - def finish(self): - if not self.wfile.closed: - self.wfile.flush() - self.wfile.close() - self.rfile.close() - - -class DatagramRequestHandler(BaseRequestHandler): - - # XXX Regrettably, I cannot get this working on Linux; - # s.recvfrom() doesn't return a meaningful client address. - - """Define self.rfile and self.wfile for datagram sockets.""" - - def setup(self): - try: - from cStringIO import StringIO - except ImportError: - from StringIO import StringIO - self.packet, self.socket = self.request - self.rfile = StringIO(self.packet) - self.wfile = StringIO() - - def finish(self): - self.socket.sendto(self.wfile.getvalue(), self.client_address) From 58ebd45efed8ee1e30d42a5abfff94080bf45951 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sat, 24 Sep 2011 18:33:58 +0200 Subject: [PATCH 36/47] [IMP] openerp.service: import at the top. bzr revid: vmt@openerp.com-20110924163358-svx1yjuwdybxxr6k --- openerp/service/__init__.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 2a4c03a0892..3da7a50e1f0 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -19,10 +19,18 @@ # ############################################################################## +import logging +import threading +import time + import http_server import netrpc_server import web_services +import openerp.netsvc +import openerp.osv +import openerp.tools + #.apidoc title: RPC Services """ Classes of this module implement the network protocols that the @@ -35,14 +43,15 @@ import web_services """ def start_services(): - import openerp - http_server = openerp.service.http_server - netrpc_server = openerp.service.netrpc_server + """ Start all services. + Services include the different servers and cron threads. + + """ # Instantiate local services (this is a legacy design). openerp.osv.osv.start_object_proxy() # Export (for RPC) services. - openerp.service.web_services.start_web_services() + web_services.start_web_services() # Initialize the HTTP stack. http_server.init_servers() @@ -57,10 +66,7 @@ def start_services(): openerp.netsvc.Server.startAll() def stop_services(): - import openerp - import logging - import threading - import time + """ Stop all services. """ openerp.netsvc.Agent.quit() openerp.netsvc.Server.quitAll() config = openerp.tools.config @@ -69,14 +75,14 @@ def stop_services(): logger.info("Hit CTRL-C again or send a second signal to force the shutdown.") logging.shutdown() - # manually join() all threads before calling sys.exit() to allow a second signal + # Manually join() all threads before calling sys.exit() to allow a second signal # to trigger _force_quit() in case some non-daemon threads won't exit cleanly. - # threading.Thread.join() should not mask signals (at least in python 2.5) + # threading.Thread.join() should not mask signals (at least in python 2.5). for thread in threading.enumerate(): if thread != threading.currentThread() and not thread.isDaemon(): while thread.isAlive(): - # need a busyloop here as thread.join() masks signals - # and would present the forced shutdown + # Need a busyloop here as thread.join() masks signals + # and would prevent the forced shutdown. thread.join(0.05) time.sleep(0.05) From 0a02a387399880395c1275efb78480ee7e6c8692 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 24 Sep 2011 18:38:28 +0200 Subject: [PATCH 37/47] [IMP] web.common.sesion local backend part1 bzr revid: al@openerp.com-20110924163828-gbys8t65gyz0qkcu --- addons/web/__init__.py | 4 ---- addons/web/common/dispatch.py | 12 +++++------- addons/web/common/openerplib/main.py | 20 +++++++++++++++++++- addons/web/common/session.py | 7 +++---- openerp-web.py | 2 +- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 2478edb81b7..6934ef1e81a 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -20,9 +20,5 @@ def wsgi_postload(): o.backend = 'local' app = common.dispatch.Root(o) - #import openerp.wsgi openerp.wsgi.register_wsgi_handler(app) -# TODO -# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point - diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py index 98b948704fa..2a5546ac85a 100644 --- a/addons/web/common/dispatch.py +++ b/addons/web/common/dispatch.py @@ -21,8 +21,7 @@ import werkzeug.wsgi import ast import nonliterals import http -# import backendlocal as backend -import session as backend +import session import openerplib __all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller', @@ -79,12 +78,12 @@ class WebRequest(object): .. attribute:: session_id - opaque identifier for the :class:`backend.OpenERPSession` instance of + opaque identifier for the :class:`session.OpenERPSession` instance of the current request .. attribute:: session - :class:`~backend.OpenERPSession` instance for the current request + :class:`~session.OpenERPSession` instance for the current request .. attribute:: context @@ -100,13 +99,12 @@ class WebRequest(object): self.httpresponse = None self.httpsession = request.session self.config = config + def init(self, params): self.params = dict(params) # OpenERP session setup self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex - self.session = self.httpsession.setdefault( - self.session_id, backend.OpenERPSession( - self.config.server_host, self.config.server_port)) + self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession(self.config)) self.context = self.params.pop('context', None) self.debug = self.params.pop('debug', False) != False diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py index 21f8d48ff63..75941b4ee9e 100644 --- a/addons/web/common/openerplib/main.py +++ b/addons/web/common/openerplib/main.py @@ -190,6 +190,22 @@ class NetRPCConnector(Connector): socket.disconnect() return result +class LocalConnector(Connector): + """ + A type of connector that uses the XMLRPC protocol. + """ + PROTOCOL = 'local' + + __logger = _getChildLogger(_logger, 'connector.local') + + def __init__(self): + pass + + def send(self, service_name, method, *args): + import openerp + # todo local + print service_name, method, args + class Service(object): """ A class to execute RPC calls on a specific service of the remote server. @@ -377,8 +393,10 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"): return XmlRPCConnector(hostname, port) elif protocol == "netrpc": return NetRPCConnector(hostname, port) + elif protocol == "local": + return LocalConnector() else: - raise ValueError("You must choose xmlrpc or netrpc") + raise ValueError("You must choose xmlrpc or netrpc or local") def get_connection(hostname, protocol="xmlrpc", port='auto', database=None, login=None, password=None, user_id=None): diff --git a/addons/web/common/session.py b/addons/web/common/session.py index d1997a3f903..670ab75e1e7 100644 --- a/addons/web/common/session.py +++ b/addons/web/common/session.py @@ -26,9 +26,8 @@ class OpenERPSession(object): Used to store references to non-literal domains which need to be round-tripped to the client browser. """ - def __init__(self, server='127.0.0.1', port=8069): - self._server = server - self._port = port + def __init__(self, config): + self.config = config self._db = False self._uid = False self._login = False @@ -42,7 +41,7 @@ class OpenERPSession(object): self.client_timezone = False def build_connection(self): - return openerplib.get_connection(hostname=self._server, port=self._port, + return openerplib.get_connection(hostname=self.config.server_host, port=self.config.server_port, database=self._db, login=self._login, user_id=self._uid, password=self._password) diff --git a/openerp-web.py b/openerp-web.py index 92b411dd63e..e5b28c83e0a 100755 --- a/openerp-web.py +++ b/openerp-web.py @@ -56,7 +56,7 @@ import web.common.dispatch if __name__ == "__main__": (options, args) = optparser.parse_args(sys.argv[1:]) - options.backend = 'rpc' + options.backend = 'xmlrpc' os.environ["TZ"] = "UTC" From c845bef20657b57e83d44cc3dd3ded83ef058381 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 24 Sep 2011 18:57:05 +0200 Subject: [PATCH 38/47] [IMP] web.common.sesion local backend part2 bzr revid: al@openerp.com-20110924165705-c0wd7hcup9ib74ry --- addons/web/__init__.py | 6 ++++-- addons/web/common/openerplib/main.py | 4 ++-- addons/web/common/session.py | 13 +++++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 6934ef1e81a..263e0b8be17 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -2,16 +2,18 @@ import common import controllers import common.dispatch import logging +import optparse _logger = logging.getLogger(__name__) +class Options(object): + pass + def wsgi_postload(): import openerp.wsgi import os import tempfile _logger.info("embedded mode") - class Options(object): - pass o = Options() o.dbfilter = '.*' o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py index 75941b4ee9e..33e8a4075f8 100644 --- a/addons/web/common/openerplib/main.py +++ b/addons/web/common/openerplib/main.py @@ -379,7 +379,7 @@ class Model(object): records = self.read(record_ids, fields or [], context or {}) return records -def get_connector(hostname, protocol="xmlrpc", port="auto"): +def get_connector(hostname=None, protocol="xmlrpc", port="auto"): """ A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC. @@ -398,7 +398,7 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"): else: raise ValueError("You must choose xmlrpc or netrpc or local") -def get_connection(hostname, protocol="xmlrpc", port='auto', database=None, +def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None, login=None, password=None, user_id=None): """ A shortcut method to easily create a connection to a remote OpenERP server. diff --git a/addons/web/common/session.py b/addons/web/common/session.py index 670ab75e1e7..c06763bebe2 100644 --- a/addons/web/common/session.py +++ b/addons/web/common/session.py @@ -39,11 +39,16 @@ class OpenERPSession(object): self._lang = {} self.remote_timezone = 'utc' self.client_timezone = False - + def build_connection(self): - return openerplib.get_connection(hostname=self.config.server_host, port=self.config.server_port, - database=self._db, login=self._login, - user_id=self._uid, password=self._password) + if self.config.backend == 'local': + conn = openerplib.get_connection(protocol='local', database=self._db, + login=self._login, user_id=self._uid, password=self._password) + else: + conn = openerplib.get_connection(hostname=self.config.server_host, + port=self.config.server_port, database=self._db, login=self._login, + user_id=self._uid, password=self._password) + return conn def proxy(self, service): return self.build_connection().get_service(service) From 895a60f3229aaefd3b72e82159d71d64e7a60e79 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 01:44:16 +0200 Subject: [PATCH 39/47] [IMP] web runs nicely in the server, That s one small commit for a man, a giant leap for openerp bzr revid: al@openerp.com-20110924234416-b9pzbhpqfxi319te --- addons/web/common/openerplib/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/addons/web/common/openerplib/main.py b/addons/web/common/openerplib/main.py index 33e8a4075f8..d5110196c87 100644 --- a/addons/web/common/openerplib/main.py +++ b/addons/web/common/openerplib/main.py @@ -203,8 +203,19 @@ class LocalConnector(Connector): def send(self, service_name, method, *args): import openerp - # todo local - print service_name, method, args + # TODO Exception handling + # This will be changed to be xmlrpc compatible + # OpenERPWarning code 1 + # OpenERPException code 2 + #except openerp.netsvc.OpenERPDispatcherException, e: + # fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback) + # response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + #except: + # exc_type, exc_value, exc_tb = sys.exc_info() + # fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)) + # response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) + result = openerp.netsvc.dispatch_rpc(service_name, method, args, None) + return result class Service(object): """ From eda28246eeffaa6e700b36d3a52a437f393698fe Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 02:33:22 +0200 Subject: [PATCH 40/47] [REM] http static crap (you may use web controllers or /static dir) bzr revid: al@openerp.com-20110925003322-yioqd1cdlffvr6uc --- openerp/service/http_server.py | 45 ---------------------------------- openerp/tools/config.py | 8 ------ 2 files changed, 53 deletions(-) diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 9bfb9397d19..57fe21d8fea 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -246,7 +246,6 @@ class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.Simple self.connection = dummyconn() self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys()) - def init_xmlrpc(): if tools.config.get('xmlrpc', False): # Example of http file serving: @@ -260,50 +259,6 @@ def init_xmlrpc(): reg_http_service('/xmlrpc/', XMLRPCRequestHandler, secure_only=True) logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only") -class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): - _logger = logging.getLogger('httpd') - _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] } - - def __init__(self,request, client_address, server): - HTTPHandler.__init__(self,request,client_address,server) - document_root = tools.config.get('static_http_document_root', False) - assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!" - self.__basepath = document_root - - def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - - """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - path = posixpath.normpath(urllib.unquote(path)) - words = path.split('/') - words = filter(None, words) - path = self.__basepath - for word in words: - if word in (os.curdir, os.pardir): continue - path = os.path.join(path, word) - return path - -def init_static_http(): - if not tools.config.get('static_http_enable', False): - return - - document_root = tools.config.get('static_http_document_root', False) - assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)" - - base_path = tools.config.get('static_http_url_prefix', '/') - - reg_http_service(base_path, StaticHTTPHandler) - - logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \ - (document_root, base_path)) - import security class OpenERPAuthProvider(AuthProvider): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 69155f99966..94660939af8 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -143,13 +143,6 @@ class configmanager(object): help="disable the NETRPC protocol") parser.add_option_group(group) - # Static HTTP - group = optparse.OptionGroup(parser, "Static HTTP service") - group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", my_default=False, help="enable static HTTP service for serving plain HTML files") - group.add_option("--static-http-document-root", dest="static_http_document_root", help="specify the directory containing your static HTML files (e.g '/var/www/')") - group.add_option("--static-http-url-prefix", dest="static_http_url_prefix", help="specify the URL root prefix where you want web browsers to access your static HTML files (e.g '/')") - parser.add_option_group(group) - # Testing Group group = optparse.OptionGroup(parser, "Testing Configuration") group.add_option("--test-file", dest="test_file", my_default=False, @@ -341,7 +334,6 @@ class configmanager(object): 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', 'secure_cert_file', 'secure_pkey_file', - 'static_http_enable', 'static_http_document_root', 'static_http_url_prefix' ] for arg in keys: From d3b76ea7f1eb05f9eb502541c3dfef25d10fc0a0 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 02:54:37 +0200 Subject: [PATCH 41/47] [IMP] temporary web dbfilter before metaoptions bzr revid: al@openerp.com-20110925005437-7jdgrzl7i4ihx44y --- openerp/tools/config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 94660939af8..2d9d0b4f587 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -143,6 +143,13 @@ class configmanager(object): help="disable the NETRPC protocol") parser.add_option_group(group) + # WEB + # TODO move to web addons after MetaOption merge + group = optparse.OptionGroup(parser, "Web interface Configuration") + group.add_option("--db-filter", dest="dbfilter", default='.*', + help="Filter listed database", metavar="REGEXP") + parser.add_option_group(group) + # Testing Group group = optparse.OptionGroup(parser, "Testing Configuration") group.add_option("--test-file", dest="test_file", my_default=False, @@ -333,7 +340,7 @@ class configmanager(object): 'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path', 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', - 'secure_cert_file', 'secure_pkey_file', + 'secure_cert_file', 'secure_pkey_file', 'dbfilter' ] for arg in keys: From cd95f48ee7a9ce48f0bc5669e74e9b95f57ab0d4 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 02:55:23 +0200 Subject: [PATCH 42/47] [IMP] dbfilter option from openerp-server in embedded mode bzr revid: al@openerp.com-20110925005523-krlap0jdgc5ukott --- addons/web/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/__init__.py b/addons/web/__init__.py index 263e0b8be17..681fd6b42b3 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -11,11 +11,12 @@ class Options(object): def wsgi_postload(): import openerp.wsgi + import openerp.tools import os import tempfile _logger.info("embedded mode") o = Options() - o.dbfilter = '.*' + o.dbfilter = openerp.tools.config['dbfilter'] o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions") o.addons_path = os.path.dirname(os.path.dirname(__file__)) o.serve_static = True From 404e54536590e56fc681e3b6f4d8f5e963bc6740 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 03:20:39 +0200 Subject: [PATCH 43/47] [FIX] revert 3651 al@openerp.com-20110925003322-yioqd1cdlffvr6uc http static is used by webdav bzr revid: al@openerp.com-20110925012039-4hmqj2v5aaxxtxwy --- openerp/service/http_server.py | 45 ++++++++++++++++++++++++++++++++++ openerp/tools/config.py | 8 ++++++ 2 files changed, 53 insertions(+) diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 57fe21d8fea..9bfb9397d19 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -246,6 +246,7 @@ class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.Simple self.connection = dummyconn() self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys()) + def init_xmlrpc(): if tools.config.get('xmlrpc', False): # Example of http file serving: @@ -259,6 +260,50 @@ def init_xmlrpc(): reg_http_service('/xmlrpc/', XMLRPCRequestHandler, secure_only=True) logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only") +class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): + _logger = logging.getLogger('httpd') + _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] } + + def __init__(self,request, client_address, server): + HTTPHandler.__init__(self,request,client_address,server) + document_root = tools.config.get('static_http_document_root', False) + assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!" + self.__basepath = document_root + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + path = posixpath.normpath(urllib.unquote(path)) + words = path.split('/') + words = filter(None, words) + path = self.__basepath + for word in words: + if word in (os.curdir, os.pardir): continue + path = os.path.join(path, word) + return path + +def init_static_http(): + if not tools.config.get('static_http_enable', False): + return + + document_root = tools.config.get('static_http_document_root', False) + assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)" + + base_path = tools.config.get('static_http_url_prefix', '/') + + reg_http_service(base_path, StaticHTTPHandler) + + logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \ + (document_root, base_path)) + import security class OpenERPAuthProvider(AuthProvider): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 2d9d0b4f587..3304a4f4a99 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -150,6 +150,13 @@ class configmanager(object): help="Filter listed database", metavar="REGEXP") parser.add_option_group(group) + # Static HTTP + group = optparse.OptionGroup(parser, "Static HTTP service") + group.add_option("--static-http-enable", dest="static_http_enable", action="store_true", my_default=False, help="enable static HTTP service for serving plain HTML files") + group.add_option("--static-http-document-root", dest="static_http_document_root", help="specify the directory containing your static HTML files (e.g '/var/www/')") + group.add_option("--static-http-url-prefix", dest="static_http_url_prefix", help="specify the URL root prefix where you want web browsers to access your static HTML files (e.g '/')") + parser.add_option_group(group) + # Testing Group group = optparse.OptionGroup(parser, "Testing Configuration") group.add_option("--test-file", dest="test_file", my_default=False, @@ -340,6 +347,7 @@ class configmanager(object): 'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path', 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', + 'static_http_enable', 'static_http_document_root', 'static_http_url_prefix', 'secure_cert_file', 'secure_pkey_file', 'dbfilter' ] From da705a6b554c4ffa5ef419db8f446504be23057d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Sun, 25 Sep 2011 03:42:43 +0200 Subject: [PATCH 44/47] [FIX] rpc: honor *_interface options + log it bzr revid: odo@openerp.com-20110925014243-0q1o67twburpga0z --- openerp/service/netrpc_server.py | 2 +- openerp/wsgi.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openerp/service/netrpc_server.py b/openerp/service/netrpc_server.py index f99253a1e76..3667b38c0b8 100644 --- a/openerp/service/netrpc_server.py +++ b/openerp/service/netrpc_server.py @@ -108,7 +108,7 @@ class TinySocketServerThread(threading.Thread,netsvc.Server): self.socket.listen(5) self.threads = [] netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO, - "starting NET-RPC service at %s port %d" % (interface or '0.0.0.0', port,)) + "starting NET-RPC service on %s:%s" % (interface or '0.0.0.0', port,)) def run(self): try: diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 96651edf749..5a8f19f8d83 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -302,14 +302,18 @@ def serve(): global httpd - # TODO Change the xmlrpc_port option to http_port. + # TODO Change the xmlrpc_* options to http_* + interface = config['xmlrpc_interface'] or '0.0.0.0' + port = config['xmlrpc_port'] try: import werkzeug.serving - httpd = werkzeug.serving.make_server('0.0.0.0', config['xmlrpc_port'], application, threaded=True) + httpd = werkzeug.serving.make_server(interface, port, application, threaded=True) + logging.getLogger('wsgi').info('HTTP service (werkzeug) running on %s:%s', interface, port) except ImportError, e: import wsgiref.simple_server - logging.getLogger('wsgi').warn('Can not import werkzeug, ' 'falling back to wsgiref.') - httpd = wsgiref.simple_server.make_server('0.0.0.0', config['xmlrpc_port'], application) + logging.getLogger('wsgi').warn('Werkzeug module unavailable, falling back to wsgiref.') + httpd = wsgiref.simple_server.make_server(interface, port, application) + logging.getLogger('wsgi').info('HTTP service (wsgiref) running on %s:%s', interface, port) httpd.serve_forever() From adc30aa10f1ec4d36c3e1e4bcb6cdb13b7bf1f22 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 25 Sep 2011 03:50:54 +0200 Subject: [PATCH 45/47] [IMP] web.common.dispatch only load module with controllers bzr revid: al@openerp.com-20110925015054-1223jdthddb1wrah --- addons/web/common/dispatch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/common/dispatch.py b/addons/web/common/dispatch.py index 2a5546ac85a..c6834a3b715 100644 --- a/addons/web/common/dispatch.py +++ b/addons/web/common/dispatch.py @@ -387,7 +387,8 @@ class Root(object): for module in os.listdir(addons_path): if module not in addons_module: manifest_path = os.path.join(addons_path, module, '__openerp__.py') - if os.path.isfile(manifest_path): + path_controllers = os.path.join(addons_path, module, 'controllers') + if os.path.isfile(manifest_path) and os.path.isdir(path_controllers): manifest = ast.literal_eval(open(manifest_path).read()) _logger.info("Loading %s", module) m = __import__(module) From 5eb95a3fd43707b82bb0c4d1b25700be9d00783d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Sun, 25 Sep 2011 04:13:39 +0200 Subject: [PATCH 46/47] [REVERT] incorrect substitution, also causing duplicate fields in form, r.5073 Revision 5073 has revid fp@tinyerp.com-20110917153740-pbzrq432ilbi5pb5 bzr revid: odo@openerp.com-20110925021339-l6546pvwhr65j0da --- addons/crm/crm_lead_view.xml | 4 ++-- addons/hr_recruitment/hr_recruitment_view.xml | 2 +- addons/project_issue/project_issue_view.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml index a72d70949af..d631e371948 100644 --- a/addons/crm/crm_lead_view.xml +++ b/addons/crm/crm_lead_view.xml @@ -195,7 +195,7 @@ - + @@ -580,7 +580,7 @@ - + diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index 6c2c91a7589..8dd5e171206 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -172,7 +172,7 @@ icon="terp-mail-replied" type="action"/> - + diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index e2434d7f36e..544606acb5d 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -124,7 +124,7 @@ icon="terp-mail-replied" type="action"/> - + From e33f8abf7ca1385ea4d002dbf7c8da3fa7d5e80e Mon Sep 17 00:00:00 2001 From: Launchpad Translations on behalf of openerp <> Date: Sun, 25 Sep 2011 04:41:08 +0000 Subject: [PATCH 47/47] Launchpad automatic translations update. bzr revid: launchpad_translations_on_behalf_of_openerp-20110925044050-bjkzm7pf2t39mxwn bzr revid: launchpad_translations_on_behalf_of_openerp-20110925044108-geoyxpt8oeooq8op --- addons/html_view/i18n/da.po | 63 +++ addons/sale_layout/i18n/da.po | 292 ++++++++++++++ openerp/addons/base/i18n/da.po | 680 ++++++++++++++++----------------- 3 files changed, 695 insertions(+), 340 deletions(-) create mode 100644 addons/html_view/i18n/da.po create mode 100644 addons/sale_layout/i18n/da.po diff --git a/addons/html_view/i18n/da.po b/addons/html_view/i18n/da.po new file mode 100644 index 00000000000..91bd304d59b --- /dev/null +++ b/addons/html_view/i18n/da.po @@ -0,0 +1,63 @@ +# Danish translation for openobject-addons +# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2011-01-11 11:15+0000\n" +"PO-Revision-Date: 2011-09-24 09:55+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Danish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2011-09-25 04:41+0000\n" +"X-Generator: Launchpad (build 14012)\n" + +#. module: html_view +#: field:html.view,name:0 +msgid "Name" +msgstr "" + +#. module: html_view +#: field:html.view,comp_id:0 +msgid "Company" +msgstr "Virksomhed" + +#. module: html_view +#: model:ir.actions.act_window,name:html_view.action_html_view_form +#: model:ir.ui.menu,name:html_view.html_form +msgid "Html Test" +msgstr "HTML test" + +#. module: html_view +#: view:html.view:0 +msgid "Html Example" +msgstr "" + +#. module: html_view +#: model:ir.module.module,shortdesc:html_view.module_meta_information +msgid "Html View" +msgstr "" + +#. module: html_view +#: field:html.view,bank_ids:0 +msgid "Banks" +msgstr "" + +#. module: html_view +#: model:ir.module.module,description:html_view.module_meta_information +msgid "" +"\n" +" This is the test module which shows html tag supports in normal xml form " +"view.\n" +" " +msgstr "" + +#. module: html_view +#: model:ir.model,name:html_view.model_html_view +msgid "html.view" +msgstr "" diff --git a/addons/sale_layout/i18n/da.po b/addons/sale_layout/i18n/da.po new file mode 100644 index 00000000000..230becf3b19 --- /dev/null +++ b/addons/sale_layout/i18n/da.po @@ -0,0 +1,292 @@ +# Danish translation for openobject-addons +# Copyright (c) 2011 Rosetta Contributors and Canonical Ltd 2011 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2011. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2011-01-11 11:16+0000\n" +"PO-Revision-Date: 2011-09-24 17:46+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Danish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2011-09-25 04:41+0000\n" +"X-Generator: Launchpad (build 14012)\n" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Sub Total" +msgstr "Sub total" + +#. module: sale_layout +#: model:ir.module.module,description:sale_layout.module_meta_information +msgid "" +"\n" +" This module provides features to improve the layout of the Sales Order.\n" +"\n" +" It gives you the possibility to\n" +" * order all the lines of a sales order\n" +" * add titles, comment lines, sub total lines\n" +" * draw horizontal lines and put page breaks\n" +"\n" +" " +msgstr "" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Title" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Disc. (%)" +msgstr "" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Note" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Unit Price" +msgstr "Enhedspris" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Order N°" +msgstr "Ordre nummer" + +#. module: sale_layout +#: field:sale.order,abstract_line_ids:0 +msgid "Order Lines" +msgstr "Ordre linier" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Disc.(%)" +msgstr "" + +#. module: sale_layout +#: field:sale.order.line,layout_type:0 +msgid "Layout Type" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Seq." +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "UoM" +msgstr "" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Product" +msgstr "" + +#. module: sale_layout +#: sql_constraint:sale.order:0 +msgid "Order Reference must be unique !" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Description" +msgstr "Beskrivelse" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Manual Description" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Our Salesman" +msgstr "Vores sælger" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Automatic Declaration" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Invoice Lines" +msgstr "Faktura linier" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Quantity" +msgstr "Antal" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Quotation N°" +msgstr "Tilbud nummer" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "VAT" +msgstr "Moms" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Make Invoice" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Properties" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Invoice address :" +msgstr "Faktura adresse:" + +#. module: sale_layout +#: model:ir.module.module,shortdesc:sale_layout.module_meta_information +msgid "Sale Order Layout" +msgstr "Salgsordre layout" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Page Break" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Notes" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Date Ordered" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Shipping address :" +msgstr "Leveringsadresse" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Taxes" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Net Total :" +msgstr "Netto total:" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Tel. :" +msgstr "Tel:" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Total :" +msgstr "Total:" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Payment Terms" +msgstr "Betalingsbetingelser" + +#. module: sale_layout +#: view:sale.order:0 +msgid "History" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Sale Order Lines" +msgstr "Salgsordre linier" + +#. module: sale_layout +#: selection:sale.order.line,layout_type:0 +msgid "Separator Line" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Your Reference" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Quotation Date" +msgstr "Tilbudsdato" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "TVA :" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Qty" +msgstr "Antal" + +#. module: sale_layout +#: view:sale.order:0 +msgid "States" +msgstr "Stater" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Sales order lines" +msgstr "" + +#. module: sale_layout +#: model:ir.actions.report.xml,name:sale_layout.sale_order_1 +msgid "Order with Layout" +msgstr "" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Extra Info" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Taxes :" +msgstr "" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Fax :" +msgstr "Fax:" + +#. module: sale_layout +#: model:ir.model,name:sale_layout.model_sale_order +msgid "Sales Order" +msgstr "Salgsordre" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Order Line" +msgstr "Ordre linie" + +#. module: sale_layout +#: report:sale.order.layout:0 +msgid "Price" +msgstr "Pris" + +#. module: sale_layout +#: model:ir.model,name:sale_layout.model_sale_order_line +msgid "Sales Order Line" +msgstr "Salgsordrelinie" + +#. module: sale_layout +#: view:sale.order:0 +msgid "Stock Moves" +msgstr "Lagerflytninger" diff --git a/openerp/addons/base/i18n/da.po b/openerp/addons/base/i18n/da.po index da07b07a1e9..2fa355ab21a 100644 --- a/openerp/addons/base/i18n/da.po +++ b/openerp/addons/base/i18n/da.po @@ -8,14 +8,14 @@ msgstr "" "Project-Id-Version: openobject-addons\n" "Report-Msgid-Bugs-To: FULL NAME \n" "POT-Creation-Date: 2011-01-11 11:14+0000\n" -"PO-Revision-Date: 2010-02-07 05:09+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2011-09-24 16:40+0000\n" +"Last-Translator: John Mertens Pallesen \n" "Language-Team: Danish \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2011-09-01 04:40+0000\n" -"X-Generator: Launchpad (build 13827)\n" +"X-Launchpad-Export-Date: 2011-09-25 04:40+0000\n" +"X-Generator: Launchpad (build 14012)\n" #. module: base #: view:ir.filters:0 @@ -141,7 +141,7 @@ msgstr "" #: code:addons/base/res/res_user.py:507 #, python-format msgid "Warning!" -msgstr "" +msgstr "Advarsel!" #. module: base #: code:addons/base/ir/ir_model.py:304 @@ -196,7 +196,7 @@ msgstr "" #: model:ir.actions.act_window,name:base.action_res_company_tree #: model:ir.ui.menu,name:base.menu_action_res_company_tree msgid "Company's Structure" -msgstr "" +msgstr "Virksomhedens struktur" #. module: base #: selection:base.language.install,lang:0 @@ -277,7 +277,7 @@ msgstr "" #. module: base #: field:res.partner,credit_limit:0 msgid "Credit Limit" -msgstr "" +msgstr "Kreditgrænse" #. module: base #: field:ir.model.data,date_update:0 @@ -372,7 +372,7 @@ msgstr "Fransk Guyana" #. module: base #: selection:base.language.install,lang:0 msgid "Greek / Ελληνικά" -msgstr "" +msgstr "Græsk / Greek" #. module: base #: selection:base.language.install,lang:0 @@ -483,7 +483,7 @@ msgstr "" #. module: base #: view:res.config.users:0 msgid "New User" -msgstr "" +msgstr "Ny bruger" #. module: base #: view:base.language.export:0 @@ -525,7 +525,7 @@ msgstr "Eritrea" #: view:res.config:0 #: view:res.config.installer:0 msgid "description" -msgstr "" +msgstr "Beskrivelse" #. module: base #: model:ir.ui.menu,name:base.menu_base_action_rule @@ -563,7 +563,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "Swedish / svenska" -msgstr "" +msgstr "Svensk / Swedish" #. module: base #: model:res.country,name:base.rs @@ -601,7 +601,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "Albanian / Shqip" -msgstr "" +msgstr "Albanien" #. module: base #: model:ir.ui.menu,name:base.menu_crm_config_opportunity @@ -651,7 +651,7 @@ msgstr "Spanien" #. module: base #: model:ir.ui.menu,name:base.menu_translation_export msgid "Import / Export" -msgstr "" +msgstr "Import / Eksport" #. module: base #: help:ir.actions.act_window,domain:0 @@ -692,7 +692,7 @@ msgstr "Oman" #: model:ir.actions.act_window,name:base.action_payterm_form #: model:ir.model,name:base.model_res_payterm msgid "Payment term" -msgstr "" +msgstr "Betalingsbetingelse" #. module: base #: model:res.country,name:base.nu @@ -859,7 +859,7 @@ msgstr "" #: model:ir.actions.act_window,name:base.action_publisher_warranty_contract_form #: model:ir.ui.menu,name:base.menu_publisher_warranty_contract msgid "Contracts" -msgstr "" +msgstr "Kontrakter" #. module: base #: selection:base.language.install,lang:0 @@ -884,12 +884,12 @@ msgstr "Niger" #. module: base #: selection:base.language.install,lang:0 msgid "Chinese (HK)" -msgstr "" +msgstr "Kinesisk (HK)" #. module: base #: model:res.country,name:base.ba msgid "Bosnia-Herzegovina" -msgstr "" +msgstr "Bosnien-Hercegovina" #. module: base #: view:base.language.export:0 @@ -931,7 +931,7 @@ msgstr "" #. module: base #: field:base.module.import,module_name:0 msgid "Module Name" -msgstr "" +msgstr "Modulnavn" #. module: base #: model:res.country,name:base.mh @@ -990,7 +990,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_hr_dasboard msgid "Dashboard" -msgstr "" +msgstr "Oversigt" #. module: base #: model:ir.ui.menu,name:base.menu_purchase_root @@ -1011,7 +1011,7 @@ msgstr "Funktioner" #: view:ir.module.module:0 #: report:ir.module.reference.graph:0 msgid "Version" -msgstr "" +msgstr "Version" #. module: base #: view:ir.model.access:0 @@ -1382,7 +1382,7 @@ msgstr "" #. module: base #: model:res.country,name:base.fi msgid "Finland" -msgstr "" +msgstr "Finland" #. module: base #: selection:ir.actions.act_window,view_type:0 @@ -1407,7 +1407,7 @@ msgstr "" #. module: base #: view:base.language.export:0 msgid "https://help.launchpad.net/Translations" -msgstr "" +msgstr "https://help.launchpad.net/Translations" #. module: base #: field:ir.actions.act_window,view_mode:0 @@ -1430,7 +1430,7 @@ msgstr "" #. module: base #: view:res.log:0 msgid "Logs" -msgstr "" +msgstr "Logfiler" #. module: base #: selection:base.language.install,lang:0 @@ -1613,7 +1613,7 @@ msgstr "" #: selection:ir.translation,type:0 #: field:multi_company.default,field_id:0 msgid "Field" -msgstr "" +msgstr "Felt" #. module: base #: view:ir.rule:0 @@ -1623,14 +1623,14 @@ msgstr "" #. module: base #: model:res.country,name:base.fo msgid "Faroe Islands" -msgstr "" +msgstr "Færøerne" #. module: base #: selection:res.config.users,view:0 #: selection:res.config.view,view:0 #: selection:res.users,view:0 msgid "Simplified" -msgstr "" +msgstr "Forenklet" #. module: base #: model:res.country,name:base.st @@ -1640,7 +1640,7 @@ msgstr "" #. module: base #: selection:res.partner.address,type:0 msgid "Invoice" -msgstr "" +msgstr "Faktura" #. module: base #: selection:base.language.install,lang:0 @@ -1650,12 +1650,12 @@ msgstr "" #. module: base #: model:res.country,name:base.bb msgid "Barbados" -msgstr "" +msgstr "Barbados" #. module: base #: model:res.country,name:base.mg msgid "Madagascar" -msgstr "" +msgstr "Madagaskar" #. module: base #: code:addons/base/ir/ir_model.py:96 @@ -1674,12 +1674,12 @@ msgstr "" #: view:ir.ui.menu:0 #: field:ir.ui.menu,name:0 msgid "Menu" -msgstr "" +msgstr "Menu" #. module: base #: field:res.currency,rate:0 msgid "Current Rate" -msgstr "" +msgstr "Aktuel kurs" #. module: base #: field:ir.ui.view.custom,ref_id:0 @@ -1721,7 +1721,7 @@ msgstr "" #. module: base #: model:res.country,name:base.zw msgid "Zimbabwe" -msgstr "" +msgstr "Zimbabwe" #. module: base #: view:base.module.update:0 @@ -1736,7 +1736,7 @@ msgstr "" #. module: base #: field:ir.actions.server,email:0 msgid "Email Address" -msgstr "" +msgstr "E-mail adresse" #. module: base #: selection:base.language.install,lang:0 @@ -1752,12 +1752,12 @@ msgstr "" #. module: base #: model:res.country,name:base.tt msgid "Trinidad and Tobago" -msgstr "" +msgstr "Trinidad og Tobago" #. module: base #: model:res.country,name:base.lv msgid "Latvia" -msgstr "" +msgstr "Letland" #. module: base #: view:ir.values:0 @@ -1782,7 +1782,7 @@ msgstr "" #. module: base #: model:res.country,name:base.py msgid "Paraguay" -msgstr "" +msgstr "Paraguay" #. module: base #: model:ir.model,name:base.model_ir_actions_act_window_close @@ -1797,7 +1797,7 @@ msgstr "" #. module: base #: model:res.country,name:base.lt msgid "Lithuania" -msgstr "" +msgstr "Litauen" #. module: base #: model:ir.actions.act_window,name:base.action_view_partner_clear_ids @@ -1827,12 +1827,12 @@ msgstr "" #. module: base #: model:res.country,name:base.si msgid "Slovenia" -msgstr "" +msgstr "Slovenien" #. module: base #: model:res.country,name:base.pk msgid "Pakistan" -msgstr "" +msgstr "Pakistan" #. module: base #: code:addons/orm.py:1350 @@ -1843,7 +1843,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_email_gateway_form msgid "Messages" -msgstr "" +msgstr "Meddelelser" #. module: base #: code:addons/base/ir/ir_model.py:303 @@ -1855,7 +1855,7 @@ msgstr "" #: code:addons/base/module/wizard/base_update_translations.py:38 #, python-format msgid "Error!" -msgstr "" +msgstr "Fejl!" #. module: base #: view:res.lang:0 @@ -1880,7 +1880,7 @@ msgstr "" #. module: base #: model:res.country,name:base.nz msgid "New Zealand" -msgstr "" +msgstr "New Zealand" #. module: base #: code:addons/orm.py:3366 @@ -1927,7 +1927,7 @@ msgstr "" #. module: base #: model:res.country,name:base.bd msgid "Bangladesh" -msgstr "" +msgstr "Bangladesh" #. module: base #: constraint:res.company:0 @@ -1953,7 +1953,7 @@ msgstr "" #. module: base #: model:res.country,name:base.cu msgid "Cuba" -msgstr "" +msgstr "Cuba" #. module: base #: model:ir.model,name:base.model_res_partner_event @@ -1963,12 +1963,12 @@ msgstr "" #. module: base #: model:res.widget,title:base.facebook_widget msgid "Facebook" -msgstr "" +msgstr "Facebook" #. module: base #: model:res.country,name:base.am msgid "Armenia" -msgstr "" +msgstr "Armenien" #. module: base #: model:ir.actions.act_window,name:base.ir_property_form @@ -1984,14 +1984,14 @@ msgstr "" #. module: base #: model:res.country,name:base.se msgid "Sweden" -msgstr "" +msgstr "Sverige" #. module: base #: selection:ir.actions.act_window.view,view_mode:0 #: selection:ir.ui.view,type:0 #: selection:wizard.ir.model.menu.create.line,view_type:0 msgid "Gantt" -msgstr "" +msgstr "Gantt" #. module: base #: view:ir.property:0 @@ -2002,7 +2002,7 @@ msgstr "" #: model:ir.model,name:base.model_res_partner_bank_type #: view:res.partner.bank.type:0 msgid "Bank Account Type" -msgstr "" +msgstr "Bank Kontotype" #. module: base #: field:base.language.export,config_logo:0 @@ -2019,7 +2019,7 @@ msgstr "" #: field:res.config.users,config_logo:0 #: field:res.config.view,config_logo:0 msgid "Image" -msgstr "" +msgstr "Billede" #. module: base #: view:ir.actions.server:0 @@ -2034,7 +2034,7 @@ msgstr "" #. module: base #: model:res.country,name:base.at msgid "Austria" -msgstr "" +msgstr "Østrig" #. module: base #: selection:base.language.install,state:0 @@ -2049,7 +2049,7 @@ msgstr "" #: selection:ir.ui.view,type:0 #: selection:wizard.ir.model.menu.create.line,view_type:0 msgid "Calendar" -msgstr "" +msgstr "Kalender" #. module: base #: field:res.partner.address,partner_id:0 @@ -2143,7 +2143,7 @@ msgstr "" #. module: base #: field:res.partner.address,birthdate:0 msgid "Birthdate" -msgstr "" +msgstr "Fødselsdato" #. module: base #: model:ir.actions.act_window,name:base.action_partner_title_contact @@ -2183,12 +2183,12 @@ msgstr "" #. module: base #: model:res.country,name:base.uy msgid "Uruguay" -msgstr "" +msgstr "Uruguay" #. module: base #: selection:base.language.install,lang:0 msgid "Finnish / Suomi" -msgstr "" +msgstr "Finsk / Suomi" #. module: base #: field:ir.rule,perm_write:0 @@ -2203,7 +2203,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "German / Deutsch" -msgstr "" +msgstr "Tysk / Deutsch" #. module: base #: help:ir.actions.server,trigger_name:0 @@ -2218,7 +2218,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "Portugese / Português" -msgstr "" +msgstr "Portugisisk / Português" #. module: base #: model:res.partner.title,name:base.res_partner_title_sir @@ -2245,7 +2245,7 @@ msgstr "" #. module: base #: model:res.country,name:base.mt msgid "Malta" -msgstr "" +msgstr "Malta" #. module: base #: field:ir.actions.server,fields_lines:0 @@ -2261,7 +2261,7 @@ msgstr "" #: report:ir.module.reference.graph:0 #: field:ir.translation,module:0 msgid "Module" -msgstr "" +msgstr "Modul" #. module: base #: field:ir.attachment,description:0 @@ -2272,7 +2272,7 @@ msgstr "" #: field:res.partner.event,description:0 #: view:res.request:0 msgid "Description" -msgstr "" +msgstr "Beskrivelse" #. module: base #: model:ir.actions.act_window,name:base.action_workflow_instance_form @@ -2283,7 +2283,7 @@ msgstr "" #. module: base #: model:res.country,name:base.aq msgid "Antarctica" -msgstr "" +msgstr "Antarktis" #. module: base #: field:ir.actions.report.xml,auto:0 @@ -2336,7 +2336,7 @@ msgstr "" #. module: base #: view:res.payterm:0 msgid "Payment Term" -msgstr "" +msgstr "Betalingsbetingelse" #. module: base #: selection:res.lang,direction:0 @@ -2417,7 +2417,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ru msgid "Russian Federation" -msgstr "" +msgstr "Rusland" #. module: base #: selection:base.language.install,lang:0 @@ -2427,13 +2427,13 @@ msgstr "" #. module: base #: field:res.company,name:0 msgid "Company Name" -msgstr "" +msgstr "Virksomhedsnavn" #. module: base #: model:ir.actions.act_window,name:base.action_country #: model:ir.ui.menu,name:base.menu_country_partner msgid "Countries" -msgstr "" +msgstr "Lande" #. module: base #: selection:ir.translation,type:0 @@ -2448,7 +2448,7 @@ msgstr "" #. module: base #: view:ir.property:0 msgid "Field Information" -msgstr "" +msgstr "Feltinformation" #. module: base #: view:ir.actions.todo:0 @@ -2464,7 +2464,7 @@ msgstr "" #. module: base #: field:res.partner,vat:0 msgid "VAT" -msgstr "" +msgstr "Moms" #. module: base #: view:res.lang:0 @@ -2545,7 +2545,7 @@ msgstr "" #. module: base #: model:res.country,name:base.me msgid "Montenegro" -msgstr "" +msgstr "Montenegro" #. module: base #: view:ir.cron:0 @@ -2556,7 +2556,7 @@ msgstr "" #: view:res.partner:0 #: field:res.partner,category_id:0 msgid "Categories" -msgstr "" +msgstr "Kategorier" #. module: base #: view:base.language.import:0 @@ -2576,7 +2576,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ly msgid "Libya" -msgstr "" +msgstr "Libyen" #. module: base #: model:res.country,name:base.cf @@ -2586,18 +2586,18 @@ msgstr "" #. module: base #: model:res.country,name:base.li msgid "Liechtenstein" -msgstr "" +msgstr "Liechtenstein" #. module: base #: model:ir.model,name:base.model_partner_sms_send #: view:partner.sms.send:0 msgid "Send SMS" -msgstr "" +msgstr "Send SMS" #. module: base #: field:res.partner,ean13:0 msgid "EAN13" -msgstr "" +msgstr "EAN13" #. module: base #: code:addons/orm.py:1622 @@ -2608,7 +2608,7 @@ msgstr "" #. module: base #: model:res.country,name:base.pt msgid "Portugal" -msgstr "" +msgstr "Portugal" #. module: base #: sql_constraint:ir.model.data:0 @@ -2648,7 +2648,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_res_lang_act_window #: view:res.lang:0 msgid "Languages" -msgstr "" +msgstr "Sprog" #. module: base #: selection:workflow.activity,join_mode:0 @@ -2659,7 +2659,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ec msgid "Ecuador" -msgstr "" +msgstr "Equador" #. module: base #: code:addons/base/module/wizard/base_export_language.py:52 @@ -2676,12 +2676,12 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_partner_form #: view:res.partner:0 msgid "Customers" -msgstr "" +msgstr "Kunder" #. module: base #: model:res.country,name:base.au msgid "Australia" -msgstr "" +msgstr "Australien" #. module: base #: help:res.partner,lang:0 @@ -2693,7 +2693,7 @@ msgstr "" #. module: base #: report:ir.module.reference.graph:0 msgid "Menu :" -msgstr "" +msgstr "Menu:" #. module: base #: selection:ir.model.fields,state:0 @@ -2708,7 +2708,7 @@ msgstr "" #. module: base #: field:ir.actions.todo,restart:0 msgid "Restart" -msgstr "" +msgstr "Genstart" #. module: base #: field:ir.actions.report.xml,report_sxw_content:0 @@ -2776,7 +2776,7 @@ msgstr "" #. module: base #: view:res.company:0 msgid "Header/Footer" -msgstr "" +msgstr "Sidehoved/sidefod" #. module: base #: help:ir.actions.act_window,help:0 @@ -2829,13 +2829,13 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.marketing_menu msgid "Marketing" -msgstr "" +msgstr "Markedsføring" #. module: base #: view:res.partner.bank:0 #: model:res.partner.bank.type,name:base.bank_normal msgid "Bank account" -msgstr "" +msgstr "Bank konto" #. module: base #: selection:base.language.install,lang:0 @@ -2855,17 +2855,17 @@ msgstr "" #. module: base #: field:ir.module.module,license:0 msgid "License" -msgstr "" +msgstr "Licens" #. module: base #: field:ir.attachment,url:0 msgid "Url" -msgstr "" +msgstr "Adresse" #. module: base #: selection:ir.actions.todo,restart:0 msgid "Always" -msgstr "" +msgstr "Altid" #. module: base #: selection:ir.translation,type:0 @@ -2876,7 +2876,7 @@ msgstr "" #: field:ir.actions.server,srcmodel_id:0 #: field:ir.model.fields,model_id:0 msgid "Model" -msgstr "" +msgstr "Model" #. module: base #: view:base.language.install:0 @@ -2911,7 +2911,7 @@ msgstr "" #: field:res.partner.address,zip:0 #: field:res.partner.bank,zip:0 msgid "Zip" -msgstr "" +msgstr "Postnummer" #. module: base #: view:ir.module.module:0 @@ -2946,12 +2946,12 @@ msgstr "" #. module: base #: model:res.country,name:base.bo msgid "Bolivia" -msgstr "" +msgstr "Bolivia" #. module: base #: model:res.country,name:base.gh msgid "Ghana" -msgstr "" +msgstr "Ghana" #. module: base #: field:res.lang,direction:0 @@ -2973,7 +2973,7 @@ msgstr "" #: view:res.groups:0 #: field:res.groups,rule_groups:0 msgid "Rules" -msgstr "" +msgstr "Regler" #. module: base #: code:addons/base/module/module.py:216 @@ -3040,7 +3040,7 @@ msgstr "" #: view:res.config:0 #: view:res.config.installer:0 msgid "Skip" -msgstr "" +msgstr "Spring over" #. module: base #: model:res.country,name:base.ls @@ -3056,7 +3056,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ke msgid "Kenya" -msgstr "" +msgstr "Kenya" #. module: base #: view:res.partner.event:0 @@ -3092,17 +3092,17 @@ msgstr "" #. module: base #: model:res.country,name:base.sm msgid "San Marino" -msgstr "" +msgstr "San Marino" #. module: base #: model:res.country,name:base.bm msgid "Bermuda" -msgstr "" +msgstr "Bermuda" #. module: base #: model:res.country,name:base.pe msgid "Peru" -msgstr "" +msgstr "Peru" #. module: base #: selection:ir.model.fields,on_delete:0 @@ -3162,7 +3162,7 @@ msgstr "" #: view:ir.model.access:0 #: view:ir.rule:0 msgid "Full Access" -msgstr "" +msgstr "Fuld adgang" #. module: base #: view:ir.actions.act_window:0 @@ -3171,7 +3171,7 @@ msgstr "" #: view:ir.model.fields:0 #: model:ir.ui.menu,name:base.menu_security msgid "Security" -msgstr "" +msgstr "Sikkerhed" #. module: base #: model:res.widget,title:base.openerp_favorites_twitter_widget @@ -3181,14 +3181,14 @@ msgstr "" #. module: base #: model:res.country,name:base.za msgid "South Africa" -msgstr "" +msgstr "Sydafrika" #. module: base #: view:ir.module.module:0 #: selection:ir.module.module,state:0 #: selection:ir.module.module.dependency,state:0 msgid "Installed" -msgstr "" +msgstr "Installeret" #. module: base #: selection:base.language.install,lang:0 @@ -3204,12 +3204,12 @@ msgstr "" #. module: base #: model:res.country,name:base.sn msgid "Senegal" -msgstr "" +msgstr "Senegal" #. module: base #: model:res.country,name:base.hu msgid "Hungary" -msgstr "" +msgstr "Ungarn" #. module: base #: model:ir.model,name:base.model_res_groups @@ -3219,7 +3219,7 @@ msgstr "" #. module: base #: model:res.country,name:base.br msgid "Brazil" -msgstr "" +msgstr "Brasilien" #. module: base #: view:res.lang:0 @@ -3234,7 +3234,7 @@ msgstr "" #. module: base #: field:ir.sequence,number_next:0 msgid "Next Number" -msgstr "" +msgstr "Næste nummer" #. module: base #: help:workflow.transition,condition:0 @@ -3255,12 +3255,12 @@ msgstr "" #. module: base #: model:res.country,name:base.sy msgid "Syria" -msgstr "" +msgstr "Syrien" #. module: base #: view:res.lang:0 msgid "======================================================" -msgstr "" +msgstr "======================================================" #. module: base #: help:ir.actions.server,mobile:0 @@ -3288,7 +3288,7 @@ msgstr "" #: field:res.partner.event,date:0 #: field:res.request,date_sent:0 msgid "Date" -msgstr "" +msgstr "Dato" #. module: base #: field:ir.actions.report.xml,report_sxw:0 @@ -3298,7 +3298,7 @@ msgstr "" #. module: base #: view:ir.attachment:0 msgid "Data" -msgstr "" +msgstr "Data" #. module: base #: field:ir.ui.menu,parent_id:0 @@ -3364,7 +3364,7 @@ msgstr "" #. module: base #: model:res.country,name:base.mx msgid "Mexico" -msgstr "" +msgstr "Mexico" #. module: base #: model:ir.ui.menu,name:base.menu_base_config_plugins @@ -3384,7 +3384,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ni msgid "Nicaragua" -msgstr "" +msgstr "Nicaragua" #. module: base #: code:addons/orm.py:1046 @@ -3416,7 +3416,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ve msgid "Venezuela" -msgstr "" +msgstr "Venezuela" #. module: base #: view:res.lang:0 @@ -3426,7 +3426,7 @@ msgstr "" #. module: base #: model:res.country,name:base.zm msgid "Zambia" -msgstr "" +msgstr "Zambia" #. module: base #: help:res.partner,user_id:0 @@ -3453,7 +3453,7 @@ msgstr "" #. module: base #: model:res.country,name:base.kz msgid "Kazakhstan" -msgstr "" +msgstr "Kazakstan" #. module: base #: view:res.lang:0 @@ -3536,17 +3536,17 @@ msgstr "" #. module: base #: field:ir.module.module,demo:0 msgid "Demo data" -msgstr "" +msgstr "Demostrations data" #. module: base #: selection:base.language.install,lang:0 msgid "English (UK)" -msgstr "" +msgstr "Engelsk (UK)" #. module: base #: selection:base.language.install,lang:0 msgid "Japanese / 日本語" -msgstr "" +msgstr "Japansk / 日本語" #. module: base #: help:workflow.transition,act_from:0 @@ -3580,7 +3580,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "English (CA)" -msgstr "" +msgstr "Engelsk (CA)" #. module: base #: model:ir.model,name:base.model_publisher_warranty_contract @@ -3590,7 +3590,7 @@ msgstr "" #. module: base #: model:res.country,name:base.et msgid "Ethiopia" -msgstr "" +msgstr "Etiopien" #. module: base #: help:res.country.state,code:0 @@ -3631,7 +3631,7 @@ msgstr "" #. module: base #: view:ir.translation:0 msgid "Translation" -msgstr "" +msgstr "Oversættelse" #. module: base #: selection:res.request,state:0 @@ -3689,7 +3689,7 @@ msgstr "" #: field:res.partner.bank,state:0 #: field:res.partner.bank.type.field,bank_type_id:0 msgid "Bank Type" -msgstr "" +msgstr "Bank type" #. module: base #: code:addons/base/res/res_user.py:58 @@ -3757,7 +3757,7 @@ msgstr "" #. module: base #: selection:ir.cron,interval_type:0 msgid "Hours" -msgstr "" +msgstr "Timer" #. module: base #: model:res.country,name:base.gp @@ -3770,7 +3770,7 @@ msgstr "" #: code:addons/base/res/res_lang.py:161 #, python-format msgid "User Error" -msgstr "" +msgstr "Bruger fejl!" #. module: base #: help:workflow.transition,signal:0 @@ -3803,18 +3803,18 @@ msgstr "" #. module: base #: view:ir.attachment:0 msgid "Month" -msgstr "" +msgstr "Måned" #. module: base #: model:res.country,name:base.my msgid "Malaysia" -msgstr "" +msgstr "Malaysien" #. module: base #: view:base.language.install:0 #: model:ir.actions.act_window,name:base.action_view_base_language_install msgid "Load Official Translation" -msgstr "" +msgstr "Hent den officielle oversættelse" #. module: base #: model:ir.model,name:base.model_res_request_history @@ -3893,7 +3893,7 @@ msgstr "" #. module: base #: view:res.currency:0 msgid "Price Accuracy" -msgstr "" +msgstr "Pris nøjagtighed" #. module: base #: selection:base.language.install,lang:0 @@ -3909,7 +3909,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "French / Français" -msgstr "" +msgstr "Fransk / Français" #. module: base #: code:addons/orm.py:1049 @@ -3971,7 +3971,7 @@ msgstr "" #. module: base #: model:res.country,name:base.fj msgid "Fiji" -msgstr "" +msgstr "Fiji" #. module: base #: field:ir.model.fields,size:0 @@ -3981,12 +3981,12 @@ msgstr "" #. module: base #: model:res.country,name:base.sd msgid "Sudan" -msgstr "" +msgstr "Sudan" #. module: base #: model:res.country,name:base.fm msgid "Micronesia" -msgstr "" +msgstr "Mikronesien" #. module: base #: view:res.request.history:0 @@ -3998,7 +3998,7 @@ msgstr "" #: field:ir.module.module,menus_by_module:0 #: view:res.groups:0 msgid "Menus" -msgstr "" +msgstr "Menuer" #. module: base #: selection:base.language.install,lang:0 @@ -4008,7 +4008,7 @@ msgstr "" #. module: base #: model:res.country,name:base.il msgid "Israel" -msgstr "" +msgstr "Israel" #. module: base #: model:ir.actions.wizard,name:base.wizard_server_action_create @@ -4025,7 +4025,7 @@ msgstr "" #. module: base #: field:res.lang,time_format:0 msgid "Time Format" -msgstr "" +msgstr "Tidsformat" #. module: base #: view:ir.module.module:0 @@ -4045,7 +4045,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_management #: model:ir.ui.menu,name:base.menu_module_tree msgid "Modules" -msgstr "" +msgstr "Moduler" #. module: base #: view:workflow.activity:0 @@ -4071,12 +4071,12 @@ msgstr "" #: view:res.bank:0 #: field:res.partner,bank_ids:0 msgid "Banks" -msgstr "" +msgstr "Banker" #. module: base #: view:res.log:0 msgid "Unread" -msgstr "" +msgstr "Ulæst" #. module: base #: field:ir.cron,doall:0 @@ -4102,7 +4102,7 @@ msgstr "" #. module: base #: model:res.country,name:base.uk msgid "United Kingdom" -msgstr "" +msgstr "Storbritannien" #. module: base #: view:res.config:0 @@ -4124,7 +4124,7 @@ msgstr "" #. module: base #: model:res.country,name:base.bw msgid "Botswana" -msgstr "" +msgstr "Botswana" #. module: base #: model:ir.actions.act_window,name:base.action_partner_title_partner @@ -4213,7 +4213,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_main_pm msgid "Project" -msgstr "" +msgstr "Projekt" #. module: base #: field:ir.ui.menu,web_icon_hover_data:0 @@ -4274,7 +4274,7 @@ msgstr "" #. module: base #: field:res.partner,employee:0 msgid "Employee" -msgstr "" +msgstr "Ansat" #. module: base #: field:ir.model.access,perm_create:0 @@ -4289,7 +4289,7 @@ msgstr "" #. module: base #: field:ir.actions.server,copy_object:0 msgid "Copy Of" -msgstr "" +msgstr "Kopi af" #. module: base #: field:ir.model,osv_memory:0 @@ -4327,7 +4327,7 @@ msgstr "" #: view:ir.model:0 #: field:ir.model.fields,ttype:0 msgid "Field Type" -msgstr "" +msgstr "Felttype" #. module: base #: field:res.country.state,code:0 @@ -4353,14 +4353,14 @@ msgstr "" #. module: base #: model:res.country,name:base.vn msgid "Vietnam" -msgstr "" +msgstr "Vietnam" #. module: base #: field:res.config.users,signature:0 #: view:res.users:0 #: field:res.users,signature:0 msgid "Signature" -msgstr "" +msgstr "Signatur" #. module: base #: code:addons/fields.py:456 @@ -4372,7 +4372,7 @@ msgstr "" #: code:addons/fields.py:664 #, python-format msgid "Not Implemented" -msgstr "" +msgstr "Ikke implementeret" #. module: base #: model:ir.model,name:base.model_res_widget_user @@ -4382,7 +4382,7 @@ msgstr "" #. module: base #: field:res.partner.category,complete_name:0 msgid "Full Name" -msgstr "" +msgstr "Fulde navn" #. module: base #: view:base.module.configuration:0 @@ -4427,7 +4427,7 @@ msgstr "" #: view:res.partner:0 #: field:res.partner,user_id:0 msgid "Salesman" -msgstr "" +msgstr "Sælger" #. module: base #: field:res.partner,address:0 @@ -4462,7 +4462,7 @@ msgstr "" #. module: base #: model:res.country,name:base.cz msgid "Czech Republic" -msgstr "" +msgstr "Tjekkiet" #. module: base #: view:res.widget.wizard:0 @@ -4527,12 +4527,12 @@ msgstr "" #. module: base #: model:res.country,name:base.na msgid "Namibia" -msgstr "" +msgstr "Namibia" #. module: base #: model:res.country,name:base.mn msgid "Mongolia" -msgstr "" +msgstr "Mongoliet" #. module: base #: view:ir.module.module:0 @@ -4547,7 +4547,7 @@ msgstr "" #. module: base #: model:res.country,name:base.bi msgid "Burundi" -msgstr "" +msgstr "Burundi" #. module: base #: view:base.language.install:0 @@ -4568,12 +4568,12 @@ msgstr "" #. module: base #: view:res.log:0 msgid "My Logs" -msgstr "" +msgstr "Mine logfiler" #. module: base #: model:res.country,name:base.bt msgid "Bhutan" -msgstr "" +msgstr "Bhutan" #. module: base #: help:ir.sequence,number_next:0 @@ -4603,7 +4603,7 @@ msgstr "" #. module: base #: field:base.language.export,format:0 msgid "File Format" -msgstr "" +msgstr "Filformat" #. module: base #: field:res.lang,iso_code:0 @@ -4619,7 +4619,7 @@ msgstr "" #: view:res.log:0 #: field:res.log,read:0 msgid "Read" -msgstr "" +msgstr "Læst" #. module: base #: sql_constraint:res.country:0 @@ -4649,7 +4649,7 @@ msgstr "" #: field:res.config.users,password:0 #: field:res.users,password:0 msgid "Password" -msgstr "" +msgstr "Adgangskode" #. module: base #: model:ir.actions.act_window,name:base.action_model_fields @@ -4659,12 +4659,12 @@ msgstr "" #: view:ir.model.fields:0 #: model:ir.ui.menu,name:base.ir_model_model_fields msgid "Fields" -msgstr "" +msgstr "Felter" #. module: base #: model:ir.actions.act_window,name:base.action_partner_employee_form msgid "Employees" -msgstr "" +msgstr "Ansatte" #. module: base #: help:res.log,read:0 @@ -4705,12 +4705,12 @@ msgstr "" #: model:ir.actions.act_window,name:base.action_partner_address_form #: model:ir.ui.menu,name:base.menu_partner_address_form msgid "Addresses" -msgstr "" +msgstr "Adresser" #. module: base #: model:res.country,name:base.mm msgid "Myanmar" -msgstr "" +msgstr "Myanmar (Burma)" #. module: base #: selection:base.language.install,lang:0 @@ -4722,12 +4722,12 @@ msgstr "" #: field:res.partner.address,street:0 #: field:res.partner.bank,street:0 msgid "Street" -msgstr "" +msgstr "Vej" #. module: base #: model:res.country,name:base.yu msgid "Yugoslavia" -msgstr "" +msgstr "Jugoslavien" #. module: base #: field:ir.model.data,name:0 @@ -4737,12 +4737,12 @@ msgstr "" #. module: base #: model:res.country,name:base.ca msgid "Canada" -msgstr "" +msgstr "Canada" #. module: base #: selection:ir.module.module.dependency,state:0 msgid "Unknown" -msgstr "" +msgstr "Ukendt" #. module: base #: model:ir.actions.act_window,name:base.action_res_users_my @@ -4758,17 +4758,17 @@ msgstr "" #. module: base #: field:partner.sms.send,text:0 msgid "SMS Message" -msgstr "" +msgstr "SMS besked" #. module: base #: model:res.country,name:base.cm msgid "Cameroon" -msgstr "" +msgstr "Cameroun" #. module: base #: model:res.country,name:base.bf msgid "Burkina Faso" -msgstr "" +msgstr "Burkina Faso" #. module: base #: selection:ir.actions.todo,state:0 @@ -4835,7 +4835,7 @@ msgstr "" #. module: base #: report:ir.module.reference.graph:0 msgid "1cm 28cm 20cm 28cm" -msgstr "" +msgstr "1cm 28cm 20cm 28cm" #. module: base #: field:ir.module.module,maintainer:0 @@ -4855,7 +4855,7 @@ msgstr "" #. module: base #: model:ir.actions.report.xml,name:base.res_partner_address_report msgid "Labels" -msgstr "" +msgstr "Etiketter" #. module: base #: field:partner.wizard.spam,email_from:0 @@ -4945,7 +4945,7 @@ msgstr "" #. module: base #: model:res.country,name:base.us msgid "United States" -msgstr "" +msgstr "USA" #. module: base #: view:ir.module.module:0 @@ -4978,7 +4978,7 @@ msgstr "" #. module: base #: model:res.country,name:base.kw msgid "Kuwait" -msgstr "" +msgstr "Kuwait" #. module: base #: field:workflow.workitem,inst_id:0 @@ -5001,7 +5001,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ng msgid "Nigeria" -msgstr "" +msgstr "Nigeria" #. module: base #: code:addons/base/ir/ir_model.py:250 @@ -5012,7 +5012,7 @@ msgstr "" #. module: base #: model:ir.actions.act_window,name:base.action_partner_sms_send msgid "SMS Send" -msgstr "" +msgstr "SMS sendt" #. module: base #: field:res.company,user_ids:0 @@ -5037,7 +5037,7 @@ msgstr "" #. module: base #: model:res.country,name:base.hk msgid "Hong Kong" -msgstr "" +msgstr "Hong Kong" #. module: base #: help:ir.actions.server,name:0 @@ -5059,12 +5059,12 @@ msgstr "" #. module: base #: model:res.country,name:base.ph msgid "Philippines" -msgstr "" +msgstr "Filippinerne" #. module: base #: model:res.country,name:base.ma msgid "Morocco" -msgstr "" +msgstr "Marokko" #. module: base #: view:res.lang:0 @@ -5084,7 +5084,7 @@ msgstr "" #. module: base #: model:res.country,name:base.td msgid "Chad" -msgstr "" +msgstr "Tchad" #. module: base #: model:ir.model,name:base.model_workflow_transition @@ -5131,7 +5131,7 @@ msgstr "" #. module: base #: model:res.country,name:base.np msgid "Nepal" -msgstr "" +msgstr "Nepal" #. module: base #: code:addons/orm.py:2307 @@ -5233,12 +5233,12 @@ msgstr "" #: field:base.language.export,data:0 #: field:base.language.import,data:0 msgid "File" -msgstr "" +msgstr "Fil" #. module: base #: view:res.config.users:0 msgid "Add User" -msgstr "" +msgstr "Tilføj bruger" #. module: base #: model:ir.actions.act_window,name:base.action_view_base_module_upgrade_install @@ -5261,7 +5261,7 @@ msgstr "" #: field:res.partner.address,is_supplier_add:0 #: model:res.partner.category,name:base.res_partner_category_8 msgid "Supplier" -msgstr "" +msgstr "Leverandør" #. module: base #: view:ir.actions.server:0 @@ -5349,7 +5349,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ae msgid "United Arab Emirates" -msgstr "" +msgstr "De Forenede Arabiske Emirater" #. module: base #: model:ir.ui.menu,name:base.menu_crm_case_job_req_main @@ -5373,7 +5373,7 @@ msgstr "" #: view:ir.rule:0 #: field:ir.rule,global:0 msgid "Global" -msgstr "" +msgstr "Globalt" #. module: base #: model:res.country,name:base.mp @@ -5427,7 +5427,7 @@ msgstr "" #: view:ir.translation:0 #: model:ir.ui.menu,name:base.menu_translation msgid "Translations" -msgstr "" +msgstr "Oversættelser" #. module: base #: field:ir.sequence,padding:0 @@ -5437,12 +5437,12 @@ msgstr "" #. module: base #: view:ir.actions.report.xml:0 msgid "Report" -msgstr "" +msgstr "Rapport" #. module: base #: model:res.country,name:base.ua msgid "Ukraine" -msgstr "" +msgstr "Ukraine" #. module: base #: model:res.country,name:base.to @@ -5558,12 +5558,12 @@ msgstr "" #. module: base #: model:res.country,name:base.dz msgid "Algeria" -msgstr "" +msgstr "Algeriet" #. module: base #: model:res.country,name:base.be msgid "Belgium" -msgstr "" +msgstr "Belgien" #. module: base #: model:ir.model,name:base.model_osv_memory_autovacuum @@ -5579,12 +5579,12 @@ msgstr "" #: field:res.partner,lang:0 #: field:res.users,context_lang:0 msgid "Language" -msgstr "" +msgstr "Sprog" #. module: base #: model:res.country,name:base.gm msgid "Gambia" -msgstr "" +msgstr "Gambia" #. module: base #: model:ir.actions.act_window,name:base.action_res_company_form @@ -5596,7 +5596,7 @@ msgstr "" #: view:res.users:0 #: field:res.users,company_ids:0 msgid "Companies" -msgstr "" +msgstr "Virksomheder" #. module: base #: view:res.lang:0 @@ -5661,12 +5661,12 @@ msgstr "" #. module: base #: selection:base.language.export,format:0 msgid "PO File" -msgstr "" +msgstr "PO fil" #. module: base #: model:res.country,name:base.nt msgid "Neutral Zone" -msgstr "" +msgstr "Neutral Zone" #. module: base #: selection:base.language.install,lang:0 @@ -5686,7 +5686,7 @@ msgstr "" #. module: base #: model:res.partner.category,name:base.res_partner_category_9 msgid "Components Supplier" -msgstr "" +msgstr "Komponent leverandør" #. module: base #: model:ir.actions.act_window,name:base.action_res_users @@ -5697,7 +5697,7 @@ msgstr "" #: field:res.groups,users:0 #: view:res.users:0 msgid "Users" -msgstr "" +msgstr "Brugere" #. module: base #: field:ir.module.module,published_version:0 @@ -5707,7 +5707,7 @@ msgstr "" #. module: base #: model:res.country,name:base.is msgid "Iceland" -msgstr "" +msgstr "Island" #. module: base #: model:ir.actions.act_window,name:base.ir_action_window @@ -5728,7 +5728,7 @@ msgstr "" #. module: base #: model:res.country,name:base.de msgid "Germany" -msgstr "" +msgstr "Tyskland" #. module: base #: view:ir.sequence:0 @@ -5738,7 +5738,7 @@ msgstr "" #. module: base #: model:res.partner.category,name:base.res_partner_category_14 msgid "Bad customers" -msgstr "" +msgstr "Dårlige kunder" #. module: base #: report:ir.module.reference.graph:0 @@ -5748,7 +5748,7 @@ msgstr "" #. module: base #: model:res.country,name:base.gy msgid "Guyana" -msgstr "" +msgstr "Guyana" #. module: base #: help:ir.actions.act_window,view_type:0 @@ -5771,7 +5771,7 @@ msgstr "" #. module: base #: model:res.country,name:base.hn msgid "Honduras" -msgstr "" +msgstr "Honduras" #. module: base #: help:res.config.users,menu_tips:0 @@ -5783,7 +5783,7 @@ msgstr "" #. module: base #: model:res.country,name:base.eg msgid "Egypt" -msgstr "" +msgstr "Ægypten" #. module: base #: field:ir.rule,perm_read:0 @@ -5805,7 +5805,7 @@ msgstr "" #. module: base #: field:base.language.import,name:0 msgid "Language Name" -msgstr "" +msgstr "Sprog navn" #. module: base #: selection:ir.property,type:0 @@ -5886,7 +5886,7 @@ msgstr "" #: field:res.partner,comment:0 #: model:res.widget,title:base.note_widget msgid "Notes" -msgstr "" +msgstr "Noter" #. module: base #: field:ir.config_parameter,value:0 @@ -5911,7 +5911,7 @@ msgstr "" #: field:res.bank,code:0 #: field:res.partner.bank.type,code:0 msgid "Code" -msgstr "" +msgstr "Kode" #. module: base #: model:ir.model,name:base.model_res_config_installer @@ -5921,17 +5921,17 @@ msgstr "" #. module: base #: model:res.country,name:base.mc msgid "Monaco" -msgstr "" +msgstr "Monaco" #. module: base #: selection:ir.cron,interval_type:0 msgid "Minutes" -msgstr "" +msgstr "Minutter" #. module: base #: selection:ir.translation,type:0 msgid "Help" -msgstr "" +msgstr "Hjælp" #. module: base #: help:res.config.users,menu_id:0 @@ -5986,7 +5986,7 @@ msgstr "" #. module: base #: model:res.country,name:base.fr msgid "France" -msgstr "" +msgstr "Frankrig" #. module: base #: model:ir.model,name:base.model_res_log @@ -6008,7 +6008,7 @@ msgstr "" #. module: base #: selection:ir.cron,interval_type:0 msgid "Weeks" -msgstr "" +msgstr "Uger" #. module: base #: model:res.country,name:base.af @@ -6019,7 +6019,7 @@ msgstr "" #: code:addons/base/module/wizard/base_module_import.py:67 #, python-format msgid "Error !" -msgstr "" +msgstr "Fejl!" #. module: base #: model:res.partner.bank.type.field,name:base.bank_normal_field_contry @@ -6047,7 +6047,7 @@ msgstr "" #: field:res.bank,fax:0 #: field:res.partner.address,fax:0 msgid "Fax" -msgstr "" +msgstr "Fax" #. module: base #: field:res.lang,thousands_sep:0 @@ -6094,7 +6094,7 @@ msgstr "" #. module: base #: model:res.country,name:base.pa msgid "Panama" -msgstr "" +msgstr "Panama" #. module: base #: model:res.partner.title,name:base.res_partner_title_ltd @@ -6116,7 +6116,7 @@ msgstr "" #. module: base #: model:res.country,name:base.gi msgid "Gibraltar" -msgstr "" +msgstr "Gibraltar" #. module: base #: field:ir.actions.report.xml,report_name:0 @@ -6144,7 +6144,7 @@ msgstr "" #: field:res.config.users,name:0 #: field:res.users,name:0 msgid "User Name" -msgstr "" +msgstr "Brugernavn" #. module: base #: view:ir.sequence:0 @@ -6173,7 +6173,7 @@ msgstr "" #. module: base #: selection:ir.cron,interval_type:0 msgid "Months" -msgstr "" +msgstr "Måneder" #. module: base #: field:ir.actions.act_window,search_view:0 @@ -6198,7 +6198,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_sale_config_sales #: model:ir.ui.menu,name:base.menu_sales msgid "Sales" -msgstr "" +msgstr "Salg" #. module: base #: field:ir.actions.server,child_ids:0 @@ -6234,17 +6234,17 @@ msgstr "" #: field:res.partner.address,city:0 #: field:res.partner.bank,city:0 msgid "City" -msgstr "" +msgstr "By" #. module: base #: model:res.country,name:base.qa msgid "Qatar" -msgstr "" +msgstr "Qatar" #. module: base #: model:res.country,name:base.it msgid "Italy" -msgstr "" +msgstr "Italien" #. module: base #: view:ir.actions.todo:0 @@ -6262,12 +6262,12 @@ msgstr "" #: field:res.partner,email:0 #: field:res.users,email:0 msgid "E-mail" -msgstr "" +msgstr "E-mail" #. module: base #: selection:ir.module.module,license:0 msgid "GPL-3 or later version" -msgstr "" +msgstr "GPL-3 eller senere version" #. module: base #: field:workflow.activity,action:0 @@ -6277,7 +6277,7 @@ msgstr "" #. module: base #: selection:base.language.install,lang:0 msgid "English (US)" -msgstr "" +msgstr "Engelsk (US)" #. module: base #: model:ir.actions.act_window,name:base.action_model_data @@ -6313,12 +6313,12 @@ msgstr "" #: view:res.users:0 #: field:res.users,address_id:0 msgid "Address" -msgstr "" +msgstr "Adresse" #. module: base #: field:ir.module.module,latest_version:0 msgid "Installed version" -msgstr "" +msgstr "Installeret version" #. module: base #: selection:base.language.install,lang:0 @@ -6328,7 +6328,7 @@ msgstr "" #. module: base #: model:res.country,name:base.mr msgid "Mauritania" -msgstr "" +msgstr "Mauretanien" #. module: base #: model:ir.model,name:base.model_ir_translation @@ -6344,7 +6344,7 @@ msgstr "" #: view:workflow.activity:0 #: field:workflow.workitem,act_id:0 msgid "Activity" -msgstr "" +msgstr "Aktivitet" #. module: base #: view:res.partner:0 @@ -6370,7 +6370,7 @@ msgstr "" #. module: base #: model:res.country,name:base.cg msgid "Congo" -msgstr "" +msgstr "Congo" #. module: base #: view:res.lang:0 @@ -6385,7 +6385,7 @@ msgstr "Standardværdi" #. module: base #: model:ir.ui.menu,name:base.menu_tools msgid "Tools" -msgstr "" +msgstr "Værktøj" #. module: base #: model:res.country,name:base.kn @@ -6427,7 +6427,7 @@ msgstr "" #: selection:ir.module.module,state:0 #: selection:ir.module.module.dependency,state:0 msgid "Not Installed" -msgstr "" +msgstr "Ikke installeret" #. module: base #: view:workflow.activity:0 @@ -6438,7 +6438,7 @@ msgstr "" #. module: base #: field:ir.ui.menu,icon:0 msgid "Icon" -msgstr "" +msgstr "Ikon" #. module: base #: help:ir.model.fields,model_id:0 @@ -6466,12 +6466,12 @@ msgstr "" #. module: base #: model:res.country,name:base.ye msgid "Yemen" -msgstr "" +msgstr "Yemen" #. module: base #: selection:workflow.activity,split_mode:0 msgid "Or" -msgstr "" +msgstr "Eller" #. module: base #: model:ir.actions.act_window,name:base.res_log_act_window @@ -6482,12 +6482,12 @@ msgstr "" #. module: base #: model:res.country,name:base.al msgid "Albania" -msgstr "" +msgstr "Albanien" #. module: base #: model:res.country,name:base.ws msgid "Samoa" -msgstr "" +msgstr "Samoa" #. module: base #: code:addons/base/res/res_lang.py:161 @@ -6555,14 +6555,14 @@ msgstr "" #. module: base #: model:res.country,name:base.la msgid "Laos" -msgstr "" +msgstr "Laos" #. module: base #: selection:ir.actions.server,state:0 #: field:res.config.users,user_email:0 #: field:res.users,user_email:0 msgid "Email" -msgstr "" +msgstr "E-mail" #. module: base #: field:res.config.users,action_id:0 @@ -6593,7 +6593,7 @@ msgstr "" #. module: base #: model:res.country,name:base.tg msgid "Togo" -msgstr "" +msgstr "Togo" #. module: base #: selection:ir.module.module,license:0 @@ -6603,7 +6603,7 @@ msgstr "" #. module: base #: selection:workflow.activity,kind:0 msgid "Stop All" -msgstr "" +msgstr "Stop alle" #. module: base #: code:addons/orm.py:412 @@ -6644,7 +6644,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ro msgid "Romania" -msgstr "" +msgstr "Rumænien" #. module: base #: help:ir.cron,doall:0 @@ -6678,7 +6678,7 @@ msgstr "" #: field:res.config.users,context_tz:0 #: field:res.users,context_tz:0 msgid "Timezone" -msgstr "" +msgstr "Tidszone" #. module: base #: model:ir.model,name:base.model_ir_actions_report_xml @@ -6731,7 +6731,7 @@ msgstr "" #. module: base #: model:res.country,name:base.by msgid "Belarus" -msgstr "" +msgstr "Hviderusland" #. module: base #: field:ir.actions.act_window,name:0 @@ -6755,7 +6755,7 @@ msgstr "" #. module: base #: selection:res.request,priority:0 msgid "Normal" -msgstr "" +msgstr "Normal" #. module: base #: field:res.bank,street2:0 @@ -6786,12 +6786,12 @@ msgstr "" #: view:res.users:0 #: field:res.widget.user,user_id:0 msgid "User" -msgstr "" +msgstr "Bruger" #. module: base #: model:res.country,name:base.pr msgid "Puerto Rico" -msgstr "" +msgstr "Puerto Rico" #. module: base #: view:ir.actions.act_window:0 @@ -6816,7 +6816,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ch msgid "Switzerland" -msgstr "" +msgstr "Schweiz" #. module: base #: model:res.country,name:base.gd @@ -6841,7 +6841,7 @@ msgstr "" #. module: base #: view:base.language.install:0 msgid "Load" -msgstr "" +msgstr "Indlæs" #. module: base #: help:res.config.users,name:0 @@ -6870,7 +6870,7 @@ msgstr "" #. module: base #: model:res.country,name:base.so msgid "Somalia" -msgstr "" +msgstr "Somalia" #. module: base #: selection:publisher_warranty.contract,state:0 @@ -6892,7 +6892,7 @@ msgstr "" #: field:res.request,act_to:0 #: field:res.request.history,act_to:0 msgid "To" -msgstr "" +msgstr "Til" #. module: base #: view:ir.cron:0 @@ -6909,12 +6909,12 @@ msgstr "" #. module: base #: selection:ir.module.module,license:0 msgid "GPL Version 2" -msgstr "" +msgstr "GPL Version 2" #. module: base #: selection:ir.module.module,license:0 msgid "GPL Version 3" -msgstr "" +msgstr "GPL Version 3" #. module: base #: code:addons/orm.py:836 @@ -6939,7 +6939,7 @@ msgstr "" #: field:res.partner.address,is_customer_add:0 #: model:res.partner.category,name:base.res_partner_category_0 msgid "Customer" -msgstr "" +msgstr "Kunde" #. module: base #: selection:base.language.install,lang:0 @@ -6949,7 +6949,7 @@ msgstr "" #. module: base #: field:ir.module.module,shortdesc:0 msgid "Short Description" -msgstr "" +msgstr "Kort beskrivelse" #. module: base #: field:ir.actions.act_window,context:0 @@ -6975,7 +6975,7 @@ msgstr "" #. module: base #: field:res.request.history,date_sent:0 msgid "Date sent" -msgstr "" +msgstr "Dato sendt" #. module: base #: view:ir.sequence:0 @@ -7002,12 +7002,12 @@ msgstr "" #. module: base #: model:res.country,name:base.tn msgid "Tunisia" -msgstr "" +msgstr "Tunesien" #. module: base #: model:ir.ui.menu,name:base.menu_mrp_root msgid "Manufacturing" -msgstr "" +msgstr "Produktion" #. module: base #: model:res.country,name:base.km @@ -7153,12 +7153,12 @@ msgstr "" #: code:addons/base/res/res_user.py:580 #, python-format msgid "Warning !" -msgstr "" +msgstr "Advarsel!" #. module: base #: model:res.widget,title:base.google_maps_widget msgid "Google Maps" -msgstr "" +msgstr "Google Maps" #. module: base #: model:ir.ui.menu,name:base.menu_base_config @@ -7210,17 +7210,17 @@ msgstr "" #. module: base #: model:res.country,name:base.tr msgid "Turkey" -msgstr "" +msgstr "Tyrkiet" #. module: base #: model:res.country,name:base.fk msgid "Falkland Islands" -msgstr "" +msgstr "Falklandsøerne" #. module: base #: model:res.country,name:base.lb msgid "Lebanon" -msgstr "" +msgstr "Libanon" #. module: base #: view:ir.actions.report.xml:0 @@ -7263,7 +7263,7 @@ msgstr "" #: view:base.language.install:0 #: model:ir.ui.menu,name:base.menu_view_base_language_install msgid "Load an Official Translation" -msgstr "" +msgstr "Hent en officiel oversættelse" #. module: base #: view:res.currency:0 @@ -7375,7 +7375,7 @@ msgstr "" #: view:ir.module.module:0 #: field:ir.module.module,category_id:0 msgid "Category" -msgstr "" +msgstr "Kategori" #. module: base #: view:ir.attachment:0 @@ -7388,17 +7388,17 @@ msgstr "" #: field:ir.actions.server,sms:0 #: selection:ir.actions.server,state:0 msgid "SMS" -msgstr "" +msgstr "SMS" #. module: base #: model:res.country,name:base.cr msgid "Costa Rica" -msgstr "" +msgstr "Costa Rica" #. module: base #: view:workflow.activity:0 msgid "Conditions" -msgstr "" +msgstr "Betingelser" #. module: base #: model:ir.actions.act_window,name:base.action_partner_other_form @@ -7410,7 +7410,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_action_currency_form #: view:res.currency:0 msgid "Currencies" -msgstr "" +msgstr "Valutaer" #. module: base #: sql_constraint:res.groups:0 @@ -7435,12 +7435,12 @@ msgstr "" #. module: base #: model:res.country,name:base.dk msgid "Denmark" -msgstr "" +msgstr "Danmark" #. module: base #: field:res.country,code:0 msgid "Country Code" -msgstr "" +msgstr "Lande kode" #. module: base #: model:ir.model,name:base.model_workflow_instance @@ -7485,7 +7485,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ee msgid "Estonia" -msgstr "" +msgstr "Estland" #. module: base #: model:ir.ui.menu,name:base.dashboard @@ -7506,7 +7506,7 @@ msgstr "" #. module: base #: model:res.country,name:base.nl msgid "Netherlands" -msgstr "" +msgstr "Holland" #. module: base #: model:ir.ui.menu,name:base.next_id_4 @@ -7541,7 +7541,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_emails #: model:ir.ui.menu,name:base.menu_mail_gateway msgid "Emails" -msgstr "" +msgstr "E-mails" #. module: base #: model:res.country,name:base.cd @@ -7563,7 +7563,7 @@ msgstr "" #. module: base #: model:res.country,name:base.jp msgid "Japan" -msgstr "" +msgstr "Japan" #. module: base #: field:ir.cron,numbercall:0 @@ -7591,7 +7591,7 @@ msgstr "" #. module: base #: model:res.country,name:base.gr msgid "Greece" -msgstr "" +msgstr "Grækenland" #. module: base #: field:res.request,trigger_date:0 @@ -7643,7 +7643,7 @@ msgstr "" #: view:ir.model.fields:0 #: field:ir.model.fields,translate:0 msgid "Translate" -msgstr "" +msgstr "Oversæt" #. module: base #: field:res.request.history,body:0 @@ -7653,7 +7653,7 @@ msgstr "" #. module: base #: view:partner.wizard.spam:0 msgid "Send Email" -msgstr "" +msgstr "Send e-mail" #. module: base #: field:res.config.users,menu_id:0 @@ -7692,7 +7692,7 @@ msgstr "" #: model:ir.ui.menu,name:base.menu_procurement_management_supplier_name #: view:res.partner:0 msgid "Suppliers" -msgstr "" +msgstr "Leverandører" #. module: base #: view:publisher_warranty.contract.wizard:0 @@ -7729,12 +7729,12 @@ msgstr "" #. module: base #: model:res.country,name:base.gl msgid "Greenland" -msgstr "" +msgstr "Grønland" #. module: base #: field:res.partner.bank,acc_number:0 msgid "Account Number" -msgstr "" +msgstr "Kontonummer" #. module: base #: view:res.lang:0 @@ -7749,7 +7749,7 @@ msgstr "" #. module: base #: model:res.country,name:base.cy msgid "Cyprus" -msgstr "" +msgstr "Cypern" #. module: base #: view:base.module.import:0 @@ -7764,13 +7764,13 @@ msgstr "" #: field:partner.wizard.spam,subject:0 #: field:res.request,name:0 msgid "Subject" -msgstr "" +msgstr "Emne" #. module: base #: field:res.request,act_from:0 #: field:res.request.history,act_from:0 msgid "From" -msgstr "" +msgstr "Fra" #. module: base #: view:res.users:0 @@ -7811,7 +7811,7 @@ msgstr "" #. module: base #: model:res.country,name:base.cn msgid "China" -msgstr "" +msgstr "Kina" #. module: base #: code:addons/base/res/res_user.py:516 @@ -7841,7 +7841,7 @@ msgstr "" #. module: base #: model:res.country,name:base.id msgid "Indonesia" -msgstr "" +msgstr "Indonesien" #. module: base #: view:base.update.translations:0 @@ -7861,7 +7861,7 @@ msgstr "" #. module: base #: model:res.country,name:base.bg msgid "Bulgaria" -msgstr "" +msgstr "Bulgarien" #. module: base #: view:publisher_warranty.contract.wizard:0 @@ -7871,7 +7871,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ao msgid "Angola" -msgstr "" +msgstr "Angola" #. module: base #: model:res.country,name:base.tf @@ -7886,7 +7886,7 @@ msgstr "" #: field:res.currency,name:0 #: field:res.currency.rate,currency_id:0 msgid "Currency" -msgstr "" +msgstr "Valuta" #. module: base #: field:res.partner.canal,name:0 @@ -7917,7 +7917,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_administration msgid "Administration" -msgstr "" +msgstr "Administration" #. module: base #: view:base.module.update:0 @@ -7971,12 +7971,12 @@ msgstr "" #. module: base #: model:res.country,name:base.ki msgid "Kiribati" -msgstr "" +msgstr "Kiribati" #. module: base #: model:res.country,name:base.iq msgid "Iraq" -msgstr "" +msgstr "Irak" #. module: base #: model:ir.ui.menu,name:base.menu_association @@ -7986,14 +7986,14 @@ msgstr "" #. module: base #: model:res.country,name:base.cl msgid "Chile" -msgstr "" +msgstr "Chile" #. module: base #: model:ir.ui.menu,name:base.menu_address_book #: model:ir.ui.menu,name:base.menu_config_address_book #: model:ir.ui.menu,name:base.menu_procurement_management_supplier msgid "Address Book" -msgstr "" +msgstr "Adressebog" #. module: base #: model:ir.model,name:base.model_ir_sequence_type @@ -8003,12 +8003,12 @@ msgstr "" #. module: base #: selection:base.language.export,format:0 msgid "CSV File" -msgstr "" +msgstr "CSV fil" #. module: base #: field:res.company,account_no:0 msgid "Account No." -msgstr "" +msgstr "Konto nummer" #. module: base #: code:addons/base/res/res_lang.py:157 @@ -8034,7 +8034,7 @@ msgstr "" #. module: base #: model:res.country,name:base.dj msgid "Djibouti" -msgstr "" +msgstr "Djibouti" #. module: base #: field:ir.translation,value:0 @@ -8044,7 +8044,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ag msgid "Antigua and Barbuda" -msgstr "" +msgstr "Antigua og Barbuda" #. module: base #: code:addons/orm.py:3166 @@ -8057,7 +8057,7 @@ msgstr "" #. module: base #: model:res.country,name:base.zr msgid "Zaire" -msgstr "" +msgstr "Zaire" #. module: base #: field:ir.model.data,res_id:0 @@ -8091,7 +8091,7 @@ msgstr "" #. module: base #: view:res.request:0 msgid "Reply" -msgstr "" +msgstr "Svar" #. module: base #: selection:base.language.install,lang:0 @@ -8166,7 +8166,7 @@ msgstr "" #. module: base #: model:res.country,name:base.hr msgid "Croatia" -msgstr "" +msgstr "Kroatien" #. module: base #: help:res.bank,bic:0 @@ -8176,7 +8176,7 @@ msgstr "" #. module: base #: model:res.country,name:base.tm msgid "Turkmenistan" -msgstr "" +msgstr "Turkmenistan" #. module: base #: code:addons/base/ir/ir_actions.py:597 @@ -8208,7 +8208,7 @@ msgstr "" #: code:addons/orm.py:3199 #, python-format msgid "Error" -msgstr "" +msgstr "Fejl!" #. module: base #: model:res.country,name:base.pm @@ -8239,12 +8239,12 @@ msgstr "" #. module: base #: model:res.country,name:base.tz msgid "Tanzania" -msgstr "" +msgstr "Tanzania" #. module: base #: selection:base.language.install,lang:0 msgid "Danish / Dansk" -msgstr "" +msgstr "Danish / Dansk" #. module: base #: selection:ir.model.fields,select_level:0 @@ -8309,7 +8309,7 @@ msgstr "" #. module: base #: view:res.request:0 msgid "Send" -msgstr "" +msgstr "Send" #. module: base #: field:res.config.users,menu_tips:0 @@ -8370,7 +8370,7 @@ msgstr "" #. module: base #: model:res.country,name:base.do msgid "Dominican Republic" -msgstr "" +msgstr "Den Dominikanske Republik" #. module: base #: selection:base.language.install,lang:0 @@ -8388,7 +8388,7 @@ msgstr "" #. module: base #: model:res.country,name:base.sa msgid "Saudi Arabia" -msgstr "" +msgstr "Saudi-Arabien" #. module: base #: help:res.partner,supplier:0 @@ -8432,7 +8432,7 @@ msgstr "" #. module: base #: field:ir.actions.report.xml,report_xml:0 msgid "XML path" -msgstr "" +msgstr "XML sti" #. module: base #: selection:ir.actions.todo,restart:0 @@ -8442,12 +8442,12 @@ msgstr "" #. module: base #: model:res.country,name:base.gn msgid "Guinea" -msgstr "" +msgstr "Guinea" #. module: base #: model:res.country,name:base.lu msgid "Luxembourg" -msgstr "" +msgstr "Luxembourg" #. module: base #: help:ir.values,key2:0 @@ -8484,14 +8484,14 @@ msgstr "" #. module: base #: model:res.country,name:base.sv msgid "El Salvador" -msgstr "" +msgstr "El Salvador" #. module: base #: field:res.bank,phone:0 #: field:res.partner,phone:0 #: field:res.partner.address,phone:0 msgid "Phone" -msgstr "" +msgstr "Telefon" #. module: base #: field:ir.cron,active:0 @@ -8509,12 +8509,12 @@ msgstr "" #: view:workflow.instance:0 #: view:workflow.workitem:0 msgid "Active" -msgstr "" +msgstr "Aktiv" #. module: base #: model:res.country,name:base.th msgid "Thailand" -msgstr "" +msgstr "Thailand" #. module: base #: model:ir.ui.menu,name:base.menu_crm_config_lead @@ -8535,7 +8535,7 @@ msgstr "" #: selection:workflow.activity,join_mode:0 #: selection:workflow.activity,split_mode:0 msgid "And" -msgstr "" +msgstr "Og" #. module: base #: field:ir.model.fields,relation:0 @@ -8551,7 +8551,7 @@ msgstr "" #. module: base #: model:res.country,name:base.uz msgid "Uzbekistan" -msgstr "" +msgstr "Usbekistan" #. module: base #: model:ir.model,name:base.model_ir_actions_act_window @@ -8572,7 +8572,7 @@ msgstr "" #. module: base #: model:res.country,name:base.tw msgid "Taiwan" -msgstr "" +msgstr "Taiwan" #. module: base #: model:ir.model,name:base.model_res_currency_rate @@ -8682,7 +8682,7 @@ msgstr "" #: field:base.language.export,name:0 #: field:ir.attachment,datas_fname:0 msgid "Filename" -msgstr "" +msgstr "Filnavn" #. module: base #: field:ir.model,access_ids:0 @@ -8693,7 +8693,7 @@ msgstr "" #. module: base #: model:res.country,name:base.sk msgid "Slovak Republic" -msgstr "" +msgstr "Slovakiet" #. module: base #: model:ir.ui.menu,name:base.publisher_warranty @@ -8708,7 +8708,7 @@ msgstr "" #. module: base #: model:res.country,name:base.ar msgid "Argentina" -msgstr "" +msgstr "Argentina" #. module: base #: field:res.groups,name:0 @@ -8718,7 +8718,7 @@ msgstr "" #. module: base #: model:res.country,name:base.bh msgid "Bahrain" -msgstr "" +msgstr "Bahrain" #. module: base #: model:res.partner.category,name:base.res_partner_category_12 @@ -8740,7 +8740,7 @@ msgstr "" #: view:res.users:0 #: field:res.users,company_id:0 msgid "Company" -msgstr "" +msgstr "Virksomhed" #. module: base #: view:res.users:0 @@ -8780,7 +8780,7 @@ msgstr "" #. module: base #: model:res.country,name:base.jm msgid "Jamaica" -msgstr "" +msgstr "Jamaica" #. module: base #: model:ir.actions.act_window,help:base.action_partner_category_form @@ -8800,7 +8800,7 @@ msgstr "" #: code:addons/base/res/partner/partner.py:250 #, python-format msgid "Warning" -msgstr "" +msgstr "Advarsel!" #. module: base #: selection:base.language.install,lang:0 @@ -8840,7 +8840,7 @@ msgstr "" #. module: base #: model:res.country,name:base.rw msgid "Rwanda" -msgstr "" +msgstr "Rwanda" #. module: base #: view:ir.sequence:0 @@ -8865,7 +8865,7 @@ msgstr "" #. module: base #: model:res.country,name:base.sg msgid "Singapore" -msgstr "" +msgstr "Singapore" #. module: base #: selection:ir.actions.act_window,target:0 @@ -8895,7 +8895,7 @@ msgstr "" #: field:res.partner.address,country_id:0 #: field:res.partner.bank,country_id:0 msgid "Country" -msgstr "" +msgstr "Land" #. module: base #: field:ir.model.fields,complete_name:0 @@ -8918,12 +8918,12 @@ msgstr "" #. module: base #: field:res.partner.category,name:0 msgid "Category Name" -msgstr "" +msgstr "Kategori Navn" #. module: base #: model:res.partner.category,name:base.res_partner_category_15 msgid "IT sector" -msgstr "" +msgstr "IT sektor" #. module: base #: view:ir.actions.act_window:0 @@ -8975,7 +8975,7 @@ msgstr "" #: selection:ir.ui.view,type:0 #: selection:wizard.ir.model.menu.create.line,view_type:0 msgid "Graph" -msgstr "" +msgstr "Graf" #. module: base #: model:ir.model,name:base.model_ir_actions_server @@ -9017,7 +9017,7 @@ msgstr "" #. module: base #: model:ir.ui.menu,name:base.menu_localisation msgid "Localisation" -msgstr "" +msgstr "Lokalisering" #. module: base #: view:ir.actions.server:0 @@ -9070,7 +9070,7 @@ msgstr "" #. module: base #: field:ir.actions.server,mobile:0 msgid "Mobile No" -msgstr "" +msgstr "Mobil nr." #. module: base #: model:ir.actions.act_window,name:base.action_partner_by_category @@ -9084,7 +9084,7 @@ msgstr "" #. module: base #: view:base.module.upgrade:0 msgid "System Update" -msgstr "" +msgstr "Systemopdatering" #. module: base #: selection:ir.translation,type:0 @@ -9099,18 +9099,18 @@ msgstr "" #. module: base #: model:res.country,name:base.sc msgid "Seychelles" -msgstr "" +msgstr "Seychellerne" #. module: base #: model:ir.model,name:base.model_res_partner_bank #: view:res.partner.bank:0 msgid "Bank Accounts" -msgstr "" +msgstr "Bankkonti" #. module: base #: model:res.country,name:base.sl msgid "Sierra Leone" -msgstr "" +msgstr "Sierra Leone" #. module: base #: view:res.company:0 @@ -9126,7 +9126,7 @@ msgstr "" #. module: base #: field:res.partner.bank,owner_name:0 msgid "Account Owner" -msgstr "" +msgstr "Konto ejer" #. module: base #: code:addons/base/res/res_user.py:256 @@ -9155,7 +9155,7 @@ msgstr "" #: field:res.partner.address,function:0 #: selection:workflow.activity,kind:0 msgid "Function" -msgstr "" +msgstr "Funktion" #. module: base #: view:res.widget:0 @@ -9165,12 +9165,12 @@ msgstr "" #. module: base #: selection:ir.actions.todo,restart:0 msgid "Never" -msgstr "" +msgstr "Aldrig" #. module: base #: selection:res.partner.address,type:0 msgid "Delivery" -msgstr "" +msgstr "Levering" #. module: base #: model:res.partner.title,name:base.res_partner_title_pvt_ltd @@ -9197,7 +9197,7 @@ msgstr "" #. module: base #: model:res.country,name:base.kp msgid "North Korea" -msgstr "" +msgstr "Nordkorea" #. module: base #: selection:ir.actions.server,state:0 @@ -9240,7 +9240,7 @@ msgstr "" #. module: base #: model:res.country,name:base.lk msgid "Sri Lanka" -msgstr "" +msgstr "Sri Lanka" #. module: base #: selection:base.language.install,lang:0