Christophe Simonis 2012-12-10 15:08:47 +01:00
commit 8b4f341c94
272 changed files with 115917 additions and 82586 deletions

View File

@ -1,5 +1,6 @@
import http
import controllers
import cli
from . import ir_module
wsgi_postload = http.wsgi_postload

View File

@ -0,0 +1 @@
import test_js

35
addons/web/cli/test_js.py Normal file
View File

@ -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:

View File

@ -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"

View File

@ -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

View File

@ -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

281
addons/web/doc/module.rst Normal file
View File

@ -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

View File

View File

@ -0,0 +1,7 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['base'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -0,0 +1,6 @@
.openerp .oe_web_example {
color: white;
background-color: black;
height: 100%;
font-size: 400%;
}

View File

@ -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
+}

View File

@ -0,0 +1,2 @@
// static/src/js/first_module.js
console.log("Debug statement: file loaded");

View File

@ -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");
+};

View File

@ -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);
+ };
};

View File

@ -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();
+ }
+ });
};

View File

@ -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();
- }
});
};

View File

@ -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');
+ },
});
};

View File

@ -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();
+ }
});
};

View File

@ -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>

View File

@ -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>

View File

@ -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``.

View File

@ -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

2572
addons/web/i18n/es_DO.po Normal file

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

View File

@ -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;
}
});

View File

@ -1,5 +1,5 @@
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
node: 2b62048dd1d86c45e4bf0442b5da4122534132bf
node: 142c22b230636674a0cee6bc29e6975f0f1600a5
branch: default
latesttag: 0.7
latesttagdistance: 5
latesttagdistance: 9

View File

@ -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
---------------

View File

@ -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));
}

View File

@ -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]) {

View File

@ -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;

View File

@ -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

View File

@ -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 || []);

View File

@ -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) {

View File

@ -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;

View File

@ -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...")]
];
};

View File

@ -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(

View File

@ -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) {

View File

@ -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()

View File

@ -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});

View File

@ -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));
}]);
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 &gt; 99"> 99+ </t><t t-if="menu.needaction_counter &lt;= 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 &gt; 99"> 99+ </t>
<t t-if="widget.needaction_counter &lt;= 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__ ? '&amp;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