openerp-dev-web team mailing list archive
-
openerp-dev-web team
-
Mailing list archive
-
Message #04432
lp:~openerp-dev/openobject-client-web/proto61-tests-and-runners-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61
Xavier (Open ERP) has proposed merging lp:~openerp-dev/openobject-client-web/proto61-tests-and-runners-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61.
Requested reviews:
OpenERP R&D Team (openerp-dev)
For more details, see:
https://code.launchpad.net/~openerp-dev/openobject-client-web/proto61-tests-and-runners-xmo/+merge/54202
* Addition of an initial test (menu loading) to the base addon
+ documentation of tests procedures (libraries, launching tests, location) in addons
+ reformatting of menu loading
* Addition of an initial test (OpenERPModel behavior) in core
+ documentation of testing in core
* Added test dependencies in setup.py, set test_suite in setup.py so it's possible to run tests in core via `setup.py test`
* Added some documentation on addon structure
--
https://code.launchpad.net/~openerp-dev/openobject-client-web/proto61-tests-and-runners-xmo/+merge/54202
Your team OpenERP R&D Team is requested to review the proposed merge of lp:~openerp-dev/openobject-client-web/proto61-tests-and-runners-xmo into lp:~openerp-dev/openobject-client-web/trunk-proto61.
=== modified file 'addons/base/controllers/main.py'
--- addons/base/controllers/main.py 2011-03-17 17:14:03 +0000
+++ addons/base/controllers/main.py 2011-03-21 12:36:36 +0000
@@ -25,14 +25,14 @@
@staticmethod
def convert_element(el, skip_whitespaces=True):
res = {}
- if el.tag[0]=="{":
- ns, name = el.tag.rsplit("}",1)
+ if el.tag[0] == "{":
+ ns, name = el.tag.rsplit("}", 1)
res["tag"] = name
res["namespace"] = ns[1:]
else:
res["tag"] = el.tag
res["attrs"] = {}
- for k,v in el.items():
+ for k, v in el.items():
res["attrs"][k] = v
kids = []
if el.text and (not skip_whitespaces or el.text.strip() != ''):
@@ -48,20 +48,6 @@
# OpenERP Web base Controllers
#----------------------------------------------------------
-class Hello(openerpweb.Controller):
- _cp_path = "/base/hello"
-
- def index(self):
- return "hello world"
-
- @openerpweb.jsonrequest
- def ajax_hello_world(self,req):
- return {"welcome":"hello world"}
-
- @openerpweb.jsonrequest
- def ajax_hello_error(self,req):
- raise Exception("You suck")
-
class Session(openerpweb.Controller):
_cp_path = "/base/session"
@@ -84,7 +70,7 @@
files_content = []
files_timestamp = 0
for i in file_list:
- fname = os.path.join(root,i)
+ fname = os.path.join(root, i)
ftime = os.path.getmtime(fname)
if ftime > files_timestamp:
files_timestamp = ftime
@@ -96,7 +82,7 @@
def login(self, req, db, login, password):
req.session.login(db, login, password)
return {
- "session_id" : req.session_id,
+ "session_id": req.session_id,
"uid": req.session._uid,
}
@@ -117,69 +103,83 @@
concat = self.concat_files(files)[0]
# TODO request set the Date of last modif and Etag
return concat
- css.exposed=1
+ css.exposed = True
def js(self, req, mods='base,base_hello'):
files = self.manifest_glob(mods.split(','), 'js')
concat = self.concat_files(files)[0]
# TODO request set the Date of last modif and Etag
return concat
- js.exposed=1
+ js.exposed = True
+
class Menu(openerpweb.Controller):
_cp_path = "/base/menu"
@openerpweb.jsonrequest
- def load(self,req):
- m = req.session.model('ir.ui.menu')
+ def load(self, req):
+ return {'data': self.do_load(req)}
+
+ def do_load(self, req):
+ """ Loads all menu items (all applications and their sub-menus).
+
+ :param req: A request object, with an OpenERP session attribute
+ :type req: < session -> OpenERPSession >
+ :return: the menu root
+ :rtype: dict('children': menu_nodes)
+ """
+ Menus = req.session.model('ir.ui.menu')
# menus are loaded fully unlike a regular tree view, cause there are
# less than 512 items
- menu_ids = m.search([])
- menu_items = m.read(menu_ids,['name','sequence','parent_id'])
- menu_root = {'id':False, 'name':'root', 'parent_id':[-1,'']}
+ menu_ids = Menus.search([])
+ menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
+ menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
menu_items.append(menu_root)
+
# make a tree using parent_id
- for i in menu_items:
- i['children'] = []
- d = dict([(i["id"],i) for i in menu_items])
- for i in menu_items:
- if not i['parent_id']:
- pid = False
- else:
- pid = i['parent_id'][0]
- if pid in d:
- d[pid]['children'].append(i)
+ menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
+ for menu_item in menu_items:
+ if not menu_item['parent_id']: continue
+ parent = menu_item['parent_id'][0]
+ if parent in menu_items_map:
+ menu_items_map[parent].setdefault(
+ 'children', []).append(menu_item)
+
# sort by sequence a tree using parent_id
- for i in menu_items:
- i['children'].sort(key = lambda x:x["sequence"])
+ for menu_item in menu_items:
+ menu_item.setdefault('children', []).sort(
+ key=lambda x:x["sequence"])
- return {'data': menu_root}
+ return menu_root
@openerpweb.jsonrequest
- def action(self,req,menu_id):
+ def action(self, req, menu_id):
m = req.session.model('ir.values')
r = m.get('action', 'tree_but_open', [('ir.ui.menu', menu_id)], False, {})
- res={"action":r}
+ res = {"action": r}
return res
+
class DataSet(openerpweb.Controller):
_cp_path = "/base/dataset"
@openerpweb.jsonrequest
- def fields(self,req,model):
+ def fields(self, req, model):
return {'fields': req.session.model(model).fields_get(False)}
@openerpweb.jsonrequest
- def load(self,req,model,domain=[],fields=['id']):
+ def load(self, req, model, domain=[], fields=['id']):
m = req.session.model(model)
ids = m.search(domain)
values = m.read(ids, fields)
return {'ids': ids, 'values': values}
+
class DataRecord(openerpweb.Controller):
_cp_path = "/base/datarecord"
+
@openerpweb.jsonrequest
- def load(self,req,model,id,fields):
+ def load(self, req, model, id, fields):
m = req.session.model(model)
value = {}
r = m.read([id])
@@ -187,36 +187,43 @@
value = r[0]
return {'value': value}
+
class FormView(openerpweb.Controller):
_cp_path = "/base/formview"
+
@openerpweb.jsonrequest
- def load(self,req,model,view_id):
+ def load(self, req, model, view_id):
m = req.session.model(model)
- r = m.fields_view_get(view_id,'form')
- r["arch"]=Xml2Json.convert_to_structure(r["arch"])
- return {'fields_view':r}
+ r = m.fields_view_get(view_id, 'form')
+ r["arch"] = Xml2Json.convert_to_structure(r["arch"])
+ return {'fields_view': r}
+
class ListView(openerpweb.Controller):
_cp_path = "/base/listview"
+
@openerpweb.jsonrequest
- def load(self,req,model,view_id):
+ def load(self, req, model, view_id):
m = req.session.model(model)
- r = m.fields_view_get(view_id,'tree')
- r["arch"]=Xml2Json.convert_to_structure(r["arch"])
- return {'fields_view':r}
+ r = m.fields_view_get(view_id, 'tree')
+ r["arch"] = Xml2Json.convert_to_structure(r["arch"])
+ return {'fields_view': r}
+
class SearchView(openerpweb.Controller):
_cp_path = "/base/searchview"
+
@openerpweb.jsonrequest
- def load(self,req,model,view_id):
+ def load(self, req, model, view_id):
m = req.session.model(model)
- r = m.fields_view_get(view_id,'search')
- r["arch"]=Xml2Json.convert_to_structure(r["arch"])
- return {'fields_view':r}
+ r = m.fields_view_get(view_id, 'search')
+ r["arch"] = Xml2Json.convert_to_structure(r["arch"])
+ return {'fields_view': r}
+
class Action(openerpweb.Controller):
_cp_path = "/base/action"
@openerpweb.jsonrequest
- def load(self,req,action_id):
+ def load(self, req, action_id):
return {}
=== modified file 'addons/base/static/openerp/js/base_chrome.js'
--- addons/base/static/openerp/js/base_chrome.js 2011-03-20 20:01:17 +0000
+++ addons/base/static/openerp/js/base_chrome.js 2011-03-21 12:36:36 +0000
@@ -114,7 +114,7 @@
openerp.base.Console = openerp.base.BasicController.extend({
init: function(element_id, server, port) {
this._super(element_id);
- },
+ }
});
openerp.base.Session = openerp.base.BasicController.extend({
=== modified file 'addons/base/static/openerp/js/base_views.js'
--- addons/base/static/openerp/js/base_views.js 2011-03-18 00:38:17 +0000
+++ addons/base/static/openerp/js/base_views.js 2011-03-21 12:36:36 +0000
@@ -273,9 +273,9 @@
},
on_record_loaded: function() {
for (var f in this.fields) {
- this.fields[f].set_value()
+ this.fields[f].set_value();
}
- },
+ }
});
openerp.base.ListView = openerp.base.Controller.extend({
@@ -536,7 +536,7 @@
},
on_change: function() {
//this.view.update_field(this.name,value);
- },
+ }
});
openerp.base.FieldEmail = openerp.base.Field.extend({
@@ -637,7 +637,7 @@
'boolean' : openerp.base.FieldBoolean,
'float' : openerp.base.FieldFloat,
'button' : openerp.base.WidgetButton
-}
+};
openerp.base.CalendarView = openerp.base.Controller.extend({
// Dhtmlx scheduler ?
=== added directory 'addons/base/tests'
=== added file 'addons/base/tests/__init__.py'
--- addons/base/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ addons/base/tests/__init__.py 2011-03-21 12:36:36 +0000
@@ -0,0 +1,1 @@
+# -*- coding: utf-8 -*-
=== added file 'addons/base/tests/test_menu.py'
--- addons/base/tests/test_menu.py 1970-01-01 00:00:00 +0000
+++ addons/base/tests/test_menu.py 2011-03-21 12:36:36 +0000
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+import mock
+import unittest2
+import base.controllers.main
+import openerpweb.openerpweb
+
+class Placeholder(object):
+ def __init__(self, **kwargs):
+ for k, v in kwargs.iteritems():
+ setattr(self, k, v)
+
+class LoadTest(unittest2.TestCase):
+ def setUp(self):
+ self.menu = base.controllers.main.Menu()
+ self.menus_mock = mock.Mock()
+ self.request = Placeholder(
+ session=openerpweb.openerpweb.OpenERPSession(
+ model_factory=lambda _session, _name: self.menus_mock))
+
+ def tearDown(self):
+ del self.request
+ del self.menus_mock
+ del self.menu
+
+ def test_empty(self):
+ self.menus_mock.search = mock.Mock(return_value=[])
+ self.menus_mock.read = mock.Mock(return_value=[])
+
+ root = self.menu.do_load(self.request)
+
+ self.menus_mock.search.assert_called_with([])
+ self.menus_mock.read.assert_called_with(
+ [], ['name', 'sequence', 'parent_id'])
+
+ self.assertListEqual(
+ root['children'],
+ [])
+
+ def test_applications_sort(self):
+ self.menus_mock.search = mock.Mock(return_value=[1, 2, 3])
+ self.menus_mock.read = mock.Mock(return_value=[
+ {'id': 2, 'sequence': 3, 'parent_id': [False, '']},
+ {'id': 3, 'sequence': 2, 'parent_id': [False, '']},
+ {'id': 1, 'sequence': 1, 'parent_id': [False, '']},
+ ])
+
+ root = self.menu.do_load(self.request)
+
+ self.menus_mock.read.assert_called_with(
+ [1, 2, 3], ['name', 'sequence', 'parent_id'])
+
+ self.assertEqual(
+ root['children'],
+ [{
+ 'id': 1, 'sequence': 1,
+ 'parent_id': [False, ''], 'children': []
+ }, {
+ 'id': 3, 'sequence': 2,
+ 'parent_id': [False, ''], 'children': []
+ }, {
+ 'id': 2, 'sequence': 3,
+ 'parent_id': [False, ''], 'children': []
+ }])
+
+ def test_deep(self):
+ self.menus_mock.search = mock.Mock(return_value=[1, 2, 3, 4])
+ self.menus_mock.read = mock.Mock(return_value=[
+ {'id': 1, 'sequence': 1, 'parent_id': [False, '']},
+ {'id': 2, 'sequence': 2, 'parent_id': [1, '']},
+ {'id': 3, 'sequence': 1, 'parent_id': [2, '']},
+ {'id': 4, 'sequence': 2, 'parent_id': [2, '']},
+ ])
+
+ root = self.menu.do_load(self.request)
+
+ self.assertEqual(
+ root['children'],
+ [{
+ 'id': 1,
+ 'sequence': 1,
+ 'parent_id': [False, ''],
+ 'children': [{
+ 'id': 2,
+ 'sequence': 2,
+ 'parent_id': [1, ''],
+ 'children': [{
+ 'id': 3,
+ 'sequence': 1,
+ 'parent_id': [2, ''],
+ 'children': []
+ }, {
+ 'id': 4,
+ 'sequence': 2,
+ 'parent_id': [2, ''],
+ 'children': []
+ }]
+ }]
+ }]
+ )
=== modified file 'addons/base_hello/static/openerp/base_hello.js'
--- addons/base_hello/static/openerp/base_hello.js 2011-03-17 12:22:39 +0000
+++ addons/base_hello/static/openerp/base_hello.js 2011-03-21 12:36:36 +0000
@@ -7,12 +7,12 @@
openerp.base.SearchView = openerp.base.SearchView.extend({
init:function() {
this._super.apply(this,arguments);
- this.on_search.add(function(){alert('hello')});
- },
+ this.on_search.add(function(){console.log('hello');});
+ }
});
// here you may tweak globals object, if any, and play with on_* or do_* callbacks on them
-}
+};
// vim:et fdc=0 fdl=0:
=== added file 'doc/source/addon-structure.txt'
--- doc/source/addon-structure.txt 1970-01-01 00:00:00 +0000
+++ doc/source/addon-structure.txt 2011-03-21 12:36:36 +0000
@@ -0,0 +1,9 @@
+<addon name>
+ +-- __openerp__.py
+ +-- controllers/
+ +-- static/
+ +-- openerp/
+ +-- css/
+ +-- img/
+ +-- js/
+ +-- tests/
=== modified file 'doc/source/addons.rst'
--- doc/source/addons.rst 2011-03-18 15:52:47 +0000
+++ doc/source/addons.rst 2011-03-21 12:36:36 +0000
@@ -1,7 +1,91 @@
Developing OpenERP Web Addons
=============================
-* Structure of an addon
+Structure
+---------
+
+.. literalinclude:: addon-structure.txt
+
+``__openerp__.py``
+ The addon's descriptor, contains the following information:
+
+ ``name: str``
+ The addon name, in plain, readable english
+ ``version: str``
+ The addon version, following `Semantic Versioning`_ rules
+ ``depends: [str]``
+ A list of addons this addon needs to work correctly. ``base`` is
+ an implied dependency if the list is empty.
+ ``css: [str]``
+ An ordered list of CSS files this addon provides and needs. The
+ file paths are relative to the addon's root. Because the Web
+ Client *may* perform concatenations and other various
+ optimizations on CSS files, the order is important.
+ ``js: [str]``
+ An ordered list of Javascript files this addon provides and needs
+ (including dependencies files). As with CSS files, the order is
+ important as the Web Client *may* perform contatenations and
+ minimizations of files.
+ ``active: bool``
+ Whether this addon should be enabled by default any time it is
+ found, or whether it will be enabled through other means (on a
+ by-need or by-installation basis for instance).
+
+``controllers/``
+ All of the Python controllers and JSON-RPC endpoints.
+
+``static/``
+ The static files directory, may be served via a separate web server.
+
+ The third-party dependencies should be bundled in it (each in their
+ own directory).
+
+``static/openerp/``
+ Sub-tree for all the addon's own static files.
+
+``static/openerp/{css,js,img}``
+ Location for (respectively) the addon's static CSS files, its JS
+ files and its various image resources.
+
+``tests/``
+ The directories in which all tests for the addon are located.
+
+.. _addons-testing:
+
+Testing
+-------
+
+Python
+++++++
+
+OpenERP Web uses unittest2_ for its testing needs. We selected
+unittest2 rather than unittest_ for the following reasons:
+
+* autodiscovery_ (similar to nose, via the ``unit2``
+ CLI utility) and `pluggable test discovery`_.
+
+* `new and improved assertions`_ (with improvements in type-specific
+ inequality reportings) including `pluggable custom types equality
+ assertions`_
+
+* neveral new APIs, most notably `assertRaises context manager`_,
+ `cleanup function registration`_, `test skipping`_ and `class- and
+ module-level setup and teardown`_
+
+* finally, unittest2 is a backport of Python 3's unittest. We might as
+ well get used to it.
+
+To run tests on addons (from the root directory of OpenERP Web) is as
+simple as typing ``PYTHONPATH=. unit2 discover -s addons`` [#]_. To
+test an addon which does not live in the ``addons`` directory, simply
+replace ``addons`` by the directory in which your own addon lives.
+
+.. note:: unittest2 is entirely compatible with nose_ (or the
+ other way around). If you want to use nose as your test
+ runner (due to its addons for instance) you can simply install it
+ and run ``nosetests addons`` instead of the ``unit2`` command,
+ the result should be exactly the same.
+
* Addons lifecycle (loading, execution, events, ...)
* Python-side
@@ -16,3 +100,54 @@
* Javascript public APIs
* QWeb templates description?
* OpenERP Web modules (from OpenERP modules)
+
+.. [#] the ``-s`` parameter tells ``unit2`` to start trying to
+ find tests in the provided directory (here we're testing
+ addons). However a side-effect of that is to set the
+ ``PYTHONPATH`` there as well, so it will fail to find (and
+ import) ``openerpweb``.
+
+ The ``-t`` parameter lets us set the ``PYTHONPATH``
+ independently, but it doesn't accept multiple values and here
+ we really want to have both ``.`` and ``addons`` on the
+ ``PYTHONPATH``.
+
+ The solution is to set the ``PYTHONPATH`` to ``.`` on start,
+ and the ``start-directory`` to ``addons``. This results in a
+ correct ``PYTHONPATH`` within ``unit2``.
+
+.. _unittest:
+ http://docs.python.org/library/unittest.html
+
+.. _unittest2:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml
+
+.. _autodiscovery:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-discovery
+
+.. _pluggable test discovery:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#load-tests
+
+.. _new and improved assertions:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#new-assert-methods
+
+.. _pluggable custom types equality assertions:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#add-new-type-specific-functions
+
+.. _assertRaises context manager:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#assertraises
+
+.. _cleanup function registration:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#cleanup-functions-with-addcleanup
+
+.. _test skipping:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-skipping
+
+.. _class- and module-level setup and teardown:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml#class-and-module-level-fixtures
+
+.. _Semantic Versioning:
+ http://semver.org/
+
+.. _nose:
+ http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
=== modified file 'doc/source/development.rst'
--- doc/source/development.rst 2011-03-18 16:00:26 +0000
+++ doc/source/development.rst 2011-03-21 12:36:36 +0000
@@ -6,4 +6,25 @@
* QWeb code documentation/description
* Documentation of the OpenERP APIs and choices taken based on that?
* Style guide and coding conventions (PEP8? More)
-* Test frameworks for Python and JS?
+* Test frameworks in JS?
+
+Testing
+-------
+
+Python
+++++++
+
+Testing for the OpenERP Web core is similar to :ref:`testing addons
+<addons-testing>`: the tests live in ``openerpweb.tests``, unittest2_
+is the testing framework and tests can be run via either unittest2
+(``unit2 discover``) or via nose_ (``nosetests``).
+
+Tests for the OpenERP Web core can also be run using ``setup.py
+test``.
+
+
+.. _unittest2:
+ http://www.voidspace.org.uk/python/articles/unittest2.shtml
+
+.. _nose:
+ http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
=== modified file 'doc/source/index.rst'
--- doc/source/index.rst 2011-03-18 16:00:26 +0000
+++ doc/source/index.rst 2011-03-21 12:36:36 +0000
@@ -17,6 +17,7 @@
addons
development
project
+ old-version
Indices and tables
==================
=== added file 'doc/source/old-version.rst'
--- doc/source/old-version.rst 1970-01-01 00:00:00 +0000
+++ doc/source/old-version.rst 2011-03-21 12:36:36 +0000
@@ -0,0 +1,11 @@
+Main differences with the 6.0 client
+====================================
+
+.. No more populate.sh, use virtualenvs
+
+.. Logic is mainly in Javascript (had to make a choice between JS and
+.. Python logic)
+
+.. Templating language changes
+
+.. How to port addons and modules?
=== modified file 'doc/source/production.rst'
--- doc/source/production.rst 2011-03-18 16:00:26 +0000
+++ doc/source/production.rst 2011-03-21 12:36:36 +0000
@@ -3,6 +3,12 @@
.. After release one, add upgrade instructions if any
+.. How about running the web client on alternative Python
+.. implementations e.g. pypy or Jython? Since the only lib with C
+.. accelerators we're using right now is SimpleJSON and it has a pure
+.. Python base component, we should be able to test and deploy on
+.. non-cpython no?
+
In-depth configuration
----------------------
=== modified file 'openerpweb/openerpweb.py'
--- openerpweb/openerpweb.py 2011-03-20 13:21:46 +0000
+++ openerpweb/openerpweb.py 2011-03-21 12:36:36 +0000
@@ -1,7 +1,12 @@
#!/usr/bin/python
import functools
-
-import optparse, os, re, sys, tempfile, traceback, uuid, xmlrpclib
+import optparse
+import os
+import sys
+import tempfile
+import traceback
+import uuid
+import xmlrpclib
import cherrypy
import cherrypy.lib.static
@@ -15,30 +20,37 @@
class OpenERPUnboundException(Exception):
pass
+
class OpenERPConnector(object):
pass
+
class OpenERPAuth(object):
pass
+
class OpenERPModel(object):
- def __init__(self,session,model):
+ def __init__(self, session, model):
self._session = session
self._model = model
- def __getattr__(self,name):
- return lambda *l:self._session.execute(self._model,name,*l)
+
+ def __getattr__(self, name):
+ return lambda *l:self._session.execute(self._model, name, *l)
+
class OpenERPSession(object):
- def __init__(self, server='127.0.0.1', port=8069):
+ def __init__(self, server='127.0.0.1', port=8069,
+ model_factory=OpenERPModel):
self._server = server
self._port = port
self._db = False
self._uid = False
self._login = False
self._password = False
+ self.model_factory = model_factory
def proxy(self, service):
- s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s'%(self._server, self._port, service), timeout=5)
+ s = xmlrpctimeout.TimeoutServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service), timeout=5)
return s
def bind(self, db, uid, password):
@@ -52,14 +64,14 @@
self._login = login
return uid
- def execute(self,model,func,*l,**d):
+ def execute(self, model, func, *l, **d):
if not (self._db and self._uid and self._password):
raise OpenERPUnboundException()
r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d)
return r
- def model(self,model):
- return OpenERPModel(self,model)
+ def model(self, model):
+ return self.model_factory(self, model)
#----------------------------------------------------------
# OpenERP Web RequestHandler
@@ -82,7 +94,7 @@
"""
def parse(self, request):
- self.params = request.get("params",{})
+ self.params = request.get("params", {})
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = cherrypy.session.setdefault(self.session_id, OpenERPSession())
self.context = self.params.pop('context', None)
@@ -108,7 +120,7 @@
else:
request = simplejson.loads(request)
try:
- print "--> %s.%s %s"%(controller.__class__.__name__,method.__name__,request)
+ print "--> %s.%s %s" % (controller.__class__.__name__, method.__name__, request)
error = None
result = method(controller, self, **self.parse(request))
except OpenERPUnboundException:
@@ -127,7 +139,8 @@
'data': {
'type': 'server_exception',
'fault_code': e.faultCode,
- 'debug': "Client %s\nServer %s" % ("".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
+ 'debug': "Client %s\nServer %s" % (
+ "".join(traceback.format_exception("", None, sys.exc_traceback)), e.faultString)
}
}
except Exception:
@@ -139,13 +152,13 @@
'debug': "Client %s" % traceback.format_exc()
}
}
- response = {"jsonrpc": "2.0", "id": request.get('id')}
+ response = {"jsonrpc": "2.0", "id": request.get('id')}
if error:
response["error"] = error
else:
response["result"] = result
- print "<--", response
+ print "<--", response
print
content = simplejson.dumps(response)
@@ -153,31 +166,36 @@
cherrypy.response.headers['Content-Length'] = len(content)
return content
+
def jsonrequest(f):
@cherrypy.expose
@functools.wraps(f)
def json_handler(self):
return JsonRequest().dispatch(self, f, requestf=cherrypy.request.body)
+
return json_handler
+
class HttpRequest(object):
""" Regular GET/POST request
"""
+
def __init__(self):
# result may be filled, it's content will be updated by the return
# value of the dispatched function if it's a dict
self.result = ""
def dispatch(self, controller, f, request):
- print "GET/POST --> %s.%s %s"%(controller.__class__.__name__,f.__name__,request)
- r=f(controller, self, request)
+ print "GET/POST --> %s.%s %s" % (controller.__class__.__name__, f.__name__, request)
+ r = f(controller, self, request)
return r
+
def httprequest(f):
# check cleaner wrapping:
# functools.wraps(f)(lambda x: JsonRequest().dispatch(x, f))
- l=lambda self, request: HttpRequest().dispatch(self, f, request)
- l.exposed=1
+ l = lambda self, request: HttpRequest().dispatch(self, f, request)
+ l.exposed = 1
return l
#-----------------------------------------------------------
@@ -185,7 +203,7 @@
#-----------------------------------------------------------
path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__)))
-path_addons = os.path.join(path_root,'addons')
+path_addons = os.path.join(path_root, 'addons')
cherrypy_root = None
# globals might move into a pool if needed
@@ -198,64 +216,68 @@
class ControllerType(type):
def __init__(cls, name, bases, attrs):
super(ControllerType, cls).__init__(name, bases, attrs)
- # TODO forgive me this hack and find me a clean way to get the absolute name of a class
- cls.fullname = re.search("'(.+)'",repr(cls)).group(1)
- controllers_class[cls.fullname] = cls
+ controllers_class["%s.%s" % (cls.__module__, cls.__name__)] = cls
+
class Controller(object):
__metaclass__ = ControllerType
+
class Root(object):
def __init__(self):
self.addons = {}
self._load_addons()
+
def _load_addons(self):
if path_addons not in sys.path:
- sys.path.insert(0,path_addons)
+ sys.path.insert(0, path_addons)
for i in os.listdir(path_addons):
if i not in sys.modules:
- manifest_path = os.path.join(path_addons,i,'__openerp__.py')
+ manifest_path = os.path.join(path_addons, i, '__openerp__.py')
if os.path.isfile(manifest_path):
manifest = eval(open(manifest_path).read())
- print "Loading",i
+ print "Loading", i
m = __import__(i)
addons_module[i] = m
addons_manifest[i] = manifest
- for k,v in controllers_class.items():
+ for k, v in controllers_class.items():
if k not in controllers_object:
o = v()
controllers_object[k] = o
- if hasattr(o,'_cp_path'):
+ if hasattr(o, '_cp_path'):
controllers_path[o._cp_path] = o
def default(self, *l, **kw):
#print "default",l,kw
# handle static files
- if len(l) > 2 and l[1]=='static':
+ if len(l) > 2 and l[1] == 'static':
# sanitize path
p = os.path.normpath(os.path.join(*l))
- return cherrypy.lib.static.serve_file(os.path.join(path_addons,p))
+ return cherrypy.lib.static.serve_file(os.path.join(path_addons, p))
elif len(l) > 1:
- for i in range(1,len(l)+1):
+ for i in range(1, len(l) + 1):
ps = "/" + "/".join(l[0:i])
if ps in controllers_path:
c = controllers_path[ps]
rest = l[i:] or ['index']
meth = rest[0]
- m = getattr(c,meth)
- if getattr(m,'exposed',0):
- print "Calling",ps,c,meth,m
+ m = getattr(c, meth)
+ if getattr(m, 'exposed', 0):
+ print "Calling", ps, c, meth, m
return m(**kw)
else:
raise cherrypy.HTTPRedirect('/base/static/openerp/base.html', 301)
default.exposed = True
+
def main(argv):
# Parse config
op = optparse.OptionParser()
op.add_option("-p", "--port", dest="socket_port", help="listening port", metavar="NUMBER", default=8002)
- op.add_option("-s", "--session-path", dest="storage_path", help="directory used for session storage", metavar="DIR", default=os.path.join(tempfile.gettempdir(),"cpsessions"))
+ op.add_option("-s", "--session-path", dest="storage_path",
+ help="directory used for session storage", metavar="DIR",
+ default=os.path.join(tempfile.gettempdir(), "cpsessions"))
(o, args) = op.parse_args(argv[1:])
# Prepare cherrypy config from options
=== added directory 'openerpweb/tests'
=== added file 'openerpweb/tests/__init__.py'
--- openerpweb/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ openerpweb/tests/__init__.py 2011-03-21 12:36:36 +0000
@@ -0,0 +1,1 @@
+# -*- coding: utf-8 -*-
=== added file 'openerpweb/tests/test_model.py'
--- openerpweb/tests/test_model.py 1970-01-01 00:00:00 +0000
+++ openerpweb/tests/test_model.py 2011-03-21 12:36:36 +0000
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+import mock
+import unittest2
+import openerpweb.openerpweb
+
+class OpenERPModelTest(unittest2.TestCase):
+ def test_rpc_call(self):
+ session = mock.Mock(['execute'])
+ Model = openerpweb.openerpweb.OpenERPModel(
+ session, 'a.b')
+
+ Model.search([('field', 'op', 'value')], {'key': 'value'})
+ session.execute.assert_called_once_with(
+ 'a.b', 'search', [('field', 'op', 'value')], {'key': 'value'})
+
+ session.execute.reset_mock()
+
+ Model.read([42])
+ session.execute.assert_called_once_with(
+ 'a.b', 'read', [42])
=== modified file 'setup.py'
--- setup.py 2011-03-08 10:28:13 +0000
+++ setup.py 2011-03-21 12:36:36 +0000
@@ -59,6 +59,11 @@
"simplejson >= 2.0.9",
"python-dateutil >= 1.4.1",
],
+ tests_require=[
+ 'unittest2',
+ 'mock',
+ ],
+ test_suite = 'unittest2.collector',
zip_safe=False,
packages=[
'addons',
Follow ups