merge upstream
bzr revid: chs@openerp.com-20121205150654-c7ps8cygsx1zk1ua bzr revid: chs@openerp.com-20121207104954-xz35sdqgya18pgus bzr revid: chs@openerp.com-20121210140847-yoobt0twf9kg9yau
This commit is contained in:
commit
8b4f341c94
|
@ -1,5 +1,6 @@
|
|||
import http
|
||||
import controllers
|
||||
import cli
|
||||
from . import ir_module
|
||||
|
||||
wsgi_postload = http.wsgi_postload
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import test_js
|
|
@ -0,0 +1,35 @@
|
|||
import logging
|
||||
import optparse
|
||||
import sys
|
||||
|
||||
import unittest2
|
||||
|
||||
import openerp
|
||||
import openerp.addons.web.tests
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class TestJs(openerp.cli.Command):
|
||||
def run(self, args):
|
||||
self.parser = parser = optparse.OptionParser()
|
||||
parser.add_option("-d", "--database", dest="db_name", default=False, help="specify the database name")
|
||||
parser.add_option("--xmlrpc-port", dest="xmlrpc_port", default=8069, help="specify the TCP port for the XML-RPC protocol", type="int")
|
||||
# proably need to add both --superadmin-password and --database-admin-password
|
||||
self.parser.parse_args(args)
|
||||
|
||||
# test ony uses db_name xmlrpc_port admin_passwd, so use the server one for the actual parsing
|
||||
|
||||
config = openerp.tools.config
|
||||
config.parse_config(args)
|
||||
# needed until runbot is fixed
|
||||
config['db_password'] = config['admin_passwd']
|
||||
|
||||
# run js tests
|
||||
openerp.netsvc.init_alternative_logger()
|
||||
suite = unittest2.TestSuite()
|
||||
suite.addTests(unittest2.TestLoader().loadTestsFromModule(openerp.addons.web.tests.test_js))
|
||||
r = unittest2.TextTestRunner(verbosity=2).run(suite)
|
||||
if r.errors or r.failures:
|
||||
sys.exit(1)
|
||||
|
||||
# vim:et:ts=4:sw=4:
|
|
@ -921,6 +921,10 @@ class Menu(openerpweb.Controller):
|
|||
def load(self, req):
|
||||
return {'data': self.do_load(req)}
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def load_needaction(self, req, menu_ids):
|
||||
return {'data': self.do_load_needaction(req, menu_ids)}
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def get_user_roots(self, req):
|
||||
return self.do_get_user_roots(req)
|
||||
|
@ -959,7 +963,7 @@ class Menu(openerpweb.Controller):
|
|||
Menus = req.session.model('ir.ui.menu')
|
||||
|
||||
fields = ['name', 'sequence', 'parent_id', 'action',
|
||||
'needaction_enabled', 'needaction_counter']
|
||||
'needaction_enabled']
|
||||
menu_roots = Menus.read(self.do_get_user_roots(req), fields, req.context)
|
||||
menu_root = {
|
||||
'id': False,
|
||||
|
@ -996,6 +1000,20 @@ class Menu(openerpweb.Controller):
|
|||
|
||||
return menu_root
|
||||
|
||||
def do_load_needaction(self, req, menu_ids=False):
|
||||
""" Loads needaction counters for all or some specific menu ids.
|
||||
|
||||
:return: needaction data
|
||||
:rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
|
||||
"""
|
||||
Menus = req.session.model('ir.ui.menu')
|
||||
|
||||
if menu_ids == False:
|
||||
menu_ids = Menus.search([('needaction_enabled', '=', True)], context=req.context)
|
||||
|
||||
menu_needaction_data = Menus.get_needaction_data(menu_ids, req.context)
|
||||
return menu_needaction_data
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def action(self, req, menu_id):
|
||||
actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
|
||||
|
@ -1119,41 +1137,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')
|
||||
|
@ -1177,10 +1160,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,18 +11,19 @@ Contents:
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
presentation
|
||||
module
|
||||
widget
|
||||
|
||||
async
|
||||
rpc
|
||||
qweb
|
||||
client_action
|
||||
|
||||
testing
|
||||
|
||||
widget
|
||||
qweb
|
||||
rpc
|
||||
client_action
|
||||
form_view
|
||||
search_view
|
||||
list_view
|
||||
form_view
|
||||
|
||||
changelog-7.0
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -1,79 +1,546 @@
|
|||
|
||||
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
|
||||
The rationale behind using QWeb instead of a more popular template syntax is
|
||||
that its extension mechanism is very similar to the openerp view inheritance
|
||||
mechanism. Like openerp views a QWeb template is an xml tree and therefore
|
||||
xpath or dom manipulations are easy to performs on it.
|
||||
|
||||
Here is a typical QWeb file:
|
||||
Here's an example demonstrating most of the basic QWeb features:
|
||||
|
||||
::
|
||||
.. 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
|
||||
--------
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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 {
|
||||
|
@ -795,6 +795,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_dialog_warning {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -2259,6 +2273,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;
|
||||
|
|
|
@ -671,9 +671,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
|
||||
// }}}
|
||||
// CrashManager {{{
|
||||
.oe_dialog_warning
|
||||
|
@ -1797,6 +1809,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 || []);
|
||||
|
|
|
@ -800,10 +800,18 @@ instance.web.client_actions.add("change_password", "instance.web.ChangePassword"
|
|||
instance.web.Menu = instance.web.Widget.extend({
|
||||
template: 'Menu',
|
||||
init: function() {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.has_been_loaded = $.Deferred();
|
||||
this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
|
||||
this.data = {data:{children:[]}};
|
||||
this.on("menu_loaded", this, function (e) {
|
||||
// launch the fetch of needaction counters, asynchronous
|
||||
this.rpc("/web/menu/load_needaction", {menu_ids: false}).done(function(r) {
|
||||
self.on_needaction_loaded(r);
|
||||
});
|
||||
});
|
||||
|
||||
},
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
|
@ -823,7 +831,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();
|
||||
}
|
||||
|
@ -837,6 +845,17 @@ instance.web.Menu = instance.web.Widget.extend({
|
|||
this.trigger('menu_loaded', data);
|
||||
this.has_been_loaded.resolve();
|
||||
},
|
||||
on_needaction_loaded: function(data) {
|
||||
var self = this;
|
||||
this.needaction_data = data;
|
||||
_.each(this.needaction_data.data, function (item, menu_id) {
|
||||
var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
|
||||
$item.remove('oe_menu_counter');
|
||||
if (item.needaction_counter && item.needaction_counter > 0) {
|
||||
$item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
|
||||
}
|
||||
});
|
||||
},
|
||||
limit_entries: function() {
|
||||
var maximum_visible_links = this.maximum_visible_links;
|
||||
if (maximum_visible_links === 'auto') {
|
||||
|
@ -1107,7 +1126,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) {
|
||||
|
@ -1168,26 +1186,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;
|
||||
|
|
|
@ -628,7 +628,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);
|
||||
|
@ -398,14 +398,9 @@ openerp.web.pyeval = function (instance) {
|
|||
now: py.classmethod.fromJSON(function () {
|
||||
var d = new Date();
|
||||
return py.PY_call(datetime.datetime,
|
||||
[d.getFullYear(), d.getMonth() + 1, d.getDate(),
|
||||
d.getHours(), d.getMinutes(), d.getSeconds(),
|
||||
d.getMilliseconds() * 1000]);
|
||||
}),
|
||||
today: py.classmethod.fromJSON(function () {
|
||||
var d = new Date();
|
||||
return py.PY_call(datetime.datetime,
|
||||
[d.getFullYear(), d.getMonth() + 1, d.getDate()]);
|
||||
[d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
|
||||
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
|
||||
d.getUTCMilliseconds() * 1000]);
|
||||
}),
|
||||
combine: py.classmethod.fromJSON(function () {
|
||||
var args = py.PY_parseArgs(arguments, 'date time');
|
||||
|
@ -439,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.getFullYear(), d.getMonth() + 1, d.getDate()]);
|
||||
}),
|
||||
__eq__: function (other) {
|
||||
return (this.year === other.year
|
||||
&& this.month === other.month
|
||||
|
@ -479,6 +469,17 @@ openerp.web.pyeval = function (instance) {
|
|||
return py.PY_call(datetime.date, [year, month, day])
|
||||
}
|
||||
});
|
||||
/**
|
||||
Returns the current local date, which means the date on the client (which can be different
|
||||
compared to the date of the server).
|
||||
|
||||
@return {datetime.date}
|
||||
*/
|
||||
var context_today = function() {
|
||||
var d = new Date();
|
||||
return py.PY_call(
|
||||
datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]);
|
||||
};
|
||||
datetime.time = py.type('time', null, {
|
||||
__init__: function () {
|
||||
var zero = py.float.fromJSON(0);
|
||||
|
@ -691,6 +692,7 @@ openerp.web.pyeval = function (instance) {
|
|||
return {
|
||||
uid: py.float.fromJSON(instance.session.uid),
|
||||
datetime: datetime,
|
||||
context_today: context_today,
|
||||
time: time,
|
||||
relativedelta: relativedelta,
|
||||
current_date: py.PY_call(
|
||||
|
|
|
@ -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
|
||||
|
@ -2930,6 +2910,15 @@ instance.web.form.M2ODialog = instance.web.Dialog.extend({
|
|||
|
||||
instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, {
|
||||
template: "FieldMany2One",
|
||||
events: {
|
||||
'keydown input': function (e) {
|
||||
switch (e.which) {
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.DOWN:
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
},
|
||||
init: function(field_manager, node) {
|
||||
this._super(field_manager, node);
|
||||
instance.web.form.CompletionFieldMixin.init.call(this);
|
||||
|
@ -5260,6 +5249,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();
|
||||
|
@ -939,12 +952,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.
|
|||
field = $target.closest('td').data('field'),
|
||||
$row = $target.closest('tr'),
|
||||
record_id = self.row_id($row);
|
||||
|
||||
if ($target.attr('disabled')) {
|
||||
return;
|
||||
}
|
||||
$target.attr('disabled', 'disabled');
|
||||
|
||||
// note: $.data converts data to number if it's composed only
|
||||
// of digits, nice when storing actual numbers, not nice when
|
||||
// storing strings composed only of digits. Force the action
|
||||
// name to be a string
|
||||
$(self).trigger('action', [field.toString(), record_id, function (id) {
|
||||
$target.removeAttr('disabled');
|
||||
return self.reload_record(self.records.get(id));
|
||||
}]);
|
||||
})
|
||||
|
|
|
@ -105,8 +105,15 @@ openerp.web.list_editable = function (instance) {
|
|||
* Replace do_search to handle editability process
|
||||
*/
|
||||
do_search: function(domain, context, group_by) {
|
||||
this._context_editable = !!context.set_editable;
|
||||
this._super.apply(this, arguments);
|
||||
var self=this, _super = self._super, args=arguments;
|
||||
var ready = this.editor.is_editing()
|
||||
? this.cancel_edition(true)
|
||||
: $.when();
|
||||
|
||||
return ready.then(function () {
|
||||
self._context_editable = !!context.set_editable;
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Replace do_add_record to handle editability (and adding new record
|
||||
|
|
|
@ -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
|
||||
|
@ -1204,13 +1198,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) {
|
||||
|
@ -1385,12 +1377,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) {
|
||||
|
@ -1403,7 +1436,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);
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1455,26 +1490,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
|
||||
|
|
|
@ -388,14 +388,16 @@
|
|||
t-att-data-action-model="menu.action ? menu.action.split(',')[0] : ''"
|
||||
t-att-data-action-id="menu.action ? menu.action.split(',')[1] : ''">
|
||||
<t t-esc="menu.name"/>
|
||||
<t t-if="menu.needaction_enabled and menu.needaction_counter">
|
||||
<div class="oe_tag oe_tag_dark oe_menu_counter">
|
||||
<t t-if="menu.needaction_counter > 99"> 99+ </t><t t-if="menu.needaction_counter <= 99"> <t t-esc="menu.needaction_counter"/> </t>
|
||||
</div>
|
||||
</t>
|
||||
</a>
|
||||
</t>
|
||||
|
||||
<t t-name="Menu.needaction_counter">
|
||||
<div class="oe_tag oe_tag_dark oe_menu_counter">
|
||||
<t t-if="widget.needaction_counter > 99"> 99+ </t>
|
||||
<t t-if="widget.needaction_counter <= 99"> <t t-esc="widget.needaction_counter"/> </t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="UserMenu">
|
||||
<span class="oe_user_menu oe_topbar_item oe_dropdown_toggle oe_dropdown_arrow">
|
||||
<img class="oe_topbar_avatar" t-att-data-default-src="_s + '/web/static/src/img/user_menu_avatar.png'"/>
|
||||
|
@ -436,7 +438,8 @@
|
|||
</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>
|
||||
<t t-set="debug" t-value="__debug__ ? '&debug' : ''"/>
|
||||
<a class="oe_logo" t-attf-href="/?ts=#{Date.now()}#{debug}"><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 +465,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">
|
||||
|
@ -1049,7 +1057,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>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue