1 Developing OpenERP Web Addons
2 =============================
4 An OpenERP Web addon is simply a Python package with an openerp
5 descriptor (a ``__openerp__.py`` file) which follows a few structural
11 .. literalinclude:: addon-structure.txt
14 The addon's descriptor, contains the following information:
17 The addon name, in plain, readable english
19 The addon version, following `Semantic Versioning`_ rules
21 A list of addons this addon needs to work correctly. ``base`` is
22 an implied dependency if the list is empty.
24 An ordered list of CSS files this addon provides and needs. The
25 file paths are relative to the addon's root. Because the Web
26 Client *may* perform concatenations and other various
27 optimizations on CSS files, the order is important.
29 An ordered list of Javascript files this addon provides and needs
30 (including dependencies files). As with CSS files, the order is
31 important as the Web Client *may* perform contatenations and
32 minimizations of files.
34 Whether this addon should be enabled by default any time it is
35 found, or whether it will be enabled through other means (on a
36 by-need or by-installation basis for instance).
39 All of the Python controllers and JSON-RPC endpoints.
42 The static files directory, may be served via a separate web server.
45 Third-party libraries used by the addon.
47 ``static/src/{css,js,img,xml}``
48 Location for (respectively) the addon's static CSS files, its JS
49 files, its various image resources as well as the template files
52 Javascript tests files
55 The directories in which all tests for the addon are located.
57 Some of these are guidelines (and not enforced by code), but it's
58 suggested that these be followed. Code which does not fit into these
59 categories can go wherever deemed suitable.
67 Because addons are also Python packages, they're inherently namespaced
68 and nothing special needs to be done on that front.
73 The JavaScript side of an addon has to live in the namespace
74 ``openerp.$addon_name``. For instance, everything created by the addon
75 ``base`` lives in ``openerp.base``.
77 The root namespace of the addon is a function which takes a single
78 parameter ``openerp``, which is an OpenERP client instance. Objects
79 (as well as functions, registry instances, etc...) should be added on
80 the correct namespace on that object.
82 The root function will be called by the OpenERP Web client when
83 initializing the addon.
85 .. code-block:: javascript
87 // root namespace of the openerp.example addon
89 openerp.example = function (openerp) {
90 // basic initialization code (e.g. templates loading)
91 openerp.example.SomeClass = openerp.base.Class.extend(
92 /** @lends openerp.example.SomeClass# */{
94 * Description for SomeClass's constructor here
99 // SomeClass initialization code
104 // access an object in an other addon namespace to replace it
105 openerp.base.SearchView = openerp.base.SearchView.extend({
107 this._super.apply(this, arguments);
108 console.log('Search view initialized');
113 Creating new standard roles
114 ---------------------------
119 Views are the standard high-level component in OpenERP. A view type corresponds
120 to a way to display a set of data (coming from an OpenERP model).
122 In OpenERP Web, views are standard objects registered against a dedicated
123 object registry, so the :js:class:`~openerp.base.ViewManager` knows where to
124 find and how to call them.
126 Although not mandatory, it is recommended that views inherit from
127 :js:class:`openerp.base.View`, which provides a view useful services to its
133 This is the first task to perform when creating a view, and the simplest by
134 far: simply call ``openerp.base.views.add(name, object_path)`` to register
135 the object of path ``object_path`` as the view for the view name ``name``.
137 The view name is the name you gave to your new view in the OpenERP server.
139 From that point onwards, OpenERP Web will be able to find your object and
142 Standard view behaviors
143 ~~~~~~~~~~~~~~~~~~~~~~~
145 In the normal OpenERP Web flow, views have to implement a number of methods so
146 view managers can correctly communicate with them:
149 This method will always be called after creating the view (via its
150 constructor), but not necessarily immediately.
152 It is called with no arguments and should handle the heavy setup work,
153 including remote call (to load the view's setup data from the server via
154 e.g. ``fields_view_get``, for instance).
156 ``start`` should return a `promise object`_ which *must* be resolved when
157 the view's setup is completed. This promise is used by view managers to
158 know when they can start interacting with the view.
161 Called by the view manager when it wants to replace this view by an other
162 one, but wants to keep this view around to re-activate it later.
164 Should put the view in some sort of hibernation mode, and *must* hide its
168 Called when the view manager wants to re-display the view after having
169 hidden it. The view should refresh its data display upon receiving this
172 ``do_search(domains: Array, contexts: Array, groupbys: Array)``
173 If the view is searchable, this method is called to notify it of a search
176 It should use the provided query data to perform a search and refresh its
177 internal content (and display).
179 All views are searchable by default, but they can be made non-searchable
180 by setting the property ``searchable`` to ``false``.
182 This can be done either on the view class itself (at the same level as
183 defining e.g. the ``start`` method) or at the instance level (in the
184 class's ``init``), though you should generally set it on the class.
192 * All javascript objects inheriting from
193 :js:class:`openerp.base.BasicConroller` will have all methods
194 starting with ``on_`` or ``do_`` bound to their ``this``. This means
195 they don't have to be manually bound (via ``_.bind`` or ``$.proxy``)
196 in order to be useable as bound event handlers (event handlers
197 keeping their object as ``this`` rather than taking whatever
198 ``this`` object they were called with).
200 Beware that this is only valid for methods starting with ``do_`` and
201 ``on_``, any other method will have to be bound manually.
211 OpenERP Web uses unittest2_ for its testing needs. We selected
212 unittest2 rather than unittest_ for the following reasons:
214 * autodiscovery_ (similar to nose, via the ``unit2``
215 CLI utility) and `pluggable test discovery`_.
217 * `new and improved assertions`_ (with improvements in type-specific
218 inequality reportings) including `pluggable custom types equality
221 * neveral new APIs, most notably `assertRaises context manager`_,
222 `cleanup function registration`_, `test skipping`_ and `class- and
223 module-level setup and teardown`_
225 * finally, unittest2 is a backport of Python 3's unittest. We might as
228 To run tests on addons (from the root directory of OpenERP Web) is as
229 simple as typing ``PYTHONPATH=. unit2 discover -s addons`` [#]_. To
230 test an addon which does not live in the ``addons`` directory, simply
231 replace ``addons`` by the directory in which your own addon lives.
233 .. note:: unittest2 is entirely compatible with nose_ (or the
234 other way around). If you want to use nose as your test
235 runner (due to its addons for instance) you can simply install it
236 and run ``nosetests addons`` instead of the ``unit2`` command,
237 the result should be exactly the same.
245 .. js:class:: openerp.base.Widget(view, node)
247 :param openerp.base.Controller view: The view to which the widget belongs
248 :param Object node: the ``fields_view_get`` descriptor for the widget
250 .. js:attribute:: $element
252 The widget's root element as jQuery object
254 .. js:class:: openerp.base.DataSet(session, model)
256 :param openerp.base.Session session: the RPC session object
257 :param String model: the model managed by this dataset
259 The DataSet is the abstraction for a sequence of records stored in
262 It provides interfaces for reading records based on search
263 criteria, and for selecting and fetching records based on
266 .. js:function:: fetch([offset][, limit])
268 :param Number offset: the index from which records should start
269 being returned (section)
270 :param Number limit: the maximum number of records to return
271 :returns: the dataset instance it was called on
273 Asynchronously fetches the records selected by the DataSet's
274 domain and context, in the provided sort order if any.
276 Only fetches the fields selected by the DataSet.
278 On success, triggers :js:func:`on_fetch`
280 .. js:function:: on_fetch(records, event)
282 :param Array records: an array of
283 :js:class:`openerp.base.DataRecord`
284 matching the DataSet's selection
285 :param event: a data holder letting the event handler fetch
286 meta-informations about the event.
287 :type event: OnFetchEvent
289 Fired after :js:func:`fetch` is done fetching the records
290 selected by the DataSet.
292 .. js:function:: active_ids
294 :returns: the dataset instance it was called on
296 Asynchronously fetches the active records for this DataSet.
298 On success, triggers :js:func:`on_active_ids`
300 .. js:function:: on_active_ids(records)
302 :param Array records: an array of
303 :js:class:`openerp.base.DataRecord`
304 matching the currently active ids
306 Fired after :js:func:`active_ids` fetched the records matching
307 the DataSet's active ids.
309 .. js:function:: active_id
311 :returns: the dataset instance in was called on
313 Asynchronously fetches the current active record.
315 On success, triggers :js:func:`on_active_id`
317 .. js:function:: on_active_id(record)
319 :param Object record: the record fetched by
320 :js:func:`active_id`, or ``null``
321 :type record: openerp.base.DataRecord
323 Fired after :js:func:`active_id` fetched the record matching
324 the dataset's active id
326 .. js:function:: set(options)
328 :param Object options: the options to set on the dataset
329 :type options: DataSetOptions
330 :returns: the dataset instance it was called on
332 Configures the data set by setting various properties on it
334 .. js:function:: prev
336 :returns: the dataset instance it was called on
338 Activates the id preceding the current one in the active ids
339 sequence of the dataset.
341 If the current active id is at the start of the sequence,
342 wraps back to the last id of the sequence.
344 .. js:function:: next
346 :returns: the dataset instance it was called on
348 Activates the id following the current one in the active ids
351 If the current active id is the last of the sequence, wraps
352 back to the beginning of the active ids sequence.
354 .. js:function:: select(ids)
356 :param Array ids: the identifiers to activate on the dataset
357 :returns: the dataset instance it was called on
359 Activates all the ids specified in the dataset, resets the
360 current active id to be the first id of the new sequence.
362 The internal order will be the same as the ids list provided.
364 .. js:function:: get_active_ids
366 :returns: the list of current active ids for the dataset
368 .. js:function:: activate(id)
370 :param Number id: the id to activate
371 :returns: the dataset instance it was called on
373 Activates the id provided in the dataset. If no ids are
374 selected, selects the id in the dataset.
376 If ids are already selected and the provided id is not in that
377 selection, raises an error.
379 .. js:function:: get_active_id
381 :returns: the dataset's current active id
383 Ad-hoc objects and structural types
384 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
386 These objects are not associated with any specific class, they're
387 generally literal objects created on the spot. Names are merely
388 convenient ways to refer to them and their properties.
390 .. js:class:: OnFetchEvent
392 .. js:attribute:: context
394 The context used for the :js:func:`fetch` call (domain set on
395 the :js:class:`openerp.base.DataSet` when ``fetch`` was
398 .. js:attribute:: domain
400 The domain used for the :js:func:`fetch` call
402 .. js:attribute:: limit
404 The limit with which the original :js:func:`fetch` call was
407 .. js:attribute:: offset
409 The offset with which the original :js:func:`fetch` call was
412 .. js:attribute:: sort
414 The sorting criteria active on the
415 :js:class:`openerp.base.DataSet` when :js:func:`fetch` was
418 .. js:class:: DataSetOptions
420 .. js:attribute:: context
422 .. js:attribute:: domain
424 .. js:attribute:: sort
429 .. autoclass:: openerpweb.openerpweb.OpenERPSession
432 .. autoclass:: openerpweb.openerpweb.OpenERPModel
435 * Addons lifecycle (loading, execution, events, ...)
440 * Handling static files
441 * Overridding a Python controller (object?)
442 * Overridding a Javascript controller (object?)
443 * Extending templates
444 .. how do you handle deploying static files via e.g. a separate lighttpd?
446 * QWeb templates description?
447 * OpenERP Web modules (from OpenERP modules)
449 .. [#] the ``-s`` parameter tells ``unit2`` to start trying to
450 find tests in the provided directory (here we're testing
451 addons). However a side-effect of that is to set the
452 ``PYTHONPATH`` there as well, so it will fail to find (and
453 import) ``openerpweb``.
455 The ``-t`` parameter lets us set the ``PYTHONPATH``
456 independently, but it doesn't accept multiple values and here
457 we really want to have both ``.`` and ``addons`` on the
460 The solution is to set the ``PYTHONPATH`` to ``.`` on start,
461 and the ``start-directory`` to ``addons``. This results in a
462 correct ``PYTHONPATH`` within ``unit2``.
465 http://docs.python.org/library/unittest.html
468 http://www.voidspace.org.uk/python/articles/unittest2.shtml
471 http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-discovery
473 .. _pluggable test discovery:
474 http://www.voidspace.org.uk/python/articles/unittest2.shtml#load-tests
476 .. _new and improved assertions:
477 http://www.voidspace.org.uk/python/articles/unittest2.shtml#new-assert-methods
479 .. _pluggable custom types equality assertions:
480 http://www.voidspace.org.uk/python/articles/unittest2.shtml#add-new-type-specific-functions
482 .. _assertRaises context manager:
483 http://www.voidspace.org.uk/python/articles/unittest2.shtml#assertraises
485 .. _cleanup function registration:
486 http://www.voidspace.org.uk/python/articles/unittest2.shtml#cleanup-functions-with-addcleanup
489 http://www.voidspace.org.uk/python/articles/unittest2.shtml#test-skipping
491 .. _class- and module-level setup and teardown:
492 http://www.voidspace.org.uk/python/articles/unittest2.shtml#class-and-module-level-fixtures
494 .. _Semantic Versioning:
498 http://somethingaboutorange.com/mrl/projects/nose/1.0.0/
501 http://api.jquery.com/deferred.promise/