Developing OpenERP Web Addons¶
An OpenERP Web addon is simply a Python package with an openerp
descriptor (a __openerp__.py
file) which follows a few structural
and namespacing rules.
Structure¶
<addon name>
+-- __openerp__.py
+-- controllers/
+-- static/
+-- lib/
+-- src/
+-- css/
+-- img/
+-- js/
+-- xml/
+-- test/
+-- test/
__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.
static/lib/
- Third-party libraries used by the addon.
static/src/{css,js,img,xml}
- Location for (respectively) the addon’s static CSS files, its JS files, its various image resources as well as the template files
static/test
- Javascript tests files
test/
- The directories in which all tests for the addon are located.
Some of these are guidelines (and not enforced by code), but it’s suggested that these be followed. Code which does not fit into these categories can go wherever deemed suitable.
Namespacing¶
Python¶
Because addons are also Python packages, they’re inherently namespaced and nothing special needs to be done on that front.
JavaScript¶
The JavaScript side of an addon has to live in the namespace
openerp.$addon_name
. For instance, everything created by the addon
base
lives in openerp.base
.
The root namespace of the addon is a function which takes a single
parameter openerp
, which is an OpenERP client instance. Objects
(as well as functions, registry instances, etc...) should be added on
the correct namespace on that object.
The root function will be called by the OpenERP Web client when initializing the addon.
// root namespace of the openerp.example addon
/** @namespace */
openerp.example = function (openerp) {
// basic initialization code (e.g. templates loading)
openerp.example.SomeClass = openerp.base.Class.extend(
/** @lends openerp.example.SomeClass# */{
/**
* Description for SomeClass's constructor here
*
* @constructs
*/
init: function () {
// SomeClass initialization code
}
// rest of SomeClass
});
// access an object in an other addon namespace to replace it
openerp.base.SearchView = openerp.base.SearchView.extend({
init: function () {
this._super.apply(this, arguments);
console.log('Search view initialized');
}
});
}
Creating new standard roles¶
Widget¶
This is the base class for all visual components. It provides a number of services for the management of a DOM subtree:
Rendering with QWeb
Parenting-child relations
Life-cycle management (including facilitating children destruction when a parent object is removed)
DOM insertion, via jQuery-powered insertion methods. Insertion targets can be anything the corresponding jQuery method accepts (generally selectors, DOM nodes and jQuery objects):
appendTo()
Renders the widget and inserts it as the last child of the target, uses .appendTo()
prependTo()
Renders the widget and inserts it as the first child of the target, uses .prependTo()
insertAfter()
Renders the widget and inserts it as the preceding sibling of the target, uses .insertAfter()
insertBefore()
Renders the widget and inserts it as the following sibling of the target, uses .insertBefore()
Widget()
inherits from
SessionAware()
, so subclasses can easily access the
RPC layers.
Subclassing Widget¶
Widget()
is subclassed in the standard manner (via the
extend()
method), and provides a number of
abstract properties and concrete methods (which you may or may not want to
override). Creating a subclass looks like this:
var MyWidget = openerp.base.Widget.extend({
// QWeb template to use when rendering the object
template: "MyQWebTemplate",
init: function(parent) {
this._super(parent);
// insert code to execute before rendering, for object
// initialization
},
start: function() {
this._super();
// post-rendering initialization code, at this point
// ``this.$element`` has been initialized
this.$element.find(".my_button").click(/* an example of event binding * /);
// if ``start`` is asynchronous, return a promise object so callers
// know when the object is done initializing
return this.rpc(/* … */)
}
});
The new class can then be used in the following manner:
// Create the instance
var my_widget = new MyWidget(this);
// Render and insert into DOM
my_widget.appendTo(".some-div");
After these two lines have executed (and any promise returned by appendTo
has been resolved if needed), the widget is ready to be used.
Note
the insertion methods will start the widget themselves, and will
return the result of start()
.
If for some reason you do not want to call these methods, you will
have to first call render()
on the
widget, then insert it into your DOM and start it.
If the widget is not needed anymore (because it’s transient), simply terminate it:
my_widget.stop();
will unbind all DOM events, remove the widget’s content from the DOM and destroy all widget data.
Views¶
Views are the standard high-level component in OpenERP. A view type corresponds to a way to display a set of data (coming from an OpenERP model).
In OpenERP Web, views are standard objects registered against a dedicated
object registry, so the ViewManager()
knows where to
find and how to call them.
Although not mandatory, it is recommended that views inherit from
openerp.base.View()
, which provides a view useful services to its
children.
Registering a view¶
This is the first task to perform when creating a view, and the simplest by
far: simply call openerp.base.views.add(name, object_path)
to register
the object of path object_path
as the view for the view name name
.
The view name is the name you gave to your new view in the OpenERP server.
From that point onwards, OpenERP Web will be able to find your object and instantiate it.
Standard view behaviors¶
In the normal OpenERP Web flow, views have to implement a number of methods so view managers can correctly communicate with them:
start()
This method will always be called after creating the view (via its constructor), but not necessarily immediately.
It is called with no arguments and should handle the heavy setup work, including remote call (to load the view’s setup data from the server via e.g.
fields_view_get
, for instance).start
should return a promise object which must be resolved when the view’s setup is completed. This promise is used by view managers to know when they can start interacting with the view.do_hide()
Called by the view manager when it wants to replace this view by an other one, but wants to keep this view around to re-activate it later.
Should put the view in some sort of hibernation mode, and must hide its DOM elements.
do_show()
- Called when the view manager wants to re-display the view after having hidden it. The view should refresh its data display upon receiving this notification
do_search(domain: Array, context: Object, group_by: Array)
If the view is searchable, this method is called to notify it of a search against it.
It should use the provided query data to perform a search and refresh its internal content (and display).
All views are searchable by default, but they can be made non-searchable by setting the property
searchable
tofalse
.This can be done either on the view class itself (at the same level as defining e.g. the
start
method) or at the instance level (in the class’sinit
), though you should generally set it on the class.
Frequent development tasks¶
There are a number of tasks which OpenERP Web developers do or will need to perform quite regularly. To make these easier, we have written a few guides to help you get started:
Translations¶
OpenERP Web should provide most of the tools needed to correctly translate your addons via the tool of your choice (OpenERP itself uses Launchpad’s own translation tool.
Making strings translatable¶
QWeb¶
QWeb automatically marks all text nodes (any text which is not in an XML attribute and not part of an XML tag) as translatable, and handles the replacement for you. There is nothing special to do to mark template text as translatable
JavaScript¶
OpenERP Web provides two functions to translate human-readable strings in javascript code. These functions should be “imported” in your module by aliasing them to their bare name:
var _t = openerp.web._t,
_tl = openerp.web._tl;
importing those functions under any other name is not guaranteed to work.
Note
only import them if necessary, and only the necessary one(s), no need to clutter your module’s namespace for nothing
-
openerp.web.
_t
(s)¶ Base translation function, eager, works much like gettext(3)
Return type: String
-
openerp.web.
_lt
(s)¶ Lazy equivalent to
_t()
, this function will postpone fetching the translation to its argument until the last possible moment.To use in contexts evaluated before the translation database can be fetched, usually your module’s toplevel and the attributes of classes defined in it (class attributes, not instance attributes set in the constructor).
Return type: LazyString
Text formatting & translations¶
A difficulty when translating is integrating data (from the code) into the translated string. In OpenERP Web addons, this should be done by wrapping the text to translate in an sprintf(3) call. For OpenERP Web, sprintf(3) is provided by underscore.string.
As much as possible, you should use the “named argument” form of sprintf:
var translated_string = _.str.sprintf(
_t("[%(first_record)d to %(last_record)d] of %(records_count)d"), {
first_record: first + 1,
last_record: last,
records_count: total
}));
named arguments make the string to translate much clearer for translators, and allows them to “move” sections around based on the requirements of their language (not all language order text like english).
Named arguments are specified using the following pattern: %($name)$type
where
$name
- the name of the argument, this is the key in the object/dictionary provided
as second parameter to
sprintf
$type
- a type/format specifier, see the list for all possible types.
Note
positional arguments are acceptable if the translated string has a single argument and its content is easy to guess from the text around it. Named arguments should still be preferred.
Warning
you should never use string concatenation as it robs the translator of context and make result in a completely incorrect translation
Extracting strings¶
Once strings have been marked for translation, they need to be extracted into POT files, from which most translation tools can build a database.
This can be done via the provided gen_translations.sh.
It can be called either as gen_translations.sh -a
or by providing
two parameters, a path to the addons and the complete path in which to put the
extracted POT file.
Utility behaviors¶
JavaScript¶
All javascript objects inheriting from
openerp.base.BasicConroller()
will have all methods starting withon_
ordo_
bound to theirthis
. This means they don’t have to be manually bound (via_.bind
or$.proxy
) in order to be useable as bound event handlers (event handlers keeping their object asthis
rather than taking whateverthis
object they were called with).Beware that this is only valid for methods starting with
do_
andon_
, any other method will have to be bound manually.
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
[1]. 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.
Python¶
-
class
web.common.session.
OpenERPSession
¶ An OpenERP RPC session, a given user can own multiple such sessions in a web session.
-
context
¶ The session context, a
dict
. Can be reloaded by callingopenerpweb.openerpweb.OpenERPSession.get_context()
-
domains_store
¶ A
dict
matching domain keys to evaluable (but non-literal) domains.Used to store references to non-literal domains which need to be round-tripped to the client browser.
-
assert_valid
(force=False)¶ Ensures this session is valid (logged into the openerp server)
-
base_eval_context
¶ Default evaluation context for the session.
Used to evaluate contexts and domains.
-
eval_context
(context_to_eval, context=None)¶ Evaluates the provided context_to_eval in the context (haha) of the context. Also merges the evaluated context with the session’s context.
Parameters: context_to_eval (openerpweb.nonliterals.Context) – a context to evaluate. Must be a dict or a non-literal context. If it’s a dict, will be returned as-is Returns: the evaluated context Return type: dict Raises: TypeError
ifcontext_to_eval
is neither a dict nor a Context
-
eval_domain
(domain, context=None)¶ Evaluates the provided domain using the provided context (merged with the session’s evaluation context)
Parameters: - domain (openerpweb.nonliterals.Domain) –
an OpenERP domain as a list or as a
openerpweb.nonliterals.Domain
instanceIn the second case, it will be evaluated and returned.
- context (dict) – the context to use in the evaluation, if any.
Returns: the evaluated domain
Return type: list
Raises: TypeError
ifdomain
is neither a list nor a Domain- domain (openerpweb.nonliterals.Domain) –
-
evaluation_context
(context=None)¶ Returns the session’s evaluation context, augmented with the provided context if any.
Parameters: context (dict) – to add merge in the session’s base eval context Returns: the augmented context Return type: dict
-
get_context
()¶ Re-initializes the current user’s session context (based on his preferences) by calling res.users.get_context() with the old context
Returns: the new context
-
model
(model)¶ Get an RPC proxy for the object
model
, bound to this session.Parameters: model (str) – an OpenERP model name Return type: a model object
-
-
class
web.common.openerplib.main.
Model
(connection, model_name)¶ Useful class to dialog with one of the models provided by an OpenERP server. An instance of this class depends on a Connection instance with valid authentication information.
-
search_read
(domain=None, fields=None, offset=0, limit=None, order=None, context=None)¶ A shortcut method to combine a search() and a read().
Parameters: - domain – The domain for the search.
- fields – The fields to extract (can be None or [] to extract all fields).
- offset – The offset for the rows to read.
- limit – The maximum number of rows to read.
- order – The order to class the rows.
- context – The context.
Returns: A list of dictionaries containing all the specified fields.
-
- Addons lifecycle (loading, execution, events, ...)
- Python-side
- JS-side
- Handling static files
- Overridding a Python controller (object?)
- Overridding a Javascript controller (object?)
- Extending templates .. how do you handle deploying static files via e.g. a separate lighttpd?
- Python public APIs
- QWeb templates description?
- OpenERP Web modules (from OpenERP modules)
[1] | the The The solution is to set the |