merge upstream

bzr revid: chs@openerp.com-20121114223538-qp233873r33uh3e3
This commit is contained in:
Christophe Simonis
2012-11-14 23:35:38 +01:00
33 changed files with 954 additions and 793 deletions
+2 -1
View File
@@ -39,7 +39,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/underscore/underscore.string.js",
"static/lib/backbone/backbone.js",
"static/lib/cleditor/jquery.cleditor.js",
"static/lib/py.js/lib/py.js",
"static/lib/py.js/lib/py.js",
"static/src/js/boot.js",
"static/src/js/corelib.js",
"static/src/js/coresetup.js",
@@ -68,4 +68,5 @@ This module provides the core of the OpenERP Web Client.
'qweb' : [
"static/src/xml/*.xml",
],
'bootstrap': True,
}
+51 -204
View File
@@ -81,57 +81,6 @@ def rjsmin(script):
).strip()
return result
def sass2scss(src):
# Validated by diff -u of sass2scss against:
# sass-convert -F sass -T scss openerp.sass openerp.scss
block = []
sass = ('', block)
reComment = re.compile(r'//.*$')
reIndent = re.compile(r'^\s+')
reIgnore = re.compile(r'^\s*(//.*)?$')
reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
lastLevel = 0
prevBlocks = {}
for l in src.split('\n'):
l = l.rstrip()
if reIgnore.search(l): continue
l = reComment.sub('', l)
l = l.rstrip()
indent = reIndent.match(l)
level = indent.end() if indent else 0
l = l[level:]
if level>lastLevel:
prevBlocks[lastLevel] = block
newBlock = []
block[-1] = (block[-1], newBlock)
block = newBlock
elif level<lastLevel:
block = prevBlocks[level]
lastLevel = level
if not l: continue
# Fixes
for ereg, repl in reFixes.items():
l = ereg.sub(repl if type(repl)==str else repl(), l)
block.append(l)
def write(sass, level=-1):
out = ""
indent = ' '*level
if type(sass)==tuple:
if level>=0:
out += indent+sass[0]+" {\n"
for e in sass[1]:
out += write(e, level+1)
if level>=0:
out = out.rstrip(" \n")
out += ' }\n'
if level==0:
out += "\n"
else:
out += indent+sass+";\n"
return out
return write(sass)
def db_list(req):
dbs = []
proxy = req.session.proxy("db")
@@ -142,6 +91,17 @@ def db_list(req):
dbs = [i for i in dbs if re.match(r, i)]
return dbs
def db_monodb(req):
# if only one db is listed returns it else return False
try:
dbs = db_list(req)
if len(dbs) == 1:
return dbs[0]
except xmlrpclib.Fault:
# ignore access denied
pass
return False
def module_topological_sort(modules):
""" Return a list of module names sorted so that their dependencies of the
modules are listed before the module itself
@@ -231,22 +191,14 @@ def module_installed_bypass_session(dbname):
def module_boot(req):
server_wide_modules = openerp.conf.server_wide_modules or ['web']
return [m for m in server_wide_modules if m in openerpweb.addons_manifest]
# TODO the following will be enabled once we separate the module code and translation loading
serverside = []
dbside = []
for i in server_wide_modules:
if i in openerpweb.addons_manifest:
serverside.append(i)
# if only one db load every module at boot
dbs = []
try:
dbs = db_list(req)
except xmlrpclib.Fault:
# ignore access denied
pass
if len(dbs) == 1:
dbside = module_installed_bypass_session(dbs[0])
monodb = db_monodb(req)
if monodb:
dbside = module_installed_bypass_session(monodb)
dbside = [i for i in dbside if i not in serverside]
addons = serverside + dbside
return addons
@@ -549,7 +501,7 @@ def _local_web_translations(trans_file):
messages.append({'id': x.id, 'string': x.string})
return messages
def from_elementtree(el, preserve_whitespaces=False):
def xml2json_from_elementtree(el, preserve_whitespaces=False):
""" xml2json-direct
Simple and straightforward XML-to-JSON converter in Python
New BSD Licensed
@@ -569,13 +521,12 @@ def from_elementtree(el, preserve_whitespaces=False):
if el.text and (preserve_whitespaces or el.text.strip() != ''):
kids.append(el.text)
for kid in el:
kids.append(from_elementtree(kid, preserve_whitespaces))
kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
kids.append(kid.tail)
res["children"] = kids
return res
def content_disposition(filename, req):
filename = filename.encode('utf8')
escaped = urllib2.quote(filename)
@@ -612,8 +563,7 @@ html_template = """<!DOCTYPE html>
</head>
<body>
<!--[if lte IE 8]>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
<script>
var test = function() {
CFInstall.check({
@@ -741,21 +691,20 @@ class WebClient(openerpweb.Controller):
until we have established a valid session. This is meant only
for translating the login page and db management chrome, using
the browser's language. """
lang = req.httprequest.accept_languages.best or 'en'
# For performance reasons we only load a single translation, so for
# sub-languages (that should only be partially translated) we load the
# main language PO instead - that should be enough for the login screen.
if '-' in lang: # RFC2616 uses '-' separators for sublanguages
lang = lang.split('-')[0]
lang = req.lang.split('_')[0]
translations_per_module = {}
for addon_name in mods:
addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
if not os.path.exists(f_name):
continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
if openerpweb.addons_manifest[addon_name].get('bootstrap'):
addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
if not os.path.exists(f_name):
continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
return {"modules": translations_per_module,
"lang_parameters": None}
@@ -812,21 +761,25 @@ class Database(openerpweb.Controller):
@openerpweb.jsonrequest
def get_list(self, req):
dbs = db_list(req)
return {"db_list": dbs}
return db_list(req)
@openerpweb.jsonrequest
def create(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
create_attrs = (
return req.session.proxy("db").create_database(
params['super_admin_pwd'],
params['db_name'],
bool(params.get('demo_data')),
params['db_lang'],
params['create_admin_pwd']
)
params['create_admin_pwd'])
return req.session.proxy("db").create_database(*create_attrs)
@openerpweb.jsonrequest
def duplicate(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
return req.session.proxy("db").duplicate_database(
params['super_admin_pwd'],
params['db_original_name'],
params['db_name'])
@openerpweb.jsonrequest
def drop(self, req, fields):
@@ -1296,17 +1249,6 @@ class DataSet(openerpweb.Controller):
m.write(id, { field: i + offset })
return True
class DataGroup(openerpweb.Controller):
_cp_path = "/web/group"
@openerpweb.jsonrequest
def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
Model = req.session.model(model)
context, domain = eval_context_and_domain(req.session, req.context, domain)
return Model.read_group(
domain or [], fields, group_by_fields, 0, False,
dict(context, group_by=group_by_fields), sort or False)
class View(openerpweb.Controller):
_cp_path = "/web/view"
@@ -1340,7 +1282,7 @@ class View(openerpweb.Controller):
xml = self.transform_view(arch, session, evaluation_context)
else:
xml = ElementTree.fromstring(arch)
fvg['arch'] = from_elementtree(xml, preserve_whitespaces)
fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
if 'id' in fvg['fields']:
# Special case for id's
@@ -1514,11 +1456,24 @@ class Binary(openerpweb.Controller):
try:
if not id:
res = Model.default_get([field], context).get(field)
image_data = base64.b64decode(res)
image_base64 = res
else:
res = Model.read([id], [last_update, field], context)[0]
retag = hashlib.md5(res.get(last_update)).hexdigest()
image_data = base64.b64decode(res.get(field))
image_base64 = res.get(field)
if kw.get('resize'):
resize = kw.get('resize').split(',');
if len(resize) == 2 and int(resize[0]) and int(resize[1]):
width = int(resize[0])
height = int(resize[1])
# resize maximum 500*500
if width > 500: width = 500
if height > 500: height = 500
image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
image_data = base64.b64decode(image_base64)
except (TypeError, xmlrpclib.Fault):
image_data = self.placeholder(req)
headers.append(('ETag', retag))
@@ -2008,112 +1963,4 @@ class Reports(View):
('Content-Length', len(report))],
cookies={'fileToken': int(token)})
class Import(View):
_cp_path = "/web/import"
def fields_get(self, req, model):
Model = req.session.model(model)
fields = Model.fields_get(False, req.session.eval_context(req.context))
return fields
@openerpweb.httprequest
def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
try:
data = list(csv.reader(
csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
except csv.Error, e:
csvfile.seek(0)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {
'message': 'Error parsing CSV file: %s' % e,
# decodes each byte to a unicode character, which may or
# may not be printable, but decoding will succeed.
# Otherwise simplejson will try to decode the `str` using
# utf-8, which is very likely to blow up on characters out
# of the ascii range (in range [128, 256))
'preview': csvfile.read(200).decode('iso-8859-1')}}))
try:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps(
{'records': data[:10]}, encoding=csvcode))
except UnicodeDecodeError:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({
'message': u"Failed to decode CSV file using encoding %s, "
u"try switching to a different encoding" % csvcode
}))
@openerpweb.httprequest
def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
meta):
modle_obj = req.session.model(model)
skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
simplejson.loads(meta))
error = None
if not (csvdel and len(csvdel) == 1):
error = u"The CSV delimiter must be a single character"
if not indices and fields:
error = u"You must select at least one field to import"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
# skip ignored records (@skip parameter)
# then skip empty lines (not valid csv)
# nb: should these operations be reverted?
rows_to_import = itertools.ifilter(
None,
itertools.islice(
csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
skip, None))
# if only one index, itemgetter will return an atom rather than a tuple
if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
else: mapper = operator.itemgetter(*indices)
data = None
error = None
try:
# decode each data row
data = [
[record.decode(csvcode) for record in row]
for row in itertools.imap(mapper, rows_to_import)
# don't insert completely empty rows (can happen due to fields
# filtering in case of e.g. o2m content rows)
if any(row)
]
except UnicodeDecodeError:
error = u"Failed to decode CSV file using encoding %s" % csvcode
except csv.Error, e:
error = u"Could not process CSV file: %s" % e
# If the file contains nothing,
if not data:
error = u"File to import is empty"
if error:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message': error}}))
try:
(code, record, message, _nope) = modle_obj.import_data(
fields, data, 'init', '', False,
req.session.eval_context(req.context))
except xmlrpclib.Fault, e:
error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error':error}))
if code != -1:
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'success':True}))
msg = u"Error during import: %s\n\nTrying to import record %r" % (
message, record)
return '<script>window.top.%s(%s);</script>' % (
jsonp, simplejson.dumps({'error': {'message':msg}}))
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
+16 -122
View File
@@ -26,8 +26,7 @@ Deferreds
---------
Deferreds are a form of `promises`_. OpenERP Web currently uses
`jQuery's deferred`_, but any `CommonJS Promises/A`_ implementation
should work.
`jQuery's deferred`_.
The core idea of deferreds is that potentially asynchronous methods
will return a :js:class:`Deferred` object instead of an arbitrary
@@ -44,9 +43,8 @@ directly to asynchronous methods is the ability to :ref:`compose them
Using deferreds
~~~~~~~~~~~~~~~
`CommonJS Promises/A`_ deferreds have only one method of importance:
:js:func:`Deferred.then`. This method is used to attach new callbacks
to the deferred object.
Deferreds's most important method is :js:func:`Deferred.then`. It is
used to attach new callbacks to the deferred object.
* the first parameter attaches a success callback, called when the
deferred object is successfully resolved and provided with the
@@ -178,9 +176,9 @@ Deferred chaining
A second useful composition is starting an asynchronous operation as
the result of an other asynchronous operation, and wanting the result
of both: :js:func:`Deferred.then` returns the deferred on which it was
called, so handle e.g. OpenERP's search/read sequence with this would
require something along the lines of:
of both: with the tools described so far, handling e.g. OpenERP's
search/read sequence with this would require something along the lines
of:
.. code-block:: javascript
@@ -195,21 +193,14 @@ require something along the lines of:
While it doesn't look too bad for trivial code, this quickly gets
unwieldy.
Instead, jQuery provides a tool to handle this kind of chains:
:js:func:`Deferred.pipe`.
:js:func:`~Deferred.pipe` has the same signature as
:js:func:`~Deferred.then` and could be used in the same manner
provided its return value was not used.
It differs from :js:func:`~Deferred.then` in two ways: it returns a
new promise object, not the one it was called with, and the return
values of the callbacks is actually important to it: whichever
callback is called,
But :js:func:`~Deferred.then` also allows handling this kind of
chains: it returns a new promise object, not the one it was called
with, and the return values of the callbacks is actually important to
it: whichever callback is called,
* If the callback is not set (not provided or left to null), the
resolution or rejection value(s) is simply forwarded to
:js:func:`~Deferred.pipe`'s promise (it's essentially a noop)
:js:func:`~Deferred.then`'s promise (it's essentially a noop)
* If the callback is set and does not return an observable object (a
deferred or a promise), the value it returns (``undefined`` if it
@@ -217,7 +208,7 @@ callback is called,
.. code-block:: javascript
promise.pipe(function () {
promise.then(function () {
console.log('called');
});
@@ -234,7 +225,7 @@ callback is called,
.. code-block:: javascript
return Model.search(condition).pipe(function (ids) {
return Model.search(condition).then(function (ids) {
return Model.read(ids, fields);
});
@@ -243,108 +234,11 @@ callback is called,
will be resolved with ``read``'s resolution values if the chain
executes correctly.
:js:func:`~Deferred.pipe` is also useful to adapt third-party
:js:func:`~Deferred.then` is also useful to adapt third-party
promise-based APIs, in order to filter their resolution value counts
for instance (to take advantage of :js:func:`when` 's special treatment
of single-value promises).
for instance (to take advantage of :js:func:`when` 's special
treatment of single-value promises).
jQuery.Deferred API
~~~~~~~~~~~~~~~~~~~
.. js:function:: when(deferreds…)
:param deferreds: deferred objects to multiplex
:returns: a multiplexed deferred
:rtype: :js:class:`Deferred`
.. js:class:: Deferred
.. js:function:: Deferred.then(doneCallback[, failCallback])
Attaches new callbacks to the resolution or rejection of the
deferred object. Callbacks are executed in the order they are
attached to the deferred.
To provide only a failure callback, pass ``null`` as the
``doneCallback``, to provide only a success callback the
second argument can just be ignored (and not passed at all).
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.done(doneCallback)
Attaches a new success callback to the deferred, shortcut for
``deferred.then(doneCallback)``.
This is a jQuery extension to `CommonJS Promises/A`_ providing
little value over calling :js:func:`~Deferred.then` directly,
it should be avoided.
:param doneCallback: function called when the deferred is resolved
:type doneCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.fail(failCallback)
Attaches a new failure callback to the deferred, shortcut for
``deferred.then(null, failCallback)``.
A second jQuery extension to `Promises/A <CommonJS
Promises/A>`_. Although it provides more value than
:js:func:`~Deferred.done`, it still is not much and should be
avoided as well.
:param failCallback: function called when the deferred is rejected
:type failCallback: Function
:returns: the deferred object on which it was called
:rtype: :js:class:`Deferred`
.. js:function:: Deferred.promise()
Returns a read-only view of the deferred object, with all
mutators (resolve and reject) methods removed.
.. js:function:: Deferred.resolve(value…)
Called to resolve a deferred, any value provided will be
passed onto the success handlers of the deferred object.
Resolving a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.reject(value…)
Called to reject (fail) a deferred, any value provided will be
passed onto the failure handler of the deferred object.
Rejecting a deferred which has already been resolved or
rejected has no effect.
.. js:function:: Deferred.pipe(doneFilter[, failFilter])
Filters the result of a deferred, able to transform a success
into failure and a failure into success, or to delay
resolution further.
.. [#] or simply calling :js:class:`Deferred` as a function, the
result is the same
.. [#] or not-promises, the `CommonJS Promises/B`_ role of
:js:func:`when` is to be able to treat values and promises
uniformly: :js:func:`when` will pass promises through directly,
but non-promise values and objects will be transformed into a
resolved promise (resolving themselves with the value itself).
jQuery's :js:func:`when` keeps this behavior making deferreds
easy to build from "static" values, or allowing defensive code
where expected promises are wrapped in :js:func:`when` just in
case.
.. _promises: http://en.wikipedia.org/wiki/Promise_(programming)
.. _jQuery's deferred: http://api.jquery.com/category/deferred-object/
+1 -1
View File
@@ -15,7 +15,7 @@ DataSet -> Model
----------------
The 6.1 ``DataSet`` API has been deprecated in favor of the smaller
and more orthogonal :doc:`Model </rpc>` API, which more closely
and more orthogonal :doc:`Model </dev_rpc>` API, which more closely
matches the API in OpenERP Web's Python side and in OpenObject addons
and removes most stateful behavior of DataSet.
@@ -1,7 +1,7 @@
.. highlight:: javascript
Creating a new client action
============================
Client actions
==============
Client actions are the client-side version of OpenERP's "Server
Actions": instead of allowing for semi-arbitrary code to be executed
@@ -1,6 +1,6 @@
QWeb Cookbook
=============
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:
@@ -1,5 +1,5 @@
User Interaction: Widget
========================
Widget
======
This is the base class for all visual components. It corresponds to an MVC
view. It provides a number of services to handle a section of a page:
+10 -11
View File
@@ -12,20 +12,19 @@ Contents:
:maxdepth: 1
presentation
async
dev_rpc
dev_widget
dev_qweb
dev_client_action
internal_form
internal_list
internal_search
changelog-7.0
async
rpc
widget
qweb
search-view
list-view
form-notes
guides/client-action
Indices and tables
==================
@@ -451,7 +451,7 @@ formview, delegating instead to its
created.
The result should be a valid form view, see :doc:`Form Notes
<form-notes>` for various peculiarities of the form view
<internal_form>` for various peculiarities of the form view
format.
:param editor: editor object asking for the view
+14 -8
View File
@@ -7,17 +7,17 @@ Prerequisites
Prerequisites to code addons for the OpenERP Web Client:
- Html
- Css
- Javascript
- HTML5
- CSS
- Javascript (Ecmascript 5)
- jQuery
Once you know all this, that's only the beginning. Most usages of Javascript/jQuery are small hacks to make a web page nicer. The OpenERP Client Web is different: it's a web application, not a web site. It doesn't have multiple pages generated by a server side code. Only one unique empty page is loaded and all the html is generated by Javascript code, the page is never reloaded. If you never developed that kind of application you still have lot of good practices to learn.
Recommendations
---------------
Generic Guidelines
------------------
First, here are the 5 golden rules when you create web 2.0 applications:
First, here are some generic recommandations about web 2.0 applications:
* **Do not use ids**. Html ids are evil, the key anti-feature that makes your components non-reusable. You want to identify a dom part? Save a jQuery object over that dom element. If you really need to use ids, use _.uniqueId(), but 99% of the time you don't need to, classes are sufficient.
* **Do not use predictable css class names** like "content" or "navigation", ten other developers will have the same idea than you and there will be clashes. A simple way to avoid this is to use prefixes. For example, if you need a css class for the button named "new" that is contained in the form view which is itself contained in the OpenERP application, name it "oe-form-view-new-button".
@@ -25,12 +25,18 @@ First, here are the 5 golden rules when you create web 2.0 applications:
* As a general advice, **Never assume you own the page**. When you create a component, it is never unique and is always surrounded by a bunch of crazy html. You have to do with it.
* **Learn how to use jQuery's deferreds** [1]_. That concept may seem over-complicated, but the experience tell us that it is nearly impossible to create big-size javascript applications without that.
OpenERP guidelines
------------------
More recommendations related to the specific case of the OpenERP Web Client:
* Your components should inherit from the *Widget* class.
* The code should conform to EcmasScript 5 without the "use strict" mode as we support IE9.
* Your components should inherit from the *Widget* class. And use *Widget* 's methods (*appendTo*, *replace*,...) to insert your component and its content into the dom.
* Use QWeb templates for html rendering.
* Use *Widget* 's methods (*appendTo*, *replace*,...) to insert your component and its content into the dom.
* All css classes should have the prefix *oe-* .
* Functions that call rpc() should return a deferred, even if it calls it indirectly. So a function that calls a function that calls a function that calls rpc() should return a deferred too.
.. [1] http://api.jquery.com/category/deferred-object/
+26 -2
View File
@@ -21,6 +21,7 @@ import urlparse
import uuid
import xmlrpclib
import babel.core
import simplejson
import werkzeug.contrib.sessions
import werkzeug.datastructures
@@ -91,9 +92,25 @@ class WebRequest(object):
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
self.session = self.httpsession.get(self.session_id)
if not self.session:
self.httpsession[self.session_id] = self.session = session.OpenERPSession()
self.session = session.OpenERPSession()
self.httpsession[self.session_id] = self.session
self.context = self.params.pop('context', None)
self.debug = self.params.pop('debug', False) != False
self.debug = self.params.pop('debug', False) is not False
# Determine self.lang
lang = self.params.get('lang', None)
if lang is None:
lang = self.session.eval_context(self.context).get('lang')
if lang is None:
lang = self.httprequest.cookies.get('lang')
if lang is None:
lang = self.httprequest.accept_languages.best
if lang is None:
lang = 'en_US'
# tranform 2 letters lang like 'en' into 5 letters like 'en_US'
lang = babel.core.LOCALE_ALIASES.get(lang, lang)
# we use _ as seprator where RFC2616 uses '-'
self.lang = lang.replace('-', '_')
class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP.
@@ -210,6 +227,10 @@ class JsonRequest(WebRequest):
_logger.debug("<--\n%s", pprint.pformat(response))
if jsonp:
# If we use jsonp, that's mean we are called from another host
# Some browser (IE and Safari) do no allow third party cookies
# We need then to manage http sessions manually.
response['httpsessionid'] = self.httpsession.sid
mime = 'application/javascript'
body = "%s(%s);" % (jsonp, simplejson.dumps(response, cls=nonliterals.NonLiteralEncoder),)
else:
@@ -347,6 +368,9 @@ def session_context(request, storage_path, session_cookie='httpsessionid'):
STORES[storage_path] = session_store, session_lock
sid = request.cookies.get(session_cookie)
if not sid:
sid = request.args.get('sid')
with session_lock:
if sid:
request.session = session_store.get(sid)
@@ -1115,7 +1115,7 @@ body .ui-tooltip { border-width:2px; }
/*** Input field styling from Bootstrap **/
input, textarea {
/* input, textarea {
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-ms-transition: border linear 0.2s, box-shadow linear 0.2s;
@@ -1137,12 +1137,12 @@ input[type=file]:focus, input[type=checkbox]:focus, select:focus {
-moz-box-shadow: none;
box-shadow: none;
outline: 1px dotted #666;
}
}*/
input[type="text"],
input[type="password"],
/*input[type="text"],
input[type="password"],*/
.ui-autocomplete-input,
textarea,
/*textarea,*/
.uneditable-input {
display: inline-block;
padding: 4px;
+25 -14
View File
@@ -2316,16 +2316,16 @@
text-align: justify;
}
.openerp .oe_form_editable .oe_form .oe_form_field_integer input {
width: 6em !important;
width: 6em;
}
.openerp .oe_form_editable .oe_form .oe_form_field_float input {
width: 7em !important;
width: 7em;
}
.openerp .oe_form_editable .oe_form .oe_form_field_date input {
width: 7.5em !important;
width: 7.5em;
}
.openerp .oe_form_editable .oe_form .oe_form_field_datetime input {
width: 11.5em !important;
width: 11.5em;
}
.openerp .oe_hidden_input_file {
position: relative;
@@ -2384,6 +2384,9 @@
float: right;
padding-left: 2px;
}
.openerp .oe_form_field_many2one input {
padding-right: 13px;
}
.openerp.ui-autocomplete li.oe_m2o_dropdown_option a {
font-style: italic;
padding-left: 2em;
@@ -2588,32 +2591,33 @@
top: 5px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_m2o_cm_button {
display: none;
line-height: 19px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_input_icon {
margin-top: 5px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field {
min-width: 0;
max-width: none;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
height: 27px;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea {
-moz-border-radius: 0;
-webkit-border-radius: 0;
border-radius: 0;
border: 1px solid #aaaaff;
margin: 0;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field textarea, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field select {
min-width: 0;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_float input, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_view_integer input {
text-align: right;
width: 100% !important;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime > span, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date > span {
width: 100% !important;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime input.oe_datepicker_master, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date input.oe_datepicker_master {
width: 100% !important;
}
.openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_field_float, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field .oe_form_view_integer, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_datetime, .openerp .oe_list.oe_list_editable.oe_editing .oe_form_field.oe_form_field_date {
min-width: 0 !important;
max-width: none !important;
}
.openerp .oe_list_group_name {
white-space: nowrap;
}
@@ -2678,6 +2682,9 @@
text-align: right !important;
max-width: 100px;
}
.openerp .oe_list_content td.oe_list_field_date, .openerp .oe_list_content th.oe_list_header_date {
min-width: 6em;
}
.openerp .oe_list_content > thead {
border-bottom: 2px solid #cacaca;
background: #eeeeee;
@@ -2745,6 +2752,10 @@
.openerp .oe_list_content > tbody > tr > td.oe_list_checkbox:first-child:after, .openerp .oe_list_content > tbody > tr th.oe_list_checkbox:first-child:after {
border-width: 0;
}
.openerp .oe_list_content > tbody > tr > td.oe_list_field_boolean input {
filter: alpha(opacity=50);
opacity: 0.5;
}
.openerp .oe_list_content > tbody > tr:nth-child(odd) {
background-color: #f0f0fa;
background-color: #f0f0fa;
+17 -11
View File
@@ -1846,13 +1846,13 @@ $sheet-padding: 16px
.oe_form_editable
.oe_form
.oe_form_field_integer input
width: 6em !important
width: 6em
.oe_form_field_float input
width: 7em !important
width: 7em
.oe_form_field_date input
width: 7.5em !important
width: 7.5em
.oe_form_field_datetime input
width: 11.5em !important
width: 11.5em
// }}}
// FormView.fields_binary {{{
/* http://www.quirksmode.org/dom/inputfile.html
@@ -1908,6 +1908,8 @@ $sheet-padding: 16px
line-height: 14px
float: right
padding-left: 2px
input
padding-right: 13px
&.ui-autocomplete
li.oe_m2o_dropdown_option a
font-style: italic
@@ -2050,26 +2052,26 @@ $sheet-padding: 16px
.oe_m2o_drop_down_button
top: 5px
.oe_m2o_cm_button
display: none
line-height: 19px
.oe_input_icon
margin-top: 5px
.oe_form_field
min-width: 0
max-width: none
input, textarea
height: $row-height
input, textarea
@include radius(0)
border: 1px solid #aaf
margin: 0
input, textarea, select
min-width: 0
&.oe_form_field_float,&.oe_form_view_integer
input
text-align: right
width: 100% !important
&.oe_form_field_datetime,&.oe_form_field_date
> span
width: 100% !important
input.oe_datepicker_master
width: 100% !important
.oe_form_field_float,.oe_form_view_integer,&.oe_form_field_datetime,&.oe_form_field_date
min-width: 0 !important
max-width: none !important
.oe_list_group_name
white-space: nowrap
// }}}
@@ -2128,6 +2130,8 @@ $sheet-padding: 16px
td.oe_number
text-align: right !important
max-width: 100px
td.oe_list_field_date, th.oe_list_header_date
min-width: 6em
> thead
border-bottom: 2px solid #cacaca
background: #eee
@@ -2177,6 +2181,8 @@ $sheet-padding: 16px
width: 17px
&:after
border-width: 0
> td.oe_list_field_boolean input
@include opacity()
> tr:nth-child(odd)
background-color: #f0f0fa
@include vertical-gradient(#f0f0fa, #eeeef6)
+21 -5
View File
@@ -189,7 +189,7 @@ instance.web.Dialog = instance.web.Widget.extend({
}
});
instance.web.CrashManager = instance.web.CallbackEnabled.extend({
instance.web.CrashManager = instance.web.Class.extend({
rpc_error: function(error) {
if (error.data.fault_code) {
var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
@@ -303,7 +303,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
$('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
var fetch_db = this.rpc("/web/database/get_list", {}).then(
function(result) {
self.db_list = result.db_list;
self.db_list = result;
},
function (_, ev) {
ev.preventDefault();
@@ -333,6 +333,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
self.$el.find("label").addClass("oe_form_label");
self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
@@ -418,6 +419,18 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
self.do_action(client_action);
});
},
do_duplicate: function(form) {
var self = this;
var fields = $(form).serializeArray();
self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
if (result.error) {
self.display_error(result);
return;
}
self.do_notify("Duplicating database", "The database has been duplicated.");
self.start();
});
},
do_drop: function(form) {
var self = this;
var $form = $(form),
@@ -518,6 +531,9 @@ instance.web.Login = instance.web.Widget.extend({
this.selected_db = null;
this.selected_login = null;
this.params = action.params || {};
if (_.isEmpty(this.params)) {
this.params = $.bbq.getState(true);
}
if (this.params.login_successful) {
this.on('login_successful', this, this.params.login_successful);
@@ -548,7 +564,7 @@ instance.web.Login = instance.web.Widget.extend({
return d;
},
on_db_loaded: function (result) {
this.db_list = result.db_list;
this.db_list = result;
this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
if(this.db_list.length === 0) {
this.do_action("database_manager");
@@ -911,7 +927,7 @@ instance.web.UserMenu = instance.web.Widget.extend({
if(res.company_id[0] > 1)
topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
self.$el.find('.oe_topbar_name').text(topbar_name);
var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid);
var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
$avatar.attr('src', avatar_src);
});
};
@@ -1130,7 +1146,7 @@ instance.web.WebClient = instance.web.Client.extend({
var self = this;
var state = event.getState(true);
if (!_.isEqual(this._current_state, state)) {
if(state.action_id === undefined && state.menu_id) {
if(state.action === undefined && state.menu_id) {
self.menu.has_been_loaded.done(function() {
self.menu.do_reload().done(function() {
self.menu.menu_click(state.menu_id)
+132 -149
View File
@@ -243,15 +243,17 @@ instance.web.ParentedMixin = {
/**
* Backbone's events. Do not ever use it directly, use EventDispatcherMixin instead.
*
* This class just handle the dispatching of events, it is not meant to be extended,
* nor used directly. All integration with parenting and automatic unregistration of
* events is done in EventDispatcherMixin.
*
* Copyright notice for the following Class:
*
* (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
* Backbone may be freely distributed under the MIT license.
* For all details and documentation:
* http://backbonejs.org
*
* This class just handle the dispatching of events, it is not meant to be extended,
* nor used directly. All integration with parenting and automatic unregistration of
* events is done in EventDispatcherMixin.
*
*/
var Events = instance.web.Class.extend({
@@ -334,7 +336,6 @@ var Events = instance.web.Class.extend({
return this;
}
});
// end of Jeremy Ashkenas' code
instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, {
__eventDispatcherMixin: true,
@@ -426,66 +427,94 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, {
}
});
instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, {
init: function() {
instance.web.PropertiesMixin.init.call(this);
var self = this;
// Transform on_/do_* methods into callbacks
var callback_maker = function(fn) {
return function() {
return fn.apply(self, arguments);
}
};
for (var name in this) {
if(typeof(this[name]) == "function") {
if((/^on_|^do_/).test(name)) {
this[name] = callback_maker(this[name]);
}
}
}
},
/**
* Proxies a method of the object, in order to keep the right ``this`` on
* method invocations.
*
* This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
* even more so to ``jQuery.proxy`` with a fundamental difference: its
* resolution of the method being called is lazy, meaning it will use the
* method as it is when the proxy is called, not when the proxy is created.
*
* Other methods will fix the bound method to what it is when creating the
* binding/proxy, which is fine in most javascript code but problematic in
* OpenERP Web where developers may want to replace existing callbacks with
* theirs.
*
* The semantics of this precisely replace closing over the method call.
*
* @param {String|Function} method function or name of the method to invoke
* @returns {Function} proxied method
*/
proxy: function (method) {
var self = this;
return function () {
var fn = (typeof method === 'string') ? self[method] : method;
return fn.apply(self, arguments);
}
}
});
// Classes
instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
/**
* Base class for all visual components. Provides a lot of functionalities helpful
* for the management of a part of the DOM.
*
* Widget handles:
* - Rendering with QWeb.
* - Life-cycle management and parenting (when a parent is destroyed, all its children are
* destroyed too).
* - Insertion in DOM.
*
* Guide to create implementations of the Widget class:
* ==============================================
*
* Here is a sample child class:
*
* MyWidget = instance.base.Widget.extend({
* // the name of the QWeb template to use for rendering
* template: "MyQWebTemplate",
*
* init: function(parent) {
* this._super(parent);
* // stuff that you want to init before the rendering
* },
* start: function() {
* // stuff you want to make after the rendering, `this.$el` holds a correct value
* this.$el.find(".my_button").click(/* an example of event binding * /);
*
* // if you have some asynchronous operations, it's a good idea to return
* // a promise in start()
* var promise = this.rpc(...);
* return promise;
* }
* });
*
* Now this class can simply be used with the following syntax:
*
* var my_widget = new MyWidget(this);
* my_widget.appendTo($(".some-div"));
*
* With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
* DOM inside the ".some-div" div and its events were binded.
*
* And of course, when you don't need that widget anymore, just do:
*
* my_widget.destroy();
*
* That will kill the widget in a clean way and erase its content from the dom.
*/
instance.web.Widget = instance.web.Class.extend(instance.web.PropertiesMixin, {
// Backbone-ish API
tagName: 'div',
id: null,
className: null,
attributes: {},
events: {},
/**
* The name of the QWeb template that will be used for rendering. Must be
* redefined in subclasses or the default render() method can not be used.
*
* @type string
*/
template: null,
/**
* Constructs the widget and sets its parent if a parent is given.
*
* @constructs instance.web.Widget
* @extends instance.web.CallbackEnabled
*
* @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
* When that widget is destroyed by calling destroy(), the current instance will be
* destroyed too. Can be null.
*/
init: function(parent) {
instance.web.CallbackEnabledMixin.init.call(this);
instance.web.PropertiesMixin.init.call(this);
this.setParent(parent);
// Bind on_/do_* methods to this
// We might remove this automatic binding in the future
for (var name in this) {
if(typeof(this[name]) == "function") {
if((/^on_|^do_/).test(name)) {
this[name] = this[name].bind(this);
}
}
}
// FIXME: this should not be
this.setElement(this._make_descriptive());
this.session = instance.session;
},
/**
* Destroys the current widget, also destroys all its children before destroying itself.
@@ -574,94 +603,32 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, {
*/
start: function() {
return $.when();
}
});
// Classes
instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEnabledMixin, {
init: function() {
instance.web.CallbackEnabledMixin.init.call(this);
}
});
/**
* Base class for all visual components. Provides a lot of functionalities helpful
* for the management of a part of the DOM.
*
* Widget handles:
* - Rendering with QWeb.
* - Life-cycle management and parenting (when a parent is destroyed, all its children are
* destroyed too).
* - Insertion in DOM.
*
* Guide to create implementations of the Widget class:
* ==============================================
*
* Here is a sample child class:
*
* MyWidget = instance.base.Widget.extend({
* // the name of the QWeb template to use for rendering
* template: "MyQWebTemplate",
*
* init: function(parent) {
* this._super(parent);
* // stuff that you want to init before the rendering
* },
* start: function() {
* // stuff you want to make after the rendering, `this.$el` holds a correct value
* this.$el.find(".my_button").click(/* an example of event binding * /);
*
* // if you have some asynchronous operations, it's a good idea to return
* // a promise in start()
* var promise = this.rpc(...);
* return promise;
* }
* });
*
* Now this class can simply be used with the following syntax:
*
* var my_widget = new MyWidget(this);
* my_widget.appendTo($(".some-div"));
*
* With these two lines, the MyWidget instance was inited, rendered, it was inserted into the
* DOM inside the ".some-div" div and its events were binded.
*
* And of course, when you don't need that widget anymore, just do:
*
* my_widget.destroy();
*
* That will kill the widget in a clean way and erase its content from the dom.
*/
instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
// Backbone-ish API
tagName: 'div',
id: null,
className: null,
attributes: {},
events: {},
},
/**
* The name of the QWeb template that will be used for rendering. Must be
* redefined in subclasses or the default render() method can not be used.
* Proxies a method of the object, in order to keep the right ``this`` on
* method invocations.
*
* @type string
* This method is similar to ``Function.prototype.bind`` or ``_.bind``, and
* even more so to ``jQuery.proxy`` with a fundamental difference: its
* resolution of the method being called is lazy, meaning it will use the
* method as it is when the proxy is called, not when the proxy is created.
*
* Other methods will fix the bound method to what it is when creating the
* binding/proxy, which is fine in most javascript code but problematic in
* OpenERP Web where developers may want to replace existing callbacks with
* theirs.
*
* The semantics of this precisely replace closing over the method call.
*
* @param {String|Function} method function or name of the method to invoke
* @returns {Function} proxied method
*/
template: null,
/**
* Constructs the widget and sets its parent if a parent is given.
*
* @constructs instance.web.Widget
* @extends instance.web.CallbackEnabled
*
* @param {instance.web.Widget} parent Binds the current instance to the given Widget instance.
* When that widget is destroyed by calling destroy(), the current instance will be
* destroyed too. Can be null.
*/
init: function(parent) {
instance.web.WidgetMixin.init.call(this,parent);
// FIXME: this should not be
this.setElement(this._make_descriptive());
this.session = instance.session;
proxy: function (method) {
var self = this;
return function () {
var fn = (typeof method === 'string') ? self[method] : method;
return fn.apply(self, arguments);
}
},
/**
* Renders the element. The default implementation renders the widget using QWeb,
@@ -678,7 +645,6 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, {
}
this.replaceElement($el);
},
/**
* Re-sets the widget's root element and replaces the old root element
* (if any) by the new one in the DOM.
@@ -941,7 +907,7 @@ instance.web.Registry = instance.web.Class.extend({
}
});
instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, {
triggers: {
'request': 'Request sent',
'response': 'Response received',
@@ -950,13 +916,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
},
/**
* @constructs instance.web.JsonRPC
* @extends instance.web.CallbackEnabled
*
* @param {String} [server] JSON-RPC endpoint hostname
* @param {String} [port] JSON-RPC endpoint port
*/
init: function() {
this._super();
instance.web.PropertiesMixin.init.call(this);
this.server = null;
this.debug = ($.deparam($.param.querystring()).debug != undefined);
},
@@ -1311,7 +1276,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
};
deferred.reject(error, $.Event());
});
// Allow deferred user to disable on_rpc_error in fail
// Allow deferred user to disable rpc_error call in fail
deferred.fail(function() {
deferred.fail(function(error, event) {
if (!event.isDefaultPrevented()) {
@@ -1344,9 +1309,18 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
// extracted from payload to set on the url
var data = {
session_id: this.session_id,
id: payload.id
id: payload.id,
sid: this.httpsessionid,
};
url.url = this.get_url(url.url);
var set_sid = function (response, textStatus, jqXHR) {
// If response give us the http session id, we store it for next requests...
if (response.httpsessionid) {
self.httpsessionid = response.httpsessionid;
}
};
url.url = this.url(url.url, null);
var ajax = _.extend({
type: "GET",
dataType: 'jsonp',
@@ -1361,7 +1335,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
if(payload_url.length < 2000) {
// Direct jsonp request
ajax.data.r = payload_str;
return $.ajax(ajax);
return $.ajax(ajax).done(set_sid);
} else {
// Indirect jsonp request
var ifid = _.uniqueId('oe_rpc_iframe');
@@ -1399,11 +1373,20 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({
});
// append the iframe to the DOM (will trigger the first load)
$form.after($iframe);
return deferred;
return deferred.done(set_sid);
}
},
get_url: function (file) {
return this.prefix + file;
url: function(path, params) {
var qs = '';
if (!_.isNull(params)) {
params = _.extend(params || {}, {session_id: this.session_id});
if (this.httpsessionid) {
params.sid = this.httpsessionid;
}
qs = '?' + $.param(params);
}
return this.prefix + path + qs;
},
});
+29 -22
View File
@@ -3,10 +3,13 @@
*--------------------------------------------------------*/
var console;
if (!console) {
console = {log: function () {}};
}
if (!console.debug) {
console.debug = console.log;
// Even IE9 only exposes console object if debug window opened
console = {};
('log error debug info warn assert clear dir dirxml trace group'
+ ' groupCollapsed groupEnd time timeEnd profile profileEnd count'
+ ' exception').split(/\s+/).forEach(function(property) {
console[property] = _.identity;
});
}
openerp.web.coresetup = function(instance) {
@@ -59,7 +62,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
this.session_id = this.get_cookie('session_id');
return this.session_reload().then(function(result) {
var modules = instance._modules.join(',');
var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).then(self.do_load_qweb);
var deferred = self.rpc('/web/webclient/qweblist', {mods: modules}).then(self.load_qweb.bind(self));
if(self.session_is_valid()) {
return deferred.then(function() { return self.load_modules(); });
}
@@ -161,28 +164,26 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
load_modules: function() {
var self = this;
return this.rpc('/web/session/modules', {}).then(function(result) {
var lang = self.user_context.lang,
all_modules = _.uniq(self.module_list.concat(result));
var params = { mods: all_modules, lang: lang};
var all_modules = _.uniq(self.module_list.concat(result));
var to_load = _.difference(result, self.module_list).join(',');
self.module_list = all_modules;
var loaded = self.rpc('/web/webclient/translations', params).done(function(trans) {
instance.web._t.database.set_bundle(trans);
});
var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"];
var loaded = self.load_translations();
var datejs_locale = "/web/static/lib/datejs/globalization/" + self.user_context.lang.replace("_", "-") + ".js";
var file_list = [ datejs_locale ];
if(to_load.length) {
loaded = $.when(
loaded,
self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.do_load_css),
self.rpc('/web/webclient/qweblist', {mods: to_load}).then(self.do_load_qweb),
self.rpc('/web/webclient/csslist', {mods: to_load}).done(self.load_css.bind(self)),
self.rpc('/web/webclient/qweblist', {mods: to_load}).then(self.load_qweb.bind(self)),
self.rpc('/web/webclient/jslist', {mods: to_load}).done(function(files) {
file_list = file_list.concat(files);
})
);
}
return loaded.then(function () {
return self.do_load_js(file_list);
return self.load_js(file_list);
}).done(function() {
self.on_modules_loaded();
self.trigger('module_loaded');
@@ -196,29 +197,35 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
});
});
},
do_load_css: function (files) {
load_translations: function() {
var params = { mods: this.module_list, lang: this.user_context.lang };
return this.rpc('/web/webclient/translations', params).done(function(trans) {
instance.web._t.database.set_bundle(trans);
});
},
load_css: function (files) {
var self = this;
_.each(files, function (file) {
$('head').append($('<link>', {
'href': self.get_url(file),
'href': self.url(file, null),
'rel': 'stylesheet',
'type': 'text/css'
}));
});
},
do_load_js: function(files) {
load_js: function(files) {
var self = this;
var d = $.Deferred();
if(files.length != 0) {
if(files.length !== 0) {
var file = files.shift();
var tag = document.createElement('script');
tag.type = 'text/javascript';
tag.src = self.get_url(file);
tag.src = self.url(file, null);
tag.onload = tag.onreadystatechange = function() {
if ( (tag.readyState && tag.readyState != "loaded" && tag.readyState != "complete") || tag.onload_done )
return;
tag.onload_done = true;
self.do_load_js(files).done(function () {
self.load_js(files).done(function () {
d.resolve();
});
};
@@ -229,7 +236,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess
}
return d;
},
do_load_qweb: function(files) {
load_qweb: function(files) {
var self = this;
_.each(files, function(file) {
self.qweb_mutex.exec(function() {
+6 -7
View File
@@ -358,17 +358,16 @@ instance.web.Model = instance.web.Class.extend({
},
});
instance.web.DataSet = instance.web.CallbackEnabled.extend({
instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, {
/**
* Collection of OpenERP records, used to share records and the current selection between views.
*
* @constructs instance.web.DataSet
* @extends instance.web.CallbackEnabled
*
* @param {String} model the OpenERP model this dataset will manage
*/
init: function(parent, model, context) {
this._super(parent);
instance.web.PropertiesMixin.init.call(this);
this.model = model;
this.context = context || {};
this.index = null;
@@ -722,10 +721,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({
this.last_default_get = {};
},
default_get: function(fields, options) {
return this._super(fields, options).done(this.on_default_get);
},
on_default_get: function(res) {
this.last_default_get = res;
var self = this;
return this._super(fields, options).done(function(res) {
self.last_default_get = res;
});
},
create: function(data) {
var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data,
+36
View File
@@ -83,6 +83,42 @@ instance.web.strip_raw_chars = function (value) {
var normalize_format = function (format) {
return Date.normalizeFormat(instance.web.strip_raw_chars(format));
};
/**
* Check with a scary heuristic if the value is a bin_size or not.
* If not, compute an approximate size out of the base64 encoded string.
*
* @param {String} value original format
*/
instance.web.binary_to_binsize = function (value) {
if (!value) {
return instance.web.human_size(0);
}
if (value.substr(0, 10).indexOf(' ') == -1) {
// Computing approximate size out of base64 encoded string
// http://en.wikipedia.org/wiki/Base64#MIME
return instance.web.human_size(value.length / 1.37);
} else {
// already bin_size
return value;
}
};
/**
* Returns a human readable size
*
* @param {Number} numner of bytes
*/
instance.web.human_size = function(size) {
var units = _t("Bytes,Kb,Mb,Gb,Tb,Pb,Eb,Zb,Yb").split(',');
var i = 0;
while (size >= 1024) {
size /= 1024;
++i;
}
return size.toFixed(2) + ' ' + units[i];
};
/**
* Formats a single atomic value based on a field descriptor
*
+109 -27
View File
@@ -349,7 +349,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
$.when(load_view)
.then(function(r) {
self.search_view_loaded(r)
}).fail(function () {
}, function () {
self.ready.reject.apply(null, arguments);
});
}
@@ -459,7 +459,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
complete_global_search: function (req, resp) {
$.when.apply(null, _(this.inputs).chain()
.invoke('complete', req.term)
.value()).done(function () {
.value()).then(function () {
resp(_(_(arguments).compact()).flatten(true));
});
},
@@ -527,7 +527,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
childView.on('blurred', self, self.proxy('childBlurred'));
});
$.when.apply(null, started).done(function () {
$.when.apply(null, started).then(function () {
var input_to_focus;
// options.at: facet inserted at given index, focus next input
// otherwise just focus last input
@@ -608,7 +608,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
// add Filters to this.inputs, need view.controls filled
(new instance.web.search.Filters(this));
// add custom filters to this.inputs
(new instance.web.search.CustomFilters(this));
this.custom_filters = new instance.web.search.CustomFilters(this);
// add Advanced to this.inputs
(new instance.web.search.Advanced(this));
},
@@ -635,16 +635,41 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea
// load defaults
var defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
'facet_for_defaults', this.defaults)).done(function () {
self.query.reset(_(arguments).compact(), {preventSearch: true});
});
'facet_for_defaults', this.defaults))
.then(this.proxy('setup_default_query'));
return $.when(drawer_started, defaults_fetched)
.done(function () {
self.trigger("search_view_loaded", data);
self.ready.resolve();
});
},
setup_default_query: function () {
// Hacky implementation of CustomFilters#facet_for_defaults ensure
// CustomFilters will be ready (and CustomFilters#filters will be
// correctly filled) by the time this method executes.
var custom_filters = this.custom_filters.filters;
if (!_(custom_filters).isEmpty()) {
// Check for any is_default custom filter
var personal_filter = _(custom_filters).find(function (filter) {
return filter.user_id && filter.is_default;
});
if (personal_filter) {
this.custom_filters.enable_filter(personal_filter, true);
return;
}
var global_filter = _(custom_filters).find(function (filter) {
return !filter.user_id && filter.is_default;
});
if (global_filter) {
this.custom_filters.enable_filter(global_filter, true);
return;
}
}
// No custom filter, or no is_default custom filter, apply view defaults
this.query.reset(_(arguments).compact(), {preventSearch: true});
},
/**
* Extract search data from the view's facets.
*
@@ -1460,10 +1485,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
instance.web.search.CustomFilters = instance.web.search.Input.extend({
template: 'SearchView.CustomFilters',
_in_drawer: true,
init: function () {
this.is_ready = $.Deferred();
this._super.apply(this, arguments);
},
start: function () {
var self = this;
this.model = new instance.web.Model('ir.filters');
this.filters = {};
this.$filters = {};
this.view.query
.on('remove', function (facet) {
if (!facet.get('is_custom_filter')) {
@@ -1479,24 +1509,78 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
// FIXME: local eval of domain and context to get rid of special endpoint
return this.rpc('/web/searchview/get_filters', {
model: this.view.model
}).then(this.proxy('set_filters'));
})
.then(this.proxy('set_filters'))
.then(function () {
self.is_ready.resolve(null);
}, function () {
self.is_ready.reject();
});
},
/**
* Special implementation delaying defaults until CustomFilters is loaded
*/
facet_for_defaults: function () {
return this.is_ready;
},
/**
* Generates a mapping key (in the filters and $filter mappings) for the
* filter descriptor object provided (as returned by ``get_filters``).
*
* The mapping key is guaranteed to be unique for a given (user_id, name)
* pair.
*
* @param {Object} filter
* @param {String} filter.name
* @param {Number|Pair<Number, String>} [filter.user_id]
* @return {String} mapping key corresponding to the filter
*/
key_for: function (filter) {
var user_id = filter.user_id;
var uid = (user_id instanceof Array) ? user_id[0] : user_id;
return _.str.sprintf('(%s)%s', uid, filter.name);
},
/**
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
* filter descriptor
*
* @param {Object} filter
* @param {String} filter.name
* @param {Object} [filter.context]
* @param {Array} [filter.domain]
* @return {Object}
*/
facet_for: function (filter) {
return {
category: _t("Custom Filter"),
icon: 'M',
field: {
get_context: function () { return filter.context; },
get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; }
},
is_custom_filter: true,
values: [{label: filter.name, value: null}]
};
},
clear_selection: function () {
this.$('li.oe_selected').removeClass('oe_selected');
},
append_filter: function (filter) {
var self = this;
var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name);
var key = this.key_for(filter);
var $filter;
if (key in this.filters) {
$filter = this.filters[key];
if (key in this.$filters) {
$filter = this.$filters[key];
} else {
var id = filter.id;
$filter = this.filters[key] = $('<li></li>')
this.filters[key] = filter;
$filter = this.$filters[key] = $('<li></li>')
.appendTo(this.$('.oe_searchview_custom_list'))
.addClass(filter.user_id ? 'oe_searchview_custom_private'
: 'oe_searchview_custom_public')
.toggleClass('oe_searchview_custom_default', filter.is_default)
.text(filter.name);
$('<a class="oe_searchview_custom_delete">x</a>')
@@ -1504,33 +1588,30 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
e.stopPropagation();
self.model.call('unlink', [id]).done(function () {
$filter.remove();
delete self.$filters[key];
delete self.filters[key];
});
})
.appendTo($filter);
}
$filter.unbind('click').click(function () {
self.view.query.reset([{
category: _t("Custom Filter"),
icon: 'M',
field: {
get_context: function () { return filter.context; },
get_groupby: function () { return [filter.context]; },
get_domain: function () { return filter.domain; }
},
is_custom_filter: true,
values: [{label: filter.name, value: null}]
}]);
$filter.addClass('oe_selected');
self.enable_filter(filter);
});
},
enable_filter: function (filter, preventSearch) {
this.view.query.reset([this.facet_for(filter)], {
preventSearch: preventSearch || false});
this.$filters[this.key_for(filter)].addClass('oe_selected');
},
set_filters: function (filters) {
_(filters).map(_.bind(this.append_filter, this));
},
save_current: function () {
var self = this;
var $name = this.$('input:first');
var private_filter = !this.$('input:last').prop('checked');
var private_filter = !this.$('#oe_searchview_custom_public').prop('checked');
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
var search = this.view.build_search_data();
this.rpc('/web/session/eval_domain_and_context', {
@@ -1546,7 +1627,8 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({
user_id: private_filter ? instance.session.uid : false,
model_id: self.view.model,
context: results.context,
domain: results.domain
domain: results.domain,
is_default: set_as_default
};
// FIXME: current context?
return self.model.call('create_or_replace', [filter]).done(function (id) {
+71 -54
View File
@@ -173,10 +173,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM
} else {
this.$el.find('.oe_form_buttons').replaceWith(this.$buttons);
}
this.$buttons.on('click', '.oe_form_button_create', this.on_button_create);
this.$buttons.on('click', '.oe_form_button_edit', this.on_button_edit);
this.$buttons.on('click', '.oe_form_button_save', this.on_button_save);
this.$buttons.on('click', '.oe_form_button_cancel', this.on_button_cancel);
this.$buttons.on('click', '.oe_form_button_create',
this.guard_active(this.on_button_create));
this.$buttons.on('click', '.oe_form_button_edit',
this.guard_active(this.on_button_edit));
this.$buttons.on('click', '.oe_form_button_save',
this.guard_active(this.on_button_save));
this.$buttons.on('click', '.oe_form_button_cancel',
this.guard_active(this.on_button_cancel));
if (this.options.footer_to_buttons) {
this.$el.find('footer').appendTo(this.$buttons);
}
@@ -1187,14 +1191,36 @@ 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>');
$('button', doc).each(function() {
$(this).attr('data-button-type', $(this).attr('type'));
});
xml = instance.web.xml_to_str(doc);
return $(xml);
},
render_to: function($target) {
var self = this;
this.$target = $target;
// TODO: I know this will save the world and all the kitten for a moment,
// but one day, we will have to get rid of xml2json
var xml = instance.web.json_node_to_xml(this.fvg.arch);
this.$form = $('<div class="oe_form">' + xml + '</div>');
this.$form = this.view_arch_to_dom_node(this.fvg.arch);
this.process_version();
@@ -1863,6 +1889,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi
instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({
template: 'WidgetButton',
init: function(field_manager, node) {
node.attrs.type = node.attrs['data-button-type'];
this._super(field_manager, node);
this.force_disabled = false;
this.string = (this.node.attrs.string || '').replace(/_/g, '');
@@ -2152,17 +2179,10 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w
},
set_dimensions: function (height, width) {
// remove width css property
this.$el.css('width', '');
// extract style (without width)
var old_style = this.$el.attr('style');
// jQuery doesn't understand/use !important
var style = 'width:' + width + 'px !important;';
if (old_style) {
style += old_style
}
this.$el.attr('style', style);
this.$el.css('minHeight', height);
this.$el.css({
width: width,
minHeight: height
});
},
commit_value: function() {
return $.when();
@@ -2386,7 +2406,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({
self.$input.focus();
return;
}
self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date());
self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date());
self.$input_picker.show();
self.picker('show');
self.$input_picker.hide();
@@ -2639,22 +2659,18 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({
}
});
/**
The progressbar field expect a float from 0 to 100.
*/
instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({
template: 'FieldProgressBar',
start: function() {
this._super.apply(this, arguments);
render_value: function() {
this.$el.progressbar({
value: this.get('value'),
value: this.get('value') || 0,
disabled: this.get("effective_readonly")
});
},
render_value: function() {
var show_value = Number(this.get('value'));
if (isNaN(show_value)) {
show_value = 0;
}
var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0');
this.$el.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%');
var formatted_value = instance.web.format_value(this.get('value') || 0, { type : 'float' });
this.$('span').html(formatted_value + '%');
}
});
@@ -3999,13 +4015,18 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in
self._drop_shown = true;
});
self.tags = self.$text.textext()[0].tags();
self.$text.focusout(function() {
self.$text.trigger("setInputData", "");
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
}
});
self.$text
.focusin(function () {
self.trigger('focused');
})
.focusout(function() {
self.$text.trigger("setInputData", "");
self.trigger('blurred');
}).keydown(function(e) {
if (e.which === $.ui.keyCode.TAB && self._drop_shown) {
self.$text.textext()[0].autocomplete().selectFromDropdown();
}
});
},
set_value: function(value_) {
value_ = value_ || [];
@@ -4794,15 +4815,6 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as);
this.$el.find('.oe_form_binary_file_clear').click(this.on_clear);
},
human_filesize : function(size) {
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
var i = 0;
while (size >= 1024) {
size /= 1024;
++i;
}
return size.toFixed(2) + ' ' + units[i];
},
on_file_change: function(e) {
var self = this;
var file_node = e.target;
@@ -4850,6 +4862,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
link.href = "data:application/octet-stream;base64," + value;
} else {
instance.web.blockUI();
var c = instance.webclient.crashmanager;
this.session.get_file({
url: '/web/binary/saveas_ajax',
data: {data: JSON.stringify({
@@ -4860,7 +4873,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.
context: this.view.dataset.get_context()
})},
complete: instance.web.unblockUI,
error: instance.webclient.crashmanager.on_rpc_error
error: c.rpc_error.bind(c)
});
ev.stopPropagation();
return false;
@@ -4919,7 +4932,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({
on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
this.binary_value = true;
this.internal_set_value(file_base64);
var show_value = name + " (" + this.human_filesize(size) + ")";
var show_value = name + " (" + instance.web.human_size(size) + ")";
this.$el.find('input').eq(0).val(show_value);
this.set_filename(name);
},
@@ -4943,8 +4956,12 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({
var field = this.name;
if (this.options.preview_image)
field = this.options.preview_image;
url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime());
url = this.session.url('/web/binary/image', {
model: this.view.dataset.model,
id: id,
field: field,
t: (new Date().getTime()),
});
} else {
url = this.placeholder;
}
@@ -5004,7 +5021,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); });
},
get_file_url: function (attachment) {
return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id'];
return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']});
},
render_value: function () {
var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this}));
@@ -5030,7 +5047,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
}
// block UI or not
if(this.node.attrs.blockui) {
if(this.node.attrs.blockui>0) {
instance.web.blockUI();
}
@@ -5064,7 +5081,7 @@ instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractFiel
},
on_file_loaded: function (event, result) {
// unblock UI
if(this.node.attrs.blockui) {
if(this.node.attrs.blockui>0) {
instance.web.unblockUI();
}
+22 -14
View File
@@ -503,12 +503,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi
return this.reload_content();
},
reload_record: function (record) {
var self = this;
return this.dataset.read_ids(
[record.get('id')],
_.pluck(_(this.columns).filter(function (r) {
return r.tag === 'field';
}), 'name')
).done(function (records) {
if (!records[0]) {
self.records.remove(record);
return;
}
_(records[0]).each(function (value, key) {
record.set(key, value, {silent: true});
});
@@ -1557,9 +1562,8 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we
}
});
var DataGroup = instance.web.CallbackEnabled.extend({
var DataGroup = instance.web.Class.extend({
init: function(parent, model, domain, context, group_by, level) {
this._super(parent, null);
this.model = new instance.web.Model(model, context, domain);
this.group_by = group_by;
this.context = context;
@@ -2152,7 +2156,7 @@ instance.web.list.Boolean = instance.web.list.Column.extend({
* @private
*/
_format: function (row_data, options) {
return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>',
return _.str.sprintf('<input type="checkbox" %s readonly="readonly"/>',
row_data[this.id].value ? 'checked="checked"' : '');
}
});
@@ -2164,20 +2168,24 @@ instance.web.list.Binary = instance.web.list.Column.extend({
*/
_format: function (row_data, options) {
var text = _t("Download");
var download_url = _.str.sprintf(
'/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d',
instance.session.session_id, options.model, this.id, options.id);
if (this.filename) {
download_url += '&filename_field=' + this.filename;
if (row_data[this.filename]) {
text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
row_data[this.filename].value, {type: 'char'}));
var value = row_data[this.id].value;
var download_url;
if (value && value.substr(0, 10).indexOf(' ') == -1) {
download_url = "data:application/octet-stream;base64," + value;
} else {
download_url = this.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id});
if (this.filename) {
download_url += '&filename_field=' + this.filename;
}
}
return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', {
if (this.filename && row_data[this.filename]) {
text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value(
row_data[this.filename].value, {type: 'char'}));
}
return _.template('<a href="<%-href%>"><%-text%></a> (<%-size%>)', {
text: text,
href: download_url,
size: row_data[this.id].value
size: instance.web.binary_to_binsize(value),
});
}
});
@@ -2217,7 +2225,7 @@ instance.web.list.Many2OneButton = instance.web.list.Column.extend({
});
instance.web.list.Many2Many = instance.web.list.Column.extend({
_format: function (row_data, options) {
if (row_data[this.id].value) {
if (!_.isEmpty(row_data[this.id].value)) {
// If value, use __display version for printing
row_data[this.id] = row_data[this.id + '__display'];
}
+47 -44
View File
@@ -117,13 +117,6 @@ openerp.web.list_editable = function (instance) {
e.preventDefault();
self.cancel_edition();
});
this.$el
.off('click', 'tbody td:not(.oe_list_field_cell)')
.on('click', 'tbody td:not(.oe_list_field_cell)', function () {
if (!self.editor.is_editing()) {
self.start_edition();
}
});
this.editor.destroy();
// Editor is not restartable due to formview not being
// restartable
@@ -164,13 +157,23 @@ openerp.web.list_editable = function (instance) {
* @returns {$.Deferred}
*/
ensure_saved: function () {
var self = this;
return this.saving_mutex.exec(function() {
if (!self.editor.is_editing()) {
return $.when();
}
return self.save_edition();
});
return this.save_edition();
},
/**
* Builds a record with the provided id (``false`` for a creation),
* setting all columns with ``false`` value so code which relies on
* having an actual value behaves correctly
*
* @param {*} id
* @return {instance.web.list.Record}
*/
make_empty_record: function (id) {
var attrs = {id: id};
_(this.columns).chain()
.filter(function (x) { return x.tag === 'field'})
.pluck('name')
.each(function (field) { attrs[field] = false; });
return new instance.web.list.Record(attrs);
},
/**
* Set up the edition of a record of the list view "inline"
@@ -186,12 +189,7 @@ openerp.web.list_editable = function (instance) {
if (record) {
item = record.attributes;
} else {
var attrs = {id: false};
_(this.columns).chain()
.filter(function (x) { return x.tag === 'field'})
.pluck('name')
.each(function (field) { attrs[field] = false; });
record = new instance.web.list.Record(attrs);
record = this.make_empty_record(false);
this.records.add(record, {
at: this.prepends_on_create() ? 0 : null});
}
@@ -269,29 +267,34 @@ openerp.web.list_editable = function (instance) {
*/
save_edition: function () {
var self = this;
return this.with_event('save', {
editor: this.editor,
form: this.editor.form,
cancel: false
}, function () {
return this.editor.save().then(function (attrs) {
var created = false;
var record = self.records.get(attrs.id);
if (!record) {
// new record
created = true;
record = self.records.find(function (r) {
return !r.get('id');
}).set('id', attrs.id);
}
// onwrite callback could be altering & reloading the
// record which has *just* been saved, so first perform all
// onwrites then do a final reload of the record
return self.handle_onwrite(record)
.then(function () {
return self.reload_record(record); })
.then(function () {
return { created: created, record: record }; });
return self.saving_mutex.exec(function() {
if (!self.editor.is_editing()) {
return $.when();
}
return self.with_event('save', {
editor: self.editor,
form: self.editor.form,
cancel: false
}, function () {
return self.editor.save().then(function (attrs) {
var created = false;
var record = self.records.get(attrs.id);
if (!record) {
// new record
created = true;
record = self.records.find(function (r) {
return !r.get('id');
}).set('id', attrs.id);
}
// onwrite callback could be altering & reloading the
// record which has *just* been saved, so first perform all
// onwrites then do a final reload of the record
return self.handle_onwrite(record)
.then(function () {
return self.reload_record(record); })
.then(function () {
return { created: created, record: record }; });
});
});
});
},
@@ -391,7 +394,7 @@ openerp.web.list_editable = function (instance) {
if (!record) {
// insert after the source record
var index = this.records.indexOf(source_record) + 1;
record = new instance.web.list.Record({id: id});
record = this.make_empty_record(id);
this.records.add(record, {at: index});
this.dataset.ids.splice(index, 0, id);
}
+38 -11
View File
@@ -213,8 +213,12 @@ instance.web.ActionManager = instance.web.Widget.extend({
if (run_action) {
this.null_action();
action_loaded = this.do_action(state.action);
instance.webclient.menu.has_been_loaded.done(function() {
instance.webclient.menu.open_action(state.action);
$.when(action_loaded || null).done(function() {
instance.webclient.menu.has_been_loaded.done(function() {
if (self.inner_action && self.inner_action.id) {
instance.webclient.menu.open_action(self.inner_action.id);
}
});
});
}
}
@@ -397,6 +401,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
}).done(function(res) {
action = _.clone(action);
action.context = res.context;
var c = instance.webclient.crashmanager;
self.session.get_file({
url: '/web/report',
data: {action: JSON.stringify(action)},
@@ -407,7 +412,7 @@ instance.web.ActionManager = instance.web.Widget.extend({
}
self.dialog_stop();
},
error: instance.webclient.crashmanager.on_rpc_error
error: c.rpc_error.bind(c)
})
});
},
@@ -504,7 +509,7 @@ instance.web.ViewManager = instance.web.Widget.extend({
.find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]')
.parent().addClass('active');
r = $.when(view_promise).done(function () {
return $.when(view_promise).done(function () {
_.each(_.keys(self.views), function(view_name) {
var controller = self.views[view_name].controller;
if (controller) {
@@ -520,7 +525,6 @@ instance.web.ViewManager = instance.web.Widget.extend({
});
self.trigger('switch_mode', view_type, no_store, view_options);
});
return r;
},
do_create_view: function(view_type) {
// Lazy loading of views
@@ -1098,11 +1102,11 @@ instance.web.Sidebar = instance.web.Widget.extend({
on_attachments_loaded: function(attachments) {
var self = this;
var items = [];
var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id=';
var prefix = this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'name'});
_.each(attachments,function(a) {
a.label = a.name;
if(a.type === "binary") {
a.url = prefix + a.id + '&t=' + (new Date().getTime());
a.url = prefix + '&id=' + a.id + '&t=' + (new Date().getTime());
}
});
self.items['files'] = attachments;
@@ -1277,6 +1281,27 @@ instance.web.View = instance.web.Widget.extend({
do_hide: function () {
this.$el.hide();
},
is_active: function () {
var manager = this.getParent();
return !manager || !manager.active_view
|| manager.views[manager.active_view].controller === this;
}, /**
* Wraps fn to only call it if the current view is the active one. If the
* current view is not active, doesn't call fn.
*
* fn can not return anything, as a non-call to fn can't return anything
* either
*
* @param {Function} fn function to wrap in the active guard
*/
guard_active: function (fn) {
var self = this;
return function () {
if (self.is_active()) {
fn.apply(self, arguments);
}
}
},
do_push_state: function(state) {
if (this.getParent() && this.getParent().do_push_state) {
this.getParent().do_push_state(state);
@@ -1384,14 +1409,16 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) {
} else {
return r + '/>';
}
}
};
instance.web.xml_to_str = function(node) {
if (window.ActiveXObject) {
if (window.XMLSerializer) {
return (new XMLSerializer()).serializeToString(node);
} else if (window.ActiveXObject) {
return node.xml;
} else {
return (new XMLSerializer()).serializeToString(node);
throw new Error("Could not serialize XML");
}
}
};
instance.web.str_to_xml = function(s) {
if (window.DOMParser) {
var dp = new DOMParser();
+60 -29
View File
@@ -152,6 +152,32 @@
</tr>
</table>
</form>
<form id="db_duplicate" name="duplicate_db_form" style="display: none;">
<div class="oe_view_manager oe_view_manager_current">
<div class="oe_view_manager_header" style="padding: 8px;">
<div class="oe_header_row">
<h2 class="oe_view_title">
<span class="oe_view_title_text oe_breadcrumb_title">Duplicate Database</span>
</h2>
<button type="submit" class="oe_button oe_highlight db_duplicate">Duplicate</button>
</div>
</div>
</div>
<table align="center" class="db_option_table">
<tr>
<td><label for="super_admin_pwd">Master password:</label></td>
<td><input type="password" name="super_admin_pwd" class="required" value="admin" /></td>
</tr>
<tr>
<td><label for="db_original_name">Original database name:</label></td>
<td><input type="text" name="db_original_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" autofocus="true"/></td>
</tr>
<tr>
<td><label for="db_name">New database name:</label></td>
<td><input type="text" name="db_name" class="required" matches="^[a-zA-Z][a-zA-Z0-9_-]+$" /></td>
</tr>
</table>
</form>
<form id="db_drop" name="drop_db_form" style="display: none; ">
<div class="oe_view_manager oe_view_manager_current">
<div class="oe_view_manager_header" style="padding: 8px;">
@@ -279,6 +305,7 @@
<div class="oe_secondary_menu_section">Database Management</div>
<ul class="oe_secondary_submenu">
<li><a href="#db_create">Create</a></li>
<li><a href="#db_duplicate">Duplicate</a></li>
<li><a href="#db_drop">Drop</a></li>
<li><a href="#db_backup">Backup</a></li>
<li><a href="#db_restore">Restore</a></li>
@@ -1182,26 +1209,28 @@
</t>
<t t-name="FieldBinaryFileUploader.files">
<div class="oe_attachments">
<t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<div class="oe_attachment">
<span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
<span class="oe_fileuploader_in_process">...Upload in progress...</span>
<t t-raw="file.name || file.filename"/>
</span>
<a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
<t t-if="(!file.upload or file.percent_loaded&gt;=100)">
<a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a>
</t>
</div>
</t>
<t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<div>
<a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
</div>
<t t-if="widget.get('value')">
<t t-if="!widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<div class="oe_attachment">
<span t-if="(file.upload or file.percent_loaded&lt;100)" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}" t-attf-name="{file.name || file.filename}">
<span class="oe_fileuploader_in_process">...Upload in progress...</span>
<t t-raw="file.name || file.filename"/>
</span>
<a t-if="(!file.upload or file.percent_loaded&gt;=100)" t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
<t t-if="(!file.upload or file.percent_loaded&gt;=100)">
<a class="oe_right oe_delete oe_e" title="Delete this file" t-attf-data-id="{file.id}">[</a>
</t>
</div>
</t>
<t t-if="widget.get('effective_readonly')" t-foreach="widget.get('value')" t-as="file">
<div>
<a t-att-href="file.url" t-attf-title="{(file.name || file.filename) + (file.date?' \n('+file.date+')':'' )}">
<t t-raw="file.name || file.filename"/>
</a>
</div>
</t>
</t>
</div>
</t>
@@ -1512,17 +1541,19 @@
<h3><span class="oe_i">M</span> Custom Filters</h3>
<ul class="oe_searchview_custom_list"/>
<div class="oe_searchview_custom">
<h4>Save current filter</h4>
<form>
<p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
<p><input id="oe_searchview_custom_public" type="checkbox"/>
<label for="oe_searchview_custom_public">Share with all users</label></p>
<button>Save</button>
</form>
<h4>Save current filter</h4>
<form>
<p><input id="oe_searchview_custom_input" placeholder="Filter name"/></p>
<p>
<input id="oe_searchview_custom_public" type="checkbox"/>
<label for="oe_searchview_custom_public">Share with all users</label>
<input id="oe_searchview_custom_default" type="checkbox"/>
<label for="oe_searchview_custom_default">Use by default</label>
</p>
<button>Save</button>
</form>
</div>
</div>
<div>
</div>
</div>
<div t-name="SearchView.advanced" class="oe_searchview_advanced">
+76
View File
@@ -346,4 +346,80 @@ $(document).ready(function () {
})
.fail(function (e) { ok(false, e && e.message || e); });
});
module('list-edition-onwrite', {
setup: function () {
baseSetup();
}
});
asyncTest('record-to-read', 4, function () {
instance.session.responses['/web/view/load'] = function () {
return {result: {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: {
tag: 'tree',
attrs: { on_write: 'on_write', colors: 'red:a == "foo"' },
children: [
{tag: 'field', attrs: {name: 'a'}}
]
}
}};
};
instance.session.responses['/web/dataset/call_kw:read'] = function (req) {
if (_.isEmpty(req.params.args[0])) {
return {result: []};
} else if (_.isEqual(req.params.args[0], [1])) {
return {result: [
{id: 1, a: 'some value'}
]};
} else if (_.isEqual(req.params.args[0], [42])) {
return {result: [
{id: 42, a: 'foo'}
]};
}
throw new Error(JSON.stringify(req.params));
};
instance.session.responses['/web/dataset/call_kw:default_get'] = function () {
return {result: {}};
};
instance.session.responses['/web/dataset/call_kw:create'] = function () {
return {result: 1};
};
instance.session.responses['/web/dataset/call_kw:on_write'] = function () {
return {result: [42]};
};
var ds = new instance.web.DataSetStatic(null, 'demo', null, []);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.appendTo($fix)
.then(l.proxy('reload_content'))
.then(function () {
return l.start_edition();
})
.then(function () {
$fix.find('.oe_form_field input').val("some value").change();
})
.then(function () {
return l.save_edition();
})
.always(function () { start(); })
.then(function () {
strictEqual(ds.ids.length, 2,
'should have id of created + on_write');
strictEqual(l.records.length, 2,
'should have record of created + on_write');
strictEqual(
$fix.find('tbody tr:eq(1)').css('color'), 'rgb(255, 0, 0)',
'shoud have color applied');
strictEqual(
$fix.find('tbody tr:eq(2)').css('color'), 'rgb(0, 0, 0)',
'should have default color applied');
}, function (e) {
ok(false, e && e.message || e);
});
});
});
+68
View File
@@ -0,0 +1,68 @@
$(document).ready(function () {
var instance;
var $fix = $('#qunit-fixture');
module('list.buttons', {
setup: function () {
instance = openerp.testing.instanceFor('list');
openerp.testing.loadTemplate(instance);
openerp.testing.mockifyRPC(instance);
}
});
asyncTest('record-deletion', 2, function () {
instance.session.responses['/web/view/load'] = function () {
return {result: {
type: 'tree',
fields: {
a: {type: 'char', string: "A"}
},
arch: {
tag: 'tree',
attrs: { },
children: [
{tag: 'field', attrs: {name: 'a'}},
{tag: 'button', attrs: {type: 'object', name: 'foo'}}
]
}
}};
};
instance.session.responses['/web/dataset/call_kw:read'] = function (req) {
var args = req.params.args[0];
if (_.isEqual(args, [1, 2, 3])) {
return {result: [
{id: 1, a: 'foo'}, {id: 2, a: 'bar'}, {id: 3, a: 'baz'}
]};
} else if (_.isEqual(args, [2])) {
// button action virtually removed record
return {result: []};
}
throw new Error(JSON.stringify(req.params));
};
instance.session.responses['/web/dataset/call_button'] = function () {
return {result: false};
};
var ds = new instance.web.DataSetStatic(null, 'demo', null, [1, 2, 3]);
var l = new instance.web.ListView({}, ds, false, {editable: 'top'});
l.appendTo($fix)
.then(l.proxy('reload_content'))
.then(function () {
var d = $.Deferred();
l.records.bind('remove', function () {
d.resolve();
});
$fix.find('table tbody tr:eq(1) button').click();
return d.promise();
})
.always(function () { start(); })
.then(function () {
strictEqual(l.records.length, 2,
"should have 2 records left");
strictEqual($fix.find('table tbody tr[data-id]').length, 2,
"should have 2 rows left");
}, function (e) {
ok(false, e && e.message || e);
});
});
});
+1
View File
@@ -58,5 +58,6 @@
<script type="text/javascript" src="/web/static/test/evals.js"></script>
<script type="text/javascript" src="/web/static/test/search.js"></script>
<script type="text/javascript" src="/web/static/test/Widget.js"></script>
<script type="text/javascript" src="/web/static/test/list.js"></script>
<script type="text/javascript" src="/web/static/test/list-editable.js"></script>
</html>
+16 -1
View File
@@ -2,6 +2,22 @@
* OpenERP web_calendar
*---------------------------------------------------------*/
(function() {
// Monkey patch dhtml scheduler in order to fix a bug.
// It manually implements some kind of dbl click event
// bubbling but fails to do it properly.
if (this.scheduler) {
var old_scheduler_dblclick = scheduler._on_dbl_click;
scheduler._on_dbl_click = function(e, src) {
if (src && !src.className) {
return;
} else {
old_scheduler_dblclick.apply(this, arguments);
}
};
}
}());
openerp.web_calendar = function(instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
@@ -219,7 +235,6 @@ instance.web_calendar.CalendarView = instance.web.View.extend({
if (!date.between(this.range_start, this.range_stop)) {
this.update_range_dates(date);
this.ranged_search();
this.$el.find(".dhx_cal_navline div").removeAttr('style');
}
this.ready.resolve();
},
+48 -44
View File
@@ -27,6 +27,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
this.group_by = null;
this.group_by_field = {};
this.grouped_by_m2o = false;
this.many2manys = [];
this.state = {
groups : {},
records : {}
@@ -130,12 +131,11 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
}
switch (node.tag) {
case 'field':
if(this.fields_view.fields[ node.attrs['name'] ].type == 'many2many'){
node.tag = 'div';
node.attrs['class'] = 'oe_form_field oe_tags';
node.attrs['model'] = this.fields_view.fields[node.attrs['name']].relation;
node.attrs['t-att-data'] = 'record.' + node.attrs['name'] + '.raw_value';
}else {
if (this.fields_view.fields[node.attrs.name].type === 'many2many') {
this.many2manys.push(node.attrs.name);
node.tag = 'div';
node.attrs['class'] = (node.attrs['class'] || '') + ' oe_form_field oe_tags';
} else {
node.tag = QWeb.prefix;
node.attrs[QWeb.prefix + '-esc'] = 'record.' + node.attrs['name'] + '.value';
}
@@ -388,7 +388,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
} else {
this.$el.find('.oe_kanban_draghandle').removeClass('oe_kanban_draghandle');
}
instance.web_kanban.KanbanView.postprocessing_widget_many2many_tags(self);
this.postprocess_m2m_tags();
},
on_record_moved : function(record, old_group, old_index, new_group, new_index) {
var self = this;
@@ -470,44 +470,48 @@ instance.web_kanban.KanbanView = instance.web.View.extend({
this.$el.find('.oe_view_nocontent').click(function() {
create_nocontent.effect('bounce', {distance: 18, times: 5}, 150);
});
},
/*
* postprocessing of fields type many2many
* make the rpc request for all ids/model and insert value inside .oe_tags fields
*/
postprocess_m2m_tags: function() {
var self = this;
if (!this.many2manys.length) {
return;
}
var relations = {};
this.groups.forEach(function(group) {
group.records.forEach(function(record) {
self.many2manys.forEach(function(name) {
var field = record.record[name];
var $el = record.$('.oe_form_field.oe_tags[name=' + name + ']');
if (!relations[field.relation]) {
relations[field.relation] = { ids: [], elements: {}};
}
var rel = relations[field.relation];
field.raw_value.forEach(function(id) {
rel.ids.push(id);
if (!rel.elements[id]) {
rel.elements[id] = [];
}
rel.elements[id].push($el[0]);
});
});
});
});
_.each(relations, function(rel, rel_name) {
var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context());
dataset.name_get(_.uniq(rel.ids)).done(function(result) {
result.forEach(function(nameget) {
$(rel.elements[nameget[0]]).append('<span class="oe_tag">' + _.str.escapeHTML(nameget[1]) + '</span>');
});
});
});
}
});
/*
* widget for list of tags/categories
* make the rpc request for all ids/model and insert value inside .oe_tags fields
*/
instance.web_kanban.KanbanView.postprocessing_widget_many2many_tags = function(self){
var model_list_id={};
// select all widget for the kanban view or the widget inside the record
self.$el.find(".oe_form_field.oe_tags").each(function(){
var model = $(this).attr("model");
if(model.length){
var data = $(this).attr("data");
var list = data.split(",");
//select all id (per model)
if(!model_list_id[model]) model_list_id[model]=[];
for(var t=0;t<list.length;t++) if(list[t]!="") model_list_id[model].push( list[t] );
}
});
// rpc and insert
for(var model in model_list_id){
if(model_list_id[model].length>0){
var block = self.$el.find(".oe_form_field.oe_tags[model='" + model + "']");
var dataset = new instance.web.DataSetSearch(self, model, self.session.context);
dataset.name_get(_.uniq( model_list_id[model] )).done(
function(result) {
for(var t=0;t<result.length;t++){
block.filter(function(){ return this.getAttribute("data").match(new RegExp('(^|,)'+result[t][0]+'(,|$)')); })
.append('<span class="oe_tag" data-list_id="' + result[t][0] +'"">'+result[t][1]+'</span>');
}
}
);
}
}
}
function get_class(name) {
return new instance.web.Registry({'tmp' : name}).get_object("tmp");
@@ -946,7 +950,7 @@ instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
self.$el.data('widget', self);
self.bind_events();
self.group.compute_cards_auto_height();
instance.web_kanban.KanbanView.postprocessing_widget_many2many_tags(self);
self.view.postprocess_m2m_tags();
} else {
self.destroy();
}
@@ -991,7 +995,7 @@ instance.web_kanban.KanbanRecord = instance.web.Widget.extend({
id = escape(JSON.stringify(id));
if (options.preview_image)
field = options.preview_image;
url = instance.session.prefix + '/web/binary/image?session_id=' + this.session.session_id + '&model=' + model + '&field=' + field + '&id=' + id;
url = this.session.url('/web/binary/image', {model: model, field: field, id: id});
if (cache !== undefined) {
// Set the cache duration in seconds.
url += '&cache=' + parseInt(cache, 10);