diff options
| author | Géry Debongnie <ged@odoo.com> | 2015-03-09 16:34:27 +0100 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2015-03-18 09:01:49 +0100 |
| commit | e8a00bc50da435d3859118ca127d20a2eb9a0cab (patch) | |
| tree | 039f6ec40b37392e76c8830a8e518ac40f7e7fec /addons/web/static/src/js/views/form_view.js | |
| parent | 9036084f065c7aca3b2199903f4da67db3501ba7 (diff) | |
[REF] web: add module system to the web client
This commit introduces a new module system for the javascript code.
Instead of using global (namespaced) variables everywhere (like
openerp.web.whatever, ...), each file declare one (or more) module
with its dependencies. Each module will be loaded after its
dependencies, and will be given a reference to the return values.
This change is a massive departure from the way the client worked
previously. It will sadly break some/most history, but I am sure that
the future git ecosystem will allow you to easily browse the history
past this revision (one can dream).
This commit feature:
* main namespace is now odoo instead of openerp
* a way to define modules and dependencies (inspired by requirejs)
* many files have been splitted in simpler subfiles
* basic support for debugging in odoo.__DEBUG__
* no global variable (i.e. nothing in window.openerp)
* many bug fixes, many new bugs
Future vision: this modularization should simplify future code
development, simplify debugging and will allow a serious testing effort
Diffstat (limited to 'addons/web/static/src/js/views/form_view.js')
| -rw-r--r-- | addons/web/static/src/js/views/form_view.js | 1587 |
1 files changed, 1587 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js new file mode 100644 index 00000000000..ac7ea1d7888 --- /dev/null +++ b/addons/web/static/src/js/views/form_view.js @@ -0,0 +1,1587 @@ +odoo.define('web.FormView', ['web.core', 'web.crash_manager', 'web.data', 'web.Dialog', 'web.form_common', 'web.Model', 'web.Sidebar', 'web.utils', 'web.View'], function (require) { +"use strict"; + +var core = require('web.core'); +var crash_manager = require('web.crash_manager'); +var data = require('web.data'); +var Dialog = require('web.Dialog'); +var common = require('web.form_common'); +var Model = require('web.Model'); +var Sidebar = require('web.Sidebar'); +var utils = require('web.utils'); +var View = require('web.View'); + +var _t = core._t; +var _lt = core._lt; +var QWeb = core.qweb; +var Class = core.Class; + +/** @namespace */ +// instance.web.form = instance.web.form || {}; + +/** + * Properties: + * - actual_mode: always "view", "edit" or "create". Read-only property. Determines + * the mode used by the view. + */ +var FormView = View.extend(common.FieldManagerMixin, { + /** + * Indicates that this view is not searchable, and thus that no search + * view should be displayed (if there is one active). + */ + searchable: false, + template: "FormView", + display_name: _lt('Form'), + view_type: "form", + /** + * @constructs instance.web.FormView + * @extends instance.web.View + * + * @param {instance.web.Session} session the current openerp session + * @param {instance.web.DataSet} dataset the dataset this view will work with + * @param {String} view_id the identifier of the OpenERP view object + * + * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance + */ + init: function(parent, dataset, view_id, options) { + var self = this; + this._super(parent); + this.ViewManager = parent; + this.set_default_options(options); + this.dataset = dataset; + this.model = dataset.model; + this.view_id = view_id || false; + this.fields_view = {}; + this.fields = {}; + this.fields_order = []; + this.datarecord = {}; + this._onchange_specs = {}; + this.onchanges_mutex = new utils.Mutex(); + this.default_focus_field = null; + this.default_focus_button = null; + this.fields_registry = core.form_widget_registry; + this.tags_registry = core.form_tag_registry; + this.widgets_registry = core.form_custom_registry; + this.has_been_loaded = $.Deferred(); + this.translatable_fields = []; + _.defaults(this.options, { + "not_interactible_on_create": false, + "initial_mode": "view", + "disable_autofocus": false, + "footer_to_buttons": false, + }); + this.is_initialized = $.Deferred(); + this.mutating_mutex = new utils.Mutex(); + this.save_list = []; + this.render_value_defs = []; + this.reload_mutex = new utils.Mutex(); + this.__clicked_inside = false; + this.__blur_timeout = null; + this.rendering_engine = new FormRenderingEngine(this); + self.set({actual_mode: self.options.initial_mode}); + this.has_been_loaded.done(function() { + self._build_onchange_specs(); + self.on("change:actual_mode", self, self.check_actual_mode); + self.check_actual_mode(); + self.on("change:actual_mode", self, self.init_pager); + self.init_pager(); + }); + self.on("load_record", self, self.load_record); + core.bus.on('clear_uncommitted_changes', this, function(e) { + if (!this.can_be_discarded()) { + e.preventDefault(); + } + }); + }, + view_loading: function(r) { + return this.load_form(r); + }, + destroy: function() { + _.each(this.get_widgets(), function(w) { + w.off('focused blurred'); + w.destroy(); + }); + if (this.$el) { + this.$el.off('.formBlur'); + } + this._super(); + }, + load_form: function(data) { + var self = this; + if (!data) { + throw new Error(_t("No data provided.")); + } + if (this.arch) { + throw "Form view does not support multiple calls to load_form"; + } + this.fields_order = []; + this.fields_view = data; + + this.rendering_engine.set_fields_registry(this.fields_registry); + this.rendering_engine.set_tags_registry(this.tags_registry); + this.rendering_engine.set_widgets_registry(this.widgets_registry); + this.rendering_engine.set_fields_view(data); + var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); + this.rendering_engine.render_to($dest); + + this.$el.on('mousedown.formBlur', function () { + self.__clicked_inside = true; + }); + + this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); + if (this.options.$buttons) { + this.$buttons.appendTo(this.options.$buttons); + } else { + this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); + } + this.$buttons.on('click', '.oe_form_button_create', + this.guard_active(this.on_button_create)); + this.$buttons.on('click', '.oe_form_button_edit', + this.guard_active(this.on_button_edit)); + this.$buttons.on('click', '.oe_form_button_save', + this.guard_active(this.on_button_save)); + this.$buttons.on('click', '.oe_form_button_cancel', + this.guard_active(this.on_button_cancel)); + if (this.options.footer_to_buttons) { + this.$el.find('footer').appendTo(this.$buttons); + } + + this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar'); + if (!this.sidebar && this.options.$sidebar) { + this.sidebar = new Sidebar(this); + this.sidebar.appendTo(this.$sidebar); + if (this.fields_view.toolbar) { + this.sidebar.add_toolbar(this.fields_view.toolbar); + } + this.sidebar.add_items('other', _.compact([ + self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete }, + self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate } + ])); + } + + this.has_been_loaded.resolve(); + + // Add bounce effect on button 'Edit' when click on readonly page view. + this.$el.find(".oe_form_group_row,.oe_form_field,label,h1,.oe_title,.oe_notebook_page, .oe_list_content").on('click', function (e) { + if(self.get("actual_mode") == "view") { + var $button = self.options.$buttons.find(".oe_form_button_edit"); + $button.openerpBounce(); + e.stopPropagation(); + core.bus.trigger('click', e); + } + }); + //bounce effect on red button when click on statusbar. + this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { + if((self.get("actual_mode") == "view")) { + var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'}); + $button.openerpBounce(); + e.stopPropagation(); + } + }); + this.trigger('form_view_loaded', data); + return $.when(); + }, + 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('blurred'); + }, 0); + }, + + do_load_state: function(state, warm) { + if (state.id && this.datarecord.id != state.id) { + if (this.dataset.get_id_index(state.id) === null) { + this.dataset.ids.push(state.id); + } + this.dataset.select_id(state.id); + this.do_show(); + } + }, + /** + * + * @param {Object} [options] + * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". + * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @return {$.Deferred} + */ + do_show: function (options) { + var self = this; + options = options || {}; + if (this.sidebar) { + this.sidebar.$el.show(); + } + if (this.$buttons) { + this.$buttons.show(); + } + this.$el.show().css({ + opacity: '0', + filter: 'alpha(opacity = 0)' + }); + this.$el.add(this.$buttons).removeClass('oe_form_dirty'); + + var shown = this.has_been_loaded; + if (options.reload !== false) { + shown = shown.then(function() { + if (self.dataset.index === null) { + // null index means we should start a new record + return self.on_button_new(); + } + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, { + context: { 'bin_size': true, 'future_display_name' : true } + }).then(function(r) { + self.trigger('load_record', r); + }); + }); + } + return $.when(shown, this._super()).then(function() { + self._actualize_mode(options.mode || self.options.initial_mode); + self.$el.css({ + opacity: '1', + filter: 'alpha(opacity = 100)' + }); + core.bus.trigger('form_view_shown', self); + }); + }, + do_hide: function () { + if (this.sidebar) { + this.sidebar.$el.hide(); + } + if (this.$pager) { + this.$pager.hide(); + } + this._super(); + }, + load_record: function(record) { + var self = this, set_values = []; + if (!record) { + this.set({ 'title' : undefined }); + this.do_warn(_t("Form"), _t("The record could not be found in the database."), true); + return $.Deferred().reject(); + } + this.datarecord = record; + this._actualize_mode(); + this.set({ 'title' : record.id ? record.display_name : _t("New") }); + + _(this.fields).each(function (field, f) { + field._dirty_flag = false; + field._inhibit_on_change_flag = true; + var result = field.set_value(self.datarecord[f] || false); + field._inhibit_on_change_flag = false; + set_values.push(result); + }); + return $.when.apply(null, set_values).then(function() { + if (!record.id) { + // trigger onchanges + self.do_onchange(null); + } + self.on_form_changed(); + self.rendering_engine.init_fields(); + self.is_initialized.resolve(); + self.do_update_pager(record.id === null || record.id === undefined); + if (self.sidebar) { + self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); + } + if (record.id) { + self.do_push_state({id:record.id}); + } else { + self.do_push_state({}); + } + self.$el.add(self.$buttons).removeClass('oe_form_dirty'); + self.autofocus(); + }); + }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var self = this; + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys).then(function(r) { + self.trigger('load_record', r); + }); + } + return self.trigger('load_record', {}); + }, + on_form_changed: function() { + this.trigger("view_content_has_changed"); + }, + do_notify_change: function() { + this.$el.add(this.$buttons).addClass('oe_form_dirty'); + }, + execute_pager_action: function(action) { + if (this.can_be_discarded()) { + switch (action) { + case 'first': + this.dataset.index = 0; + break; + case 'previous': + this.dataset.previous(); + break; + case 'next': + this.dataset.next(); + break; + case 'last': + this.dataset.index = this.dataset.ids.length - 1; + break; + } + var def = this.reload(); + this.trigger('pager_action_executed'); + return def; + } + return $.when(); + }, + init_pager: function() { + var self = this; + if (this.$pager) + this.$pager.remove(); + if (this.get("actual_mode") === "create") + return; + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$el.find('.oe_form_pager').replaceWith(this.$pager); + } + this.$pager.on('click','a[data-pager-action]',function() { + var $el = $(this); + if ($el.attr("disabled")) + return; + var action = $el.data('pager-action'); + var def = $.when(self.execute_pager_action(action)); + $el.attr("disabled"); + def.always(function() { + $el.removeAttr("disabled"); + }); + }); + this.do_update_pager(); + }, + do_update_pager: function(hide_index) { + this.$pager.toggle(this.dataset.ids.length > 1); + if (hide_index) { + $(".oe_form_pager_state", this.$pager).html(""); + } else { + $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length)); + } + }, + + _build_onchange_specs: function() { + var self = this; + var find = function(field_name, root) { + var fields = [root]; + while (fields.length) { + var node = fields.pop(); + if (!node) { + continue; + } + if (node.tag === 'field' && node.attrs.name === field_name) { + return node.attrs.on_change || ""; + } + fields = _.union(fields, node.children); + } + return ""; + }; + + self._onchange_fields = []; + self._onchange_specs = {}; + _.each(this.fields, function(field, name) { + self._onchange_fields.push(name); + self._onchange_specs[name] = find(name, field.node); + _.each(field.field.views, function(view) { + _.each(view.fields, function(_, subname) { + self._onchange_specs[name + '.' + subname] = find(subname, view.arch); + }); + }); + }); + }, + _get_onchange_values: function() { + var field_values = this.get_fields_values(); + if (field_values.id.toString().match(data.BufferedDataSet.virtual_id_regex)) { + delete field_values.id; + } + if (this.dataset.parent_view) { + // this belongs to a parent view: add parent field if possible + var parent_view = this.dataset.parent_view; + var child_name = this.dataset.child_name; + var parent_name = parent_view.get_field_desc(child_name).relation_field; + if (parent_name) { + // consider all fields except the inverse of the parent field + var parent_values = parent_view.get_fields_values(); + delete parent_values[child_name]; + field_values[parent_name] = parent_values; + } + } + return field_values; + }, + + do_onchange: function(widget) { + var self = this; + var onchange_specs = self._onchange_specs; + try { + var def = $.when({}); + var change_spec = widget ? onchange_specs[widget.name] : null; + if (!widget || (!_.isEmpty(change_spec) && change_spec !== "0")) { + var ids = [], + trigger_field_name = widget ? widget.name : self._onchange_fields, + values = self._get_onchange_values(), + context = new data.CompoundContext(self.dataset.get_context()); + + if (widget && widget.build_context()) { + context.add(widget.build_context()); + } + if (self.dataset.parent_view) { + var parent_name = self.dataset.parent_view.get_field_desc(self.dataset.child_name).relation_field; + context.add({field_parent: parent_name}); + } + + if (self.datarecord.id && !data.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 Model(self.dataset.model).call( + "onchange", [ids, values, trigger_field_name, onchange_specs, context])); + } + this.onchanges_mutex.exec(function(){ + return def.then(function(response) { + var fields = {}; + if (widget){ + fields[widget.name] = widget.field; + } + else{ + fields = self.fields_view.fields; + } + var defs = []; + _.each(fields, function(field, fieldname){ + if (field && field.change_default) { + var value_; + if (response.value && (fieldname in response.value)) { + // Use value from onchange if onchange executed + value_ = response.value[fieldname]; + } else { + // otherwise get form value for field + value_ = self.fields[fieldname].get_value(); + } + var condition = fieldname + '=' + value_; + + if (value_) { + defs.push(self.alive(new Model('ir.values').call( + 'get_defaults', [self.model, condition] + )).then(function (results) { + if (!results.length) { + return response; + } + if (!response.value) { + response.value = {}; + } + for(var i=0; i<results.length; ++i) { + // [whatever, key, value] + var triplet = results[i]; + response.value[triplet[1]] = triplet[2]; + } + return response; + })); + } + } + }); + return _.isEmpty(defs) ? response : $.when.apply(null, defs); + }).then(function(response) { + return self.on_processed_onchange(response); + }); + }); + return this.onchanges_mutex.def; + } catch(e) { + console.error(e); + crash_manager.show_message(e); + return $.Deferred().reject(); + } + }, + on_processed_onchange: function(result) { + try { + var fields = this.fields; + _(result.domain).each(function (domain, fieldname) { + var field = fields[fieldname]; + if (!field) { return; } + field.node.attrs.domain = domain; + }); + + if (!_.isEmpty(result.value)) { + this._internal_set_values(result.value); + } + // FIXME XXX a list of warnings? + if (!_.isEmpty(result.warning)) { + new Dialog(this, { + size: 'medium', + title:result.warning.title, + buttons: [ + {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }} + ] + }, QWeb.render("CrashManager.warning", result.warning)).open(); + } + + return $.Deferred().resolve(); + } catch(e) { + console.error(e); + crash_manager.show_message(e); + return $.Deferred().reject(); + } + }, + _process_operations: function() { + var self = this; + return this.mutating_mutex.exec(function() { + function iterate() { + + var mutex = new utils.Mutex(); + _.each(self.fields, function(field) { + self.onchanges_mutex.def.then(function(){ + mutex.exec(function(){ + return field.commit_value(); + }); + }); + }); + + var args = _.toArray(arguments); + return $.when.apply(null, [mutex.def, self.onchanges_mutex.def]).then(function() { + var save_obj = self.save_list.pop(); + if (save_obj) { + return self._process_save(save_obj).then(function() { + save_obj.ret = _.toArray(arguments); + return iterate(); + }, function() { + save_obj.error = true; + }); + } + return $.when(); + }).fail(function() { + self.save_list.pop(); + return $.when(); + }); + } + return iterate(); + }); + }, + _internal_set_values: function(values) { + for (var f in values) { + if (!values.hasOwnProperty(f)) { continue; } + var field = this.fields[f]; + // If field is not defined in the view, just ignore it + if (field) { + var value_ = values[f]; + if (field.get_value() != value_) { + field._inhibit_on_change_flag = true; + field.set_value(value_); + field._inhibit_on_change_flag = false; + field._dirty_flag = true; + } + } + } + this.on_form_changed(); + }, + set_values: function(values) { + var self = this; + return this.mutating_mutex.exec(function() { + self._internal_set_values(values); + }); + }, + /** + * Ask the view to switch to view mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_view_mode: function() { + this._actualize_mode("view"); + }, + /** + * Ask the view to switch to edit mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_edit_mode: function() { + this.onchanges_mutex = new utils.Mutex(); + this._actualize_mode("edit"); + }, + /** + * Ask the view to switch to a precise mode if possible. The view is free to + * not respect this command if the state of the dataset is not compatible with + * the new mode. For example, it is not possible to switch to edit mode if + * the current record is not yet saved in database. + * + * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If + * undefined the view will test the actual mode to check if it is still consistent + * with the dataset state. + */ + _actualize_mode: function(switch_to) { + var mode = switch_to || this.get("actual_mode"); + if (! this.datarecord.id) { + mode = "create"; + } else if (mode === "create") { + mode = "edit"; + } + this.render_value_defs = []; + this.set({actual_mode: mode}); + }, + check_actual_mode: function(source, options) { + var self = this; + if(this.get("actual_mode") === "view") { + self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly'); + self.$buttons.find('.oe_form_buttons_edit').hide(); + self.$buttons.find('.oe_form_buttons_view').show(); + self.$sidebar.show(); + } else { + self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable'); + self.$buttons.find('.oe_form_buttons_edit').show(); + self.$buttons.find('.oe_form_buttons_view').hide(); + self.$sidebar.hide(); + this.autofocus(); + } + }, + autofocus: function() { + if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) { + var fields_order = this.fields_order.slice(0); + if (this.default_focus_field) { + fields_order.unshift(this.default_focus_field.name); + } + for (var i = 0; i < fields_order.length; i += 1) { + var field = this.fields[fields_order[i]]; + if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.$label) { + if (field.focus() !== false) { + break; + } + } + } + } + }, + on_button_save: function(e) { + var self = this; + $(e.target).attr("disabled", true); + return this.save().done(function(result) { + self.trigger("save", result); + self.reload().then(function() { + self.to_view_mode(); + core.bus.trigger('do_reload_needaction'); + core.bus.trigger('form_view_saved', self); + }); + }).always(function(){ + $(e.target).attr("disabled", false); + }); + }, + on_button_cancel: function(event) { + var self = this; + if (this.can_be_discarded()) { + if (this.get('actual_mode') === 'create') { + this.trigger('history_back'); + } else { + this.to_view_mode(); + $.when.apply(null, this.render_value_defs).then(function(){ + self.trigger('load_record', self.datarecord); + }); + } + } + this.trigger('on_button_cancel'); + return false; + }, + on_button_new: function() { + var self = this; + this.to_edit_mode(); + return $.when(this.has_been_loaded).then(function() { + if (self.can_be_discarded()) { + return self.load_defaults(); + } + }); + }, + on_button_edit: function() { + return this.to_edit_mode(); + }, + on_button_create: function() { + this.dataset.index = null; + this.do_show(); + }, + on_button_duplicate: function() { + var self = this; + return this.has_been_loaded.then(function() { + return self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { + self.record_created(new_id); + self.to_edit_mode(); + }); + }); + }, + on_button_delete: function() { + var self = this; + var def = $.Deferred(); + this.has_been_loaded.done(function() { + if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { + self.dataset.unlink([self.datarecord.id]).done(function() { + if (self.dataset.size()) { + self.execute_pager_action('next'); + } else { + self.do_action('history_back'); + } + def.resolve(); + }); + } else { + $.async_when().done(function () { + def.reject(); + }); + } + }); + return def.promise(); + }, + can_be_discarded: function() { + if (this.$el.is('.oe_form_dirty')) { + if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) { + return false; + } + this.$el.removeClass('oe_form_dirty'); + } + return true; + }, + /** + * Triggers saving the form's record. Chooses between creating a new + * record or saving an existing one depending on whether the record + * already has an id property. + * + * @param {Boolean} [prepend_on_create=false] if ``save`` creates a new + * record, should that record be inserted at the start of the dataset (by + * default, records are added at the end) + */ + save: function(prepend_on_create) { + var self = this; + var save_obj = {prepend_on_create: prepend_on_create, ret: null}; + this.save_list.push(save_obj); + return self._process_operations().then(function() { + if (save_obj.error) + return $.Deferred().reject(); + return $.when.apply($, save_obj.ret); + }).done(function(result) { + self.$el.removeClass('oe_form_dirty'); + }); + }, + _process_save: function(save_obj) { + var self = this; + var prepend_on_create = save_obj.prepend_on_create; + try { + var form_invalid = false, + values = {}, + first_invalid_field = null, + readonly_values = {}; + for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } + f = self.fields[f]; + if (!f.is_valid()) { + form_invalid = true; + if (!first_invalid_field) { + first_invalid_field = f; + } + } 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 + if (!f.get("readonly")) { + values[f.name] = f.get_value(true); + } else { + readonly_values[f.name] = f.get_value(true); + } + } + } + if (form_invalid) { + self.set({'display_invalid_fields': true}); + first_invalid_field.focus(); + self.on_invalid(); + return $.Deferred().reject(); + } else { + self.set({'display_invalid_fields': false}); + var save_deferral; + if (!self.datarecord.id) { + // Creation save + 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)) { + // Not dirty, noop save + save_deferral = $.Deferred().resolve({}).promise(); + } else { + // Write save + save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { + return self.record_saved(r); + }, null); + } + return save_deferral; + } + } catch (e) { + console.error(e); + return $.Deferred().reject(); + } + }, + on_invalid: function() { + var warnings = _(this.fields).chain() + .filter(function (f) { return !f.is_valid(); }) + .map(function (f) { + return _.str.sprintf('<li>%s</li>', + _.escape(f.string)); + }).value(); + warnings.unshift('<ul>'); + warnings.push('</ul>'); + this.do_warn(_t("The following fields are invalid:"), warnings.join('')); + }, + /** + * Reload the form after saving + * + * @param {Object} r result of the write function. + */ + record_saved: function(r) { + this.trigger('record_saved', r); + if (!r) { + // should not happen in the server, but may happen for internal purpose + return $.Deferred().reject(); + } + return r; + }, + /** + * Updates the form' dataset to contain the new record: + * + * * Adds the newly created record to the current dataset (at the end by + * default) + * * Selects that record (sets the dataset's index to point to the new + * record's id). + * * Updates the pager and sidebar displays + * + * @param {Object} r + * @param {Boolean} [prepend_on_create=false] adds the newly created record + * at the beginning of the dataset instead of the end + */ + record_created: function(r, prepend_on_create) { + var self = this; + if (!r) { + // should not happen in the server, but may happen for internal purpose + this.trigger('record_created', r); + return $.Deferred().reject(); + } else { + this.datarecord.id = r; + if (!prepend_on_create) { + this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); + this.dataset.index = this.dataset.ids.length - 1; + } else { + this.dataset.alter_ids([this.datarecord.id].concat(this.dataset.ids)); + this.dataset.index = 0; + } + this.do_update_pager(); + if (this.sidebar) { + this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); + } + //openerp.log("The record has been created with id #" + this.datarecord.id); + return $.when(this.reload()).then(function () { + self.trigger('record_created', r); + return _.extend(r, {created: true}); + }); + } + }, + on_action: function (action) { + console.debug('Executing action', action); + }, + reload: function() { + var self = this; + return this.reload_mutex.exec(function() { + if (self.dataset.index === null || self.dataset.index === undefined) { + self.trigger("previous_view"); + return $.Deferred().reject().promise(); + } + if (self.dataset.index < 0) { + return $.when(self.on_button_new()); + } else { + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, + { + context: { + 'bin_size': true, + 'future_display_name': true + }, + check_access_rule: true + }).then(function(r) { + self.trigger('load_record', r); + }).fail(function (){ + self.do_action('history_back'); + }); + } + }); + }, + get_widgets: function() { + return _.filter(this.getChildren(), function(obj) { + return obj instanceof common.FormWidget; + }); + }, + get_fields_values: function() { + var values = {}; + var ids = this.get_selected_ids(); + values["id"] = ids.length > 0 ? ids[0] : false; + _.each(this.fields, function(value_, key) { + values[key] = value_.get_value(); + }); + return values; + }, + get_selected_ids: function() { + var id = this.dataset.ids[this.dataset.index]; + return id ? [id] : []; + }, + recursive_save: function() { + var self = this; + return $.when(this.save()).then(function(res) { + if (self.dataset.parent_view) + return self.dataset.parent_view.recursive_save(); + }); + }, + recursive_reload: function() { + var self = this; + var pre = $.when(); + if (self.dataset.parent_view) + pre = self.dataset.parent_view.recursive_reload(); + return pre.then(function() { + return self.reload(); + }); + }, + is_dirty: function() { + return _.any(this.fields, function (value_) { + return value_._dirty_flag; + }); + }, + is_interactible_record: function() { + var id = this.datarecord.id; + if (!id) { + if (this.options.not_interactible_on_create) + return false; + } else if (typeof(id) === "string") { + if(data.BufferedDataSet.virtual_id_regex.test(id)) + return false; + } + return true; + }, + sidebar_eval_context: function () { + return $.when(this.build_eval_context()); + }, + open_defaults_dialog: function () { + var self = this; + var display = function (field, value) { + var FieldSelection = core.form_widget_registry.get('selection'), + FieldMany2One = core.form_widget_registry.get('many2one'); + + if (!value) { return value; } + if (field instanceof FieldSelection) { + return _(field.get('values')).find(function (option) { + return option[0] === value; + })[1]; + } else if (field instanceof FieldMany2One) { + return field.get_displayed(); + } + return value; + }; + var fields = _.chain(this.fields) + .map(function (field) { + var value = field.get_value(); + // ignore fields which are empty, invisible, readonly, o2m + // or m2m + if (!value + || field.get('invisible') + || field.get("readonly") + || field.field.type === 'one2many' + || field.field.type === 'many2many' + || field.field.type === 'binary' + || field.password) { + return false; + } + + return { + name: field.name, + string: field.string, + value: value, + displayed: display(field, value), + }; + }) + .compact() + .sortBy(function (field) { return field.string; }) + .value(); + var conditions = _.chain(self.fields) + .filter(function (field) { return field.field.change_default; }) + .map(function (field) { + var value = field.get_value(); + return { + name: field.name, + string: field.string, + value: value, + displayed: display(field, value), + }; + }) + .value(); + var d = new Dialog(this, { + title: _t("Set Default"), + args: { + fields: fields, + conditions: conditions + }, + buttons: [ + {text: _t("Close"), click: function () { d.close(); }}, + {text: _t("Save default"), click: function () { + var $defaults = d.$el.find('#formview_default_fields'); + var field_to_set = $defaults.val(); + if (!field_to_set) { + $defaults.parent().addClass('oe_form_invalid'); + return; + } + var condition = d.$el.find('#formview_default_conditions').val(), + all_users = d.$el.find('#formview_default_all').is(':checked'); + new data.DataSet(self, 'ir.values').call( + 'set_default', [ + self.dataset.model, + field_to_set, + self.fields[field_to_set].get_value(), + all_users, + true, + condition || false + ]).done(function () { d.close(); }); + }} + ] + }); + d.template = 'FormView.set_default'; + d.open(); + }, + register_field: function(field, name) { + this.fields[name] = field; + this.fields_order.push(name); + if (JSON.parse(field.node.attrs.default_focus || "0")) { + this.default_focus_field = field; + } + + field.on('focused', null, this.proxy('widgetFocused')) + .on('blurred', null, this.proxy('widgetBlurred')); + if (this.get_field_desc(name).translate) { + this.translatable_fields.push(field); + } + field.on('changed_value', this, function() { + if (field.is_syntax_valid()) { + this.trigger('field_changed:' + name); + } + if (field._inhibit_on_change_flag) { + return; + } + field._dirty_flag = true; + if (field.is_syntax_valid()) { + this.do_onchange(field); + this.on_form_changed(true); + this.do_notify_change(); + } + }); + }, + get_field_desc: function(field_name) { + return this.fields_view.fields[field_name]; + }, + get_field_value: function(field_name) { + return this.fields[field_name].get_value(); + }, + compute_domain: function(expression) { + return data.compute_domain(expression, this.fields); + }, + _build_view_fields_values: function() { + var a_dataset = this.dataset; + var fields_values = this.get_fields_values(); + var active_id = a_dataset.ids[a_dataset.index]; + _.extend(fields_values, { + active_id: active_id || false, + active_ids: active_id ? [active_id] : [], + active_model: a_dataset.model, + parent: {} + }); + if (a_dataset.parent_view) { + fields_values.parent = a_dataset.parent_view.get_fields_values(); + } + return fields_values; + }, + build_eval_context: function() { + var a_dataset = this.dataset; + return new data.CompoundContext(a_dataset.get_context(), this._build_view_fields_values()); + }, +}); + +/** + * Interface to be implemented by rendering engines for the form view. + */ +var FormRenderingEngineInterface = Class.extend({ + set_fields_view: function(fields_view) {}, + set_fields_registry: function(fields_registry) {}, + render_to: function($el) {}, +}); + +/** + * Default rendering engine for the form view. + * + * It is necessary to set the view using set_view() before usage. + */ +var FormRenderingEngine = FormRenderingEngineInterface.extend({ + init: function(view) { + this.view = view; + }, + set_fields_view: function(fvg) { + this.fvg = fvg; + this.version = parseFloat(this.fvg.arch.attrs.version); + if (isNaN(this.version)) { + this.version = 7.0; + } + }, + set_tags_registry: function(tags_registry) { + this.tags_registry = tags_registry; + }, + set_fields_registry: function(fields_registry) { + this.fields_registry = fields_registry; + }, + set_widgets_registry: function(widgets_registry) { + this.widgets_registry = widgets_registry; + }, + // Backward compatibility tools, current default version: v7 + process_version: function() { + if (this.version < 7.0) { + this.$form.find('form:first').wrapInner('<group col="4"/>'); + this.$form.find('page').each(function() { + if (!$(this).parents('field').length) { + $(this).wrapInner('<group col="4"/>'); + } + }); + } + }, + get_arch_fragment: function() { + var doc = $.parseXML(utils.json_node_to_xml(this.fvg.arch)).documentElement; + // IE won't allow custom button@type and will revert it to spec default : 'submit' + $('button', doc).each(function() { + $(this).attr('data-button-type', $(this).attr('type')).attr('type', 'button'); + }); + // IE's html parser is also a css parser. How convenient... + $('board', doc).each(function() { + $(this).attr('layout', $(this).attr('style')); + }); + return $('<div class="oe_form"/>').append(utils.xml_to_str(doc)); + }, + render_to: function($target) { + var self = this; + this.$target = $target; + + this.$form = this.get_arch_fragment(); + + this.process_version(); + + this.fields_to_init = []; + this.tags_to_init = []; + this.widgets_to_init = []; + this.labels = {}; + this.process(this.$form); + + this.$form.appendTo(this.$target); + + this.to_replace = []; + + _.each(this.fields_to_init, function($elem) { + var name = $elem.attr("name"); + if (!self.fvg.fields[name]) { + throw new Error(_.str.sprintf(_t("Field '%s' specified in view could not be found."), name)); + } + var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); + if (!obj) { + throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget'))); + } + var w = new (obj)(self.view, utils.xml_to_json($elem[0])); + var $label = self.labels[$elem.attr("name")]; + if ($label) { + w.set_input_id($label.attr("for")); + } + self.alter_field(w); + self.view.register_field(w, $elem.attr("name")); + self.to_replace.push([w, $elem]); + }); + _.each(this.tags_to_init, function($elem) { + var tag_name = $elem[0].tagName.toLowerCase(); + var obj = self.tags_registry.get(tag_name); + var w = new (obj)(self.view, utils.xml_to_json($elem[0])); + self.to_replace.push([w, $elem]); + }); + _.each(this.widgets_to_init, function($elem) { + var widget_type = $elem.attr("type"); + var obj = self.widgets_registry.get(widget_type); + var w = new (obj)(self.view, utils.xml_to_json($elem[0])); + self.to_replace.push([w, $elem]); + }); + }, + init_fields: function() { + var defs = []; + _.each(this.to_replace, function(el) { + defs.push(el[0].replace(el[1])); + if (el[1].children().length) { + el[0].$el.append(el[1].children()); + } + }); + this.to_replace = []; + return $.when.apply($, defs); + }, + render_element: function(template /* dictionaries */) { + var dicts = [].slice.call(arguments).slice(1); + var dict = _.extend.apply(_, dicts); + dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb + return $(QWeb.render(template, dict)); + }, + alter_field: function(field) { + }, + toggle_layout_debugging: function() { + if (!this.$target.has('.oe_layout_debug_cell:first').length) { + this.$target.find('[title]').removeAttr('title'); + this.$target.find('.oe_form_group_cell').each(function() { + var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'); + $(this).attr('title', text); + }); + } + this.$target.toggleClass('oe_layout_debugging'); + }, + process: function($tag) { + var self = this; + var tagname = $tag[0].nodeName.toLowerCase(); + if (this.tags_registry.contains(tagname)) { + this.tags_to_init.push($tag); + return (tagname === 'button') ? this.process_button($tag) : $tag; + } + var fn = self['process_' + tagname]; + if (fn) { + var args = [].slice.call(arguments); + args[0] = $tag; + return fn.apply(self, args); + } else { + // generic tag handling, just process children + $tag.children().each(function() { + self.process($(this)); + }); + self.handle_common_properties($tag, $tag); + $tag.removeAttr("modifiers"); + return $tag; + } + }, + process_button: function ($button) { + var self = this; + $button.children().each(function() { + self.process($(this)); + }); + return $button; + }, + process_widget: function($widget) { + this.widgets_to_init.push($widget); + return $widget; + }, + process_sheet: function($sheet) { + var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); + this.handle_common_properties($new_sheet, $sheet); + var $dst = $new_sheet.find('.oe_form_sheet'); + $sheet.contents().appendTo($dst); + $sheet.before($new_sheet).remove(); + this.process($new_sheet); + }, + process_form: function($form) { + if ($form.find('> sheet').length === 0) { + $form.addClass('oe_form_nosheet'); + } + var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); + this.handle_common_properties($new_form, $form); + $form.contents().appendTo($new_form); + if ($form[0] === this.$form[0]) { + // If root element, replace it + this.$form = $new_form; + } else { + $form.before($new_form).remove(); + } + this.process($new_form); + }, + /* + * Used by direct <field> children of a <group> tag only + * This method will add the implicit <label...> for every field + * in the <group> + */ + preprocess_field: function($field) { + var self = this; + var name = $field.attr('name'), + field_colspan = parseInt($field.attr('colspan'), 10), + field_modifiers = JSON.parse($field.attr('modifiers') || '{}'); + + if ($field.attr('nolabel') === '1') + return; + $field.attr('nolabel', '1'); + var found = false; + this.$form.find('label[for="' + name + '"]').each(function(i ,el) { + $(el).parents().each(function(unused, tag) { + var name = tag.tagName.toLowerCase(); + if (name === "field" || name in self.tags_registry.map) + found = true; + }); + }); + if (found) + return; + + var $label = $('<label/>').attr({ + 'for' : name, + "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), + "string": $field.attr('string'), + "help": $field.attr('help'), + "class": $field.attr('class'), + }); + $label.insertBefore($field); + if (field_colspan > 1) { + $field.attr('colspan', field_colspan - 1); + } + return $label; + }, + process_field: function($field) { + if ($field.parent().is('group')) { + // No implicit labels for normal fields, only for <group> direct children + var $label = this.preprocess_field($field); + if ($label) { + this.process($label); + } + } + this.fields_to_init.push($field); + return $field; + }, + process_group: function($group) { + var self = this; + $group.children('field').each(function() { + self.preprocess_field($(this)); + }); + var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes()); + var $table; + if ($new_group.first().is('table.oe_form_group')) { + $table = $new_group; + } else if ($new_group.filter('table.oe_form_group').length) { + $table = $new_group.filter('table.oe_form_group').first(); + } else { + $table = $new_group.find('table.oe_form_group').first(); + } + + var $tr, $td, + cols = parseInt($group.attr('col') || 2, 10), + row_cols = cols; + + var children = []; + $group.children().each(function() { + var $child = $(this); + var colspan = parseInt($child.attr('colspan') || 1, 10); + var tagName = $child[0].tagName.toLowerCase(); + var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); + var newline = tagName === 'newline'; + + // Note FME: those classes are used in layout debug mode + if ($tr && row_cols > 0 && (newline || row_cols < colspan)) { + $tr.addClass('oe_form_group_row_incomplete'); + if (newline) { + $tr.addClass('oe_form_group_row_newline'); + } + } + if (newline) { + $tr = null; + return; + } + if (!$tr || row_cols < colspan) { + $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); + row_cols = cols; + } else if (tagName==='group') { + // When <group> <group/><group/> </group>, we need a spacing between the two groups + $td.addClass('oe_group_right'); + } + row_cols -= colspan; + + // invisibility transfer + var field_modifiers = JSON.parse($child.attr('modifiers') || '{}'); + var invisible = field_modifiers.invisible; + self.handle_common_properties($td, $("<dummy>").attr("modifiers", JSON.stringify({invisible: invisible}))); + + $tr.append($td.append($child)); + children.push($child[0]); + }); + if (row_cols && $td) { + $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols); + } + $group.before($new_group).remove(); + + $table.find('> tbody > tr').each(function() { + var to_compute = [], + row_cols = cols, + total = 100; + $(this).children().each(function() { + var $td = $(this), + $child = $td.children(':first'); + if ($child.attr('cell-class')) { + $td.addClass($child.attr('cell-class')); + } + switch ($child[0].tagName.toLowerCase()) { + case 'separator': + break; + case 'label': + if ($child.attr('for')) { + $td.attr('width', '1%').addClass('oe_form_group_cell_label'); + row_cols-= $td.attr('colspan') || 1; + total--; + } + break; + default: + var width = _.str.trim($child.attr('width') || ''), + iwidth = parseInt(width, 10); + if (iwidth) { + if (width.substr(-1) === '%') { + total -= iwidth; + width = iwidth + '%'; + } else { + // Absolute width + $td.css('min-width', width + 'px'); + } + $td.attr('width', width); + $child.removeAttr('width'); + row_cols-= $td.attr('colspan') || 1; + } else { + to_compute.push($td); + } + + } + }); + if (row_cols) { + var unit = Math.floor(total / row_cols); + if (!$(this).is('.oe_form_group_row_incomplete')) { + _.each(to_compute, function($td) { + var width = parseInt($td.attr('colspan'), 10) * unit; + $td.attr('width', width + '%'); + total -= width; + }); + } + } + }); + _.each(children, function(el) { + self.process($(el)); + }); + this.handle_common_properties($new_group, $group); + return $new_group; + }, + process_notebook: function($notebook) { + var self = this; + + // Extract useful info from the notebook xml declaration + var pages = []; + $notebook.find('> page').each(function() { + var $page = $(this); + var page_attrs = $page.getAttributes(); + page_attrs.id = _.uniqueId('notebook_page_'); + page_attrs.contents = $page.html(); + page_attrs.ref = $page; // reference to the current page node in the xml declaration + pages.push(page_attrs); + }); + + // Render the notebook and replace $notebook with it + var $new_notebook = self.render_element('FormRenderingNotebook', {'pages': pages}); + $notebook.before($new_notebook).remove(); + + // Bind the invisibility changers and find the page to display + var pageid_to_display; + _.each(pages, function(page) { + var $tab = $new_notebook.find('a[href=#' + page.id + ']').parent(); + var $content = $new_notebook.find('#' + page.id); + + // Case: <page autofocus="autofocus">; + // We attach the autofocus attribute to the node because it can be useful during the + // page execution + self.attach_node_attr($tab, page.ref, 'autofocus'); + self.attach_node_attr($content, page.ref, 'autofocus'); + if (!pageid_to_display && page.autofocus) { + // If multiple autofocus, keep the first one + pageid_to_display = page.id; + } + + // Case: <page attrs="{'invisible': domain}">; + self.handle_common_properties($tab, page.ref, common.NotebookInvisibilityChanger); + self.handle_common_properties($content, page.ref, common.NotebookInvisibilityChanger); + }); + if (!pageid_to_display) { + pageid_to_display = $new_notebook.find('div[role="tabpanel"]:not(.oe_form_invisible):first').attr('id'); + } + + // Display page. Note: we can't use bootstrap's show function because it is looking for + // document attached DOM, and the form view is only attached when everything is processed + $new_notebook.find('a[href=#' + pageid_to_display + ']').parent().addClass('active'); + $new_notebook.find('#' + pageid_to_display).addClass('active'); + + self.process($new_notebook.children()); + self.handle_common_properties($new_notebook, $notebook); + + return $new_notebook; + }, + process_separator: function($separator) { + var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes()); + $separator.before($new_separator).remove(); + this.handle_common_properties($new_separator, $separator); + return $new_separator; + }, + process_label: function($label) { + var name = $label.attr("for"), + field_orm = this.fvg.fields[name]; + var dict = { + string: $label.attr('string') || (field_orm || {}).string || '', + help: $label.attr('help') || (field_orm || {}).help || '', + _for: name ? _.uniqueId('oe-field-input-') : undefined, + }; + var align = parseFloat(dict.align); + if (isNaN(align) || align === 1) { + align = 'right'; + } else if (align === 0) { + align = 'left'; + } else { + align = 'center'; + } + dict.align = align; + var $new_label = this.render_element('FormRenderingLabel', dict); + $label.before($new_label).remove(); + this.handle_common_properties($new_label, $label); + if (name) { + this.labels[name] = $new_label; + } + return $new_label; + }, + handle_common_properties: function($new_element, $node, InvisibilityChanger) { + var str_modifiers = $node.attr("modifiers") || "{}"; + var modifiers = JSON.parse(str_modifiers); + var ic = null; + if (modifiers.invisible !== undefined) { + var InvisibilityChangerCls = InvisibilityChanger || common.InvisibilityChanger; + ic = new InvisibilityChangerCls(this.view, this.view, modifiers.invisible, $new_element); + } + $new_element.addClass($node.attr("class") || ""); + $new_element.attr('style', $node.attr('style')); + return {invisibility_changer: ic,}; + }, + /** + * Attach a node attribute to an html element to be able to use it after + * the processing of the page. + */ + attach_node_attr: function($new_element, $node, attr) { + $new_element.data(attr, $node.attr(attr)); + } +}); + +core.view_registry.add('form', FormView); + +return FormView; + +}); + |
