[MERGE] from trunk
bzr revid: chm@openerp.com-20121207081026-3edlt0698csc6gk8
This commit is contained in:
@@ -1121,41 +1121,6 @@ class DataSet(openerpweb.Controller):
|
||||
class View(openerpweb.Controller):
|
||||
_cp_path = "/web/view"
|
||||
|
||||
def fields_view_get(self, req, model, view_id, view_type,
|
||||
transform=True, toolbar=False, submenu=False):
|
||||
Model = req.session.model(model)
|
||||
fvg = Model.fields_view_get(view_id, view_type, req.context, toolbar, submenu)
|
||||
# todo fme?: check that we should pass the evaluated context here
|
||||
self.process_view(req.session, fvg, req.context, transform, (view_type == 'kanban'))
|
||||
return fvg
|
||||
|
||||
def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
|
||||
# depending on how it feels, xmlrpclib.ServerProxy can translate
|
||||
# XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
|
||||
# enjoy unicode strings which can not be trivially converted to
|
||||
# strings, and it blows up during parsing.
|
||||
|
||||
# So ensure we fix this retardation by converting view xml back to
|
||||
# bit strings.
|
||||
if isinstance(fvg['arch'], unicode):
|
||||
arch = fvg['arch'].encode('utf-8')
|
||||
else:
|
||||
arch = fvg['arch']
|
||||
fvg['arch_string'] = arch
|
||||
|
||||
fvg['arch'] = xml2json_from_elementtree(
|
||||
ElementTree.fromstring(arch), preserve_whitespaces)
|
||||
|
||||
if 'id' in fvg['fields']:
|
||||
# Special case for id's
|
||||
id_field = fvg['fields']['id']
|
||||
id_field['original_type'] = id_field['type']
|
||||
id_field['type'] = 'id'
|
||||
|
||||
for field in fvg['fields'].itervalues():
|
||||
for view in field.get("views", {}).itervalues():
|
||||
self.process_view(session, view, None, transform)
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def add_custom(self, req, view_id, arch):
|
||||
CustomView = req.session.model('ir.ui.view.custom')
|
||||
@@ -1179,10 +1144,6 @@ class View(openerpweb.Controller):
|
||||
return {'result': True}
|
||||
return {'result': False}
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def load(self, req, model, view_id, view_type, toolbar=False):
|
||||
return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
|
||||
|
||||
class TreeView(View):
|
||||
_cp_path = "/web/treeview"
|
||||
|
||||
|
||||
@@ -32,11 +32,15 @@ information:
|
||||
|
||||
The major difference is in the lifecycle of these:
|
||||
|
||||
* if the client action maps to a function, the function will simply be
|
||||
called when executing the action. The function can have no further
|
||||
* if the client action maps to a function, the function will be called
|
||||
when executing the action. The function can have no further
|
||||
interaction with the Web Client itself, although it can return an
|
||||
action which will be executed after it.
|
||||
|
||||
The function takes 2 parameters: the ActionManager calling it and
|
||||
the descriptor for the current action (the ``ir.actions.client``
|
||||
dictionary).
|
||||
|
||||
* if, on the other hand, the client action maps to a
|
||||
:js:class:`~openerp.web.Widget`, that
|
||||
:js:class:`~openerp.web.Widget` will be instantiated and added to
|
||||
@@ -51,7 +55,7 @@ object::
|
||||
// Registers the object 'openerp.web_dashboard.Widget' to the client
|
||||
// action tag 'board.home.widgets'
|
||||
instance.web.client_actions.add(
|
||||
'board.home.widgets', 'openerp.web_dashboard.Widget');
|
||||
'board.home.widgets', 'instance.web_dashboard.Widget');
|
||||
instance.web_dashboard.Widget = instance.web.Widget.extend({
|
||||
template: 'HomeWidget'
|
||||
});
|
||||
@@ -60,15 +64,15 @@ At this point, the generic :js:class:`~openerp.web.Widget` lifecycle
|
||||
takes over, the template is rendered, inserted in the client DOM,
|
||||
bound on the object's ``$el`` property and the object is started.
|
||||
|
||||
If the client action takes parameters, these parameters are passed in as a
|
||||
second positional parameter to the constructor::
|
||||
The second parameter to the constructor is the descriptor for the
|
||||
action itself, which contains any parameter provided::
|
||||
|
||||
init: function (parent, params) {
|
||||
init: function (parent, action) {
|
||||
// execute the Widget's init
|
||||
this._super(parent);
|
||||
// board.home.widgets only takes a single param, the identifier of the
|
||||
// res.widget object it should display. Store it for later
|
||||
this.widget_id = params.widget_id;
|
||||
this.widget_id = action.params.widget_id;
|
||||
}
|
||||
|
||||
More complex initialization (DOM manipulations, RPC requests, ...)
|
||||
@@ -82,9 +86,6 @@ method.
|
||||
code it should return a ``$.Deferred`` so callers know when it's
|
||||
ready for interaction.
|
||||
|
||||
Although generally speaking client actions are not really
|
||||
interacted with.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
start: function () {
|
||||
@@ -93,7 +94,7 @@ method.
|
||||
// Simply read the res.widget object this action should display
|
||||
new instance.web.Model('res.widget').call(
|
||||
'read', [[this.widget_id], ['title']])
|
||||
.then(this.proxy('on_widget_loaded'));
|
||||
.then(this.proxy('on_widget_loaded'));
|
||||
}
|
||||
|
||||
The client action can then behave exactly as it wishes to within its
|
||||
|
||||
@@ -11,14 +11,14 @@ Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
presentation
|
||||
async
|
||||
|
||||
module
|
||||
testing
|
||||
|
||||
widget
|
||||
qweb
|
||||
|
||||
async
|
||||
rpc
|
||||
qweb
|
||||
client_action
|
||||
form_view
|
||||
search_view
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
Building an OpenERP Web module
|
||||
==============================
|
||||
|
||||
There is no significant distinction between an OpenERP Web module and
|
||||
an OpenERP module, the web part is mostly additional data and code
|
||||
inside a regular OpenERP module. This allows providing more seamless
|
||||
features by integrating your module deeper into the web client.
|
||||
|
||||
A Basic Module
|
||||
--------------
|
||||
|
||||
A very basic OpenERP module structure will be our starting point:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
web_example
|
||||
├── __init__.py
|
||||
└── __openerp__.py
|
||||
|
||||
.. literalinclude:: module/__openerp__.py
|
||||
:language: python
|
||||
|
||||
This is a sufficient minimal declaration of a valid OpenERP module.
|
||||
|
||||
Web Declaration
|
||||
---------------
|
||||
|
||||
There is no such thing as a "web module" declaration. An OpenERP
|
||||
module is automatically recognized as "web-enabled" if it contains a
|
||||
``static`` directory at its root, so:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
web_example
|
||||
├── __init__.py
|
||||
├── __openerp__.py
|
||||
└── static
|
||||
|
||||
is the extent of it. You should also change the dependency to list
|
||||
``web``:
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.1.diff
|
||||
:language: diff
|
||||
|
||||
.. note::
|
||||
|
||||
This does not matter in normal operation so you may not realize
|
||||
it's wrong (the web module does the loading of everything else, so
|
||||
it can only be loaded), but when e.g. testing the loading process
|
||||
is slightly different than normal, and incorrect dependency may
|
||||
lead to broken code.
|
||||
|
||||
This makes the "web" discovery system consider the module as having a
|
||||
"web part", and check if it has web controllers to mount or javascript
|
||||
files to load. The content of the ``static/`` folder is also
|
||||
automatically made available to web browser at the URL
|
||||
``$module-name/static/$file-path``. This is sufficient to provide
|
||||
pictures (of cats, usually) through your module. However there are
|
||||
still a few more steps to running javascript code.
|
||||
|
||||
Getting Things Done
|
||||
-------------------
|
||||
|
||||
The first one is to add javascript code. It's customary to put it in
|
||||
``static/src/js``, to have room for e.g. other file types, or
|
||||
third-party libraries.
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js
|
||||
:language: javascript
|
||||
|
||||
The client won't load any file unless specified, thus the new file
|
||||
should be listed in the module's manifest file, under a new key ``js``
|
||||
(a list of file names, or glob patterns):
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.2.diff
|
||||
:language: diff
|
||||
|
||||
At this point, if the module is installed and the client reloaded the
|
||||
message should appear in your browser's development console.
|
||||
|
||||
.. note::
|
||||
|
||||
Because the manifest file has been edited, you will have to
|
||||
restart the OpenERP server itself for it to be taken in account.
|
||||
|
||||
You may also want to open your browser's console *before*
|
||||
reloading, depending on the browser messages printed while the
|
||||
console is closed may not work or may not appear after opening it.
|
||||
|
||||
.. note::
|
||||
|
||||
If the message does not appear, try cleaning your browser's caches
|
||||
and ensure the file is correctly loaded from the server logs or
|
||||
the "resources" tab of your browser's developers tools.
|
||||
|
||||
At this point the code runs, but it runs only once when the module is
|
||||
initialized, and it can't get access to the various APIs of the web
|
||||
client (such as making RPC requests to the server). This is done by
|
||||
providing a `javascript module`_:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.1.diff
|
||||
:language: diff
|
||||
|
||||
If you reload the client, you'll see a message in the console exactly
|
||||
as previously. The differences, though invisible at this point, are:
|
||||
|
||||
* All javascript files specified in the manifest (only this one so
|
||||
far) have been fully loaded
|
||||
* An instance of the web client and a namespace inside that instance
|
||||
(with the same name as the module) have been created and are
|
||||
available for use
|
||||
|
||||
The latter point is what the ``instance`` parameter to the function
|
||||
provides: an instance of the OpenERP Web client, with the contents of
|
||||
all the new module's dependencies loaded in and initialized. These are
|
||||
the entry points to the web client's APIs.
|
||||
|
||||
To demonstrate, let's build a simple :doc:`client action
|
||||
<client_action>`: a stopwatch
|
||||
|
||||
First, the action declaration:
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.3.diff
|
||||
:language: diff
|
||||
|
||||
.. literalinclude:: module/web_example.xml
|
||||
:language: xml
|
||||
|
||||
then set up the :doc:`client action hook <client_action>` to register
|
||||
a function (for now):
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.2.diff
|
||||
:language: diff
|
||||
|
||||
Updating the module (in order to load the XML description) and
|
||||
re-starting the server should display a new menu *Example Client
|
||||
Action* at the top-level. Opening said menu will make the message
|
||||
appear, as usual, in the browser's console.
|
||||
|
||||
Paint it black
|
||||
--------------
|
||||
|
||||
The next step is to take control of the page itself, rather than just
|
||||
print little messages in the console. This we can do by replacing our
|
||||
client action function by a :doc:`widget`. Our widget will simply use
|
||||
its :js:func:`~openerp.web.Widget.start` to add some content to its
|
||||
DOM:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.3.diff
|
||||
:language: diff
|
||||
|
||||
after reloading the client (to update the javascript file), instead of
|
||||
printing to the console the menu item clears the whole screen and
|
||||
displays the specified message in the page.
|
||||
|
||||
Since we've added a class on the widget's :ref:`DOM root
|
||||
<widget-dom_root>` we can now see how to add a stylesheet to a module:
|
||||
first create the stylesheet file:
|
||||
|
||||
.. literalinclude:: module/static/src/css/web_example.css
|
||||
:language: css
|
||||
|
||||
then add a reference to the stylesheet in the module's manifest (which
|
||||
will require restarting the OpenERP Server to see the changes, as
|
||||
usual):
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.4.diff
|
||||
:language: diff
|
||||
|
||||
the text displayed by the menu item should now be huge, and
|
||||
white-on-black (instead of small and black-on-white). From there on,
|
||||
the world's your canvas.
|
||||
|
||||
.. note::
|
||||
|
||||
Prefixing CSS rules with both ``.openerp`` (to ensure the rule
|
||||
will apply only within the confines of the OpenERP Web client) and
|
||||
a class at the root of your own hierarchy of widgets is strongly
|
||||
recommended to avoid "leaking" styles in case the code is running
|
||||
embedded in an other web page, and does not have the whole screen
|
||||
to itself.
|
||||
|
||||
So far we haven't built much (any, really) DOM content. It could all
|
||||
be done in :js:func:`~openerp.web.Widget.start` but that gets unwieldy
|
||||
and hard to maintain fast. It is also very difficult to extend by
|
||||
third parties (trying to add or change things in your widgets) unless
|
||||
broken up into multiple methods which each perform a little bit of the
|
||||
rendering.
|
||||
|
||||
The first way to handle this method is to delegate the content to
|
||||
plenty of sub-widgets, which can be individually overridden. An other
|
||||
method [#DOM-building]_ is to use `a template
|
||||
<http://en.wikipedia.org/wiki/Web_template>`_ to render a widget's
|
||||
DOM.
|
||||
|
||||
OpenERP Web's template language is :doc:`qweb`. Although any
|
||||
templating engine can be used (e.g. `mustache
|
||||
<http://mustache.github.com/>`_ or `_.template
|
||||
<http://underscorejs.org/#template>`_) QWeb has important features
|
||||
which other template engines may not provide, and has special
|
||||
integration to OpenERP Web widgets.
|
||||
|
||||
Adding a template file is similar to adding a style sheet:
|
||||
|
||||
.. literalinclude:: module/static/src/xml/web_example.xml
|
||||
:language: xml
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.5.diff
|
||||
:language: diff
|
||||
|
||||
The template can then easily be hooked in the widget:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.4.diff
|
||||
:language: diff
|
||||
|
||||
And finally the CSS can be altered to style the new (and more complex)
|
||||
template-generated DOM, rather than the code-generated one:
|
||||
|
||||
.. literalinclude:: module/static/src/css/web_example.css.1.diff
|
||||
:language: diff
|
||||
|
||||
.. note::
|
||||
|
||||
The last section of the CSS change is an example of "state
|
||||
classes": a CSS class (or set of classes) on the root of the
|
||||
widget, which is toggled when the state of the widget changes and
|
||||
can perform drastic alterations in rendering (usually
|
||||
showing/hiding various elements).
|
||||
|
||||
This pattern is both fairly simple (to read and understand) and
|
||||
efficient (because most of the hard work is pushed to the
|
||||
browser's CSS engine, which is usually highly optimized, and done
|
||||
in a single repaint after toggling the class).
|
||||
|
||||
The last step (until the next one) is to add some behavior and make
|
||||
our stopwatch watch. First hook some events on the buttons to toggle
|
||||
the widget's state:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.5.diff
|
||||
:language: diff
|
||||
|
||||
This demonstrates the use of the "events hash" and event delegation to
|
||||
declaratively handle events on the widget's DOM. And already changes
|
||||
the button displayed in the UI. Then comes some actual logic:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.6.diff
|
||||
:language: diff
|
||||
|
||||
* An initializer (the ``init`` method) is introduced to set-up a few
|
||||
internal variables: ``_start`` will hold the start of the timer (as
|
||||
a javascript Date object), and ``_watch`` will hold a ticker to
|
||||
update the interface regularly and display the "current time".
|
||||
|
||||
* ``update_counter`` is in charge of taking the time difference
|
||||
between "now" and ``_start``, formatting as ``HH:MM:SS`` and
|
||||
displaying the result on screen.
|
||||
|
||||
* ``watch_start`` is augmented to initialize ``_start`` with its value
|
||||
and set-up the update of the counter display every 33ms.
|
||||
|
||||
* ``watch_stop`` disables the updater, does a final update of the
|
||||
counter display and resets everything.
|
||||
|
||||
* Finally, because javascript Interval and Timeout objects execute
|
||||
"outside" the widget, they will keep going even after the widget has
|
||||
been destroyed (especially an issue with intervals as they repeat
|
||||
indefinitely). So ``_watch`` *must* be cleared when the widget is
|
||||
destroyed (then the ``_super`` must be called as well in order to
|
||||
perform the "normal" widget cleanup).
|
||||
|
||||
Starting and stopping the watch now works, and correctly tracks time
|
||||
since having started the watch, neatly formatted.
|
||||
|
||||
.. [#DOM-building] they are not alternative solutions: they work very
|
||||
well together. Templates are used to build "just
|
||||
DOM", sub-widgets are used to build DOM subsections
|
||||
*and* delegate part of the behavior (e.g. events
|
||||
handling).
|
||||
|
||||
.. _javascript module:
|
||||
http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript
|
||||
@@ -0,0 +1,7 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['base'],
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,7 +1,7 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
- 'depends': ['base'],
|
||||
+ 'depends': ['web'],
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,7 +1,8 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
+ 'js': ['static/src/js/first_module.js'],
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,8 +1,9 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
+ 'data': ['web_example.xml'],
|
||||
'js': ['static/src/js/first_module.js'],
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,9 +1,10 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'data': ['web_example.xml'],
|
||||
'js': ['static/src/js/first_module.js'],
|
||||
+ 'css': ['static/src/css/web_example.css'],
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,10 +1,11 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'data': ['web_example.xml'],
|
||||
'js': ['static/src/js/first_module.js'],
|
||||
'css': ['static/src/css/web_example.css'],
|
||||
+ 'qweb': ['static/src/xml/web_example.xml'],
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.openerp .oe_web_example {
|
||||
color: white;
|
||||
background-color: black;
|
||||
height: 100%;
|
||||
font-size: 400%;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
--- web_example/static/src/css/web_example.css
|
||||
+++ web_example/static/src/css/web_example.css
|
||||
@@ -1,6 +1,13 @@
|
||||
.openerp .oe_web_example {
|
||||
color: white;
|
||||
background-color: black;
|
||||
height: 100%;
|
||||
- font-size: 400%;
|
||||
}
|
||||
+.openerp .oe_web_example h4 {
|
||||
+ margin: 0;
|
||||
+ font-size: 200%;
|
||||
+}
|
||||
+.openerp .oe_web_example.oe_web_example_started .oe_web_example_start button,
|
||||
+.openerp .oe_web_example.oe_web_example_stopped .oe_web_example_stop button {
|
||||
+ display: none
|
||||
+}
|
||||
@@ -0,0 +1,2 @@
|
||||
// static/src/js/first_module.js
|
||||
console.log("Debug statement: file loaded");
|
||||
@@ -0,0 +1,8 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,2 +1,4 @@
|
||||
// static/src/js/first_module.js
|
||||
-console.log("Debug statement: file loaded");
|
||||
+openerp.web_example = function (instance) {
|
||||
+ console.log("Module loaded");
|
||||
+};
|
||||
@@ -0,0 +1,11 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,4 +1,7 @@
|
||||
// static/src/js/first_module.js
|
||||
openerp.web_example = function (instance) {
|
||||
- console.log("Module loaded");
|
||||
+ instance.web.client_actions.add('example.action', 'instance.web_example.action');
|
||||
+ instance.web_example.action = function (parent, action) {
|
||||
+ console.log("Executed the action", action);
|
||||
+ };
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,7 +1,11 @@
|
||||
// static/src/js/first_module.js
|
||||
openerp.web_example = function (instance) {
|
||||
- instance.web.client_actions.add('example.action', 'instance.web_example.action');
|
||||
- instance.web_example.action = function (parent, action) {
|
||||
- console.log("Executed the action", action);
|
||||
- };
|
||||
+ instance.web.client_actions.add('example.action', 'instance.web_example.Action');
|
||||
+ instance.web_example.Action = instance.web.Widget.extend({
|
||||
+ className: 'oe_web_example',
|
||||
+ start: function () {
|
||||
+ this.$el.text("Hello, world!");
|
||||
+ return this._super();
|
||||
+ }
|
||||
+ });
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,11 +1,7 @@
|
||||
// static/src/js/first_module.js
|
||||
openerp.web_example = function (instance) {
|
||||
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
|
||||
instance.web_example.Action = instance.web.Widget.extend({
|
||||
+ template: 'web_example.action'
|
||||
- className: 'oe_web_example',
|
||||
- start: function () {
|
||||
- this.$el.text("Hello, world!");
|
||||
- return this._super();
|
||||
- }
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,7 +1,19 @@
|
||||
// static/src/js/first_module.js
|
||||
openerp.web_example = function (instance) {
|
||||
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
|
||||
instance.web_example.Action = instance.web.Widget.extend({
|
||||
- template: 'web_example.action'
|
||||
+ template: 'web_example.action',
|
||||
+ events: {
|
||||
+ 'click .oe_web_example_start button': 'watch_start',
|
||||
+ 'click .oe_web_example_stop button': 'watch_stop'
|
||||
+ },
|
||||
+ watch_start: function () {
|
||||
+ this.$el.addClass('oe_web_example_started')
|
||||
+ .removeClass('oe_web_example_stopped');
|
||||
+ },
|
||||
+ watch_stop: function () {
|
||||
+ this.$el.removeClass('oe_web_example_started')
|
||||
+ .addClass('oe_web_example_stopped');
|
||||
+ },
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,19 +1,52 @@
|
||||
// static/src/js/first_module.js
|
||||
openerp.web_example = function (instance) {
|
||||
instance.web.client_actions.add('example.action', 'instance.web_example.Action');
|
||||
instance.web_example.Action = instance.web.Widget.extend({
|
||||
template: 'web_example.action',
|
||||
events: {
|
||||
'click .oe_web_example_start button': 'watch_start',
|
||||
'click .oe_web_example_stop button': 'watch_stop'
|
||||
},
|
||||
+ init: function () {
|
||||
+ this._super.apply(this, arguments);
|
||||
+ this._start = null;
|
||||
+ this._watch = null;
|
||||
+ },
|
||||
+ update_counter: function () {
|
||||
+ var h, m, s;
|
||||
+ // Subtracting javascript dates returns the difference in milliseconds
|
||||
+ var diff = new Date() - this._start;
|
||||
+ s = diff / 1000;
|
||||
+ m = Math.floor(s / 60);
|
||||
+ s -= 60*m;
|
||||
+ h = Math.floor(m / 60);
|
||||
+ m -= 60*h;
|
||||
+ this.$('.oe_web_example_timer').text(
|
||||
+ _.str.sprintf("%02d:%02d:%02d", h, m, s));
|
||||
+ },
|
||||
watch_start: function () {
|
||||
this.$el.addClass('oe_web_example_started')
|
||||
.removeClass('oe_web_example_stopped');
|
||||
+ this._start = new Date();
|
||||
+ // Update the UI to the current time
|
||||
+ this.update_counter();
|
||||
+ // Update the counter at 30 FPS (33ms/frame)
|
||||
+ this._watch = setInterval(
|
||||
+ this.proxy('update_counter'),
|
||||
+ 33);
|
||||
},
|
||||
watch_stop: function () {
|
||||
+ clearInterval(this._watch);
|
||||
+ this.update_counter();
|
||||
+ this._start = this._watch = null;
|
||||
this.$el.removeClass('oe_web_example_started')
|
||||
.addClass('oe_web_example_stopped');
|
||||
},
|
||||
+ destroy: function () {
|
||||
+ if (this._watch) {
|
||||
+ clearInterval(this._watch);
|
||||
+ }
|
||||
+ this._super();
|
||||
+ }
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
<templates>
|
||||
<div t-name="web_example.action" class="oe_web_example oe_web_example_stopped">
|
||||
<h4 class="oe_web_example_timer">00:00:00</h4>
|
||||
<p class="oe_web_example_start">
|
||||
<button type="button">Start</button>
|
||||
</p>
|
||||
<p class="oe_web_example_stop">
|
||||
<button type="button">Stop</button>
|
||||
</p>
|
||||
</div>
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
<!-- web_example/web_example.xml -->
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.actions.client" id="action_client_example">
|
||||
<field name="name">Example Client Action</field>
|
||||
<field name="tag">example.action</field>
|
||||
</record>
|
||||
<menuitem action="action_client_example"
|
||||
id="menu_client_example"/>
|
||||
</data>
|
||||
</openerp>
|
||||
+523
-61
@@ -1,79 +1,541 @@
|
||||
|
||||
QWeb
|
||||
====
|
||||
|
||||
QWeb is the template engine used by the OpenERP Web Client. It is a home made engine create by OpenERP developers. There are a few things to note about it:
|
||||
QWeb is the template engine used by the OpenERP Web Client. It is an
|
||||
XML-based templating language, similar to `Genshi
|
||||
<http://en.wikipedia.org/wiki/Genshi_(templating_language)>`_,
|
||||
`Thymeleaf <http://en.wikipedia.org/wiki/Thymeleaf>`_ or `Facelets
|
||||
<http://en.wikipedia.org/wiki/Facelets>`_ with a few peculiarities:
|
||||
|
||||
* Template are rendered in javascript on the client-side, the server does nothing.
|
||||
* It is an xml template engine, like Facelets_ for example. The source file must be a valid xml.
|
||||
* Templates are not interpreted. There are compiled to javascript. This makes them a lot faster to render, but sometimes harder to debug.
|
||||
* Most of the time it is used through the Widget class, but you can also use it directly using *openerp.web.qweb.render()* .
|
||||
* It's implemented fully in javascript and rendered in the browser.
|
||||
* Each template file (XML files) contains multiple templates, where
|
||||
template engine usually have a 1:1 mapping between template files
|
||||
and templates.
|
||||
* It has special support in OpenERP Web's
|
||||
:class:`~instance.web.Widget`, though it can be used outside of
|
||||
OpenERP Web (and it's possible to use :class:`~instance.web.Widget`
|
||||
without relying on the QWeb integration).
|
||||
|
||||
.. _Facelets: http://en.wikipedia.org/wiki/Facelets
|
||||
Here's an example demonstrating most of the basic QWeb features:
|
||||
|
||||
Here is a typical QWeb file:
|
||||
.. code-block:: xml
|
||||
|
||||
::
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="Template1">
|
||||
<div>...</div>
|
||||
</t>
|
||||
<t t-name="Template2">
|
||||
<div>...</div>
|
||||
</t>
|
||||
<div t-name="example_template" t-attf-class="base #{cls}">
|
||||
<h4 t-if="title"><t t-esc="title"/></h4>
|
||||
<ul>
|
||||
<li t-foreach="items" t-as="item" t-att-class="item_parity">
|
||||
<t t-call="example_template.sub">
|
||||
<t t-set="arg" t-value="item_value"/>
|
||||
</t>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-name="example_template.sub">
|
||||
<t t-esc="arg.name"/>
|
||||
<dl>
|
||||
<t t-foreach="arg.tags" t-as="tag" t-if="tag_index lt 5">
|
||||
<dt><t t-esc="tag"/></dt>
|
||||
<dd><t t-esc="tag_value"/></dd>
|
||||
</t>
|
||||
</dl>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
A QWeb file contains multiple templates, they are simply identified by a name.
|
||||
rendered with the following context:
|
||||
|
||||
Here is a sample QWeb template:
|
||||
.. code-block:: json
|
||||
|
||||
::
|
||||
{
|
||||
"class1": "foo",
|
||||
"title": "Random Title",
|
||||
"items": [
|
||||
{ "name": "foo", "tags": {"bar": "baz", "qux": "quux"} },
|
||||
{ "name": "Lorem", "tags": {
|
||||
"ipsum": "dolor",
|
||||
"sit": "amet",
|
||||
"consectetur": "adipiscing",
|
||||
"elit": "Sed",
|
||||
"hendrerit": "ullamcorper",
|
||||
"ante": "id",
|
||||
"vestibulum": "Lorem",
|
||||
"ipsum": "dolor",
|
||||
"sit": "amet"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
<t t-name="UserPage">
|
||||
<div>
|
||||
<p>Name: <t t-esc="widget.user_name"/></p>
|
||||
<p>Password: <input type="text" t-att-value="widget.password"/></p>
|
||||
<p t-if="widget.is_admin">This user is an Administrator</p>
|
||||
<t t-foreach="widget.roles" t-as="role">
|
||||
<p>User has role: <t t-esc="role"/></p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
will yield this section of HTML document (reformated for readability):
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
*widget* is a variable given to the template engine by Widget sub-classes when they decide to render their associated template, it is simply *this*. Here is the corresponding Widget sub-class:
|
||||
|
||||
::
|
||||
|
||||
UserPageWidget = openerp.base.Widget.extend({
|
||||
template: "UserPage",
|
||||
init: function(parent) {
|
||||
this._super(parent);
|
||||
this.user_name = "Xavier";
|
||||
this.password = "lilo";
|
||||
this.is_admin = true;
|
||||
this.roles = ["Web Developer", "IE Hater", "Steve Jobs Worshiper"];
|
||||
},
|
||||
});
|
||||
|
||||
It could output something like this:
|
||||
|
||||
::
|
||||
|
||||
<div>
|
||||
<p>Name: Xavier</p>
|
||||
<p>Password: <input type="text" value="lilo"/></p>
|
||||
<p>This user is an Administrator</p
|
||||
<p>User has role: Web Developer</p>
|
||||
<p>User has role: IE Hater</p>
|
||||
<p>User has role: Steve Jobs Worshiper</p>
|
||||
<div class="base foo">
|
||||
<h4>Random Title</h4>
|
||||
<ul>
|
||||
<li class="even">
|
||||
foo
|
||||
<dl>
|
||||
<dt>bar</dt>
|
||||
<dd>baz</dd>
|
||||
<dt>qux</dt>
|
||||
<dd>quux</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li class="odd">
|
||||
Lorem
|
||||
<dl>
|
||||
<dt>ipsum</dt>
|
||||
<dd>dolor</dd>
|
||||
<dt>sit</dt>
|
||||
<dd>amet</dd>
|
||||
<dt>consectetur</dt>
|
||||
<dd>adipiscing</dd>
|
||||
<dt>elit</dt>
|
||||
<dd>Sed</dd>
|
||||
<dt>hendrerit</dt>
|
||||
<dd>ullamcorper</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
A QWeb template should always contain one unique root element to be used effectively with the Widget class, here it is a *<div>*. QWeb only react to *<t>* elements or attributes prefixed by *t-*. The *<t>* is simply a null element, it is only used when you need to use a *t-* attribute without outputting an html element at the same time. Here are the effects of the most common QWeb attributes:
|
||||
API
|
||||
---
|
||||
|
||||
* *t-esc* outputs the result of the evaluation of the given javascript expression
|
||||
* *t-att-ATTR* sets the value of the *ATTR* attribute to the result of the evaluation of the given javascript expression
|
||||
* *t-if* outputs the element and its content only if the given javascript expression returns true
|
||||
* *t-foreach* outputs as many times as contained in the list returned by the given javascript expression. For each iteration, a variable with the name defined by *t-as* contains the current element in the list.
|
||||
While QWeb implements a number of attributes and methods for
|
||||
customization and configuration, only two things are really important
|
||||
to the user:
|
||||
|
||||
.. js:class:: QWeb2.Engine
|
||||
|
||||
The QWeb "renderer", handles most of QWeb's logic (loading,
|
||||
parsing, compiling and rendering templates).
|
||||
|
||||
OpenERP Web instantiates one for the user, and sets it to
|
||||
``instance.web.qweb``. It also loads all the template files of the
|
||||
various modules into that QWeb instance.
|
||||
|
||||
A :js:class:`QWeb2.Engine` also serves as a "template namespace".
|
||||
|
||||
.. js:function:: QWeb2.Engine.render(template[, context])
|
||||
|
||||
Renders a previously loaded template to a String, using
|
||||
``context`` (if provided) to find the variables accessed
|
||||
during template rendering (e.g. strings to display).
|
||||
|
||||
:param String template: the name of the template to render
|
||||
:param Object context: the basic namespace to use for template
|
||||
rendering
|
||||
:returns: String
|
||||
|
||||
The engine exposes an other method which may be useful in some
|
||||
cases (e.g. if you need a separate template namespace with, in
|
||||
OpenERP Web, Kanban views get their own :js:class:`QWeb2.Engine`
|
||||
instance so their templates don't collide with more general
|
||||
"module" templates):
|
||||
|
||||
.. js:function:: QWeb2.Engine.add_template(templates)
|
||||
|
||||
Loads a template file (a collection of templates) in the QWeb
|
||||
instance. The templates can be specified as:
|
||||
|
||||
An XML string
|
||||
QWeb will attempt to parse it to an XML document then load
|
||||
it.
|
||||
|
||||
A URL
|
||||
QWeb will attempt to download the URL content, then load
|
||||
the resulting XML string.
|
||||
|
||||
A ``Document`` or ``Node``
|
||||
QWeb will traverse the first level of the document (the
|
||||
child nodes of the provided root) and load any named
|
||||
template or template override.
|
||||
|
||||
:type templates: String | Document | Node
|
||||
|
||||
A :js:class:`QWeb2.Engine` also exposes various attributes for
|
||||
behavior customization:
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.prefix
|
||||
|
||||
Prefix used to recognize :ref:`directives <qweb-directives>`
|
||||
during parsing. A string. By default, ``t``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.debug
|
||||
|
||||
Boolean flag putting the engine in "debug mode". Normally,
|
||||
QWeb intercepts any error raised during template execution. In
|
||||
debug mode, it leaves all exceptions go through without
|
||||
intercepting them.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.jQuery
|
||||
|
||||
The jQuery instance used during :ref:`template inheritance
|
||||
<qweb-directives-inheritance>` processing. Defaults to
|
||||
``window.jQuery``.
|
||||
|
||||
.. js:attribute:: QWeb2.Engine.preprocess_node
|
||||
|
||||
A ``Function``. If present, called before compiling each DOM
|
||||
node to template code. In OpenERP Web, this is used to
|
||||
automatically translate text content and some attributes in
|
||||
templates. Defaults to ``null``.
|
||||
|
||||
.. _qweb-directives:
|
||||
|
||||
Directives
|
||||
----------
|
||||
|
||||
A basic QWeb template is nothing more than an XHTML document (as it
|
||||
must be valid XML), which will be output as-is. But the rendering can
|
||||
be customized with bits of logic called "directives". Directives are
|
||||
attributes elements prefixed by :js:attr:`~QWeb2.Engine.prefix` (this
|
||||
document will use the default prefix ``t``, as does OpenERP Web).
|
||||
|
||||
A directive will usually control or alter the output of the element it
|
||||
is set on. If no suitable element is available, the prefix itself can
|
||||
be used as a "no-operation" element solely for supporting directives
|
||||
(or internal content, which will be rendered). This means:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<t>Something something</t>
|
||||
|
||||
will simply output the string "Something something" (the element
|
||||
itself will be skipped and "unwrapped"):
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
var e = new QWeb2.Engine();
|
||||
e.add_template('<templates>\
|
||||
<t t-name="test1"><t>Test 1</t></t>\
|
||||
<t t-name="test2"><span>Test 2</span></t>\
|
||||
</templates>');
|
||||
e.render('test1'); // Test 1
|
||||
e.render('test2'); // <span>Test 2</span>
|
||||
|
||||
.. note::
|
||||
|
||||
The conventions used in directive descriptions are the following:
|
||||
|
||||
* directives are described as compound functions, potentially with
|
||||
optional sections. Each section of the function name is an
|
||||
attribute of the element bearing the directive.
|
||||
|
||||
* a special parameter is ``BODY``, which does not have a name and
|
||||
designates the content of the element.
|
||||
|
||||
* special parameter types (aside from ``BODY`` which remains
|
||||
untyped) are ``Name``, which designates a valid javascript
|
||||
variable name, ``Expression`` which designates a valid
|
||||
javascript expression, and ``Format`` which designates a
|
||||
Ruby-style format string (a literal string with
|
||||
``#{Expression}`` inclusions executed and replaced by their
|
||||
result)
|
||||
|
||||
.. note::
|
||||
|
||||
``Expression`` actually supports a few extensions on the
|
||||
javascript syntax: because some syntactic elements of javascript
|
||||
are not compatible with XML and must be escaped, text
|
||||
substitutions are performed from forms which don't need to be
|
||||
escaped. Thus the following "keyword operators" are available in
|
||||
an ``Expression``: ``and`` (maps to ``&&``), ``or`` (maps to
|
||||
``||``), ``gt`` (maps to ``>``), ``gte`` (maps to ``>=``), ``lt``
|
||||
(maps to ``<``) and ``lte`` (maps to ``<=``).
|
||||
|
||||
.. _qweb-directives-templates:
|
||||
|
||||
Defining Templates
|
||||
++++++++++++++++++
|
||||
|
||||
.. _qweb-directive-name:
|
||||
|
||||
.. function:: t-name=name
|
||||
|
||||
:param String name: an arbitrary javascript string. Each template
|
||||
name is unique in a given
|
||||
:js:class:`QWeb2.Engine` instance, defining a
|
||||
new template with an existing name will
|
||||
overwrite the previous one without warning.
|
||||
|
||||
When multiple templates are related, it is
|
||||
customary to use dotted names as a kind of
|
||||
"namespace" e.g. ``foo`` and ``foo.bar`` which
|
||||
will be used either by ``foo`` or by a
|
||||
sub-widget of the widget used by ``foo``.
|
||||
|
||||
Templates can only be defined as the children of the document
|
||||
root. The document root's name is irrelevant (it's not checked)
|
||||
but is usually ``<templates>`` for simplicity.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<templates>
|
||||
<t t-name="template1">
|
||||
<!-- template code -->
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
:ref:`t-name <qweb-directive-name>` can be used on an element with
|
||||
an output as well:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<templates>
|
||||
<div t-name="template2">
|
||||
<!-- template code -->
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
which ensures the template has a single root (if a template has
|
||||
multiple roots and is then passed directly to jQuery, odd things
|
||||
occur).
|
||||
|
||||
.. _qweb-directives-output:
|
||||
|
||||
Output
|
||||
++++++
|
||||
|
||||
.. _qweb-directive-esc:
|
||||
|
||||
.. function:: t-esc=content
|
||||
|
||||
:param Expression content:
|
||||
|
||||
Evaluates, html-escapes and outputs ``content``.
|
||||
|
||||
.. _qweb-directive-escf:
|
||||
|
||||
.. function:: t-escf=content
|
||||
|
||||
:param Format content:
|
||||
|
||||
Similar to :ref:`t-esc <qweb-directive-esc>` but evaluates a
|
||||
``Format`` instead of just an expression.
|
||||
|
||||
.. _qweb-directive-raw:
|
||||
|
||||
.. function:: t-raw=content
|
||||
|
||||
:param Expression content:
|
||||
|
||||
Similar to :ref:`t-esc <qweb-directive-esc>` but does *not*
|
||||
html-escape the result of evaluating ``content``. Should only ever
|
||||
be used for known-secure content, or will be an XSS attack vector.
|
||||
|
||||
.. _qweb-directive-rawf:
|
||||
|
||||
.. function:: t-rawf=content
|
||||
|
||||
:param Format content:
|
||||
|
||||
``Format``-based version of :ref:`t-raw <qweb-directive-raw>`.
|
||||
|
||||
.. _qweb-directive-att:
|
||||
|
||||
.. function:: t-att=map
|
||||
|
||||
:param Expression map:
|
||||
|
||||
Evaluates ``map`` expecting an ``Object`` result, sets each
|
||||
key:value pair as an attribute (and its value) on the holder
|
||||
element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<span t-att="{foo: 3, bar: 42}"/>
|
||||
|
||||
will yield
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<span foo="3" bar="42"/>
|
||||
|
||||
.. function:: t-att-ATTNAME=value
|
||||
|
||||
:param Name ATTNAME:
|
||||
:param Expression value:
|
||||
|
||||
Evaluates ``value`` and sets it on the attribute ``ATTNAME`` on
|
||||
the holder element.
|
||||
|
||||
If ``value``'s result is ``undefined``, suppresses the creation of
|
||||
the attribute.
|
||||
|
||||
.. _qweb-directive-attf:
|
||||
|
||||
.. function:: t-attf-ATTNAME=value
|
||||
|
||||
:param Name ATTNAME:
|
||||
:param Format value:
|
||||
|
||||
Similar to :ref:`t-att-* <qweb-directive-att>` but the value of
|
||||
the attribute is specified via a ``Format`` instead of an
|
||||
expression. Useful for specifying e.g. classes mixing literal
|
||||
classes and computed ones.
|
||||
|
||||
.. _qweb-directives-flow:
|
||||
|
||||
Flow Control
|
||||
++++++++++++
|
||||
|
||||
.. _qweb-directive-set:
|
||||
|
||||
.. function:: t-set=name (t-value=value | BODY)
|
||||
|
||||
:param Name name:
|
||||
:param Expression value:
|
||||
:param BODY:
|
||||
|
||||
Creates a new binding in the template context. If ``value`` is
|
||||
specified, evaluates it and sets it to the specified
|
||||
``name``. Otherwise, processes ``BODY`` and uses that instead.
|
||||
|
||||
.. _qweb-directive-if:
|
||||
|
||||
.. function:: t-if=condition
|
||||
|
||||
:param Expression condition:
|
||||
|
||||
Evaluates ``condition``, suppresses the output of the holder
|
||||
element and its content of the result is falsy.
|
||||
|
||||
.. _qweb-directive-foreach:
|
||||
|
||||
.. function:: t-foreach=iterable [t-as=name]
|
||||
|
||||
:param Expression iterable:
|
||||
:param Name name:
|
||||
|
||||
Evaluates ``iterable``, iterates on it and evaluates the holder
|
||||
element and its body once per iteration round.
|
||||
|
||||
If ``name`` is not specified, computes a ``name`` based on
|
||||
``iterable`` (by replacing non-``Name`` characters by ``_``).
|
||||
|
||||
If ``iterable`` yields a ``Number``, treats it as a range from 0
|
||||
to that number (excluded).
|
||||
|
||||
While iterating, :ref:`t-foreach <qweb-directive-foreach>` adds a
|
||||
number of variables in the context:
|
||||
|
||||
``#{name}``
|
||||
If iterating on an array (or a range), the current value in
|
||||
the iteration. If iterating on an *object*, the current key.
|
||||
``#{name}_all``
|
||||
The collection being iterated (the array generated for a
|
||||
``Number``)
|
||||
``#{name}_value``
|
||||
The current iteration value (current item for an array, value
|
||||
for the current item for an object)
|
||||
``#{name}_index``
|
||||
The 0-based index of the current iteration round.
|
||||
``#{name}_first``
|
||||
Whether the current iteration round is the first one.
|
||||
``#{name}_parity``
|
||||
``"odd"`` if the current iteration round is odd, ``"even"``
|
||||
otherwise. ``0`` is considered even.
|
||||
|
||||
.. _qweb-directive-call:
|
||||
|
||||
.. function:: t-call=template [BODY]
|
||||
|
||||
:param String template:
|
||||
:param BODY:
|
||||
|
||||
Calls the specified ``template`` and returns its result. If
|
||||
``BODY`` is specified, it is evaluated *before* calling
|
||||
``template`` and can be used to specify e.g. parameters. This
|
||||
usage is similar to `call-template with with-param in XSLT
|
||||
<http://zvon.org/xxl/XSLTreference/OutputOverview/xslt_with-param_frame.html>`_.
|
||||
|
||||
.. _qweb-directives-inheritance:
|
||||
|
||||
Template Inheritance and Extension
|
||||
++++++++++++++++++++++++++++++++++
|
||||
|
||||
.. _qweb-directive-extend:
|
||||
|
||||
.. function:: t-extend=template BODY
|
||||
|
||||
:param String template: name of the template to extend
|
||||
|
||||
Works similarly to OpenERP models: if used on its own, will alter
|
||||
the specified template in-place; if used in conjunction with
|
||||
:ref:`t-name <qweb-directive-name>` will create a new template
|
||||
using the old one as a base.
|
||||
|
||||
``BODY`` should be a sequence of :ref:`t-jquery
|
||||
<qweb-directive-jquery>` alteration directives.
|
||||
|
||||
.. note::
|
||||
|
||||
The inheritance in the second form is *static*: the parent
|
||||
template is copied and transformed when :ref:`t-extend
|
||||
<qweb-directive-extend>` is called. If it is altered later (by
|
||||
a :ref:`t-extend <qweb-directive-extend>` without a
|
||||
:ref:`t-name <qweb-directive-name>`), these changes will *not*
|
||||
appear in the "child" templates.
|
||||
|
||||
.. _qweb-directive-jquery:
|
||||
|
||||
.. function:: t-jquery=selector [t-operation=operation] BODY
|
||||
|
||||
:param String selector: a CSS selector into the parent template
|
||||
:param operation: one of ``append``, ``prepend``, ``before``,
|
||||
``after``, ``inner`` or ``replace``.
|
||||
:param BODY: ``operation`` argument, or alterations to perform
|
||||
|
||||
* If ``operation`` is specified, applies the selector to the
|
||||
parent template to find a *context node*, then applies
|
||||
``operation`` (as a jQuery operation) to the *context node*,
|
||||
passing ``BODY`` as parameter.
|
||||
|
||||
.. note::
|
||||
|
||||
``replace`` maps to jQuery's `replaceWith(newContent)
|
||||
<http://api.jquery.com/replaceWith/>`_, ``inner`` maps to
|
||||
`html(htmlString) <http://api.jquery.com/html/>`_.
|
||||
|
||||
* If ``operation`` is not provided, ``BODY`` is evaluated as
|
||||
javascript code, with the *context node* as ``this``.
|
||||
|
||||
.. warning::
|
||||
|
||||
While this second form is much more powerful than the first,
|
||||
it is also much harder to read and maintain and should be
|
||||
avoided. It is usually possible to either avoid it or
|
||||
replace it with a sequence of ``t-jquery:t-operation:``.
|
||||
|
||||
Escape Hatches / debugging
|
||||
++++++++++++++++++++++++++
|
||||
|
||||
.. _qweb-directive-log:
|
||||
|
||||
.. function:: t-log=expression
|
||||
|
||||
:param Expression expression:
|
||||
|
||||
Evaluates the provided expression (in the current template
|
||||
context) and logs its result via ``console.log``.
|
||||
|
||||
.. _qweb-directive-debug:
|
||||
|
||||
.. function:: t-debug
|
||||
|
||||
Injects a debugger breakpoint (via the ``debugger;`` statement) in
|
||||
the compiled template output.
|
||||
|
||||
.. _qweb-directive-js:
|
||||
|
||||
.. function:: t-js=context BODY
|
||||
|
||||
:param Name context:
|
||||
:param BODY: javascript code
|
||||
|
||||
Injects the provided ``BODY`` javascript code into the compiled
|
||||
template, passing it the current template context using the name
|
||||
specified by ``context``.
|
||||
|
||||
@@ -33,6 +33,8 @@ view. It provides a number of services to handle a section of a page:
|
||||
|
||||
* Backbone-compatible shortcuts
|
||||
|
||||
.. _widget-dom_root:
|
||||
|
||||
DOM Root
|
||||
--------
|
||||
|
||||
|
||||
+2079
-1573
File diff suppressed because it is too large
Load Diff
+2035
-1529
File diff suppressed because it is too large
Load Diff
+2069
-1563
File diff suppressed because it is too large
Load Diff
+2018
-1513
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+2067
-1561
File diff suppressed because it is too large
Load Diff
+1956
-1450
File diff suppressed because it is too large
Load Diff
+2031
-1525
File diff suppressed because it is too large
Load Diff
+2065
-1559
File diff suppressed because it is too large
Load Diff
+2032
-1526
File diff suppressed because it is too large
Load Diff
+2148
-1622
File diff suppressed because it is too large
Load Diff
+2015
-1510
File diff suppressed because it is too large
Load Diff
+2037
-1531
File diff suppressed because it is too large
Load Diff
+2046
-1537
File diff suppressed because it is too large
Load Diff
+2098
-1592
File diff suppressed because it is too large
Load Diff
+2064
-1547
File diff suppressed because it is too large
Load Diff
+2015
-1506
File diff suppressed because it is too large
Load Diff
+2010
-1505
File diff suppressed because it is too large
Load Diff
+2042
-1533
File diff suppressed because it is too large
Load Diff
+2021
-1515
File diff suppressed because it is too large
Load Diff
+2156
-1633
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+2037
-1531
File diff suppressed because it is too large
Load Diff
+2026
-1520
File diff suppressed because it is too large
Load Diff
+2016
-1511
File diff suppressed because it is too large
Load Diff
+2086
-1580
File diff suppressed because it is too large
Load Diff
+2081
-1575
File diff suppressed because it is too large
Load Diff
+2010
-1505
File diff suppressed because it is too large
Load Diff
+2086
-1579
File diff suppressed because it is too large
Load Diff
+2037
-1531
File diff suppressed because it is too large
Load Diff
+2068
-1562
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+2025
-1519
File diff suppressed because it is too large
Load Diff
+2024
-1518
File diff suppressed because it is too large
Load Diff
+2084
-1578
File diff suppressed because it is too large
Load Diff
+2022
-1516
File diff suppressed because it is too large
Load Diff
+2128
-1610
File diff suppressed because it is too large
Load Diff
+2131
-1622
File diff suppressed because it is too large
Load Diff
+2045
-1539
File diff suppressed because it is too large
Load Diff
+2130
-1613
File diff suppressed because it is too large
Load Diff
+2098
-1583
File diff suppressed because it is too large
Load Diff
+2069
-1563
File diff suppressed because it is too large
Load Diff
+2066
-1554
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+2042
-1536
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+2069
-1563
File diff suppressed because it is too large
Load Diff
+2069
-1563
File diff suppressed because it is too large
Load Diff
+2050
-1544
File diff suppressed because it is too large
Load Diff
+2009
-1504
File diff suppressed because it is too large
Load Diff
+1996
-1495
File diff suppressed because it is too large
Load Diff
+2052
-1542
File diff suppressed because it is too large
Load Diff
+2015
-1506
File diff suppressed because it is too large
Load Diff
@@ -8952,7 +8952,7 @@ $.extend( $.ui.dialog.overlay, {
|
||||
$( document ).bind( $.ui.dialog.overlay.events, function( event ) {
|
||||
// stop events if the z-index of the target is < the z-index of the overlay
|
||||
// we cannot return true when we don't want to cancel the event (#3523)
|
||||
if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) {
|
||||
if ( $(event.target).closest('.ui-dialog').zIndex() < $.ui.dialog.overlay.maxZ ) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
|
||||
node: 2b62048dd1d86c45e4bf0442b5da4122534132bf
|
||||
node: 142c22b230636674a0cee6bc29e6975f0f1600a5
|
||||
branch: default
|
||||
latesttag: 0.7
|
||||
latesttagdistance: 5
|
||||
latesttagdistance: 9
|
||||
|
||||
@@ -161,6 +161,28 @@ Object Protocol
|
||||
:returns: ``Number``
|
||||
:raises: ``TypeError`` if the object doesn't have a length
|
||||
|
||||
.. function:: py.PY_getItem(o, key)
|
||||
|
||||
Returns the element of ``o`` corresponding to the object
|
||||
``key``. This is equivalent to ``o[key]``.
|
||||
|
||||
:param o: :class:`py.object`
|
||||
:param key: :class:`py.object`
|
||||
:returns: :class:`py.object`
|
||||
:raises: ``TypeError`` if ``o`` does not support the operation, if
|
||||
``key`` or the return value is not a :class:`py.object`
|
||||
|
||||
.. function:: py.PY_setItem(o, key, v)
|
||||
|
||||
Maps the object ``key`` to the value ``v`` in ``o``. Equivalent to
|
||||
``o[key] = v``.
|
||||
|
||||
:param o: :class:`py.object`
|
||||
:param key: :class:`py.object`
|
||||
:param v: :class:`py.object`
|
||||
:raises: ``TypeError`` if ``o`` does not support the operation, or
|
||||
if ``key`` or ``v`` are not :class:`py.object`
|
||||
|
||||
Number Protocol
|
||||
---------------
|
||||
|
||||
|
||||
@@ -393,23 +393,29 @@ var py = {};
|
||||
|
||||
switch(val.constructor) {
|
||||
case Object:
|
||||
// TODO: why py.object instead of py.dict?
|
||||
var o = py.PY_call(py.object);
|
||||
for (var prop in val) {
|
||||
if (val.hasOwnProperty(prop)) {
|
||||
o[prop] = val[prop];
|
||||
var out = py.PY_call(py.object);
|
||||
for(var k in val) {
|
||||
if (val.hasOwnProperty(k)) {
|
||||
out[k] = val[k];
|
||||
}
|
||||
}
|
||||
return o;
|
||||
return out;
|
||||
case Array:
|
||||
var a = py.PY_call(py.list);
|
||||
a._values = val;
|
||||
return a;
|
||||
return py.list.fromJSON(val);
|
||||
}
|
||||
|
||||
throw new Error("Could not convert " + val + " to a pyval");
|
||||
}
|
||||
|
||||
var typename = function (obj) {
|
||||
if (obj.__class__) { // py type
|
||||
return obj.__class__.__name__;
|
||||
} else if(typeof obj !== 'object') { // JS primitive
|
||||
return typeof obj;
|
||||
} else { // JS object
|
||||
return obj.constructor.name;
|
||||
}
|
||||
};
|
||||
// JSAPI, JS-level utility functions for implementing new py.js
|
||||
// types
|
||||
py.py = {};
|
||||
@@ -522,16 +528,10 @@ var py = {};
|
||||
if (py.PY_isInstance(v, py.str)) {
|
||||
return v;
|
||||
}
|
||||
var typename;
|
||||
if (v.__class__) { // py type
|
||||
typename = v.__class__.__name__;
|
||||
} else if(typeof v !== 'object') { // JS primitive
|
||||
typename = typeof v;
|
||||
} else { // JS object
|
||||
typename = v.constructor.name;
|
||||
}
|
||||
throw new Error(
|
||||
'TypeError: __str__ returned non-string (type '+typename+')');
|
||||
'TypeError: __str__ returned non-string (type '
|
||||
+ typename(v)
|
||||
+')');
|
||||
};
|
||||
py.PY_isInstance = function (inst, cls) {
|
||||
var fn = function () {};
|
||||
@@ -574,7 +574,7 @@ var py = {};
|
||||
}
|
||||
throw new Error(
|
||||
"TypeError: __nonzero__ should return bool, returned "
|
||||
+ res.__class__.__name__);
|
||||
+ typename(res));
|
||||
};
|
||||
py.PY_not = function (o) {
|
||||
return !py.PY_isTrue(o);
|
||||
@@ -583,7 +583,7 @@ var py = {};
|
||||
if (!o.__len__) {
|
||||
throw new Error(
|
||||
"TypeError: object of type '" +
|
||||
o.__class__.__name__ +
|
||||
typename(o) +
|
||||
"' has no len()");
|
||||
}
|
||||
var v = o.__len__();
|
||||
@@ -592,6 +592,44 @@ var py = {};
|
||||
}
|
||||
return v;
|
||||
};
|
||||
py.PY_getItem = function (o, key) {
|
||||
if (!('__getitem__' in o)) {
|
||||
throw new Error(
|
||||
"TypeError: '" + typename(o) +
|
||||
"' object is unsubscriptable")
|
||||
}
|
||||
if (!py.PY_isInstance(key, py.object)) {
|
||||
throw new Error(
|
||||
"TypeError: '" + typename(key) +
|
||||
"' is not a py.js object");
|
||||
}
|
||||
var res = o.__getitem__(key);
|
||||
if (!py.PY_isInstance(key, py.object)) {
|
||||
throw new Error(
|
||||
"TypeError: __getitem__ must return a py.js object, got "
|
||||
+ typename(res));
|
||||
}
|
||||
return res;
|
||||
};
|
||||
py.PY_setItem = function (o, key, v) {
|
||||
if (!('__setitem__' in o)) {
|
||||
throw new Error(
|
||||
"TypeError: '" + typename(o) +
|
||||
"' object does not support item assignment");
|
||||
}
|
||||
if (!py.PY_isInstance(key, py.object)) {
|
||||
throw new Error(
|
||||
"TypeError: '" + typename(key) +
|
||||
"' is not a py.js object");
|
||||
}
|
||||
if (!py.PY_isInstance(v, py.object)) {
|
||||
throw new Error(
|
||||
"TypeError: '" + typename(v) +
|
||||
"' is not a py.js object");
|
||||
}
|
||||
o.__setitem__(key, v);
|
||||
};
|
||||
|
||||
py.PY_add = function (o1, o2) {
|
||||
return PY_op(o1, o2, '+');
|
||||
};
|
||||
@@ -608,7 +646,7 @@ var py = {};
|
||||
if (!o.__neg__) {
|
||||
throw new Error(
|
||||
"TypeError: bad operand for unary -: '"
|
||||
+ o.__class__.__name
|
||||
+ typename(o)
|
||||
+ "'");
|
||||
}
|
||||
return o.__neg__();
|
||||
@@ -617,7 +655,7 @@ var py = {};
|
||||
if (!o.__pos__) {
|
||||
throw new Error(
|
||||
"TypeError: bad operand for unary +: '"
|
||||
+ o.__class__.__name
|
||||
+ typename(o)
|
||||
+ "'");
|
||||
}
|
||||
return o.__pos__();
|
||||
@@ -676,7 +714,7 @@ var py = {};
|
||||
return (this === other) ? py.True : py.False;
|
||||
},
|
||||
__ne__: function (other) {
|
||||
if (this.__eq__(other) === py.True) {
|
||||
if (py.PY_isTrue(this.__eq__(other))) {
|
||||
return py.False;
|
||||
} else {
|
||||
return py.True;
|
||||
@@ -690,7 +728,7 @@ var py = {};
|
||||
return this.__unicode__();
|
||||
},
|
||||
__unicode__: function () {
|
||||
return py.str.fromJSON('<' + this.__class__.__name__ + ' object>');
|
||||
return py.str.fromJSON('<' + typename(this) + ' object>');
|
||||
},
|
||||
__nonzero__: function () {
|
||||
return py.True;
|
||||
@@ -724,7 +762,14 @@ var py = {};
|
||||
|
||||
// Conversion
|
||||
toJSON: function () {
|
||||
throw new Error(this.constructor.name + ' can not be converted to JSON');
|
||||
var out = {};
|
||||
for(var k in this) {
|
||||
if (this.hasOwnProperty(k) && !/^__/.test(k)) {
|
||||
var val = this[k];
|
||||
out[k] = val.toJSON ? val.toJSON() : val;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
});
|
||||
var NoneType = py.type('NoneType', null, {
|
||||
@@ -776,7 +821,7 @@ var py = {};
|
||||
return;
|
||||
}
|
||||
throw new Error('TypeError: __float__ returned non-float (type ' +
|
||||
res.__class__.__name__ + ')');
|
||||
typename(res) + ')');
|
||||
}
|
||||
throw new Error('TypeError: float() argument must be a string or a number');
|
||||
},
|
||||
@@ -810,6 +855,10 @@ var py = {};
|
||||
}
|
||||
return this._value >= other._value ? py.True : py.False;
|
||||
},
|
||||
__abs__: function () {
|
||||
return py.float.fromJSON(
|
||||
Math.abs(this._value));
|
||||
},
|
||||
__add__: function (other) {
|
||||
if (!py.PY_isInstance(other, py.float)) {
|
||||
return py.NotImplemented;
|
||||
@@ -927,14 +976,14 @@ var py = {};
|
||||
},
|
||||
__contains__: function (value) {
|
||||
for(var i=0, len=this._values.length; i<len; ++i) {
|
||||
if (this._values[i].__eq__(value) === py.True) {
|
||||
if (py.PY_isTrue(this._values[i].__eq__(value))) {
|
||||
return py.True;
|
||||
}
|
||||
}
|
||||
return py.False;
|
||||
},
|
||||
__getitem__: function (index) {
|
||||
return PY_ensurepy(this._values[index.toJSON()]);
|
||||
return this._values[index.toJSON()];
|
||||
},
|
||||
toJSON: function () {
|
||||
var out = [];
|
||||
@@ -942,6 +991,16 @@ var py = {};
|
||||
out.push(this._values[i].toJSON());
|
||||
}
|
||||
return out;
|
||||
},
|
||||
fromJSON: function (ar) {
|
||||
if (!(ar instanceof Array)) {
|
||||
throw new Error("Can only create a py.tuple from an Array");
|
||||
}
|
||||
var t = py.PY_call(py.tuple);
|
||||
for(var i=0; i<ar.length; ++i) {
|
||||
t._values.push(PY_ensurepy(ar[i]));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
});
|
||||
py.list = py.tuple;
|
||||
@@ -1023,6 +1082,16 @@ var py = {};
|
||||
}
|
||||
});
|
||||
|
||||
py.abs = new py.PY_def.fromJSON(function abs() {
|
||||
var args = py.PY_parseArgs(arguments, ['number']);
|
||||
if (!args.number.__abs__) {
|
||||
throw new Error(
|
||||
"TypeError: bad operand type for abs(): '"
|
||||
+ typename(args.number)
|
||||
+ "'");
|
||||
}
|
||||
return args.number.__abs__();
|
||||
});
|
||||
py.len = new py.PY_def.fromJSON(function len() {
|
||||
var args = py.PY_parseArgs(arguments, ['object']);
|
||||
return py.float.fromJSON(py.PY_size(args.object));
|
||||
@@ -1090,8 +1159,7 @@ var py = {};
|
||||
}
|
||||
throw new Error(
|
||||
"TypeError: unsupported operand type(s) for " + op + ": '"
|
||||
+ o1.__class__.__name__ + "' and '"
|
||||
+ o2.__class__.__name__ + "'");
|
||||
+ typename(o1) + "' and '" + typename(o2) + "'");
|
||||
};
|
||||
|
||||
var PY_builtins = {
|
||||
@@ -1111,6 +1179,7 @@ var py = {};
|
||||
list: py.list,
|
||||
dict: py.dict,
|
||||
|
||||
abs: py.abs,
|
||||
len: py.len,
|
||||
isinstance: py.isinstance,
|
||||
issubclass: py.issubclass,
|
||||
@@ -1130,7 +1199,7 @@ var py = {};
|
||||
case 'in':
|
||||
return b.__contains__(a);
|
||||
case 'not in':
|
||||
return b.__contains__(a) === py.True ? py.False : py.True;
|
||||
return py.PY_isTrue(b.__contains__(a)) ? py.False : py.True;
|
||||
case '==': case '!=': case '<>':
|
||||
case '<': case '<=':
|
||||
case '>': case '>=':
|
||||
@@ -1165,20 +1234,20 @@ var py = {};
|
||||
expr.operators[i],
|
||||
left,
|
||||
left = py.evaluate(expr.expressions[i+1], context));
|
||||
if (result === py.False) { return py.False; }
|
||||
if (py.PY_not(result)) { return py.False; }
|
||||
}
|
||||
return py.True;
|
||||
case 'not':
|
||||
return py.PY_isTrue(py.evaluate(expr.first, context)) ? py.False : py.True;
|
||||
case 'and':
|
||||
var and_first = py.evaluate(expr.first, context);
|
||||
if (and_first.__nonzero__() === py.True) {
|
||||
if (py.PY_isTrue(and_first.__nonzero__())) {
|
||||
return py.evaluate(expr.second, context);
|
||||
}
|
||||
return and_first;
|
||||
case 'or':
|
||||
var or_first = py.evaluate(expr.first, context);
|
||||
if (or_first.__nonzero__() === py.True) {
|
||||
if (py.PY_isTrue(or_first.__nonzero__())) {
|
||||
return or_first
|
||||
}
|
||||
return py.evaluate(expr.second, context);
|
||||
@@ -1205,26 +1274,23 @@ var py = {};
|
||||
tuple_values.push(py.evaluate(
|
||||
tuple_exprs[j], context));
|
||||
}
|
||||
var t = py.PY_call(py.tuple);
|
||||
t._values = tuple_values;
|
||||
return t;
|
||||
return py.tuple.fromJSON(tuple_values);
|
||||
case '[':
|
||||
if (expr.second) {
|
||||
return py.evaluate(expr.first, context)
|
||||
.__getitem__(py.evaluate(expr.second, context));
|
||||
return py.PY_getItem(
|
||||
py.evaluate(expr.first, context),
|
||||
py.evaluate(expr.second, context));
|
||||
}
|
||||
var list_exprs = expr.first, list_values = [];
|
||||
for (var k=0; k<list_exprs.length; ++k) {
|
||||
list_values.push(py.evaluate(
|
||||
list_exprs[k], context));
|
||||
}
|
||||
var l = py.PY_call(py.list);
|
||||
l._values = list_values;
|
||||
return l;
|
||||
return py.list.fromJSON(list_values);
|
||||
case '{':
|
||||
var dict_exprs = expr.first, dict = py.PY_call(py.dict);
|
||||
for(var l=0; l<dict_exprs.length; ++l) {
|
||||
dict.__setitem__(
|
||||
py.PY_setItem(dict,
|
||||
py.evaluate(dict_exprs[l][0], context),
|
||||
py.evaluate(dict_exprs[l][1], context));
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ QWeb2.Engine = (function() {
|
||||
}
|
||||
if (name) {
|
||||
this.templates[name] = node;
|
||||
this.compiled_templates[name] = null;
|
||||
} else if (extend) {
|
||||
delete(this.compiled_templates[extend]);
|
||||
if (this.extend_templates[extend]) {
|
||||
|
||||
@@ -652,7 +652,7 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
.openerp .oe_dropdown_toggle {
|
||||
color: #4C4C4C;
|
||||
color: #4c4c4c;
|
||||
font-weight: normal;
|
||||
}
|
||||
.openerp .oe_dropdown_hover:hover .oe_dropdown_menu, .openerp .oe_dropdown_menu.oe_opened {
|
||||
@@ -798,6 +798,20 @@
|
||||
.openerp .oe_notification {
|
||||
z-index: 1050;
|
||||
}
|
||||
.openerp .oe_webclient_timezone_notification a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.openerp .oe_webclient_timezone_notification p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.openerp .oe_webclient_timezone_notification dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
.openerp .oe_timezone_systray span {
|
||||
margin-top: 1px;
|
||||
background-color: #f6cf3b;
|
||||
}
|
||||
.openerp .oe_login {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAYAAADgzO9IAAAAKUlEQVQIHWO8e/fufwYsgAUkJigoiCIF5DMyoYggcUiXgNnBiGQKmAkARpcEQeriln4AAAAASUVORK5CYII=);
|
||||
text-align: center;
|
||||
@@ -2252,6 +2266,7 @@
|
||||
.openerp .oe_form .oe_form_field_url button img {
|
||||
vertical-align: top;
|
||||
}
|
||||
.openerp .oe_form .oe_form_field_monetary,
|
||||
.openerp .oe_form .oe_form_field_date,
|
||||
.openerp .oe_form .oe_form_field_datetime {
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -674,9 +674,21 @@ $sheet-padding: 16px
|
||||
border-bottom-right-radius: 8px
|
||||
border-bottom-left-radius: 8px
|
||||
// }}}
|
||||
// Notification {{{
|
||||
// Notifications {{{
|
||||
.oe_notification
|
||||
z-index: 1050
|
||||
.oe_webclient_timezone_notification
|
||||
a
|
||||
color: white
|
||||
text-decoration: underline
|
||||
p
|
||||
margin-top: 1em
|
||||
dt
|
||||
font-weight: bold
|
||||
.oe_timezone_systray
|
||||
span
|
||||
margin-top: 1px
|
||||
background-color: #f6cf3b
|
||||
// }}}
|
||||
// Login {{{
|
||||
.oe_login
|
||||
@@ -1791,6 +1803,7 @@ $sheet-padding: 16px
|
||||
border-left: 8px solid #eee
|
||||
.oe_form_field_url button img
|
||||
vertical-align: top
|
||||
.oe_form_field_monetary,
|
||||
.oe_form_field_date,
|
||||
.oe_form_field_datetime
|
||||
white-space: nowrap
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* @param {Array|String} modules list of modules to initialize
|
||||
*/
|
||||
init: function(modules) {
|
||||
if (modules === "fuck your shit, don't load anything you cunt") {
|
||||
if (modules === null) {
|
||||
modules = [];
|
||||
} else {
|
||||
modules = _.union(['web'], modules || []);
|
||||
|
||||
@@ -822,7 +822,7 @@ instance.web.Menu = instance.web.Widget.extend({
|
||||
this.renderElement();
|
||||
this.limit_entries();
|
||||
// Hide toplevel item if there is only one
|
||||
var $toplevel = this.$("li")
|
||||
var $toplevel = this.$("li");
|
||||
if($toplevel.length == 1) {
|
||||
$toplevel.hide();
|
||||
}
|
||||
@@ -1106,7 +1106,6 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||
start: function() {
|
||||
var self = this;
|
||||
return $.when(this._super()).then(function() {
|
||||
self.$(".oe_logo").attr("href", $.param.fragment("" + window.location, "", 2).slice(0, -1));
|
||||
if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
|
||||
$("body").addClass("kitten-mode-activated");
|
||||
if ($.blockUI) {
|
||||
@@ -1167,26 +1166,32 @@ instance.web.WebClient = instance.web.Client.extend({
|
||||
},
|
||||
check_timezone: function() {
|
||||
var self = this;
|
||||
var user_offset = instance.session.user_context.tz_offset;
|
||||
var offset = -(new Date().getTimezoneOffset());
|
||||
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
|
||||
var browser_offset = (offset < 0) ? "-" : "+";
|
||||
browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
|
||||
browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
|
||||
if (browser_offset !== user_offset) {
|
||||
var notification = this.do_warn(_t("Timezone"), QWeb.render('WebClient.timezone_notification', {
|
||||
user_timezone: instance.session.user_context.tz || 'UTC',
|
||||
user_offset: user_offset,
|
||||
browser_offset: browser_offset,
|
||||
}), true);
|
||||
notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
|
||||
notification.close();
|
||||
}).find('a').on('click', function() {
|
||||
notification.close();
|
||||
self.user_menu.on_menu_settings();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) {
|
||||
var user_offset = result[0]['tz_offset'];
|
||||
var offset = -(new Date().getTimezoneOffset());
|
||||
// _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
|
||||
var browser_offset = (offset < 0) ? "-" : "+";
|
||||
browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
|
||||
browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
|
||||
if (browser_offset !== user_offset) {
|
||||
var $icon = $(QWeb.render('WebClient.timezone_systray'));
|
||||
$icon.on('click', function() {
|
||||
var notification = self.do_warn(_t("Timezone mismatch"), QWeb.render('WebClient.timezone_notification', {
|
||||
user_timezone: instance.session.user_context.tz || 'UTC',
|
||||
user_offset: user_offset,
|
||||
browser_offset: browser_offset,
|
||||
}), true);
|
||||
notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
|
||||
notification.close();
|
||||
}).find('a').on('click', function() {
|
||||
notification.close();
|
||||
self.user_menu.on_menu_settings();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
$icon.appendTo(self.$('.oe_systray'));
|
||||
}
|
||||
});
|
||||
},
|
||||
destroy_content: function() {
|
||||
_.each(_.clone(this.getChildren()), function(el) {
|
||||
|
||||
@@ -969,6 +969,9 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
|
||||
if (_.isString(url)) {
|
||||
url = { url: url };
|
||||
}
|
||||
_.defaults(params, {
|
||||
context: this.user_context || {}
|
||||
});
|
||||
// Construct a JSON-RPC2 request, method is currently unused
|
||||
if (this.debug)
|
||||
params.debug = 1;
|
||||
|
||||
@@ -622,7 +622,7 @@ var messages_by_seconds = function() {
|
||||
[120, _t("Don't leave yet,<br />it's still loading...")],
|
||||
[300, _t("You may not believe it,<br />but the application is actually loading...")],
|
||||
[420, _t("Take a minute to get a coffee,<br />because it's loading...")],
|
||||
[3600, _t("Maybe you should consider reloading the application by pressing F5...")],
|
||||
[3600, _t("Maybe you should consider reloading the application by pressing F5...")]
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ openerp.web.pyeval = function (instance) {
|
||||
var mod = a%b;
|
||||
// in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a
|
||||
// round of b
|
||||
if (mod > 0 && b < 0 || mod < 0 && b > 0) {
|
||||
if (mod > 0 && b < 0 || mod < 0 && b > 0) {
|
||||
mod += b;
|
||||
}
|
||||
return fn(Math.floor(a/b), mod);
|
||||
@@ -434,11 +434,6 @@ openerp.web.pyeval = function (instance) {
|
||||
throw new Error('ValueError: No known conversion for ' + m);
|
||||
}));
|
||||
},
|
||||
today: py.classmethod.fromJSON(function () {
|
||||
var d = new Date();
|
||||
return py.PY_call(
|
||||
datetime.date, [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]);
|
||||
}),
|
||||
__eq__: function (other) {
|
||||
return (this.year === other.year
|
||||
&& this.month === other.month
|
||||
|
||||
@@ -341,12 +341,10 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
|
||||
if (this.headless) {
|
||||
this.ready.resolve();
|
||||
} else {
|
||||
var load_view = this.rpc("/web/view/load", {
|
||||
model: this.model,
|
||||
var load_view = instance.web.fields_view_get({
|
||||
model: this.dataset._model,
|
||||
view_id: this.view_id,
|
||||
view_type: 'search',
|
||||
context: instance.web.pyeval.eval(
|
||||
'context', this.dataset.get_context())
|
||||
});
|
||||
|
||||
$.when(load_view).then(function (r) {
|
||||
|
||||
@@ -10,7 +10,7 @@ openerp.testing = {};
|
||||
formats: ['coresetup', 'dates'],
|
||||
chrome: ['corelib', 'coresetup'],
|
||||
views: ['corelib', 'coresetup', 'data', 'chrome'],
|
||||
search: ['data', 'coresetup', 'formats'],
|
||||
search: ['views', 'formats'],
|
||||
list: ['views', 'data'],
|
||||
form: ['data', 'views', 'list', 'formats'],
|
||||
list_editable: ['list', 'form', 'data'],
|
||||
@@ -263,7 +263,7 @@ openerp.testing = {};
|
||||
++di;
|
||||
}
|
||||
|
||||
instance = openerp.init("fuck your shit, don't load anything you cunt");
|
||||
instance = openerp.init(null);
|
||||
_(d).chain()
|
||||
.reverse()
|
||||
.uniq()
|
||||
|
||||
@@ -1189,36 +1189,19 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt
|
||||
});
|
||||
}
|
||||
},
|
||||
view_arch_to_dom_node: function(arch) {
|
||||
// Historic mess for views arch
|
||||
//
|
||||
// server:
|
||||
// -> got xml as string
|
||||
// -> parse to xml and manipulate domains and contexts
|
||||
// -> convert to json
|
||||
// client:
|
||||
// -> got view as json
|
||||
// -> convert back to xml as string
|
||||
// -> parse it as xml doc (manipulate button@type for IE)
|
||||
// -> convert back to string
|
||||
// -> parse it as dom element with jquery
|
||||
// -> for each widget, convert node to json
|
||||
//
|
||||
// Wow !!!
|
||||
var xml = instance.web.json_node_to_xml(arch);
|
||||
|
||||
var doc = $.parseXML('<div class="oe_form">' + xml + '</div>');
|
||||
get_arch_fragment: function() {
|
||||
var doc = $.parseXML(instance.web.json_node_to_xml(this.fvg.arch)).documentElement;
|
||||
// IE won't allow custom button@type and will revert it to spec default : 'submit'
|
||||
$('button', doc).each(function() {
|
||||
$(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button');
|
||||
});
|
||||
xml = instance.web.xml_to_str(doc);
|
||||
return $(xml);
|
||||
return $('<div class="oe_form"/>').append(instance.web.xml_to_str(doc));
|
||||
},
|
||||
render_to: function($target) {
|
||||
var self = this;
|
||||
this.$target = $target;
|
||||
|
||||
this.$form = this.view_arch_to_dom_node(this.fvg.arch);
|
||||
this.$form = this.get_arch_fragment();
|
||||
|
||||
this.process_version();
|
||||
|
||||
@@ -2680,13 +2663,10 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan
|
||||
init: function(field_manager, node) {
|
||||
var self = this;
|
||||
this._super(field_manager, node);
|
||||
this.values = _.clone(this.field.selection);
|
||||
_.each(this.values, function(v, i) {
|
||||
if (v[0] === false && v[1] === '') {
|
||||
self.values.splice(i, 1);
|
||||
}
|
||||
});
|
||||
this.values.unshift([false, '']);
|
||||
this.values = _(this.field.selection).chain()
|
||||
.reject(function (v) { return v[0] === false && v[1] === ''; })
|
||||
.unshift([false, ''])
|
||||
.value();
|
||||
},
|
||||
initialize_content: function() {
|
||||
// Flag indicating whether we're in an event chain containing a change
|
||||
@@ -5271,6 +5251,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({
|
||||
|
||||
instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({
|
||||
template: "FieldMonetary",
|
||||
widget_class: 'oe_form_field_float oe_form_field_monetary',
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.set({"currency": false});
|
||||
|
||||
@@ -600,6 +600,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
|
||||
|
||||
this.dataset.index = _(this.dataset.ids).indexOf(ids[0]);
|
||||
if (this.sidebar) {
|
||||
this.options.$sidebar.show();
|
||||
this.sidebar.$el.show();
|
||||
}
|
||||
|
||||
@@ -922,6 +923,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
||||
}, this);
|
||||
|
||||
this.$current = $('<tbody>')
|
||||
.delegate('input[readonly=readonly]', 'click', function (e) {
|
||||
/*
|
||||
Against all logic and sense, as of right now @readonly
|
||||
apparently does nothing on checkbox and radio inputs, so
|
||||
the trick of using @readonly to have, well, readonly
|
||||
checkboxes (which still let clicks go through) does not
|
||||
work out of the box. We *still* need to preventDefault()
|
||||
on the event, otherwise the checkbox's state *will* toggle
|
||||
on click
|
||||
*/
|
||||
e.preventDefault();
|
||||
})
|
||||
.delegate('th.oe_list_record_selector', 'click', function (e) {
|
||||
e.stopPropagation();
|
||||
var selection = self.get_selection();
|
||||
|
||||
@@ -9,6 +9,7 @@ var QWeb = instance.web.qweb,
|
||||
instance.web.views.add('tree', 'instance.web.TreeView');
|
||||
instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeView# */{
|
||||
display_name: _lt('Tree'),
|
||||
view_type: 'tree',
|
||||
/**
|
||||
* Indicates that this view is not searchable, and thus that no search
|
||||
* view should be displayed (if there is one active).
|
||||
@@ -36,18 +37,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
|
||||
this.options = _.extend({}, this.defaults, options || {});
|
||||
|
||||
_.bindAll(this, 'color_for');
|
||||
this.on('view_loaded', this, this.load_tree);
|
||||
},
|
||||
|
||||
start: function () {
|
||||
return this.rpc("/web/treeview/load", {
|
||||
model: this.model,
|
||||
view_id: this.view_id,
|
||||
view_type: "tree",
|
||||
toolbar: this.view_manager ? !!this.view_manager.sidebar : false,
|
||||
context: instance.web.pyeval.eval(
|
||||
'context', this.dataset.get_context())
|
||||
}).done(this.on_loaded);
|
||||
},
|
||||
/**
|
||||
* Returns the list of fields needed to correctly read objects.
|
||||
*
|
||||
@@ -64,7 +56,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
on_loaded: function (fields_view) {
|
||||
load_tree: function (fields_view) {
|
||||
var self = this;
|
||||
var has_toolbar = !!fields_view.arch.attrs.toolbar;
|
||||
// field name in OpenERP is kinda stupid: this is the name of the field
|
||||
|
||||
@@ -275,7 +275,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
|
||||
}
|
||||
if (action.domain) {
|
||||
action.domain = instance.web.pyeval.eval(
|
||||
'domain', action.domain);
|
||||
'domain', action.domain, action.context || {});
|
||||
}
|
||||
|
||||
if (!action.type) {
|
||||
@@ -758,12 +758,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
|
||||
dataset.index = 0;
|
||||
}
|
||||
this.dataset = dataset;
|
||||
|
||||
// setup storage for session-wise menu hiding
|
||||
if (this.session.hidden_menutips) {
|
||||
return;
|
||||
}
|
||||
this.session.hidden_menutips = {};
|
||||
},
|
||||
/**
|
||||
* Initializes the ViewManagerAction: sets up the searchview (if the
|
||||
@@ -1209,13 +1203,11 @@ instance.web.View = instance.web.Widget.extend({
|
||||
} else {
|
||||
if (! this.view_type)
|
||||
console.warn("view_type is not defined", this);
|
||||
view_loaded = this.rpc("/web/view/load", {
|
||||
"model": this.dataset.model,
|
||||
view_loaded = instance.web.fields_view_get({
|
||||
"model": this.dataset._model,
|
||||
"view_id": this.view_id,
|
||||
"view_type": this.view_type,
|
||||
toolbar: !!this.options.$sidebar,
|
||||
context: instance.web.pyeval.eval(
|
||||
'context', this.dataset.get_context(context))
|
||||
"toolbar": !!this.options.$sidebar,
|
||||
});
|
||||
}
|
||||
return view_loaded.then(function(r) {
|
||||
@@ -1390,12 +1382,53 @@ instance.web.View = instance.web.Widget.extend({
|
||||
}
|
||||
});
|
||||
|
||||
instance.web.xml_to_json = function(node) {
|
||||
/**
|
||||
* Performs a fields_view_get and apply postprocessing.
|
||||
* return a {$.Deferred} resolved with the fvg
|
||||
*
|
||||
* @param {Object} [args]
|
||||
* @param {String|Object} args.model instance.web.Model instance or string repr of the model
|
||||
* @param {null|Object} args.context context if args.model is a string
|
||||
* @param {null|Number} args.view_id id of the view to be loaded, default view if null
|
||||
* @param {null|String} args.view_type type of view to be loaded if view_id is null
|
||||
* @param {Boolean} [args.toolbar=false] get the toolbar definition
|
||||
*/
|
||||
instance.web.fields_view_get = function(args) {
|
||||
function postprocess(fvg) {
|
||||
var doc = $.parseXML(fvg.arch).documentElement;
|
||||
fvg.arch = instance.web.xml_to_json(doc, (doc.nodeName.toLowerCase() !== 'kanban'));
|
||||
if ('id' in fvg.fields) {
|
||||
// Special case for id's
|
||||
var id_field = fvg.fields['id'];
|
||||
id_field.original_type = id_field.type;
|
||||
id_field.type = 'id';
|
||||
}
|
||||
_.each(fvg.fields, function(field) {
|
||||
_.each(field.views || {}, function(view) {
|
||||
postprocess(view);
|
||||
});
|
||||
});
|
||||
return fvg;
|
||||
}
|
||||
args = _.defaults(args, {
|
||||
toolbar: false,
|
||||
});
|
||||
var model = args.model;
|
||||
if (typeof model === 'string') {
|
||||
model = new instance.web.Model(args.model, args.context);
|
||||
}
|
||||
return args.model.call('fields_view_get', [args.view_id, args.view_type, model.context(), args.toolbar]).then(function(fvg) {
|
||||
return postprocess(fvg);
|
||||
});
|
||||
};
|
||||
|
||||
instance.web.xml_to_json = function(node, strip_whitespace) {
|
||||
switch (node.nodeType) {
|
||||
case 9:
|
||||
return instance.web.xml_to_json(node.documentElement, strip_whitespace);
|
||||
case 3:
|
||||
case 4:
|
||||
return node.data;
|
||||
break;
|
||||
return (strip_whitespace && node.data.trim() === '') ? undefined : node.data;
|
||||
case 1:
|
||||
var attrs = $(node).getAttributes();
|
||||
_.each(['domain', 'filter_domain', 'context', 'default_get'], function(key) {
|
||||
@@ -1408,7 +1441,9 @@ instance.web.xml_to_json = function(node) {
|
||||
return {
|
||||
tag: node.tagName.toLowerCase(),
|
||||
attrs: attrs,
|
||||
children: _.map(node.childNodes, instance.web.xml_to_json)
|
||||
children: _.compact(_.map(node.childNodes, function(node) {
|
||||
return instance.web.xml_to_json(node, strip_whitespace);
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1460,26 +1495,6 @@ instance.web.xml_to_str = function(node) {
|
||||
throw new Error(_t("Could not serialize XML"));
|
||||
}
|
||||
};
|
||||
instance.web.str_to_xml = function(s) {
|
||||
if (window.DOMParser) {
|
||||
var dp = new DOMParser();
|
||||
var r = dp.parseFromString(s, "text/xml");
|
||||
if (r.body && r.body.firstChild && r.body.firstChild.nodeName == 'parsererror') {
|
||||
throw new Error(_t("Could not parse string to xml"));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
var xDoc;
|
||||
try {
|
||||
xDoc = new ActiveXObject("MSXML2.DOMDocument");
|
||||
} catch (e) {
|
||||
throw new Error(_.str.sprintf( _t("Could not find a DOM Parser: %s"), e.message));
|
||||
}
|
||||
xDoc.async = false;
|
||||
xDoc.preserveWhiteSpace = true;
|
||||
xDoc.loadXML(s);
|
||||
return xDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for all the main views
|
||||
|
||||
@@ -436,7 +436,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="oe_leftbar" valign="top">
|
||||
<a class="oe_logo" href="#"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
|
||||
<a class="oe_logo" t-attf-href="/?ts=#{Date.now()}"><img t-att-src='_s + "/web/static/src/img/logo.png"'/></a>
|
||||
<div class="oe_secondary_menus_container"/>
|
||||
<div class="oe_footer">
|
||||
Powered by <a href="http://www.openerp.com" target="_blank"><span>OpenERP</span></a>
|
||||
@@ -462,6 +462,11 @@
|
||||
<p><a href="#">Click here to change your user's timezone.</a></p>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="WebClient.timezone_systray">
|
||||
<div class="oe_topbar_item oe_timezone_systray" title="Timezone mismatch">
|
||||
<span class="ui-icon ui-state-error ui-icon-alert"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="EmbedClient">
|
||||
<div class="openerp">
|
||||
@@ -1065,7 +1070,10 @@
|
||||
t-att-autofocus="widget.node.attrs.autofocus"
|
||||
t-att-id="widget.id_for_label">
|
||||
<t t-foreach="widget.values" t-as="option">
|
||||
<option><t t-esc="widget.node.attrs.placeholder" t-if="option[0] == false and widget.node.attrs.placeholder"/><t t-esc="option[1]" t-if="option[0] != false"/></option>
|
||||
<option>
|
||||
<t t-esc="widget.node.attrs.placeholder" t-if="option[0] === false and widget.node.attrs.placeholder"/>
|
||||
<t t-esc="option[1]" t-if="option[0] !== false"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</span>
|
||||
|
||||
@@ -482,11 +482,11 @@ openerp.testing.section('eval.edc.nonliterals', {
|
||||
});
|
||||
});
|
||||
test('date', {asserts: 1}, function (instance) {
|
||||
var d = "[('state','!=','cancel'),('opening_date','>',datetime.date.today().strftime('%Y-%m-%d'))]";
|
||||
var d = "[('state','!=','cancel'),('opening_date','>',context_today().strftime('%Y-%m-%d'))]";
|
||||
return instance.edc([d]).then(function (result) {
|
||||
var d = new Date();
|
||||
var today = _.str.sprintf("%04d-%02d-%02d",
|
||||
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
||||
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||
deepEqual(result.domain, [
|
||||
['state', '!=', 'cancel'],
|
||||
['opening_date', '>', today]
|
||||
@@ -494,14 +494,14 @@ openerp.testing.section('eval.edc.nonliterals', {
|
||||
});
|
||||
});
|
||||
test('delta', {asserts: 1}, function (instance) {
|
||||
var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(datetime.date.today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]";
|
||||
var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(context_today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]";
|
||||
return instance.edc([d]).then(function (result) {
|
||||
var d = new Date();
|
||||
var today = _.str.sprintf("%04d-%02d-%02d",
|
||||
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
||||
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||
d.setDate(d.getDate() - 15);
|
||||
var ago_15_d = _.str.sprintf("%04d-%02d-%02d",
|
||||
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
||||
d.getFullYear(), d.getMonth() + 1, d.getDate());
|
||||
deepEqual(result.domain, [
|
||||
['type', '=', 'in'],
|
||||
['day', '<=', today],
|
||||
|
||||
@@ -183,7 +183,7 @@ openerp.testing.section('list.edition', {
|
||||
}
|
||||
return [];
|
||||
});
|
||||
mock('/web/view/load', function () {
|
||||
mock('demo:fields_view_get', function () {
|
||||
return {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
@@ -191,15 +191,7 @@ openerp.testing.section('list.edition', {
|
||||
b: {type: 'char', string: "B"},
|
||||
c: {type: 'char', string: "C"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: {},
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}},
|
||||
{tag: 'field', attrs: {name: 'b'}},
|
||||
{tag: 'field', attrs: {name: 'c'}}
|
||||
]
|
||||
}
|
||||
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -247,7 +239,7 @@ openerp.testing.section('list.edition.events', {
|
||||
mock('demo:read', function () {
|
||||
return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }];
|
||||
});
|
||||
mock('/web/view/load', function () {
|
||||
mock('demo:fields_view_get', function () {
|
||||
return {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
@@ -255,15 +247,7 @@ openerp.testing.section('list.edition.events', {
|
||||
b: {type: 'char', string: "B"},
|
||||
c: {type: 'char', string: "C"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: {},
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}},
|
||||
{tag: 'field', attrs: {name: 'b'}},
|
||||
{tag: 'field', attrs: {name: 'c'}}
|
||||
]
|
||||
}
|
||||
arch: '<tree><field name="a"/><field name="b"/><field name="c"/></tree>',
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -320,19 +304,13 @@ openerp.testing.section('list.edition.onwrite', {
|
||||
templates: true,
|
||||
}, function (test) {
|
||||
test('record-to-read', {asserts: 4}, function (instance, $fix, mock) {
|
||||
mock('/web/view/load', function () {
|
||||
mock('demo:fields_view_get', function () {
|
||||
return {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
a: {type: 'char', string: "A"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: { on_write: 'on_write', colors: 'red:a == "foo"' },
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}}
|
||||
]
|
||||
}
|
||||
arch: '<tree on_write="on_write" colors="red:a == \'foo\'"><field name="a"/></tree>',
|
||||
};
|
||||
});
|
||||
mock('demo:read', function (args, kwargs) {
|
||||
|
||||
@@ -4,20 +4,13 @@ openerp.testing.section('list.buttons', {
|
||||
templates: true
|
||||
}, function (test) {
|
||||
test('record-deletion', {asserts: 2}, function (instance, $fix, mock) {
|
||||
mock('/web/view/load', function () {
|
||||
mock('demo:fields_view_get', function () {
|
||||
return {
|
||||
type: 'tree',
|
||||
fields: {
|
||||
a: {type: 'char', string: "A"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'tree',
|
||||
attrs: { },
|
||||
children: [
|
||||
{tag: 'field', attrs: {name: 'a'}},
|
||||
{tag: 'button', attrs: {type: 'object', name: 'foo'}}
|
||||
]
|
||||
}
|
||||
arch: '<tree><field name="a"/><button type="object" name="foo"/></tree>',
|
||||
};
|
||||
});
|
||||
mock('demo:read', function (args, kwargs) {
|
||||
|
||||
@@ -152,25 +152,14 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
|
||||
instance.dummy = {};
|
||||
instance.dummy.DummyWidget = instance.web.search.Field.extend(
|
||||
dummy_widget_attributes || {});
|
||||
if (!('/web/view/load' in instance.session.responses)) {
|
||||
instance.session.responses['/web/view/load'] = function () {
|
||||
if (!('dummy.model:fields_view_get' in instance.session.responses)) {
|
||||
instance.session.responses['dummy.model:fields_view_get'] = function () {
|
||||
return {
|
||||
type: 'search',
|
||||
fields: {
|
||||
dummy: {type: 'char', string: "Dummy"}
|
||||
},
|
||||
arch: {
|
||||
tag: 'search',
|
||||
attrs: {},
|
||||
children: [{
|
||||
tag: 'field',
|
||||
attrs: {
|
||||
name: 'dummy',
|
||||
widget: 'dummy'
|
||||
},
|
||||
children: []
|
||||
}]
|
||||
}
|
||||
arch: '<search><field name="dummy" widget="dummy"/></search>'
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -183,7 +172,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) {
|
||||
};
|
||||
};
|
||||
|
||||
var dataset = {model: 'dummy.model', get_context: function () { return {}; }};
|
||||
var dataset = new instance.web.DataSet(null, 'dummy.model');
|
||||
var view = new instance.web.SearchView(null, dataset, false, defaults);
|
||||
var self = this;
|
||||
view.on('invalid_search', self, function () {
|
||||
@@ -926,31 +915,16 @@ openerp.testing.section('filters', {
|
||||
rpc: 'mock',
|
||||
templates: true,
|
||||
setup: function (instance, $s, mock) {
|
||||
mock('/web/view/load', function () {
|
||||
mock('dummy.model:fields_view_get', function () {
|
||||
// view with a single group of filters
|
||||
return {
|
||||
type: 'search',
|
||||
fields: {},
|
||||
arch: {
|
||||
tag: 'search',
|
||||
attrs: {},
|
||||
children: [{
|
||||
tag: 'filter',
|
||||
attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] },
|
||||
children: []
|
||||
}, {
|
||||
tag: 'filter',
|
||||
attrs: {
|
||||
name: 'foo2',
|
||||
string: "Foo2",
|
||||
domain: [ ['foo', '=', '2'] ] },
|
||||
children: []
|
||||
}, {
|
||||
tag: 'filter',
|
||||
attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] },
|
||||
children: []
|
||||
}]
|
||||
}
|
||||
arch: '<search>' +
|
||||
'<filter string="Foo1" domain="[ [\'foo\', \'=\', \'1\'] ]"/>' +
|
||||
'<filter name="foo2" string="Foo2" domain="[ [\'foo\', \'=\', \'2\'] ]"/>' +
|
||||
'<filter string="Foo3" domain="[ [\'foo\', \'=\', \'3\'] ]"/>' +
|
||||
'</search>',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user