diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 678eacd7f3e..269aa13d58e 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -26,6 +26,7 @@ This module provides the core of the OpenERP Web Client. "static/lib/spinjs/spin.js", "static/lib/jquery.autosize/jquery.autosize.js", "static/lib/jquery.blockUI/jquery.blockUI.js", + "static/lib/jquery.placeholder/jquery.placeholder.js", "static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js", "static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js", "static/lib/jquery.ui.notify/js/jquery.notify.js", diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index e1a3c476a32..8f1855f205d 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -13,7 +13,10 @@ import os import re import simplejson import time +import urllib import urllib2 +import urlparse +import xmlrpclib import zlib from xml.etree import ElementTree from cStringIO import StringIO @@ -90,16 +93,50 @@ 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 exists, return it else return False +def db_monodb_redirect(req): + db = False + redirect = False + + # 1 try the db in the url + db_url = req.params.get('db') + if db_url: + return (db_url, False) + try: dbs = db_list(req) - if len(dbs) == 1: - return dbs[0] except Exception: # ignore access denied - pass - return False + dbs = [] + + # 2 use the database from the cookie if it's listable and still listed + cookie_db = req.httprequest.cookies.get('last_used_database') + if cookie_db in dbs: + db = cookie_db + + # 3 use the first db + if dbs and not db: + db = dbs[0] + + # redirect to the chosen db if multiple are available + if db and len(dbs) > 1: + query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True)) + query.update({ 'db': db }) + redirect = req.httprequest.path + '?' + urllib.urlencode(query) + return (db, redirect) + +def db_monodb(req): + # if only one db exists, return it else return False + return db_monodb_redirect(req)[0] + +def redirect_with_hash(req, url, code=303): + if req.httprequest.user_agent.browser == 'msie': + try: + version = float(req.httprequest.user_agent.version) + if version < 10: + return "" % url + except Exception: + pass + return werkzeug.utils.redirect(url, code) def module_topological_sort(modules): """ Return a list of module names sorted so that their dependencies of the @@ -290,20 +327,19 @@ def manifest_glob(req, extension, addons=None, db=None): return r def manifest_list(req, extension, mods=None, db=None): + """ list ressources to load specifying either: + mods: a comma separated string listing modules + db: a database name (return all installed modules in that database) + """ if not req.debug: path = '/web/webclient/' + extension if mods is not None: - path += '?mods=' + mods + path += '?' + urllib.urlencode({'mods': mods}) elif db: - path += '?db=' + db + path += '?' + urllib.urlencode({'db': db}) return [path] files = manifest_glob(req, extension, addons=mods, db=db) - i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \ - req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1 - if i_am_diabetic: - return [wp for _fp, wp in files] - else: - return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files] + return [wp for _fp, wp in files] def get_last_modified(files): """ Returns the modification time of the most recently modified @@ -533,6 +569,10 @@ class Home(openerpweb.Controller): @openerpweb.httprequest def index(self, req, s_action=None, db=None, **kw): + db, redir = db_monodb_redirect(req) + if redir: + return redirect_with_hash(req, redir) + js = "\n ".join('' % i for i in manifest_list(req, 'js', db=db)) css = "\n ".join('' % i for i in manifest_list(req, 'css', db=db)) @@ -683,7 +723,7 @@ class WebClient(openerpweb.Controller): @openerpweb.jsonrequest def version_info(self, req): - return openerp.service.web_services.RPC_VERSION_1 + return openerp.service.common.exp_version() class Proxy(openerpweb.Controller): _cp_path = '/web/proxy' @@ -868,7 +908,7 @@ class Session(openerpweb.Controller): """ saved_actions = req.httpsession.get('saved_actions') if not saved_actions: - saved_actions = {"next":0, "actions":{}} + saved_actions = {"next":1, "actions":{}} req.httpsession['saved_actions'] = saved_actions # we don't allow more than 10 stored actions if len(saved_actions["actions"]) >= 10: @@ -1310,7 +1350,7 @@ class Binary(openerpweb.Controller): 'id': attachment_id } except Exception: - args = {'error':e.faultCode } + args = {'error': "Something horrible happened"} return out % (simplejson.dumps(callback), simplejson.dumps(args)) @openerpweb.httprequest diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index 5692139f002..b6327bf4622 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -32,12 +32,12 @@ NOMODULE_TEMPLATE = Template(u""" -""") +""", default_filters=['h']) NOTFOUND = Template(u"""

Unable to find the module [${module}], please check that the module name is correct and the module is on OpenERP's path.

<< Back to tests -""") +""", default_filters=['h']) TESTING = Template(u""" <%def name="to_path(module, p)">/${module}/${p} @@ -51,9 +51,9 @@ TESTING = Template(u""" @@ -83,7 +83,7 @@ TESTING = Template(u""" % endif % endfor -""") +""", default_filters=['h']) class TestRunnerController(http.Controller): _cp_path = '/web/tests' diff --git a/addons/web/doc/search_view.rst b/addons/web/doc/search_view.rst index 16a91c5e9ac..12b0eaf3104 100644 --- a/addons/web/doc/search_view.rst +++ b/addons/web/doc/search_view.rst @@ -107,6 +107,12 @@ formatted differently). If an input *may* fetch multiple completion items, it *should* prefix those with a section title using its own name. This has no technical consequence but is clearer for users. +.. note:: + + If a field is :js:func:`invisible + `, its completion function will + *not* be called. + Providing drawer/supplementary UI +++++++++++++++++++++++++++++++++ @@ -145,6 +151,11 @@ started only once (per view). dynamically collects, lays out and renders filters? => exercises drawer thingies +.. note:: + + An :js:func:`invisible ` input + will not be inserted into the drawer. + Converting from facet objects +++++++++++++++++++++++++++++ diff --git a/addons/web/http.py b/addons/web/http.py index f92a1b8c687..05069574da1 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -92,6 +92,14 @@ class WebRequest(object): if not self.session: self.session = session.OpenERPSession() self.httpsession[self.session_id] = self.session + + # set db/uid trackers - they're cleaned up at the WSGI + # dispatching phase in openerp.service.wsgi_server.application + if self.session._db: + threading.current_thread().dbname = self.session._db + if self.session._uid: + threading.current_thread().uid = self.session._uid + self.context = self.params.pop('context', {}) self.debug = self.params.pop('debug', False) is not False # Determine self.lang @@ -286,7 +294,8 @@ class HttpRequest(WebRequest): _logger.debug("%s --> %s.%s %r", self.httprequest.method, method.im_class.__name__, method.__name__, akw) try: r = method(self, **self.params) - except Exception: + except Exception, e: + _logger.exception("An exception occured during an http request") se = serialize_exception(e) error = { 'code': 200, diff --git a/addons/web/session.py b/addons/web/session.py index 13c9d28230c..13c3bc7cb94 100644 --- a/addons/web/session.py +++ b/addons/web/session.py @@ -38,6 +38,7 @@ class Model(object): self.proxy = self.session.proxy('object') def __getattr__(self, method): + self.session.assert_valid() def proxy(*args, **kw): result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw) # reorder read @@ -109,8 +110,8 @@ class OpenERPSession(object): if self._uid and not force: return # TODO use authenticate instead of login - uid = self.proxy("common").login(self._db, self._login, self._password) - if not uid: + self._uid = self.proxy("common").login(self._db, self._login, self._password) + if not self._uid: raise AuthenticationError("Authentication failure") def ensure_valid(self): @@ -121,7 +122,6 @@ class OpenERPSession(object): self._uid = None def execute(self, model, func, *l, **d): - self.assert_valid() model = self.model(model) r = getattr(model, func)(*l, **d) return r diff --git a/addons/web/static/lib/datejs/globalization/ar-SY.js b/addons/web/static/lib/datejs/globalization/ar-SY.js index 317881211e6..b504a21d324 100644 --- a/addons/web/static/lib/datejs/globalization/ar-SY.js +++ b/addons/web/static/lib/datejs/globalization/ar-SY.js @@ -11,8 +11,8 @@ Date.CultureInfo = { firstLetterDayNames: ["أ", "ا", "ث", "أ", "خ", "ج", "س"], /* Month Name Strings */ - monthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], - abbreviatedMonthNames: ["كانون الثاني", "شباط", "آذار", "نيسان", "أيار", "حزيران", "تموز", "آب", "أيلول", "تشرين الأول", "تشرين الثاني", "كانون الأول"], + monthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], + abbreviatedMonthNames: ["يناير", "فبراير", "مارس", "أبريل", "مايو", "يونيو", "يوليو", "أغسطس", "سبتمبر", "أكتوبر", "نوفمبر", "ديسمبر"], /* AM/PM Designators */ amDesignator: "ص", @@ -82,18 +82,18 @@ Date.CultureInfo = { * As well, please review the list of "Future Strings" section below. */ regexPatterns: { - jan: /^كانون الثاني/i, - feb: /^شباط/i, - mar: /^آذار/i, - apr: /^نيسان/i, - may: /^أيار/i, - jun: /^حزيران/i, - jul: /^تموز/i, - aug: /^آب/i, - sep: /^أيلول/i, - oct: /^تشرين الأول/i, - nov: /^تشرين الثاني/i, - dec: /^كانون الأول/i, + jan: /^يناير/i, + feb: /^فبراير/i, + mar: /^مارس/i, + apr: /^أبريل/i, + may: /^مايو/i, + jun: /^يونيو/i, + jul: /^يوليو/i, + aug: /^أغسطس/i, + sep: /^سبتمبر/i, + oct: /^أكتوبر/i, + nov: /^نوفمبر/i, + dec: /^ديسمبر/i, sun: /^الاحد/i, mon: /^ا(1)?/i, @@ -192,4 +192,4 @@ Date.CultureInfo = { * end end * long long * short short - */ \ No newline at end of file + */ diff --git a/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js b/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js new file mode 100755 index 00000000000..d8051fa269d --- /dev/null +++ b/addons/web/static/lib/jquery.placeholder/jquery.placeholder.js @@ -0,0 +1,157 @@ +/*! http://mths.be/placeholder v2.0.7 by @mathias */ +;(function(window, document, $) { + + var isInputSupported = 'placeholder' in document.createElement('input'), + isTextareaSupported = 'placeholder' in document.createElement('textarea'), + prototype = $.fn, + valHooks = $.valHooks, + hooks, + placeholder; + + if (isInputSupported && isTextareaSupported) { + + placeholder = prototype.placeholder = function() { + return this; + }; + + placeholder.input = placeholder.textarea = true; + + } else { + + placeholder = prototype.placeholder = function() { + var $this = this; + $this + .filter((isInputSupported ? 'textarea' : ':input') + '[placeholder]') + .not('.placeholder') + .bind({ + 'focus.placeholder': clearPlaceholder, + 'blur.placeholder': setPlaceholder + }) + .data('placeholder-enabled', true) + .trigger('blur.placeholder'); + return $this; + }; + + placeholder.input = isInputSupported; + placeholder.textarea = isTextareaSupported; + + hooks = { + 'get': function(element) { + var $element = $(element); + return $element.data('placeholder-enabled') && $element.hasClass('placeholder') ? '' : element.value; + }, + 'set': function(element, value) { + var $element = $(element); + if (!$element.data('placeholder-enabled')) { + return element.value = value; + } + if (value == '') { + element.value = value; + // Issue #56: Setting the placeholder causes problems if the element continues to have focus. + if (element != document.activeElement) { + // We can't use `triggerHandler` here because of dummy text/password inputs :( + setPlaceholder.call(element); + } + } else if ($element.hasClass('placeholder')) { + clearPlaceholder.call(element, true, value) || (element.value = value); + } else { + element.value = value; + } + // `set` can not return `undefined`; see http://jsapi.info/jquery/1.7.1/val#L2363 + return $element; + } + }; + + isInputSupported || (valHooks.input = hooks); + isTextareaSupported || (valHooks.textarea = hooks); + + $(function() { + // Look for forms + $(document).delegate('form', 'submit.placeholder', function() { + // Clear the placeholder values so they don't get submitted + var $inputs = $('.placeholder', this).each(clearPlaceholder); + setTimeout(function() { + $inputs.each(setPlaceholder); + }, 10); + }); + }); + + // Clear placeholder values upon page reload + $(window).bind('beforeunload.placeholder', function() { + $('.placeholder').each(function() { + this.value = ''; + }); + }); + + } + + function args(elem) { + // Return an object of element attributes + var newAttrs = {}, + rinlinejQuery = /^jQuery\d+$/; + $.each(elem.attributes, function(i, attr) { + if (attr.specified && !rinlinejQuery.test(attr.name)) { + newAttrs[attr.name] = attr.value; + } + }); + return newAttrs; + } + + function clearPlaceholder(event, value) { + var input = this, + $input = $(input); + if (input.value == $input.attr('placeholder') && $input.hasClass('placeholder')) { + if ($input.data('placeholder-password')) { + $input = $input.hide().next().show().attr('id', $input.removeAttr('id').data('placeholder-id')); + // If `clearPlaceholder` was called from `$.valHooks.input.set` + if (event === true) { + return $input[0].value = value; + } + $input.focus(); + } else { + input.value = ''; + $input.removeClass('placeholder'); + input == document.activeElement && input.select(); + } + } + } + + function setPlaceholder() { + var $replacement, + input = this, + $input = $(input), + $origInput = $input, + id = this.id; + if (input.value == '') { + if (input.type == 'password') { + if (!$input.data('placeholder-textinput')) { + try { + $replacement = $input.clone().attr({ 'type': 'text' }); + } catch(e) { + $replacement = $('').attr($.extend(args(this), { 'type': 'text' })); + } + $replacement + .removeAttr('name') + .data({ + 'placeholder-password': true, + 'placeholder-id': id + }) + .bind('focus.placeholder', clearPlaceholder); + $input + .data({ + 'placeholder-textinput': $replacement, + 'placeholder-id': id + }) + .before($replacement); + } + $input = $input.removeAttr('id').hide().prev().attr('id', id).show(); + // Note: `$input[0] != input` now! + } + $input.addClass('placeholder'); + $input[0].value = $input.attr('placeholder'); + } else { + $input.removeClass('placeholder'); + } + } + +}(this, document, jQuery)); \ No newline at end of file diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 1420ab577a8..96b8a23a705 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1,4 +1,4 @@ -@charset "UTF-8"; +@charset "utf-8"; @font-face { font-family: "mnmliconsRegular"; src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot"); @@ -355,6 +355,12 @@ float: right; margin-left: 8px; } +.openerp .oe_text_center { + text-align: center; +} +.openerp .oe_text_left { + text-align: left; +} .openerp .oe_text_right { text-align: right; } @@ -1263,7 +1269,7 @@ color: white; padding: 2px 4px; margin: 1px 6px 0 0; - border: 1px solid lightgrey; + border: 1px solid lightGray; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -moz-border-radius: 4px; -webkit-border-radius: 4px; @@ -1295,7 +1301,7 @@ transform: scale(1.1); } .openerp .oe_secondary_submenu .oe_active { - border-top: 1px solid lightgrey; + border-top: 1px solid lightGray; border-bottom: 1px solid #dedede; text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2), inset 0 -1px 3px rgba(40, 40, 40, 0.2); @@ -1415,7 +1421,13 @@ display: inline-block; overflow: hidden; } +.openerp .oe_view_manager { + display: table; + height: inherit; + width: 100%; +} .openerp .oe_view_manager .oe_view_manager_body { + display: table-row; height: inherit; } .openerp .oe_view_manager .oe_view_manager_view_kanban { @@ -1800,7 +1812,7 @@ } .openerp .oe_searchview .oe_searchview_drawer { position: absolute; - z-index: 100; + z-index: 2; margin-top: 4px; top: 100%; right: -1px; @@ -2225,6 +2237,26 @@ .openerp .oe_form .oe_subtotal_footer label.oe_form_label_help { font-weight: normal; } +.openerp .oe_form .oe_form_box_info { + background: #ffee99; + border-bottom: 1px solid #ccbb66; + padding: 4px; +} +.openerp .oe_form .oe_form_box_info > p { + margin: auto; +} +.openerp .oe_form .oe_form_box_warning { + background: #bd362f; + border-bottom: 1px solid #990000; + padding: 4px; +} +.openerp .oe_form .oe_form_box_warning * { + color: white; + text-shadow: none; +} +.openerp .oe_form .oe_form_box_warning > p { + margin: auto; +} .openerp .oe_form .oe_form_button { margin: 2px; } @@ -2254,7 +2286,7 @@ } .openerp .oe_form .oe_form_label_help[for] span, .openerp .oe_form .oe_form_label[for] span { font-size: 80%; - color: darkgreen; + color: darkGreen; vertical-align: top; position: relative; top: -4px; @@ -2305,6 +2337,13 @@ .openerp .oe_form .oe_form_field_text { width: 100%; } +.openerp .oe_form .oe_form_field_text .oe_form_text_content { + text-overflow: ellipsis; + display: inline-block; + white-space: pre-wrap; + overflow-x: hidden; + width: 100%; +} .openerp .oe_form .oe_form_field_char input, .openerp .oe_form .oe_form_field_url input, .openerp .oe_form .oe_form_field_email input, @@ -2326,6 +2365,7 @@ width: 100%; display: inline-block; padding: 2px 2px 2px 0px; + vertical-align: top; } .openerp .oe_form .oe_form_field input { margin: 0px; @@ -2371,7 +2411,6 @@ white-space: nowrap; } .openerp .oe_form .oe_form_field_boolean { - padding-top: 4px; width: auto; } .openerp .oe_form .oe_datepicker_container { @@ -3093,8 +3132,25 @@ color: #333333; } +@-moz-document url-prefix() { + .openerp .oe_view_manager .oe_view_manager_switch li { + line-height: 21px; + } + .openerp .oe_searchview .oe_searchview_search { + top: -1px; + } + .openerp .oe_form_field_many2one .oe_m2o_cm_button { + line-height: 18px; + } + .openerp .oe_secondary_submenu { + line-height: 14px; + } + .openerp .oe_webclient .oe_star_on, .openerp .oe_webclient .oe_star_off { + top: 0px; + } +} + .kitten-mode-activated { - background-image: url(http://placekitten.com/g/1365/769); background-size: cover; background-attachment: fixed; } @@ -3147,6 +3203,14 @@ div.ui-widget-overlay { border-radius: 3px; } +.openerp .db_option_table td { + padding-bottom: 10px !important; +} + +.openerp_ie .placeholder { + color: #afafb6 !important; + font-style: italic !important; +} .openerp_ie .oe_form_field_boolean input { background: white; } @@ -3165,6 +3229,9 @@ div.ui-widget-overlay { padding-top: 0; padding-bottom: 0; } +.openerp_ie .oe_view_manager_view_kanban { + display: table-cell; +} .openerp_ie .oe_view_manager_buttons button.oe_write_full { padding-top: 0; padding-bottom: 0; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index c31ffbe43a3..1e70d09e0fd 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -370,6 +370,10 @@ $sheet-padding: 16px .oe_right float: right margin-left: 8px + .oe_text_center + text-align: center + .oe_text_left + text-align: left .oe_text_right text-align: right .oe_clear @@ -1137,7 +1141,11 @@ $sheet-padding: 16px // }}} // ViewManager common {{{ .oe_view_manager + display: table + height: inherit + width: 100% .oe_view_manager_body + display: table-row height: inherit .oe_view_manager_view_kanban height: inherit @@ -1432,7 +1440,7 @@ $sheet-padding: 16px .oe_searchview_drawer position: absolute - z-index: 100 + z-index: 2 // detach drawer from field slightly margin-top: 4px top: 100% @@ -1761,7 +1769,21 @@ $sheet-padding: 16px padding: 2px 11px 2px 0px !important label.oe_form_label_help font-weight: normal - + .oe_form_box_info + background: #fe9 + border-bottom: 1px solid #cb6 + padding: 4px + > p + margin: auto + .oe_form_box_warning + background: #bd362f + border-bottom: 1px solid #900 + padding: 4px + * + color: white + text-shadow: none + > p + margin: auto // }}} // FormView.group {{{ .oe_form @@ -1833,6 +1855,12 @@ $sheet-padding: 16px .oe_form .oe_form_field_text width: 100% + .oe_form_text_content + text-overflow: ellipsis + display: inline-block + white-space: pre-wrap + overflow-x: hidden + width: 100% .oe_form_field_char input, .oe_form_field_url input, .oe_form_field_email input, @@ -1850,6 +1878,7 @@ $sheet-padding: 16px width: 100% display: inline-block padding: 2px 2px 2px 0px + vertical-align: top input margin: 0px input[type="text"], input[type="password"], input[type="file"], select @@ -1879,7 +1908,6 @@ $sheet-padding: 16px .oe_form_field_datetime white-space: nowrap .oe_form_field_boolean - padding-top: 4px width: auto .oe_datepicker_container display: none @@ -2441,9 +2469,22 @@ $sheet-padding: 16px float: right color: #333 // }}} +@-moz-document url-prefix() + .openerp + .oe_view_manager .oe_view_manager_switch li + line-height: 21px + .oe_searchview .oe_searchview_search + top: -1px + .oe_form_field_many2one .oe_m2o_cm_button + line-height: 18px + .oe_secondary_submenu + line-height: 14px + .oe_webclient + .oe_star_on, .oe_star_off + top: 0px + // Kitten Mode {{{ .kitten-mode-activated - background-image: url(http://placekitten.com/g/1365/769) background-size: cover background-attachment: fixed >* @@ -2485,8 +2526,16 @@ div.ui-widget-overlay @include radius(3px) // }}} +.openerp + .db_option_table + td + padding-bottom: 10px !important + // Internet Explorer 9+ specifics {{{ .openerp_ie + .placeholder + color: $tag-border !important + font-style: italic !important .oe_form_field_boolean input background: #fff .db_option_table .oe_form_field_selection @@ -2502,6 +2551,8 @@ div.ui-widget-overlay button.oe_highlight padding-top: 0 padding-bottom: 0 + .oe_view_manager_view_kanban + display: table-cell .oe_view_manager_buttons button.oe_write_full padding-top: 0 diff --git a/addons/web/static/src/img/back-enable.jpg b/addons/web/static/src/img/back-enable.jpg new file mode 100644 index 00000000000..79a7098f6da Binary files /dev/null and b/addons/web/static/src/img/back-enable.jpg differ diff --git a/addons/web/static/src/img/icons/gtk-normal.png b/addons/web/static/src/img/icons/gtk-normal.png new file mode 100644 index 00000000000..c153cc1c528 Binary files /dev/null and b/addons/web/static/src/img/icons/gtk-normal.png differ diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 1407df4d33f..696f25109a9 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -327,6 +327,34 @@ instance.web.ExceptionHandler = { */ instance.web.crash_manager_registry = new instance.web.Registry(); +/** + * Handle redirection warnings, which behave more or less like a regular + * warning, with an additional redirection button. + */ +instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, { + init: function(parent, error) { + this._super(parent); + this.error = error; + }, + display: function() { + error = this.error; + error.data.message = error.data.arguments[0]; + + instance.web.dialog($('
' + QWeb.render('CrashManager.warning', {error: error}) + '
'), { + title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"), + buttons: [ + {text: _t("Ok"), click: function() { $(this).dialog("close"); }}, + {text: error.data.arguments[2], click: function() { + window.location.href='#action='+error.data.arguments[1]; + $(this).dialog("close"); + }} + ] + }); + this.destroy(); + } +}); +instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler'); + instance.web.Loading = instance.web.Widget.extend({ template: _t("Loading"), init: function(parent) { @@ -502,7 +530,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ 'login': 'admin', 'password': form_obj['create_admin_pwd'], 'login_successful': function() { - self.do_action("reload"); + var action = { + type: "ir.actions.client", + tag: 'reload', + params: { + url_search : { + db: form_obj['db_name'], + }, + } + }; + self.do_action(action); }, }, _push_me: false, @@ -613,6 +650,11 @@ instance.web.client_actions.add("database_manager", "instance.web.DatabaseManage instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, + events: { + 'change input[name=db],select[name=db]': function(ev) { + this.set('database_selector', $(ev.currentTarget).val()); + }, + }, init: function(parent, action) { this._super(parent); @@ -624,18 +666,18 @@ instance.web.Login = instance.web.Widget.extend({ if (_.isEmpty(this.params)) { this.params = $.bbq.getState(true); } + if (action && action.params && action.params.db) { + this.params.db = action.params.db; + } else if ($.deparam.querystring().db) { + this.params.db = $.deparam.querystring().db; + } + if (this.params.db) { + this.selected_db = this.params.db; + } if (this.params.login_successful) { this.on('login_successful', this, this.params.login_successful); } - - if (this.has_local_storage && this.remember_credentials) { - this.selected_db = localStorage.getItem('last_db_login_success'); - this.selected_login = localStorage.getItem('last_login_login_success'); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - this.selected_password = localStorage.getItem('last_password_login_success'); - } - } }, start: function() { var self = this; @@ -643,10 +685,10 @@ instance.web.Login = instance.web.Widget.extend({ self.$el.find('.oe_login_manage_db').click(function() { self.do_action("database_manager"); }); + self.on('change:database_selector', this, function() { + this.database_selected(this.get('database_selector')); + }); var d = $.when(); - if ($.deparam.querystring().db) { - self.params.db = $.deparam.querystring().db; - } if ($.param.fragment().token) { self.params.token = $.param.fragment().token; } @@ -654,16 +696,44 @@ instance.web.Login = instance.web.Widget.extend({ if (self.params.db && self.params.login && self.params.password) { d = self.do_login(self.params.db, self.params.login, self.params.password); } else { - if (self.params.db) { - self.on_db_loaded([self.params.db]) - } else { - d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed); - } + d = self.rpc("/web/database/get_list", {}) + .done(self.on_db_loaded) + .fail(self.on_db_failed) + .always(function() { + if (self.selected_db && self.has_local_storage && self.remember_credentials) { + self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || ''); + if (self.session.debug) { + self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || ''); + } + } + }); } return d; }, + remember_last_used_database: function(db) { + // This cookie will be used server side in order to avoid db reloading on first visit + var ttl = 24 * 60 * 60 * 365; + document.cookie = [ + 'last_used_database=' + db, + 'path=/', + 'max-age=' + ttl, + 'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString() + ].join(';'); + }, + database_selected: function(db) { + var params = $.deparam.querystring(); + params.db = db; + this.remember_last_used_database(db); + this.$('.oe_login_dbpane').empty().text(_t('Loading...')); + this.$('[name=login], [name=password]').prop('readonly', true); + instance.web.redirect('/?' + $.param(params)); + }, on_db_loaded: function (result) { + var self = this; this.db_list = result; + if (!this.selected_db) { + this.selected_db = result[0]; + } 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"); @@ -704,17 +774,11 @@ instance.web.Login = instance.web.Widget.extend({ self.hide_error(); self.$(".oe_login_pane").fadeOut("slow"); return this.session.session_authenticate(db, login, password).then(function() { - if (self.has_local_storage) { - if(self.remember_credentials) { - localStorage.setItem('last_db_login_success', db); - localStorage.setItem('last_login_login_success', login); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - localStorage.setItem('last_password_login_success', password); - } - } else { - localStorage.setItem('last_db_login_success', ''); - localStorage.setItem('last_login_login_success', ''); - localStorage.setItem('last_password_login_success', ''); + self.remember_last_used_database(db); + if (self.has_local_storage && self.remember_credentials) { + localStorage.setItem(db + '|last_login', login); + if (self.session.debug) { + localStorage.setItem(db + '|last_password', password); } } self.trigger('login_successful'); @@ -772,6 +836,9 @@ instance.web.Reload = function(parent, action) { var sobj = $.deparam(l.search.substr(1)); sobj.ts = new Date().getTime(); + if (params.url_search) { + sobj = _.extend(sobj, params.url_search); + } var search = '?' + $.param(sobj); var hash = l.hash; @@ -1056,7 +1123,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ if (!self.session.uid) return; var func = new instance.web.Model("res.users").get_func("read"); - return func(self.session.uid, ["name", "company_id"]).then(function(res) { + return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) { var topbar_name = res.name; if(instance.session.debug) topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db); @@ -1072,6 +1139,9 @@ instance.web.UserMenu = instance.web.Widget.extend({ }; this.update_promise = this.update_promise.then(fct, fct); }, + on_menu_help: function() { + window.open('http://help.openerp.com', '_blank'); + }, on_menu_logout: function() { this.trigger('user_logout'); }, @@ -1185,6 +1255,7 @@ instance.web.WebClient = instance.web.Client.extend({ return $.when(this._super()).then(function() { if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) { $("body").addClass("kitten-mode-activated"); + $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")"); if ($.blockUI) { $.blockUI.defaults.message = ''; } @@ -1271,7 +1342,7 @@ instance.web.WebClient = instance.web.Client.extend({ }, logo_edit: function(ev) { var self = this; - new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"]).then(function(res) { + self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) { self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) { result.res_id = res['company_id'][0]; result.target = "new"; @@ -1292,7 +1363,7 @@ instance.web.WebClient = instance.web.Client.extend({ }, check_timezone: function() { var self = this; - return new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']]).then(function(result) { + return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) { var user_offset = result[0]['tz_offset']; var offset = -(new Date().getTimezoneOffset()); // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually @@ -1368,8 +1439,9 @@ instance.web.WebClient = instance.web.Client.extend({ }, on_hashchange: function(event) { var self = this; - var state = event.getState(true); - if (!_.isEqual(this._current_state, state)) { + var stringstate = event.getState(false); + if (!_.isEqual(this._current_state, stringstate)) { + var state = event.getState(true); if(!state.action && state.menu_id) { self.menu.has_been_loaded.done(function() { self.menu.do_reload().done(function() { @@ -1381,13 +1453,13 @@ instance.web.WebClient = instance.web.Client.extend({ this.action_manager.do_load_state(state, !!this._current_state); } } - this._current_state = state; + this._current_state = stringstate; }, do_push_state: function(state) { this.set_title(state.title); delete state.title; var url = '#' + $.param(state); - this._current_state = _.clone(state); + this._current_state = $.deparam($.param(state), false); // stringify all values $.bbq.pushState(url); this.trigger('state_pushed', state); }, @@ -1397,9 +1469,10 @@ instance.web.WebClient = instance.web.Client.extend({ .then(function (result) { return self.action_mutex.exec(function() { if (options.needaction) { - result.context = new instance.web.CompoundContext( - result.context, - {search_default_message_unread: true}); + result.context = new instance.web.CompoundContext(result.context, { + search_default_message_unread: true, + search_disable_custom_filters: true, + }); } var completed = $.Deferred(); $.when(self.action_manager.do_action(result, { @@ -1466,8 +1539,9 @@ instance.web.EmbeddedClient = instance.web.Client.extend({ }); }, - do_action: function(action) { - return this.action_manager.do_action(action); + do_action: function(/*...*/) { + var am = this.action_manager; + return am.do_action.apply(am, arguments); }, authenticate: function() { diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index edd23e14522..c4bb81117f8 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -228,6 +228,42 @@ instance.web.ParentedMixin = { isDestroyed : function() { return this.__parentedDestroyed; }, + /** + Utility method to only execute asynchronous actions if the current + object has not been destroyed. + + @param {$.Deferred} promise The promise representing the asynchronous + action. + @param {bool} [reject=false] If true, the returned promise will be + rejected with no arguments if the current + object is destroyed. If false, the + returned promise will never be resolved + or rejected. + @returns {$.Deferred} A promise that will mirror the given promise if + everything goes fine but will either be rejected + with no arguments or never resolved if the + current object is destroyed. + */ + alive: function(promise, reject) { + var def = $.Deferred(); + var self = this; + promise.done(function() { + if (! self.isDestroyed()) { + if (! reject) + def.resolve.apply(def, arguments); + else + def.reject(); + } + }).fail(function() { + if (! self.isDestroyed()) { + if (! reject) + def.reject.apply(def, arguments); + else + def.reject(); + } + }); + return def.promise(); + }, /** * Inform the object it should destroy itself, releasing any * resource it could have reserved. @@ -495,16 +531,7 @@ instance.web.Controller = instance.web.Class.extend(instance.web.PropertiesMixin return false; }, rpc: function(url, data, options) { - var def = $.Deferred(); - var self = this; - instance.session.rpc(url, data, options).done(function() { - if (!self.isDestroyed()) - def.resolve.apply(def, arguments); - }).fail(function() { - if (!self.isDestroyed()) - def.reject.apply(def, arguments); - }); - return def.promise(); + return this.alive(instance.session.rpc(url, data, options)); } }); @@ -674,10 +701,10 @@ instance.web.Widget = instance.web.Controller.extend({ * Method called after rendering. Mostly used to bind actions, perform asynchronous * calls, etc... * - * By convention, the method should return a promise to inform the caller when - * this widget has been initialized. + * By convention, this method should return an object that can be passed to $.when() + * to inform the caller when this widget has been initialized. * - * @returns {jQuery.Deferred} + * @returns {jQuery.Deferred or any} */ start: function() { return $.when(); diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 43d292b66db..dedb4a49710 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -89,6 +89,10 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess }); }, session_is_valid: function() { + var db = $.deparam.querystring().db; + if (db && this.db !== db) { + return false; + } return !!this.uid; }, /** diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index bbff633c687..f1b043bacf9 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -481,9 +481,12 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, * Creates a new record in db * * @param {Object} data field values to set on the new record + * @param {Object} options Dictionary that can contain the following keys: + * - readonly_fields: Values from readonly fields that were updated by + * on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * @returns {$.Deferred} */ - create: function(data) { + create: function(data, options) { return this._model.call('create', [data], {context: this.get_context()}); }, /** @@ -491,8 +494,10 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, * * @param {Number|String} id identifier for the record to alter * @param {Object} data field values to write into the record - * @param {Function} callback function called with operation result - * @param {Function} error_callback function called in case of write error + * @param {Object} options Dictionary that can contain the following keys: + * - context: The context to use in the server-side call. + * - readonly_fields: Values from readonly fields that were updated by + * on_changes. Only used by the BufferedDataSet to make the o2m work correctly. * @returns {$.Deferred} */ write: function (id, data, options) { @@ -732,10 +737,13 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ self.last_default_get = res; }); }, - create: function(data) { - var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, - defaults: this.last_default_get}; - this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)})); + create: function(data, options) { + var cached = { + id:_.uniqueId(this.virtual_id_prefix), + values: _.extend({}, data, (options || {}).readonly_fields || {}), + defaults: this.last_default_get + }; + this.to_create.push(_.extend(_.clone(cached), {values: _.clone(data)})); this.cache.push(cached); return $.Deferred().resolve(cached.id).promise(); }, @@ -762,7 +770,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ cached = {id: id, values: {}}; this.cache.push(cached); } - $.extend(cached.values, record.values); + $.extend(cached.values, _.extend({}, record.values, (options || {}).readonly_fields || {})); if (dirty) this.trigger("dataset_changed", id, data, options); return $.Deferred().resolve(true).promise(); @@ -860,8 +868,16 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } return completion.promise(); }, - call_button: function (method, args) { - var id = args[0][0], index; + /** + * Invalidates caching of a record in the dataset to ensure the next read + * of that record will hit the server. + * + * Of use when an action is going to remote-alter a record which will then + * need to be reloaded, e.g. action button. + * + * @param {Object} id record to remove from the BDS's cache + */ + evict_record: function (id) { for(var i=0, len=this.cache.length; i} filters elements of the group - * @param {instance.web.SearchView} view view in which the filters are contained + * @param {instance.web.SearchView} parent parent in which the filters are contained */ - init: function (filters, view) { + init: function (filters, parent) { // If all filters are group_by and we're not initializing a GroupbyGroup, // create a GroupbyGroup instead of the current FilterGroup if (!(this instanceof instance.web.search.GroupbyGroup) && _(filters).all(function (f) { - return f.attrs.context && f.attrs.context.group_by; })) { - return new instance.web.search.GroupbyGroup(filters, view); + if (!f.attrs.context) { return false; } + var c = instance.web.pyeval.eval('context', f.attrs.context); + return !_.isEmpty(c.group_by);})) { + return new instance.web.search.GroupbyGroup(filters, parent); } - this._super(view); + this._super(parent); this.filters = filters; this.view.query.on('add remove change reset', this.proxy('search_change')); }, @@ -1096,6 +1138,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in var self = this; item = item.toLowerCase(); var facet_values = _(this.filters).chain() + .filter(function (filter) { return filter.visible(); }) .filter(function (filter) { var at = { string: filter.attrs.string || '', @@ -1122,8 +1165,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ icon: 'w', completion_label: _lt("Group by: %s"), - init: function (filters, view) { - this._super(filters, view); + init: function (filters, parent) { + this._super(filters, parent); // Not flanders: facet unicity is handled through the // (category, field) pair of facet attributes. This is all well and // good for regular filter groups where a group matches a facet, but for @@ -1131,8 +1174,8 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ // view which proxies to the first GroupbyGroup, so it can be used // for every GroupbyGroup and still provides the various methods needed // by the search view. Use weirdo name to avoid risks of conflicts - if (!this.getParent()._s_groupby) { - this.getParent()._s_groupby = { + if (!this.view._s_groupby) { + this.view._s_groupby = { help: "See GroupbyGroup#init", get_context: this.proxy('get_context'), get_domain: this.proxy('get_domain'), @@ -1141,14 +1184,14 @@ instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({ } }, match_facet: function (facet) { - return facet.get('field') === this.getParent()._s_groupby; + return facet.get('field') === this.view._s_groupby; }, make_facet: function (values) { return { category: _t("GroupBy"), icon: this.icon, values: values, - field: this.getParent()._s_groupby + field: this.view._s_groupby }; } }); @@ -1166,10 +1209,10 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc * @extends instance.web.search.Input * * @param node - * @param view + * @param parent */ - init: function (node, view) { - this._super(view); + init: function (node, parent) { + this._super(parent); this.load_attrs(node.attrs); }, facet_for: function () { return $.when(null); }, @@ -1185,10 +1228,10 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc * * @param view_section * @param field - * @param view + * @param parent */ - init: function (view_section, field, view) { - this._super(view); + init: function (view_section, field, parent) { + this._super(parent); this.load_attrs(_.extend({}, field, view_section.attrs)); }, facet_for: function (value) { @@ -1228,7 +1271,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc * * @param {String} name the field's name * @param {String} operator the field's operator (either attribute-specified or default operator for the field - * @param {Number|String} value parsed value for the field + * @param {Number|String} facet parsed value for the field * @returns {Array} domain to include in the resulting search */ make_domain: function (name, operator, facet) { @@ -1460,18 +1503,21 @@ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @le }); instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ default_operator: {}, - init: function (view_section, field, view) { - this._super(view_section, field, view); + init: function (view_section, field, parent) { + this._super(view_section, field, parent); this.model = new instance.web.Model(this.attrs.relation); }, complete: function (needle) { var self = this; - // TODO: context // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) + var context = instance.web.pyeval.eval( + 'contexts', [this.view.dataset.get_context()]); return this.model.call('name_search', [], { name: needle, + args: instance.web.pyeval.eval( + 'domains', this.attrs.domain ? [this.attrs.domain] : [], context), limit: 8, - context: {} + context: context }).then(function (results) { if (_.isEmpty(results)) { return null; } return [{label: self.attrs.string}].concat( @@ -1541,6 +1587,9 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ }) .on('reset', this.proxy('clear_selection')); this.$el.on('submit', 'form', this.proxy('save_current')); + this.$el.on('click', 'input[type=checkbox]', function() { + $(this).siblings('input[type=checkbox]').prop('checked', false); + }); this.$el.on('click', 'h4', function () { self.$el.toggleClass('oe_opened'); }); @@ -1591,6 +1640,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ get_groupby: function () { return [filter.context]; }, get_domain: function () { return filter.domain; } }, + _id: filter['id'], is_custom_filter: true, values: [{label: filter.name, value: null}] }; @@ -1632,10 +1682,18 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ } $filter.unbind('click').click(function () { - self.enable_filter(filter); + self.toggle_filter(filter); }); }, - enable_filter: function (filter, preventSearch) { + toggle_filter: function (filter, preventSearch) { + var current = this.view.query.find(function (facet) { + return facet.get('_id') === filter.id; + }); + if (current) { + this.view.query.remove(current); + this.$filters[this.key_for(filter)].removeClass('oe_selected'); + return; + } this.view.query.reset([this.facet_for(filter)], { preventSearch: preventSearch || false}); this.$filters[this.key_for(filter)].addClass('oe_selected'); @@ -1658,6 +1716,13 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ if (!_.isEmpty(results.group_by)) { results.context.group_by = results.group_by; } + // Don't save user_context keys in the custom filter, otherwise end + // up with e.g. wrong uid or lang stored *and used in subsequent + // reqs* + var ctx = results.context; + _(_.keys(instance.session.user_context)).each(function (key) { + delete ctx[key]; + }); var filter = { name: $name.val(), user_id: private_filter ? instance.session.uid : false, @@ -1687,22 +1752,28 @@ instance.web.search.Filters = instance.web.search.Input.extend({ var running_count = 0; // get total filters count var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; }; - var filters_count = _(this.view.controls).chain() + var visible_filters = _(this.view.controls).chain().reject(function (group) { + return _(_(group.children).filter(is_group)).isEmpty() + || group.modifiers.invisible; + }); + var filters_count = visible_filters + .pluck('children') .flatten() .filter(is_group) .map(function (i) { return i.filters.length; }) .sum() .value(); - var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) { - var filters = _(inputs).filter(is_group); - return { - name: group === 'null' ? "q " + _t("Filters") : "w " + group, - filters: filters, - length: _(filters).chain().map(function (i) { - return i.filters.length; }).sum().value() - }; - }); + var col1 = [], col2 = visible_filters.map(function (group) { + var filters = _(group.children).filter(is_group); + return { + name: _.str.sprintf("%s %s", + group.icon, group.name), + filters: filters, + length: _(filters).chain().map(function (i) { + return i.filters.length; }).sum().value() + }; + }).value(); while (col2.length) { // col1 + group should be smaller than col2 + group @@ -1800,6 +1871,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ template: 'SearchView.extended_search.proposition', events: { 'change .searchview_extended_prop_field': 'changed', + 'change .searchview_extended_prop_op': 'operator_changed', 'click .searchview_extended_delete_prop': function (e) { e.stopPropagation(); this.getParent().remove_proposition(this); @@ -1831,6 +1903,17 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ this.select_field(_.detect(this.fields, function(x) {return x.name == nval;})); } }, + operator_changed: function (e) { + var $value = this.$('.searchview_extended_prop_value'); + switch ($(e.target).val()) { + case '∃': + case '∄': + $value.hide(); + break; + default: + $value.show(); + } + }, /** * Selects the provided field object * @@ -1859,7 +1942,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ .text(String(operator.text)) .appendTo(self.$('.searchview_extended_prop_op')); }); - var $value_loc = this.$('.searchview_extended_prop_value').empty(); + var $value_loc = this.$('.searchview_extended_prop_value').show().empty(); this.value.appendTo($value_loc); }, @@ -1867,19 +1950,12 @@ instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @ if ( this.attrs.selected == null) return null; var field = this.attrs.selected; - var op = this.$('.searchview_extended_prop_op')[0]; - var operator = op.options[op.selectedIndex]; + var op_select = this.$('.searchview_extended_prop_op')[0]; + var operator = op_select.options[op_select.selectedIndex]; + return { - label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), { - field: field.string, - // According to spec, HTMLOptionElement#label should return - // HTMLOptionElement#text when not defined/empty, but it does - // not in older Webkit (between Safari 5.1.5 and Chrome 17) and - // Gecko (pre Firefox 7) browsers, so we need a manual fallback - // for those - operator: operator.label || operator.text, - value: this.value}), - value: [field.name, operator.value, this.value.get_value()] + label: this.value.get_label(field, operator), + value: this.value.get_domain(field, operator), }; } }); @@ -1889,6 +1965,37 @@ instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend this._super(parent); this.field = field; }, + get_label: function (field, operator) { + var format; + switch (operator.value) { + case '∃': case '∄': format = _t('%(field)s %(operator)s'); break; + default: format = _t('%(field)s %(operator)s "%(value)s"'); break; + } + return this.format_label(format, field, operator); + }, + format_label: function (format, field, operator) { + return _.str.sprintf(format, { + field: field.string, + // According to spec, HTMLOptionElement#label should return + // HTMLOptionElement#text when not defined/empty, but it does + // not in older Webkit (between Safari 5.1.5 and Chrome 17) and + // Gecko (pre Firefox 7) browsers, so we need a manual fallback + // for those + operator: operator.label || operator.text, + value: this + }); + }, + get_domain: function (field, operator) { + switch (operator.value) { + case '∃': return this.make_domain(field.name, '!=', false); + case '∄': return this.make_domain(field.name, '=', false); + default: return this.make_domain( + field.name, operator.value, this.get_value()); + } + }, + make_domain: function (field, operator, value) { + return [field, operator, value]; + }, /** * Returns a human-readable version of the value, in case the "logical" * and the "semantic" values of a field differ (as for selection fields, @@ -1908,7 +2015,9 @@ instance.web.search.ExtendedSearchProposition.Char = instance.web.search.Extende {value: "ilike", text: _lt("contains")}, {value: "not ilike", text: _lt("doesn't contain")}, {value: "=", text: _lt("is equal to")}, - {value: "!=", text: _lt("is not equal to")} + {value: "!=", text: _lt("is not equal to")}, + {value: "∃", text: _lt("is set")}, + {value: "∄", text: _lt("is not set")} ], get_value: function() { return this.$el.val(); @@ -1922,7 +2031,9 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext {value: ">", text: _lt("greater than")}, {value: "<", text: _lt("less than")}, {value: ">=", text: _lt("greater or equal than")}, - {value: "<=", text: _lt("less or equal than")} + {value: "<=", text: _lt("less or equal than")}, + {value: "∃", text: _lt("is set")}, + {value: "∄", text: _lt("is not set")} ], /** * Date widgets live in view_form which is not yet loaded when this is @@ -1956,7 +2067,9 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte {value: ">", text: _lt("greater than")}, {value: "<", text: _lt("less than")}, {value: ">=", text: _lt("greater or equal than")}, - {value: "<=", text: _lt("less or equal than")} + {value: "<=", text: _lt("less or equal than")}, + {value: "∃", text: _lt("is set")}, + {value: "∄", text: _lt("is not set")} ], toString: function () { return this.$el.val(); @@ -1981,7 +2094,9 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend {value: ">", text: _lt("greater than")}, {value: "<", text: _lt("less than")}, {value: ">=", text: _lt("greater or equal than")}, - {value: "<=", text: _lt("less or equal than")} + {value: "<=", text: _lt("less or equal than")}, + {value: "∃", text: _lt("is set")}, + {value: "∄", text: _lt("is not set")} ], toString: function () { return this.$el.val(); @@ -1999,7 +2114,9 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex template: 'SearchView.extended_search.proposition.selection', operators: [ {value: "=", text: _lt("is")}, - {value: "!=", text: _lt("is not")} + {value: "!=", text: _lt("is not")}, + {value: "∃", text: _lt("is set")}, + {value: "∄", text: _lt("is not set")} ], toString: function () { var select = this.$el[0]; @@ -2016,7 +2133,10 @@ instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.Exte {value: "=", text: _lt("is true")}, {value: "!=", text: _lt("is false")} ], - toString: function () { return ''; }, + get_label: function (field, operator) { + return this.format_label( + _t('%(field)s %(operator)s'), field, operator); + }, get_value: function() { return true; } diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 4880b3882ff..b057616b6ab 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -247,13 +247,11 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { - if (!this.dataset.get_id_index(state.id)) { + if (this.dataset.get_id_index(state.id) === null) { this.dataset.ids.push(state.id); } this.dataset.select_id(state.id); - if (warm) { - this.do_show(); - } + this.do_show({ reload: warm }); } }, /** @@ -511,9 +509,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var on_change = widget.node.attrs.on_change; if (on_change) { var change_spec = self.parse_on_change(on_change, widget); - var id = [self.datarecord.id == null ? [] : [self.datarecord.id]]; - def = new instance.web.Model(self.dataset.model).call( - change_spec.method, id.concat(change_spec.args)); + var ids = []; + if (self.datarecord.id && !instance.web.BufferedDataSet.virtual_id_regex.test(self.datarecord.id)) { + // In case of a o2m virtual id, we should pass an empty ids list + ids.push(self.datarecord.id); + } + def = self.alive(new instance.web.Model(self.dataset.model).call( + change_spec.method, [ids].concat(change_spec.args))); } else { def = $.when({}); } @@ -531,9 +533,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var condition = fieldname + '=' + value_; if (value_) { - return new instance.web.Model('ir.values').call( + return self.alive(new instance.web.Model('ir.values').call( 'get_defaults', [self.model, condition] - ).then(function (results) { + )).then(function (results) { if (!results.length) { return response; } @@ -808,7 +810,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM try { var form_invalid = false, values = {}, - first_invalid_field = null; + first_invalid_field = null, + readonly_values = {}; for (var f in self.fields) { if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; @@ -817,11 +820,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { + } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields - values[f.name] = f.get_value(); + if (!f.get("readonly")) { + values[f.name] = f.get_value(); + } else { + readonly_values[f.name] = f.get_value(); + } } } if (form_invalid) { @@ -834,7 +841,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM var save_deferral; if (!self.datarecord.id) { // Creation save - save_deferral = self.dataset.create(values).then(function(r) { + save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) { return self.record_created(r, prepend_on_create); }, null); } else if (_.isEmpty(values)) { @@ -842,7 +849,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM save_deferral = $.Deferred().resolve({}).promise(); } else { // Write save - save_deferral = self.dataset.write(self.datarecord.id, values, {}).then(function(r) { + save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { return self.record_saved(r); }, null); } @@ -1191,6 +1198,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt $('button', doc).each(function() { $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button'); }); + // IE's html parser is also a css parser. How convenient... + $('board', doc).each(function() { + $(this).attr('layout', $(this).attr('style')); + }); return $('
').append(instance.web.xml_to_str(doc)); }, render_to: function($target) { @@ -1881,10 +1892,11 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; } - this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { this._super.apply(this, arguments); + this.view.on('view_content_has_changed', this, this.check_disable); + this.check_disable(); this.$el.click(this.on_click); if (this.node.attrs.help || instance.session.debug) { this.do_attach_tooltip(); @@ -1910,18 +1922,20 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ modal: true, buttons: [ {text: _t("Cancel"), click: function() { - def.resolve(); $(this).dialog("close"); } }, {text: _t("Ok"), click: function() { - self.on_confirmed().done(function() { - def.resolve(); + var self2 = this; + self.on_confirmed().always(function() { + $(self2).dialog("close"); }); - $(this).dialog("close"); } } - ] + ], + beforeClose: function() { + def.resolve(); + }, }); return def.promise(); } else { @@ -2212,6 +2226,18 @@ instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.Reini }, }); +/** + Some hack to make placeholders work in ie9. +*/ +if ($.browser.msie && $.browser.version === "9.0") { + document.addEventListener("DOMNodeInserted",function(event){ + var nodename = event.target.nodeName.toLowerCase(); + if ( nodename === "input" || nodename == "textarea" ) { + $(event.target).placeholder(); + } + }); +} + instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldChar', widget_class: 'oe_form_field_char', @@ -2327,11 +2353,12 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ this._super(); } else { var tmp = this.get('value'); - var s = /(\w+):(.+)/.exec(tmp); + var s = /(\w+):(.+)|^\.{0,2}\//.exec(tmp); if (!s) { tmp = "http://" + this.get('value'); } - this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : ''); + var text = this.get('value') ? this.node.attrs.text || tmp : ''; + this.$el.find('a').attr('href', tmp).text(text); } }, on_button_clicked: function() { @@ -2395,6 +2422,11 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ showButtonPanel: true, firstDay: Date.CultureInfo.firstDayOfWeek }); + // Some clicks in the datepicker dialog are not stopped by the + // datepicker and "bubble through", unexpectedly triggering the bus's + // click event. Prevent that. + this.picker('widget').click(function (e) { e.stopPropagation(); }); + this.$el.find('img.oe_datepicker_trigger').click(function() { if (self.get("effective_readonly") || self.picker('widget').is(':visible')) { self.$input.focus(); @@ -2534,42 +2566,45 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we }, 'change textarea': 'store_dom_value', }, - init: function (field_manager, node) { - this._super(field_manager, node); - }, initialize_content: function() { var self = this; - this.$textarea = this.$el.find('textarea'); - this.auto_sized = false; - this.default_height = this.$textarea.css('height'); - if (this.get("effective_readonly")) { - this.$textarea.attr('disabled', 'disabled'); + if (! this.get("effective_readonly")) { + this.$textarea = this.$el.find('textarea'); + this.auto_sized = false; + this.default_height = this.$textarea.css('height'); + if (this.get("effective_readonly")) { + this.$textarea.attr('disabled', 'disabled'); + } + this.setupFocus(this.$textarea); + } else { + this.$textarea = undefined; } - this.setupFocus(this.$textarea); }, commit_value: function () { - this.store_dom_value(); + if (! this.get("effective_readonly") && this.$textarea) { + this.store_dom_value(); + } return this._super(); }, store_dom_value: function () { - if (!this.get('effective_readonly') && this.$('textarea').length) { - this.internal_set_value( - instance.web.parse_value( - this.$textarea.val(), - this)); - } + this.internal_set_value(instance.web.parse_value(this.$textarea.val(), this)); }, render_value: function() { - var show_value = instance.web.format_value(this.get('value'), this, ''); - if (show_value === '') { - this.$textarea.css('height', parseInt(this.default_height)+"px"); - } - this.$textarea.val(show_value); - if (! this.auto_sized) { - this.auto_sized = true; - this.$textarea.autosize(); + if (! this.get("effective_readonly")) { + var show_value = instance.web.format_value(this.get('value'), this, ''); + if (show_value === '') { + this.$textarea.css('height', parseInt(this.default_height)+"px"); + } + this.$textarea.val(show_value); + if (! this.auto_sized) { + this.auto_sized = true; + this.$textarea.autosize(); + } else { + this.$textarea.trigger("autosize"); + } } else { - this.$textarea.trigger("autosize"); + var txt = this.get("value") || ''; + this.$(".oe_form_text_content").text(txt); } }, is_syntax_valid: function() { @@ -2587,14 +2622,18 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we return this.get('value') === '' || this._super(); }, focus: function($el) { - this.$textarea[0].focus(); + if (!this.get("effective_readonly") && this.$textarea) { + this.$textarea[0].focus(); + } }, set_dimensions: function (height, width) { this._super(height, width); - this.$textarea.css({ - width: width, - minHeight: height - }); + if (!this.get("effective_readonly") && this.$textarea) { + this.$textarea.css({ + width: width, + minHeight: height + }); + } }, }); @@ -2625,7 +2664,7 @@ instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instanc "| removeformat | bullets numbering | outdent " + "indent | link unlink | source", bodyStyle: // style to assign to document body contained within the editor - "margin:4px; color:#4c4c4c; font-size:13px; font-family:\"Lucida Grande\",Helvetica,Verdana,Arial,sans-serif; cursor:text" + "margin:4px; color:#4c4c4c; font-size:13px; font-family:'Lucida Grande',Helvetica,Verdana,Arial,sans-serif; cursor:text" }); this.$cleditor = this.$textarea.cleditor()[0]; this.$cleditor.change(function() { @@ -2957,7 +2996,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc case $.ui.keyCode.DOWN: e.stopPropagation(); } - } + }, }, init: function(field_manager, node) { this._super(field_manager, node); @@ -2988,6 +3027,26 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc if (!this.get("effective_readonly")) this.render_editable(); }, + destroy_content: function () { + if (this.$drop_down) { + this.$drop_down.off('click'); + delete this.$drop_down; + } + if (this.$input) { + this.$input.closest(".ui-dialog .ui-dialog-content").off('scroll'); + this.$input.off('keyup blur autocompleteclose autocompleteopen ' + + 'focus focusout change keydown'); + delete this.$input; + } + if (this.$follow_button) { + this.$follow_button.off('blur focus click'); + delete this.$follow_button; + } + }, + destroy: function () { + this.destroy_content(); + return this._super(); + }, init_error_displayer: function() { // nothing }, @@ -3047,9 +3106,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc this.$input.keydown(input_changed); this.$input.change(input_changed); this.$drop_down.click(function() { + self.$input.focus(); if (self.$input.autocomplete("widget").is(":visible")) { - self.$input.autocomplete("close"); - self.$input.focus(); + self.$input.autocomplete("close"); } else { if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); @@ -3058,6 +3117,15 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc } } }); + + // Autocomplete close on dialog content scroll + var close_autocomplete = _.debounce(function() { + if (self.$input.autocomplete("widget").is(":visible")) { + self.$input.autocomplete("close"); + } + }, 50); + this.$input.closest(".ui-dialog .ui-dialog-content").on('scroll', this, close_autocomplete); + self.ed_def = $.Deferred(); self.uned_def = $.Deferred(); var ed_delay = 200; @@ -3209,7 +3277,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc res_model: self.field.relation, res_id: self.get("value"), views: [[false, 'form']], - target: 'current' + target: 'current', + context: self.build_context().eval(), }); return false; }); @@ -3240,7 +3309,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc }, focus: function () { if (!this.get('effective_readonly')) { - this.$input[0].focus(); + this.$input && this.$input[0].focus(); } }, _quick_create: function() { @@ -3659,8 +3728,8 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { title: _t("Open: ") + self.o2m.string, - create_function: function(data) { - return self.o2m.dataset.create(data).done(function(r) { + create_function: function(data, options) { + return self.o2m.dataset.create(data, options).done(function(r) { self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.trigger("dataset_changed", r); }); @@ -3719,8 +3788,12 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this.o2m.trigger_on_change(); }, is_valid: function () { - var form = this.editor.form; - + var editor = this.editor; + var form = editor.form; + // If no edition is pending, the listview can not be invalid (?) + if (!editor.record) { + return true + } // If the form has not been modified, the view can only be valid // NB: is_dirty will also be set on defaults/onchanges/whatever? // oe_form_dirty seems to only be set on actual user actions @@ -3750,11 +3823,11 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ title: _t("Create: ") + self.o2m.string, initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, - create_function: function(data, callback, error_callback) { - return self.o2m.dataset.create(data).done(function(r) { + create_function: function(data, options) { + return self.o2m.dataset.create(data, options).done(function(r) { self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); self.o2m.dataset.trigger("dataset_changed", r); - }).done(callback).fail(error_callback); + }); }, read_function: function() { return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); @@ -3967,6 +4040,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in if (this.get("effective_readonly")) return; var self = this; + var ignore_blur = false; self.$text = this.$("textarea"); self.$text.textext({ plugins : 'tags arrow autocomplete', @@ -3985,6 +4059,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in if (data.id) { self.add_id(data.id); } else { + ignore_blur = true; data.action(); } }, @@ -4035,10 +4110,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self.$text .focusin(function () { self.trigger('focused'); + ignore_blur = false; }) .focusout(function() { self.$text.trigger("setInputData", ""); - self.trigger('blurred'); + if (!ignore_blur) { + self.trigger('blurred'); + } }).keydown(function(e) { if (e.which === $.ui.keyCode.TAB && self._drop_shown) { self.$text.textext()[0].autocomplete().selectFromDropdown(); @@ -4094,6 +4172,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in focus: function () { this.$text[0].focus(); }, + set_dimensions: function (height, width) { + this._super(height, width); + this.$("textarea").css({ + width: width, + minHeight: height + }); + }, }); /** @@ -4238,6 +4323,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in }); } }, + is_action_enabled: function () { return true; }, }); instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { @@ -4450,6 +4536,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ * options: * -readonly: only applicable when not in creation mode, default to false * - alternative_form_view + * - view_id * - write_function * - read_function * - create_function @@ -4471,9 +4558,9 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ this.created_elements = []; this.dataset = new instance.web.ProxyDataSet(this, this.model, this.context); this.dataset.read_function = this.options.read_function; - this.dataset.create_function = function(data, sup) { + this.dataset.create_function = function(data, options, sup) { var fct = self.options.create_function || sup; - return fct.call(this, data).done(function(r) { + return fct.call(this, data, options).done(function(r) { self.trigger('create_completed saved', r); self.created_elements.push(r); }); @@ -4516,7 +4603,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ _.extend(options, { $buttons: this.$buttonpane, }); - this.view_form = new instance.web.FormView(this, this.dataset, false, options); + this.view_form = new instance.web.FormView(this, this.dataset, this.options.view_id || false, options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } @@ -4545,6 +4632,7 @@ instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ }); var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close"); $cbutton.click(function() { + self.view_form.trigger('on_button_cancel'); self.check_exit(); }); self.view_form.do_show(); @@ -5014,7 +5102,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ }); /** - * Widget for (one2many field) to upload one or more file in same time and display in list. + * Widget for (many2many field) to upload one or more file in same time and display in list. * The user can delete his files. * Options on attribute ; "blockui" {Boolean} block the UI or not * during the file is uploading @@ -5028,6 +5116,8 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie if(this.field.type != "many2many" || this.field.relation != 'ir.attachment') { throw _.str.sprintf(_t("The type of the field '%s' must be a many2many field with a relation to 'ir.attachment' model."), this.field.string); } + this.data = {}; + this.set_value([]); this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); this.fileupload_id = _.uniqueId('oe_fileupload_temp'); $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); @@ -5037,73 +5127,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); }, set_value: function(value_) { - var value_ = value_ || []; - var self = this; - var ids = []; - _.each(value_, function(command) { - if (isNaN(command) && command.id == undefined) { - switch (command[0]) { - case commands.CREATE: - ids = ids.concat(command[2]); - return; - case commands.REPLACE_WITH: - ids = ids.concat(command[2]); - return; - case commands.UPDATE: - ids = ids.concat(command[2]); - return; - case commands.LINK_TO: - ids = ids.concat(command[1]); - return; - case commands.DELETE: - ids = _.filter(ids, function (id) { return id != command[1];}); - return; - case commands.DELETE_ALL: - ids = []; - return; - } - } else { - ids.push(command); - } - }); - this._super( ids ); + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; + } + this._super(value_); }, get_value: function() { - return _.map(this.get('value'), function (value) { return commands.link_to( isNaN(value) ? value.id : value ); }); + var tmp = [commands.replace_with(this.get("value"))]; + return tmp; }, get_file_url: function (attachment) { return this.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: attachment['id']}); }, read_name_values : function () { var self = this; - // select the list of id for a get_name - var values = []; - _.each(this.get('value'), function (val) { - if (typeof val != 'object') { - values.push(val); - } - }); + // don't reset know values + var _value = _.filter(this.get('value'), function (id) { return typeof self.data[id] == 'undefined'; } ); // send request for get_name - if (values.length) { - return this.ds_file.call('read', [values, ['id', 'name', 'datas_fname']]).done(function (datas) { + if (_value.length) { + return this.ds_file.call('read', [_value, ['id', 'name', 'datas_fname']]).done(function (datas) { _.each(datas, function (data) { data.no_unlink = true; data.url = self.session.url('/web/binary/saveas', {model: 'ir.attachment', field: 'datas', filename_field: 'datas_fname', id: data.id}); - - _.each(self.get('value'), function (val, key) { - if(val == data.id) { - self.get('value')[key] = data; - } - }); + self.data[data.id] = data; }); }); } else { - return $.when(this.get('value')); + return $.when(); } }, render_value: function () { var self = this; - this.read_name_values().then(function (datas) { + this.read_name_values().then(function () { var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': self})); render.on('click', '.oe_delete', _.bind(self.on_file_delete, self)); @@ -5121,45 +5177,36 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie var self = this; var $target = $(event.target); if ($target.val() !== '') { - var filename = $target.val().replace(/.*[\\\/]/,''); - - // if the files is currently uploded, don't send again - if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) { + // don't uplode more of one file in same time + if (self.data[0] && self.data[0].upload ) { return false; } + for (var id in this.get('value')) { + // if the files exits, delete the file before upload (if it's a new file) + if (self.data[id] && (self.data[id].filename || self.data[id].name) == filename && !self.data[id].no_unlink ) { + self.ds_file.unlink([id]); + } + } // block UI or not if(this.node.attrs.blockui>0) { instance.web.blockUI(); } - // if the files exits for this answer, delete the file before upload - var files = _.filter(this.get('value'), function (file) { - if((file.filename || file.name) == filename) { - self.ds_file.unlink([file.id]); - return false; - } else { - return true; - } - }); - // TODO : unactivate send on wizard and form // submit file this.$('form.oe_form_binary_form').submit(); this.$(".oe_fileupload").hide(); - - // add file on result - files.push({ + // add file on data result + this.data[0] = { 'id': 0, 'name': filename, 'filename': filename, 'url': '', 'upload': true - }); - - this.set({'value': files}); + }; } }, on_file_loaded: function (event, result) { @@ -5170,39 +5217,39 @@ instance.web.form.FieldMany2ManyBinaryMultiFiles = instance.web.form.AbstractFie instance.web.unblockUI(); } - // TODO : activate send on wizard and form - if (result.error || !result.id ) { this.do_warn( _t('Uploading error'), result.error); - files = _.filter(files, function (val) { return !val.upload; }); + delete this.data[0]; } else { - for(var i in files){ - if(files[i].filename == result.filename && files[i].upload) { - files[i] = { - 'id': result.id, - 'name': result.name, - 'filename': result.filename, - 'url': this.get_file_url(result) - }; - } + if (this.data[0] && this.data[0].filename == result.filename && this.data[0].upload) { + delete this.data[0]; + this.data[result.id] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; + } else { + this.data[result.id] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; } + var values = _.clone(this.get('value')); + values.push(result.id); + this.set({'value': values}); } - - this.set({'value': files}); this.render_value() }, on_file_delete: function (event) { event.stopPropagation(); var file_id=$(event.target).data("id"); if (file_id) { - var files=[]; - for(var i in this.get('value')){ - if(file_id != this.get('value')[i].id){ - files.push(this.get('value')[i]); - } - else if(!this.get('value')[i].no_unlink) { - this.ds_file.unlink([file_id]); - } + var files = _.filter(this.get('value'), function (id) {return id != file_id;}); + if(!this.data[file_id].no_unlink) { + this.ds_file.unlink([file_id]); } this.set({'value': files}); } @@ -5216,8 +5263,20 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false; this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false; this.set({value: false}); + this.selection = []; + this.set("selection", []); + this.selection_dm = new instance.web.DropMisordered(); }, start: function() { + this.field_manager.on("view_content_has_changed", this, this.calc_domain); + this.calc_domain(); + this.on("change:value", this, this.get_selection); + this.on("change:evaluated_selection_domain", this, this.get_selection); + this.on("change:selection", this, function() { + this.selection = this.get("selection"); + this.render_value(); + }); + this.get_selection(); if (this.options.clickable) { this.$el.on('click','li',this.on_click_stage); } @@ -5234,15 +5293,25 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ }, render_value: function() { var self = this; - self.get_selection().done(function() { - var content = QWeb.render("FieldStatus.content", {widget: self}); - self.$el.html(content); - var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}"); - var color = colors[self.get('value')]; - if (color) { - self.$("oe_active").css("color", color); - } - }); + var content = QWeb.render("FieldStatus.content", {widget: self}); + self.$el.html(content); + var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}"); + var color = colors[self.get('value')]; + if (color) { + self.$("oe_active").css("color", color); + } + }, + calc_domain: function() { + var d = instance.web.pyeval.eval('domain', this.build_domain()); + var domain = []; //if there is no domain defined, fetch all the records + + if (d.length) { + domain = ['|',['id', '=', this.get('value')]].concat(d); + } + + if (! _.isEqual(domain, this.get("evaluated_selection_domain"))) { + this.set("evaluated_selection_domain", domain); + } }, /** Get the selection and render it * selection: [[identifier, value_to_display], ...] @@ -5251,32 +5320,37 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ */ get_selection: function() { var self = this; - self.selection = []; - if (this.field.type == "many2one") { - var domain = []; - if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) { - var d = instance.web.pyeval.eval('domain', self.build_domain()); - domain = ['|', ['id', '=', self.get('value')]].concat(d); - } - var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain); - return ds.read_slice(['name'], {}).then(function (records) { - for(var i = 0; i < records.length; i++) { - self.selection.push([records[i].id, records[i].name]); - } - }); - } else { - // For field type selection filter values according to - // statusbar_visible attribute of the field. For example: - // statusbar_visible="draft,open". - var selection = this.field.selection; - for(var i=0; i < selection.length; i++) { - var key = selection[i][0]; - if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { - this.selection.push(selection[i]); + var selection = []; + + var calculation = _.bind(function() { + if (this.field.type == "many2one") { + var domain = []; + var ds = new instance.web.DataSetSearch(this, this.field.relation, + self.build_context(), this.get("evaluated_selection_domain")); + return ds.read_slice(['name'], {}).then(function (records) { + for(var i = 0; i < records.length; i++) { + selection.push([records[i].id, records[i].name]); + } + }); + } else { + // For field type selection filter values according to + // statusbar_visible attribute of the field. For example: + // statusbar_visible="draft,open". + var select = this.field.selection; + for(var i=0; i < select.length; i++) { + var key = select[i][0]; + if(key == this.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { + selection.push(select[i]); + } } + return $.when(); } - return $.when(); - } + }, this); + this.selection_dm.add(calculation()).then(function () { + if (! _.isEqual(selection, self.get("selection"))) { + self.set("selection", selection); + } + }); }, on_click_stage: function (ev) { var self = this; @@ -5320,16 +5394,16 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ this.set({"currency_info": null}); return; } - return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"]) - .filter([["id", "=", self.get("currency")]]).first()).then(function(res) { + return this.ci_dm.add(self.alive(new instance.web.Model("res.currency").query(["symbol", "position"]) + .filter([["id", "=", self.get("currency")]]).first())).then(function(res) { self.set({"currency_info": res}); }); }, parse_value: function(val, def) { - return instance.web.parse_value(val, {type: "float"}, def); + return instance.web.parse_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); }, format_value: function(val, def) { - return instance.web.format_value(val, {type: "float"}, def); + return instance.web.format_value(val, {type: "float", digits: (this.node.attrs || {}).digits || this.field.digits}, def); }, }); diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index fbbfabcacca..b69ec6dbdca 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -321,9 +321,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi .appendTo($this.empty()) .click(function (e) {e.stopPropagation();}) .append('' + - '' + '' + '' + + '' + '') .change(function () { var val = parseInt($select.val(), 10); @@ -1186,7 +1186,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. } }); instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.web.ListView.Groups# */{ - passtrough_events: 'action deleted row_link', + passthrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts * with the :js:class:`~DataGroup` bound to it. @@ -1406,7 +1406,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we // can have selections spanning multiple links var selection = self.get_selection(); $this.trigger(e, [selection.ids, selection.records]); - }).bind(this.passtrough_events, function (e) { + }).bind(this.passthrough_events, function (e) { // additional positional parameters are provided to trigger as an // Array, following the event type or event object, but are // provided to the .bind event handler as *args. @@ -2212,7 +2212,7 @@ instance.web.list.Binary = instance.web.list.Column.extend({ 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}); + download_url = instance.session.url('/web/binary/saveas', {model: options.model, field: this.id, id: options.id}); if (this.filename) { download_url += '&filename_field=' + this.filename; } diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index efc442de2a9..4e042b60c5d 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -96,7 +96,8 @@ openerp.web.list_editable = function (instance) { }); }, editable: function () { - return !this.options.disable_editable_mode + return !this.grouped + && !this.options.disable_editable_mode && (this.fields_view.arch.attrs.editable || this._context_editable || this.options.editable); @@ -132,6 +133,15 @@ openerp.web.list_editable = function (instance) { var self = this; // tree/@editable takes priority on everything else if present. var result = this._super(data, grouped); + + // In case current editor was started previously, also has to run + // when toggling from editable to non-editable in case form widgets + // have setup global behaviors expecting themselves to exist + // somehow. + this.editor.destroy(); + // Editor is not restartable due to formview not being restartable + this.editor = this.make_editor(); + if (this.editable()) { this.$el.addClass('oe_list_editable'); // FIXME: any hook available to ensure this is only done once? @@ -143,10 +153,6 @@ openerp.web.list_editable = function (instance) { e.preventDefault(); self.cancel_edition(); }); - this.editor.destroy(); - // Editor is not restartable due to formview not being - // restartable - this.editor = this.make_editor(); var editor_ready = this.editor.prependTo(this.$el) .done(this.proxy('setup_events')); @@ -795,7 +801,7 @@ openerp.web.list_editable = function (instance) { }); instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", + passthrough_events: instance.web.ListView.Groups.prototype.passthrough_events + " edit saved", get_row_for: function (record) { return _(this.children).chain() .invoke('get_row_for', record) diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index fbe53711c8a..757519c0f2f 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -149,6 +149,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ for (var i = 0; i < this.breadcrumbs.length; i += 1) { var item = this.breadcrumbs[i]; var tit = item.get_title(); + if (item.hide_breadcrumb) { + continue; + } if (!_.isArray(tit)) { tit = [tit]; } @@ -190,6 +193,18 @@ instance.web.ActionManager = instance.web.Widget.extend({ }); state = _.extend(params || {}, state); } + if (this.inner_action.context) { + var active_id = this.inner_action.context.active_id; + if (active_id) { + state["active_id"] = active_id; + } + var active_ids = this.inner_action.context.active_ids; + if (active_ids && !(active_ids.length === 1 && active_ids[0] === active_id)) { + // We don't push active_ids if it's a single element array containing the active_id + // This makes the url shorter in most cases. + state["active_ids"] = this.inner_action.context.active_ids.join(','); + } + } } if(!this.dialog) { this.getParent().do_push_state(state); @@ -212,8 +227,22 @@ instance.web.ActionManager = instance.web.Widget.extend({ } else { var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action; if (run_action) { + var add_context = {}; + if (state.active_id) { + add_context.active_id = state.active_id; + } + if (state.active_ids) { + // The jQuery BBQ plugin does some parsing on values that are valid integers. + // It means that if there's only one item, it will do parseInt() on it, + // otherwise it will keep the comma seperated list as string. + add_context.active_ids = state.active_ids.toString().split(',').map(function(id) { + return parseInt(id, 10) || id; + }); + } else if (state.active_id) { + add_context.active_ids = [state.active_id]; + } this.null_action(); - action_loaded = this.do_action(state.action); + action_loaded = this.do_action(state.action, { additional_context: add_context }); $.when(action_loaded || null).done(function() { instance.webclient.menu.has_been_loaded.done(function() { if (self.inner_action && self.inner_action.id) { @@ -249,12 +278,27 @@ instance.web.ActionManager = instance.web.Widget.extend({ } }); }, + /** + * Execute an OpenERP action + * + * @param {Number|String|Object} Can be either an action id, a client action or an action descriptor. + * @param {Object} [options] + * @param {Boolean} [options.clear_breadcrumbs=false] Clear the breadcrumbs history list + * @param {Function} [options.on_reverse_breadcrumb] Callback to be executed whenever an anterior breadcrumb item is clicked on. + * @param {Function} [options.hide_breadcrumb] Do not display this widget's title in the breadcrumb + * @param {Function} [options.on_close] Callback to be executed when the dialog is closed (only relevant for target=new actions) + * @param {Function} [options.action_menu_id] Manually set the menu id on the fly. + * @param {Object} [options.additional_context] Additional context to be merged with the action's context. + * @return {jQuery.Deferred} Action loaded + */ do_action: function(action, options) { options = _.defaults(options || {}, { clear_breadcrumbs: false, on_reverse_breadcrumb: function() {}, + hide_breadcrumb: false, on_close: function() {}, action_menu_id: null, + additional_context: {}, }); if (action === false) { action = { type: 'ir.actions.act_window_close' }; @@ -269,9 +313,13 @@ instance.web.ActionManager = instance.web.Widget.extend({ } // Ensure context & domain are evaluated and can be manipulated/used - if (action.context) { - action.context = instance.web.pyeval.eval( - 'context', action.context); + var ncontext = new instance.web.CompoundContext(options.additional_context, action.context || {}); + action.context = instance.web.pyeval.eval('context', ncontext); + if (action.context.active_id || action.context.active_ids) { + // Here we assume that when an `active_id` or `active_ids` is used + // in the context, we are in a `related` action, so we disable the + // searchview's default custom filters. + action.context.search_disable_custom_filters = true; } if (action.domain) { action.domain = instance.web.pyeval.eval( @@ -360,7 +408,12 @@ instance.web.ActionManager = instance.web.Widget.extend({ widget: function () { return new instance.web.ViewManagerAction(self, action); }, action: action, klass: 'oe_act_window', - post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); } + post_process: function (widget) { + widget.add_breadcrumb({ + on_reverse_breadcrumb: options.on_reverse_breadcrumb, + hide_breadcrumb: options.hide_breadcrumb, + }); + }, }, options); }, ir_actions_client: function (action, options) { @@ -384,6 +437,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ widget: widget, title: action.name, on_reverse_breadcrumb: options.on_reverse_breadcrumb, + hide_breadcrumb: options.hide_breadcrumb, }); if (action.tag !== 'reload') { self.do_push_state({}); @@ -539,7 +593,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { - var container = self.$el.find(".oe_view_manager_view_" + view_name + ":first"); + var container = self.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_name); if (view_name === view_type) { container.show(); controller.do_show(view_options || {}); @@ -582,7 +636,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ controller.on('switch_mode', self, this.switch_mode); controller.on('previous_view', self, this.prev_view); - var container = this.$el.find(".oe_view_manager_view_" + view_type); + var container = this.$el.find("> .oe_view_manager_body > .oe_view_manager_view_" + view_type); var view_promise = controller.appendTo(container); this.views[view_type].controller = controller; this.views[view_type].deferred.resolve(view_type); @@ -597,10 +651,24 @@ instance.web.ViewManager = instance.web.Widget.extend({ self.trigger("controller_inited",view_type,controller); }); }, + /** + * @returns {Number|Boolean} the view id of the given type, false if not found + */ + get_view_id: function(view_type) { + return this.views[view_type] && this.views[view_type].view_id || false; + }, set_title: function(title) { this.$el.find('.oe_view_title_text:first').text(title); }, - add_breadcrumb: function(on_reverse_breadcrumb) { + add_breadcrumb: function(options) { + var options = options || {}; + // 7.0 backward compatibility + if (typeof options == 'function') { + options = { + on_reverse_breadcrumb: options + }; + } + // end of 7.0 backward compatibility var self = this; var views = [this.active_view || this.views_src[0].view_type]; this.on('switch_mode', self, function(mode) { @@ -612,7 +680,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ views.push(mode); } }); - this.getParent().push_breadcrumb({ + var item = _.extend({ widget: this, action: this.action, show: function(index) { @@ -646,9 +714,9 @@ instance.web.ViewManager = instance.web.Widget.extend({ titles.pop(); } return titles; - }, - on_reverse_breadcrumb: on_reverse_breadcrumb, - }); + } + }, options); + this.getParent().push_breadcrumb(item); }, /** * Returns to the view preceding the caller view in this manager's @@ -953,10 +1021,11 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ }); }, do_create_view: function(view_type) { - var r = this._super.apply(this, arguments); - var view = this.views[view_type].controller; - view.set({ 'title': this.action.name }); - return r; + var self = this; + return this._super.apply(this, arguments).then(function() { + var view = self.views[view_type].controller; + view.set({ 'title': self.action.name }); + }); }, get_action_manager: function() { var cur = this; @@ -1213,6 +1282,7 @@ instance.web.View = instance.web.Widget.extend({ "view_id": this.view_id, "view_type": this.view_type, "toolbar": !!this.options.$sidebar, + "context": this.dataset.get_context(), }); } return view_loaded_def.then(function(r) { @@ -1270,11 +1340,6 @@ instance.web.View = instance.web.Widget.extend({ active_ids: [record_id], active_model: dataset.model }); - if (("" + action.context).match(/\bactive_id\b/)) { - // Special case: when the context is evaluted using - // the active_id, we want to disable the custom filters. - ncontext.add({ search_disable_custom_filters: true }); - } } ncontext.add(action.context || {}); action.context = ncontext; @@ -1401,11 +1466,11 @@ instance.web.View = instance.web.Widget.extend({ * Performs a fields_view_get and apply postprocessing. * return a {$.Deferred} resolved with the fvg * - * @param {Object} [args] + * @param {Object} args * @param {String|Object} args.model instance.web.Model instance or string repr of the model - * @param {null|Object} args.context context if args.model is a string - * @param {null|Number} args.view_id id of the view to be loaded, default view if null - * @param {null|String} args.view_type type of view to be loaded if view_id is null + * @param {Object} [args.context] context if args.model is a string + * @param {Number} [args.view_id] id of the view to be loaded, default view if null + * @param {String} [args.view_type] type of view to be loaded if view_id is null * @param {Boolean} [args.toolbar=false] get the toolbar definition */ instance.web.fields_view_get = function(args) { @@ -1432,7 +1497,7 @@ instance.web.fields_view_get = function(args) { if (typeof model === 'string') { model = new instance.web.Model(args.model, args.context); } - return args.model.call('fields_view_get', [args.view_id, args.view_type, model.context(), args.toolbar]).then(function(fvg) { + return args.model.call('fields_view_get', [args.view_id, args.view_type, args.context, args.toolbar]).then(function(fvg) { return postprocess(fvg); }); }; @@ -1502,13 +1567,27 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { } }; instance.web.xml_to_str = function(node) { + var str = ""; if (window.XMLSerializer) { - return (new XMLSerializer()).serializeToString(node); + str = (new XMLSerializer()).serializeToString(node); } else if (window.ActiveXObject) { - return node.xml; + str = node.xml; } else { throw new Error(_t("Could not serialize XML")); } + // Browsers won't deal with self closing tags except void elements: + // http://www.w3.org/TR/html-markup/syntax.html + var void_elements = 'area base br col command embed hr img input keygen link meta param source track wbr'.split(' '); + + // The following regex is a bit naive but it's ok for the xmlserializer output + str = str.replace(/<([a-z]+)([^<>]*)\s*\/\s*>/g, function(match, tag, attrs) { + if (void_elements.indexOf(tag) < 0) { + return "<" + tag + attrs + ">"; + } else { + return match; + } + }); + return str; }; /** diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 7604814fe1d..5ac7e5c3ef6 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -40,7 +40,7 @@

- var message = d.message ? d.message : d.error.data.message; + var message = d.message ? d.message : d.error.data.fault_code; d.html_error = context.engine.tools.html_escape(message) .replace(/\n/g, '
');
@@ -71,9 +71,9 @@

  • Username
  • -
  • +
  • Password
  • -
  • +
@@ -106,33 +106,47 @@

- Create Database + Create a New Database

-
- +

+ Fill in this form to create an OpenERP database. You can + create databases for different companies or for different + goals (testing, production). Once the database is created, + you will be able to install your first application. +

+

+ By default, the master password is 'admin'. This password + is required to created, delete dump or restore databases. +

+
- + - - + + - + - - + + + + +
+ +
+ +
- + + Check this box to evaluate OpenERP.
- +