diff --git a/addons/web/common/http.py b/addons/web/common/http.py index 0af64a18e5a..487800da1b9 100644 --- a/addons/web/common/http.py +++ b/addons/web/common/http.py @@ -588,26 +588,22 @@ class LocalConnector(openerplib.Connector): import openerp import traceback import xmlrpclib + code_string = "warning -- %s\n\n%s" try: - result = openerp.netsvc.dispatch_rpc(service_name, method, args) - except Exception,e: + return openerp.netsvc.dispatch_rpc(service_name, method, args) + except openerp.osv.osv.except_osv, e: # TODO change the except to raise LibException instead of their emulated xmlrpc fault - if isinstance(e, openerp.osv.osv.except_osv): - fault = xmlrpclib.Fault('warning -- ' + e.name + '\n\n' + str(e.value), '') - elif isinstance(e, openerp.exceptions.Warning): - fault = xmlrpclib.Fault('warning -- Warning\n\n' + str(e), '') - elif isinstance(e, openerp.exceptions.AccessError): - fault = xmlrpclib.Fault('warning -- AccessError\n\n' + str(e), '') - elif isinstance(e, openerp.exceptions.AccessDenied): - fault = xmlrpclib.Fault('AccessDenied', str(e)) - elif isinstance(e, openerp.exceptions.DeferredException): - info = e.traceback - formatted_info = "".join(traceback.format_exception(*info)) - fault = xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info) - else: - info = sys.exc_info() - formatted_info = "".join(traceback.format_exception(*info)) - fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info) - raise fault - return result + raise xmlrpclib.Fault(code_string % (e.name, e.value), '') + except openerp.exceptions.Warning, e: + raise xmlrpclib.Fault(code_string % ("Warning", e), '') + except openerp.exceptions.AccessError, e: + raise xmlrpclib.Fault(code_string % ("AccessError", e), '') + except openerp.exceptions.AccessDenied, e: + raise xmlrpclib.Fault('AccessDenied', str(e)) + except openerp.exceptions.DeferredException, e: + formatted_info = "".join(traceback.format_exception(*e.traceback)) + raise xmlrpclib.Fault(openerp.tools.ustr(e.message), formatted_info) + except Exception, e: + formatted_info = "".join(traceback.format_exception(*(sys.exc_info()))) + raise xmlrpclib.Fault(openerp.tools.exception_to_unicode(e), formatted_info) diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 546a232ef3d..a498daaeb76 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -735,10 +735,10 @@ def clean_action(req, action, do_not_eval=False): if not do_not_eval: # values come from the server, we can just eval them - if isinstance(action.get('context'), basestring): + if action.get('context') and isinstance(action.get('context'), basestring): action['context'] = eval( action['context'], eval_ctx ) or {} - if isinstance(action.get('domain'), basestring): + if action.get('domain') and isinstance(action.get('domain'), basestring): action['domain'] = eval( action['domain'], eval_ctx ) or [] else: if 'context' in action: @@ -771,7 +771,7 @@ def generate_views(action): :param dict action: action descriptor dictionary to generate a views key for """ - view_id = action.get('view_id', False) + view_id = action.get('view_id') or False if isinstance(view_id, (list, tuple)): view_id = view_id[0] @@ -1289,9 +1289,10 @@ class SearchView(View): @openerpweb.jsonrequest def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''): - ctx = common.nonliterals.CompoundContext(context_to_save) - ctx.session = req.session - ctx = ctx.evaluate() + to_eval = common.nonliterals.CompoundContext(context_to_save) + to_eval.session = req.session + ctx = dict((k, v) for k, v in to_eval.evaluate().iteritems() + if not k.startswith('search_default_')) ctx['dashboard_merge_domains_contexts'] = False # TODO: replace this 6.1 workaround by attribute on domain = common.nonliterals.CompoundDomain(domain) domain.session = req.session @@ -1367,6 +1368,17 @@ class Binary(openerpweb.Controller): def placeholder(self, req): addons_path = openerpweb.addons_manifest['web']['addons_path'] return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read() + def content_disposition(self, filename, req): + filename = filename.encode('utf8') + escaped = urllib2.quote(filename) + browser = req.httprequest.user_agent.browser + version = int((req.httprequest.user_agent.version or '0').split('.')[0]) + if browser == 'msie' and version < 9: + return "attachment; filename=%s" % escaped + elif browser == 'safari': + return "attachment; filename=%s" % filename + else: + return "attachment; filename*=UTF-8''%s" % escaped @openerpweb.httprequest def saveas(self, req, model, field, id=None, filename_field=None, **kw): @@ -1402,7 +1414,7 @@ class Binary(openerpweb.Controller): filename = res.get(filename_field, '') or filename return req.make_response(filecontent, [('Content-Type', 'application/octet-stream'), - ('Content-Disposition', 'attachment; filename="%s"' % filename)]) + ('Content-Disposition', self.content_disposition(filename, req))]) @openerpweb.httprequest def saveas_ajax(self, req, data, token): @@ -1432,7 +1444,7 @@ class Binary(openerpweb.Controller): filename = res.get(filename_field, '') or filename return req.make_response(filecontent, headers=[('Content-Type', 'application/octet-stream'), - ('Content-Disposition', 'attachment; filename="%s"' % filename)], + ('Content-Disposition', self.content_disposition(filename, req))], cookies={'fileToken': int(token)}) @openerpweb.httprequest @@ -1481,19 +1493,26 @@ class Action(openerpweb.Controller): "ir.actions.act_url": "ir.actions.url", } + # For most actions, the type attribute and the model name are the same, but + # there are exceptions. This dict is used to remap action type attributes + # to the "real" model name when they differ. + action_mapping = { + "ir.actions.act_url": "ir.actions.url", + } + @openerpweb.jsonrequest def load(self, req, action_id, do_not_eval=False): Actions = req.session.model('ir.actions.actions') value = False context = req.session.eval_context(req.context) - action_type = Actions.read([action_id], ['type'], context) - if action_type: + base_action = Actions.read([action_id], ['type'], context) + if base_action: ctx = {} - if action_type[0]['type'] == 'ir.actions.report.xml': + action_type = base_action[0]['type'] + if action_type == 'ir.actions.report.xml': ctx.update({'bin_size': True}) ctx.update(context) - action_model = action_type[0]['type'] - action_model = Action.action_mapping.get(action_model, action_model) + action_model = self.action_mapping.get(action_type, action_type) action = req.session.model(action_model).read([action_id], False, ctx) if action: value = clean_action(req, action[0], do_not_eval) @@ -1886,10 +1905,14 @@ class Import(View): return '' % ( jsonp, simplejson.dumps({'error': {'message': error}})) - # skip ignored records - data_record = itertools.islice( - csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)), - skip, None) + # skip ignored records (@skip parameter) + # then skip empty lines (not valid csv) + # nb: should these operations be reverted? + rows_to_import = itertools.ifilter( + None, + itertools.islice( + csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)), + skip, None)) # if only one index, itemgetter will return an atom rather than a tuple if len(indices) == 1: mapper = lambda row: [row[indices[0]]] @@ -1901,7 +1924,7 @@ class Import(View): # decode each data row data = [ [record.decode(csvcode) for record in row] - for row in itertools.imap(mapper, data_record) + for row in itertools.imap(mapper, rows_to_import) # don't insert completely empty rows (can happen due to fields # filtering in case of e.g. o2m content rows) if any(row) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 0d4912c2840..351a3366651 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1973,6 +1973,9 @@ .openerp .oe_form .oe_form_field_one2many > .oe_view_manager .oe_list_pager_single_page { display: none; } +.openerp .oe_form_field_one2many .oe-listview .oe-edit-row-save { + background-image: url(/web/static/src/img/iconset-b-remove.png); +} .openerp .oe_form_field_one2many > .oe_view_manager .oe_header_row_top { display: none; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 35e1d08e17f..2ce5cf5a8d5 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1559,21 +1559,26 @@ $colour4: #8a89ba .oe_form .oe_form_field_one2many > .oe_view_manager .oe_list_pager_single_page display: none - .oe_form_field_one2many > .oe_view_manager - .oe_header_row_top - display: none - .oe_view_manager_header2 - td - padding: 0px 8px - line-height: 16px - .oe_i - font-size: 13px - .oe_pager_group - height: auto + .oe_form_field_one2many + // TODO: oe_form_field_one2many_list? + .oe-listview .oe-edit-row-save + background-image: url(/web/static/src/img/iconset-b-remove.png) + + &> .oe_view_manager + .oe_header_row_top + display: none + .oe_view_manager_header2 + td + padding: 0px 8px line-height: 16px - li + .oe_i + font-size: 13px + .oe_pager_group height: auto line-height: 16px + li + height: auto + line-height: 16px // }}} // FormView.many2many {{{ .oe_form .oe_form_field_many2many > .oe-listview diff --git a/addons/web/static/src/css/base_old.css b/addons/web/static/src/css/base_old.css index 74b660f7613..2faead8fde3 100644 --- a/addons/web/static/src/css/base_old.css +++ b/addons/web/static/src/css/base_old.css @@ -477,6 +477,10 @@ label.error { display: inline; margin: 0 0.5em 0 0; } +.openerp .oe_form_field_one2many .oe-listview .oe-edit-row-save, +.openerp .oe_form_field_one2many_list .oe-listview .oe-edit-row-save { + background-image: url("/web/static/src/img/iconset-b-remove.png"); +} .openerp .oe_form .oe-listview th.oe-sortable .ui-icon, .openerp .oe_form .oe-listview th.oe-sortable .ui-icon { @@ -502,7 +506,7 @@ label.error { .openerp .oe_form_readonly .oe_form_field_many2one { padding: 3px 2px 2px 2px; background-color: white; - height: 17px; + min-height: 17px; } .openerp .oe_form_readonly .oe_form_group_cell .field_text { height: auto; @@ -888,6 +892,7 @@ ul.oe-arrow-list { ul.oe-arrow-list li { display: inline-block; margin-left: -1em; + vertical-align: top; } ul.oe-arrow-list li span { vertical-align: top; @@ -896,11 +901,11 @@ ul.oe-arrow-list li span { line-height:0em; } ul.oe-arrow-list .oe-arrow-list-before { - border-left-color: rgba(0,0,0,0); + border-left-color: transparent; border-right-width:0; } ul.oe-arrow-list .oe-arrow-list-after { - border-color: rgba(0,0,0,0); + border-color: transparent; border-left-color: #DEDEDE; border-right-width:0; } @@ -908,10 +913,10 @@ ul.oe-arrow-list li.oe-arrow-list-selected span { border-color: #B5B9FF; } ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-before { - border-left-color: rgba(0,0,0,0); + border-left-color: transparent; } ul.oe-arrow-list li.oe-arrow-list-selected .oe-arrow-list-after { - border-color: rgba(0,0,0,0); + border-color: transparent; border-left-color: #B5B9FF; } .openerp ul.oe-arrow-list li:first-child span:first-child{ diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 6d3f506b6ab..563837d4f68 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -841,6 +841,18 @@ instance.web.WebClient = instance.web.Widget.extend({ this.querystring = '?' + jQuery.param.querystring(); this._current_state = null; }, + _get_version_label: function() { + if (this.session.openerp_entreprise) { + return 'OpenERP'; + } else { + return _t("OpenERP - Unsupported/Community Version"); + } + }, + set_title: function(title) { + title = _.str.clean(title); + var sep = _.isEmpty(title) ? '' : ' - '; + document.title = title + sep + 'OpenERP'; + }, start: function() { var self = this; this.$element.addClass("openerp openerp-web-client-container"); @@ -922,11 +934,11 @@ instance.web.WebClient = instance.web.Widget.extend({ self.user_menu.on_action.add(this.proxy('on_menu_action')); self.user_menu.do_update(); self.bind_hashchange(); - var version_label = _t("OpenERP - Unsupported/Community Version"); if (!self.session.openerp_entreprise) { + var version_label = self._get_version_label(); self.$element.find('.oe_footer_powered').append(_.str.sprintf(' - %s', version_label)); - document.title = version_label; } + self.set_title(); }, destroy_content: function() { _.each(_.clone(this.getChildren()), function(el) { @@ -992,6 +1004,7 @@ instance.web.WebClient = instance.web.Widget.extend({ this._current_state = state; }, do_push_state: function(state) { + this.set_title(state.title); var url = '#' + $.param(state); this._current_state = _.clone(state); $.bbq.pushState(url); diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 94c748f839c..5af51fa519a 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -99,7 +99,6 @@ openerp.web.corelib = function(instance) { for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { return function() { diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js index 30f9323f723..bc98d8a1c10 100644 --- a/addons/web/static/src/js/data_import.js +++ b/addons/web/static/src/js/data_import.js @@ -55,7 +55,8 @@ instance.web.DataImport = instance.web.Dialog.extend({ .filter(function (field) { return field.required && !_.include(self.fields_with_defaults, field.id); }) - .pluck('name') + .pluck('id') + .uniq() .value(); convert_fields(self); self.all_fields.sort(); @@ -133,6 +134,10 @@ instance.web.DataImport = instance.web.Dialog.extend({ switch (field.type) { case 'many2many': case 'many2one': + // push a copy for the bare many2one field, to allow importing + // using name_search too - even if we default to exporting the XML ID + var many2one_field = _.extend({}, f) + parent.fields.push(many2one_field); f.name += '/id'; break; case 'one2many': @@ -261,10 +266,14 @@ instance.web.DataImport = instance.web.Dialog.extend({ fields = fields || this.fields; var f; f = _(fields).detect(function (field) { - // TODO: levenshtein between header and field.string return field.name === name - || field.string.toLowerCase() === name.toLowerCase(); }); + if (!f) { + f = _(fields).detect(function (field) { + // TODO: levenshtein between header and field.string + return field.string.toLowerCase() === name.toLowerCase(); + }); + } if (f) { return f.name; } // if ``name`` is a path (o2m), we need to recurse through its .fields @@ -274,9 +283,13 @@ instance.web.DataImport = instance.web.Dialog.extend({ var column_name = name.substring(0, index); f = _(fields).detect(function (field) { // field.name for o2m is $foo/id, so we want to match on id - return field.id === column_name - || field.string.toLowerCase() === column_name.toLowerCase() + return field.id === column_name; }); + if (!f) { + f = _(fields).detect(function (field) { + return field.string.toLowerCase() === column_name.toLowerCase(); + }); + } if (!f) { return undefined; } // if we found a matching field for the first path section, recurse in @@ -330,7 +343,7 @@ instance.web.DataImport = instance.web.Dialog.extend({ if (_.isEmpty(duplicates)) { this.toggle_import_button(required_valid); } else { - var $err = $('
Destination fields should only be selected once, some fields are selected more than once:
').insertBefore(this.$element.find('#result')); + var $err = $('
'+_t("Destination fields should only be selected once, some fields are selected more than once:")+'
').insertBefore(this.$element.find('#result')); var $dupes = $('
').appendTo($err); _(duplicates).each(function(elements, value) { $('
').text(value).appendTo($dupes); @@ -344,16 +357,30 @@ instance.web.DataImport = instance.web.Dialog.extend({ }, check_required: function() { - if (!this.required_fields.length) { return true; } + var self = this; + if (!self.required_fields.length) { return true; } + + // Resolve field id based on column name, as there may be + // several ways to provide the value for a given field and + // thus satisfy the requirement. + // (e.g. m2o_id or m2o_id/id columns may be provided) + var resolve_field_id = function(column_name) { + var f = _.detect(self.fields, function(field) { + return field.name === column_name; + }); + if (!f) { return column_name; }; + return f.id; + }; var selected_fields = _(this.$element.find('.sel_fields').get()).chain() .pluck('value') .compact() + .map(resolve_field_id) .value(); var missing_fields = _.difference(this.required_fields, selected_fields); if (missing_fields.length) { - this.$element.find("#result").before('
*Required Fields are not selected : ' + missing_fields + '.
'); + this.$element.find("#result").before('
' + _t("*Required Fields are not selected :") + missing_fields + '.
'); return false; } return true; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 556e7fe73b3..1d0e5a2cf1a 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -78,13 +78,18 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); + this.__clicked_inside = false; + this.__blur_timeout = null; this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template }, destroy: function() { _.each(this.get_widgets(), function(w) { + // FIXME: use widget events + $(w).unbind('.formBlur'); w.destroy(); }); + this.$element.unbind('.formBlur'); this._super(); }, on_loaded: function(data) { @@ -106,6 +111,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.rendering_engine.render_to($dest); } + this.$element.bind('mousedown.formBlur', function () { + self.__clicked_inside = true; + }); this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); if (this.options.$buttons) { @@ -197,6 +205,30 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.translatable_fields = []; this.$element.find('.oe_form_container').empty(); }, + + widgetFocused: function() { + // Clear click flag if used to focus a widget + this.__clicked_inside = false; + if (this.__blur_timeout) { + clearTimeout(this.__blur_timeout); + this.__blur_timeout = null; + } + }, + widgetBlurred: function() { + if (this.__clicked_inside) { + // clicked in an other section of the form (than the currently + // focused widget) => just ignore the blurring entirely? + this.__clicked_inside = false; + return; + } + var self = this; + // clear timeout, if any + this.widgetFocused(); + this.__blur_timeout = setTimeout(function () { + $(self).trigger('form-blur'); + }, 0); + }, + do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -478,9 +510,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } }); }, - on_processed_onchange: function(response, processed) { + on_processed_onchange: function(result, processed) { try { - var result = response; if (result.value) { for (var f in result.value) { if (!result.value.hasOwnProperty(f)) { continue; } @@ -627,7 +658,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM * record or saving an existing one depending on whether the record * already has an id property. * - * @param {Function} success callback on save success + * @param {Function} [success] callback on save success * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) */ do_save: function(success, prepend_on_create) { @@ -701,7 +732,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { - return $.when(r).then(success); }, null); + return r; }) + .then(success); } }, /** @@ -734,8 +766,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } - this.reload(); - return $.when(_.extend(r, {created: true})).then(success); + //openerp.log("The record has been created with id #" + this.datarecord.id); + return $.when(this.reload()).pipe(function () { + return _.extend(r, {created: true}); }) + .then(success); } }, on_action: function (action) { @@ -879,6 +913,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); + + $(field).bind('widget-focus.formBlur', this.proxy('widgetFocused')) + .bind('widget-blur.formBlur', this.proxy('widgetBlurred')); if (this.get_field(name).translate) { this.translatable_fields.push(field); } @@ -1479,6 +1516,18 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi $.fn.tipsy.clear(); this._super.apply(this, arguments); }, + /** + * Sets up blur/focus forwarding from DOM elements to a widget (`this`) + * + * @param {jQuery} $e jQuery object of elements to bind focus/blur on + */ + setupFocus: function ($e) { + var self = this; + $e.bind({ + focus: function () { $(self).trigger('widget-focus'); }, + blur: function () { $(self).trigger('widget-blur'); } + }); + }, process_modifiers: function() { var compute_domain = instance.web.form.compute_domain; var to_set = {}; @@ -1576,10 +1625,12 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.click(this.on_click); + var $button = this.$element.find('button'); + $button.click(this.on_click); if (this.node.attrs.help || instance.connection.debug) { this.do_attach_tooltip(); } + this.setupFocus($button); }, on_click: function() { var self = this; @@ -1805,7 +1856,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w return this.get('value'); }, is_valid: function() { - return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); + return this.is_syntax_valid() && !(this.get('required') && this.is_false()); }, is_syntax_valid: function() { return true; @@ -1898,9 +1949,11 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we }, initialize_content: function() { var self = this; - this.$element.find('input').change(function() { + var $input = this.$element.find('input'); + $input.find('input').change(function() { self.set({'value': instance.web.parse_value(self.$element.find('input').val(), self)}); }); + this.setupFocus($input); }, set_value: function(value_) { this._super(value_); @@ -1944,7 +1997,9 @@ instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ template: 'FieldEmail', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$element.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -1968,7 +2023,9 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ template: 'FieldUrl', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$element.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -2037,12 +2094,14 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ showButtonPanel: true }); this.$element.find('img.oe_datepicker_trigger').click(function() { - if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { - self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); - self.$input_picker.show(); - self.picker('show'); - self.$input_picker.hide(); + if (self.get("effective_readonly") || self.picker('widget').is(':visible')) { + self.$input.focus(); + return; } + self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.$input_picker.show(); + self.picker('show'); + self.$input_picker.hide(); }); this.set_readonly(false); this.set({'value': false}); @@ -2052,7 +2111,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, on_picker_select: function(text, instance_) { var date = this.picker('getDate'); - this.$input.val(date ? this.format_client(date) : '').change(); + this.$input + .val(date ? this.format_client(date) : '') + .change() + .focus(); }, set_value: function(value_) { this.set({'value': value_}); @@ -2119,6 +2181,7 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc this.set({'value': this.datewidget.get_value()}); }, this)); this.datewidget.appendTo(this.$element); + this.setupFocus(this.datewidget.$input); } }, set_value: function(value_) { @@ -2165,6 +2228,7 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we } else { this.$textarea.attr('disabled', 'disabled'); } + this.setupFocus(this.$textarea); }, set_value: function(value_) { this._super.apply(this, arguments); @@ -2221,6 +2285,7 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ start: function() { this._super.apply(this, arguments); this.$checkbox = $("input", this.$element); + this.setupFocus(this.$checkbox); this.$element.click(_.bind(function() { this.set({'value': this.$checkbox.is(':checked')}); }, this)); @@ -2289,7 +2354,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan // changing the selected value), takes the action as validating the // row var ischanging = false; - this.$element.find('select') + var $select = this.$element.find('select') .change(_.bind(function() { this.set({'value': this.values[this.$element.find('select')[0].selectedIndex][0]}); }, this)) @@ -2300,6 +2365,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan e.stopPropagation(); ischanging = false; }); + this.setupFocus($select); }, set_value: function(value_) { value_ = value_ === null ? false : value_; @@ -2458,6 +2524,7 @@ instance.web.form.CompletionFieldMixin = { ); pop.on_select_elements.add(function(element_ids) { self.add_id(element_ids[0]); + self.focus(); }); }, /** @@ -2507,6 +2574,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc this.$follow_button.click(function() { if (!self.get('value')) { + self.focus(); return; } var pop = new instance.web.form.FormOpenPopup(self.view); @@ -2521,6 +2589,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc pop.on_write_completed.add_last(function() { self.display_value = {}; self.render_value(); + self.focus(); }); }); @@ -2535,13 +2604,13 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc this.$drop_down.click(function() { if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); + self.$input.focus(); } else { if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); } else { self.$input.autocomplete("search"); } - self.$input.focus(); } }); var tip_def = $.Deferred(); @@ -2628,6 +2697,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc } isSelecting = false; }); + this.setupFocus(this.$input.add(this.$follow_button)); }, render_value: function(no_recurse) { @@ -3007,13 +3077,15 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ if (!this.viewmanager.views[this.viewmanager.active_view]) return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - for (var f in view.fields) { - f = view.fields[f]; - if (!f.is_valid()) { - return false; - } - } + switch (this.viewmanager.active_view) { + case 'form': + return _(view.fields).chain() + .invoke('is_valid') + .all(_.identity) + .value(); + break; + case 'list': + return view.is_valid(); } return true; }, @@ -3073,6 +3145,46 @@ instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ instance.web.form.One2ManyListView = instance.web.ListView.extend({ _template: 'One2Many.listview', + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, _.extend(options || {}, { + ListType: instance.web.form.One2ManyList + })); + }, + is_valid: function () { + var form; + // A list not being edited is always valid + if (!(form = this.first_edition_form())) { + 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 + if (!form.$element.is('.oe_form_dirty')) { + return true; + } + + // Otherwise validate internal form + return _(form.fields).chain() + .invoke(function () { + this._check_css_flag(); + return this.is_valid(); + }) + .all(_.identity) + .value(); + }, + first_edition_form: function () { + var get_form = function (group_or_list) { + if (group_or_list.edition) { + return group_or_list.edition_form; + } + return _(group_or_list.children).chain() + .map(get_form) + .compact() + .first() + .value(); + }; + return get_form(this.groups); + }, do_add_record: function () { if (this.options.editable) { this._super.apply(this, arguments); @@ -3127,9 +3239,55 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ }); }, do_button_action: function (name, id, callback) { + var _super = _.bind(this._super, this); + + this.o2m.view.do_save().then(function () { + _super(name, id, callback); + }); + } +}); +instance.web.form.One2ManyList = instance.web.ListView.List.extend({ + KEY_RETURN: 13, + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing + __return_blur: false, + render_row_as_form: function () { var self = this; - var def = $.Deferred().then(callback).then(function() {self.o2m.view.reload();}); - return this._super(name, id, _.bind(def.resolve, def)); + return this._super.apply(this, arguments).then(function () { + // Replace the "Save Row" button with "Cancel Edition" + self.edition_form.$element + .undelegate('button.oe-edit-row-save', 'click') + .delegate('button.oe-edit-row-save', 'click', function () { + self.cancel_pending_edition(); + }); + + // Overload execute_action on the edition form to perform a simple + // reload_record after the action is done, rather than fully + // reload the parent view (or something) + var _execute_action = self.edition_form.do_execute_action; + self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) { + return _execute_action.call(this, action, dataset, record_id, function () { + self.view.reload_record( + self.view.records.get(record_id)); + }); + }; + + $(self.edition_form).bind('form-blur', function () { + if (self.__return_blur) { + delete self.__return_blur; + return; + } + if (!self.edition_form.widget_is_stopped) { + self.view.ensure_saved(); + } + }); + }); + }, + on_row_keyup: function (e) { + if (e.which === this.KEY_RETURN) { + this.__return_blur = true; + } + this._super(e); } }); @@ -3336,6 +3494,10 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ get_value: function() { return [commands.replace_with(this.get('value'))]; }, + + is_false: function () { + return _(this.dataset.ids).isEmpty(); + }, load_view: function() { var self = this; this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { @@ -3920,6 +4082,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan } }, initialize_content: function() { + var self = this; this.selection = new instance.web.form.FieldSelection(this, { attrs: { name: 'selection' }}); @@ -3939,6 +4102,10 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); this.m2o.renderElement(); this.m2o.start(); + $(this.selection).add($(this.m2o)).bind({ + 'focus': function () { $(self).trigger('widget-focus'); }, + 'blur': function () { $(self).trigger('widget-blur'); } + }); }, is_false: function() { return typeof(this.get_value()) !== 'string'; @@ -4070,6 +4237,13 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. return false; } }, + set_filename: function(value) { + var filename = this.node.attrs.filename; + if (this.view.fields[filename]) { + this.view.fields[filename].set_value(value); + this.view.fields[filename].on_ui_change(); + } + }, on_clear: function() { if (this.get('value') !== false) { this.binary_value = false; diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index a90dabe1b4c..95c425a0e1d 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -64,7 +64,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.records = new Collection(); - this.set_groups(new instance.web.ListView.Groups(this)); + this.set_groups(new (this.options.GroupsType)(this)); if (this.dataset instanceof instance.web.DataSetStatic) { this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset); @@ -87,6 +87,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.no_leaf = false; }, + set_default_options: function (options) { + this._super(options); + _.defaults(this.options, { + GroupsType: instance.web.ListView.Groups, + ListType: instance.web.ListView.List + }); + }, + /** * Retrieves the view's number of records per page (|| section) * @@ -549,6 +557,20 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi reload: function () { return this.reload_content(); }, + reload_record: function (record) { + return this.dataset.read_ids( + [record.get('id')], + _.pluck(_(this.columns).filter(function (r) { + return r.tag === 'field'; + }), 'name') + ).then(function (records) { + _(records[0]).each(function (value, key) { + record.set(key, value, {silent: true}); + }); + record.trigger('change', record); + }); + }, + do_load_state: function(state, warm) { var reload = false; if (state.page && this.page !== state.page) { @@ -1055,11 +1077,11 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. */ get_selection: function () { + var result = {ids: [], records: []}; if (!this.options.selectable) { - return []; + return result; } var records = this.records; - var result = {ids: [], records: []}; this.$current.find('th.oe-record-selector input:checked') .closest('tr').each(function () { var record = records.get($(this).data('id')); @@ -1102,17 +1124,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {$.Deferred} promise to the finalization of the reloading */ reload_record: function (record) { - return this.dataset.read_ids( - [record.get('id')], - _.pluck(_(this.columns).filter(function (r) { - return r.tag === 'field'; - }), 'name') - ).then(function (records) { - _(records[0]).each(function (value, key) { - record.set(key, value, {silent: true}); - }); - record.trigger('change', record); - }); + return this.view.reload_record(record); }, /** * Renders a list record to HTML @@ -1275,7 +1287,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.records.proxy(group.value).reset(); delete self.children[group.value]; } - var child = self.children[group.value] = new instance.web.ListView.Groups(self.view, { + var child = self.children[group.value] = new (self.view.options.GroupsType)(self.view, { records: self.records.proxy(group.value), options: self.options, columns: self.columns @@ -1378,7 +1390,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }, render_dataset: function (dataset) { var self = this, - list = new instance.web.ListView.List(this, { + list = new (this.view.options.ListType)(this, { options: this.options, columns: this.columns, dataset: dataset, diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index c80509a7b2b..27113131f41 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -125,29 +125,27 @@ openerp.web.list_editable = function (instance) { * Checks if a record is being edited, and if so cancels it */ cancel_pending_edition: function () { - var self = this, cancelled = $.Deferred(); + var self = this, cancelled; if (!this.edition) { - cancelled.resolve(); - return cancelled.promise(); + return $.when(); } - if (this.edition_id != null) { - this.reload_record(self.records.get(this.edition_id)).then(function () { - cancelled.resolve(); - }); + if (this.edition_id) { + cancelled = this.reload_record(this.records.get(this.edition_id)); } else { - cancelled.resolve(); + cancelled = $.when(); } cancelled.then(function () { self.view.unpad_columns(); self.edition_form.destroy(); self.edition_form.$element.remove(); delete self.edition_form; + self.dataset.index = null; delete self.edition_id; delete self.edition; }); this.pad_table_to(5); - return cancelled.promise(); + return cancelled; }, /** * Adapts this list's view description to be suitable to the inner form @@ -171,24 +169,29 @@ openerp.web.list_editable = function (instance) { var self = this; switch (e.which) { case KEY_RETURN: - this.save_row().then(function (result) { - if (result.created) { - self.new_record(); - return; - } + $(e.target).blur(); + e.preventDefault(); + //e.stopImmediatePropagation(); + setTimeout(function () { + self.save_row().then(function (result) { + if (result.created) { + self.new_record(); + return; + } - var next_record_id, - next_record = self.records.at( - self.records.indexOf(result.edited_record) + 1); - if (next_record) { - next_record_id = next_record.get('id'); - self.dataset.index = _(self.dataset.ids) - .indexOf(next_record_id); - } else { - self.dataset.index = 0; - next_record_id = self.records.at(0).get('id'); - } - self.edit_record(next_record_id); + var next_record_id, + next_record = self.records.at( + self.records.indexOf(result.edited_record) + 1); + if (next_record) { + next_record_id = next_record.get('id'); + self.dataset.index = _(self.dataset.ids) + .indexOf(next_record_id); + } else { + self.dataset.index = 0; + next_record_id = self.records.at(0).get('id'); + } + self.edit_record(next_record_id); + }, 0); }); break; case KEY_ESCAPE: @@ -198,7 +201,7 @@ openerp.web.list_editable = function (instance) { }, render_row_as_form: function (row) { var self = this; - this.cancel_pending_edition().then(function () { + return this.ensure_saved().pipe(function () { var record_id = $(row).data('id'); var $new_row = $('', { id: _.uniqueId('oe-editable-row-'), @@ -214,7 +217,13 @@ openerp.web.list_editable = function (instance) { }) .keyup(function () { return self.on_row_keyup.apply(self, arguments); }) - .keydown(function (e) { e.stopPropagation(); }); + .keydown(function (e) { e.stopPropagation(); }) + .keypress(function (e) { + if (e.which === KEY_RETURN) { + return false; + } + }); + if (row) { $new_row.replaceAll(row); } else if (self.options.editable) { @@ -236,14 +245,16 @@ openerp.web.list_editable = function (instance) { } self.edition = true; self.edition_id = record_id; - $new_row.addClass("oe_form_container"); - self.edition_form = new instance.web.ListEditableFormView(self.view, self.dataset, false); - self.edition_form.$element = $new_row; - self.edition_form.editable_list = self; - // HO HO - // empty - $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - // put in $.when just in case FormView.on_loaded becomes asynchronous + self.dataset.index = _(self.dataset.ids).indexOf(record_id); + if (self.dataset.index === -1) { + self.dataset.index = null; + } + self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), { + $element: $new_row, + editable_list: self + }); + // put in $.when just in case FormView.on_loaded becomes asynchronous + return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { $new_row.find('> td') .end() .find('td:last').removeClass('oe-field-cell').end(); @@ -299,7 +310,7 @@ openerp.web.list_editable = function (instance) { */ save_row: function () { //noinspection JSPotentiallyInvalidConstructorUsage - var self = this, done = $.Deferred(); + var self = this; return this.edition_form .do_save(null, this.options.editable === 'top') .pipe(function (result) { @@ -319,18 +330,24 @@ openerp.web.list_editable = function (instance) { created: result.created || false, edited_record: edited_record }; - }, null); - }, null); + }); + }); }, /** * If the current list is being edited, ensures it's saved */ ensure_saved: function () { if (this.edition) { - return this.save_row(); + // kinda-hack-ish: if the user has entered data in a field, + // oe_form_dirty will be set on the form so save, otherwise + // discard the current (entirely empty) line + if (this.edition_form.$element.is('.oe_form_dirty')) { + return this.save_row(); + } + return this.cancel_pending_edition(); } //noinspection JSPotentiallyInvalidConstructorUsage - return $.Deferred().resolve().promise(); + return $.when(); }, /** * Cancels the edition of the row for the current dataset index @@ -349,7 +366,6 @@ openerp.web.list_editable = function (instance) { [record_id, this.dataset]); }, new_record: function () { - this.dataset.index = null; this.render_row_as_form(); }, render_record: function (record) { diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 95b87e3cd86..e82d3d0ae9b 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -36,6 +36,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { if (this.inner_action) { + state['title'] = this.inner_action.name; state['model'] = this.inner_action.res_model; if (this.inner_action.id) { state['action_id'] = this.inner_action.id; @@ -894,8 +895,8 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ // TODO fme: should add the language to fields_view_get because between the fields view get // and the moment the user opens the translation dialog, the user language could have been changed this.view_language = view.session.user_context.lang; - this['on_button' + _t("Save")] = this.on_button_Save; - this['on_button' + _t("Close")] = this.on_button_Close; + this['on_button_' + _t("Save")] = this.on_btn_save; + this['on_button_' + _t("Close")] = this.on_btn_close; this._super(view, { width: '80%', height: '80%' @@ -976,7 +977,7 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ } }); }, - on_button_Save: function() { + on_btn_save: function() { var trads = {}, self = this, trads_mutex = new $.Mutex(); @@ -999,7 +1000,7 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ }); this.close(); }, - on_button_Close: function() { + on_btn_close: function() { this.close(); } }); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index e293ffc7adb..e709f56b801 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1411,8 +1411,7 @@
  • - Export diff --git a/addons/web_calendar/static/src/js/calendar.js b/addons/web_calendar/static/src/js/calendar.js index 4d738e55514..a7640d34e0d 100644 --- a/addons/web_calendar/static/src/js/calendar.js +++ b/addons/web_calendar/static/src/js/calendar.js @@ -225,22 +225,14 @@ instance.web_calendar.CalendarView = instance.web.View.extend({ var date_start = instance.web.str_to_datetime(evt[this.date_start]), date_stop = this.date_stop ? instance.web.str_to_datetime(evt[this.date_stop]) : null, date_delay = evt[this.date_delay] || 1.0, - res_text = '', - res_description = []; + res_text = ''; if (this.info_fields) { - var fld = evt[this.info_fields[0]]; - res_text = (typeof fld == 'object') ? fld[fld.length -1] : res_text = fld; - - var sliced_info_fields = this.info_fields.slice(1); - for (var sl_fld in sliced_info_fields) { - var slc_fld = evt[sliced_info_fields[sl_fld]]; - if (typeof slc_fld == 'object') { - res_description.push(slc_fld[slc_fld.length - 1]); - } else if (slc_fld) { - res_description.push(slc_fld); - } - } + res_text = _.map(this.info_fields, function(fld) { + if(evt[fld] instanceof Array) + return evt[fld][1]; + return evt[fld]; + }); } if (!date_stop && date_delay) { date_stop = date_start.clone().addHours(date_delay); @@ -248,9 +240,8 @@ instance.web_calendar.CalendarView = instance.web.View.extend({ var r = { 'start_date': date_start.toString('yyyy-MM-dd HH:mm:ss'), 'end_date': date_stop.toString('yyyy-MM-dd HH:mm:ss'), - 'text': res_text, - 'id': evt.id, - 'title': res_description.join() + 'text': res_text.join(', '), + 'id': evt.id }; if (evt.color) { r.color = evt.color; diff --git a/addons/web_dashboard/static/src/js/dashboard.js b/addons/web_dashboard/static/src/js/dashboard.js index e08113d02ab..469bf86da1c 100644 --- a/addons/web_dashboard/static/src/js/dashboard.js +++ b/addons/web_dashboard/static/src/js/dashboard.js @@ -163,16 +163,10 @@ instance.web.form.DashBoard = instance.web.form.FormWidget.extend({ var action_orig = _.extend({ flags : {} }, action); if (view_mode && view_mode != action.view_mode) { - var action_view_mode = action.view_mode.split(','); action.views = _.map(view_mode.split(','), function(mode) { - if (_.indexOf(action_view_mode, mode) < 0) { - return [false, mode == 'tree' ? 'list': mode]; - } else { - mode = mode === 'tree' ? 'list' : mode; - return _.find(action.views, function(view) { - return view[1] == mode; - }); - } + mode = mode === 'tree' ? 'list' : mode; + return _(action.views).find(function(view) { return view[1] == mode; }) + || [false, mode]; }); } diff --git a/addons/web_diagram/static/src/js/graph.js b/addons/web_diagram/static/src/js/graph.js index b61b2bf7a6b..bfe5796c0f0 100644 --- a/addons/web_diagram/static/src/js/graph.js +++ b/addons/web_diagram/static/src/js/graph.js @@ -75,6 +75,8 @@ dummy_circle.animate({'r': close_button_radius},400,'linear'); } dummy_circle.hover(hover_in,hover_out); + close_circle.hover(hover_in,hover_out); + close_label.hover(hover_in,hover_out); function click_action(){ if(!visible){ return; } @@ -97,6 +99,8 @@ } } dummy_circle.click(click_action); + close_circle.click(click_action); + close_label.click(click_action); this.show = function(){ if(!visible){