[merge]
[odoo/odoo.git] / addons / web / static / src / js / view_form.js
index 32660fc..e517e68 100644 (file)
@@ -11,7 +11,6 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
      * view should be displayed (if there is one active).
      */
     searchable: false,
-    readonly : false,
     template: "FormView",
     display_name: _lt('Form'),
     /**
@@ -51,6 +50,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
         this.on_change_mutex = new $.Mutex();
         this.reload_mutex = new $.Mutex();
         this.set({"force_readonly": false});
+        this.rendering_engine = new openerp.web.FormRenderingEngine(this);
     },
     start: function() {
         this._super();
@@ -98,8 +98,8 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
         this.fields_order = [];
         this.fields_view = data;
 
-        var form = new openerp.web.FormRenderingEngine(this, data);
-        form.appendTo(this.$element.find('.oe_form_content'));
+        this.rendering_engine.set_fields_view(data);
+        this.rendering_engine.render_to(this.$element.find('.oe_form_container'));
 
         this.$form_header = this.$element.find('.oe_form_header:first');
         this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() {
@@ -289,7 +289,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
             }
             // form field
             if (self.fields[field]) {
-                var value = self.fields[field].get_on_change_value();
+                var value = self.fields[field].get_value();
                 return value == null ? false : value;
             }
             // parent field
@@ -359,7 +359,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
                         value = response.value[fieldname];
                     } else {
                         // otherwise get form value for field
-                        value = self.fields[fieldname].get_on_change_value();
+                        value = self.fields[fieldname].get_value();
                     }
                     var condition = fieldname + '=' + value;
 
@@ -496,7 +496,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
                     if (!first_invalid_field) {
                         first_invalid_field = f;
                     }
-                } else if (f.name !== 'id' && !f.readonly && (!self.datarecord.id || f.is_dirty())) {
+                } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f.is_dirty())) {
                     // Special case 'id' field, do not save this field
                     // on 'create' : save all non readonly fields
                     // on 'edit' : save non readonly modified fields
@@ -657,7 +657,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
                 // or m2m
                 if (!value
                         || field.invisible
-                        || field.readonly
+                        || field.get("readonly")
                         || field.field.type === 'one2many'
                         || field.field.type === 'many2many') {
                     return false;
@@ -726,37 +726,45 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView#
     }
 });
 
-openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
-    init: function(parent, fvg, registry) {
-        var self = this;
-        this._super.apply(this, arguments);
+/**
+ * Interface to be implemented by rendering engines for the form view.
+ */
+openerp.web.FormRenderingEngineInterface = {
+    set_fields_view: function(fields_view) {},
+    render_to: function($element) {},
+};
+
+/**
+ * Default rendering engine for the form view.
+ * 
+ * It is necessary to set the view using set_view() before usage.
+ */
+openerp.web.FormRenderingEngine = openerp.web.Class.extend({
+    init: function(view) {
+        this.view = view;
+        this.legacy_mode = false;
+    },
+    set_fields_view: function(fvg) {
         this.fvg = fvg;
-        this.view = parent;
-        this.fields_prefix = this.view.dataset ? this.view.dataset.model : '';
+        this.legacy_mode = (this.fvg.arch.tag === 'form');
+    },
+    set_registry: function(registry) {
+        this.registry = registry;
+    },
+    render_to: function($element) {
+        var self = this;
+        this.$element = $element;
 
         // TODO: I know this will save the world and all the kitten for a moment,
         //       but one day, we will have to get rid of xml2json
-        var xml = openerp.web.json_node_to_xml(fvg.arch),
-            $form = this.$form = $(xml);
+        var xml = openerp.web.json_node_to_xml(this.fvg.arch);
+        this.$form = $(xml);
 
-        // TODO: extract embeded views before preprocessing
-        _.each(['field', 'group', 'notebook', 'separator', 'label'], function(tag) {
-            var fn = self['process_' + tag];
-            if (registry && registry.contains(tag)) {
-                fn = registry.get_object(tag);
-            }
-            $form.find(tag).each(function() {
-                fn.call(self, $(this), $form);
-            });
-        });
-    },
-    start: function() {
-        var self = this;
-        this._super.apply(this, arguments);
-        this.$form.children().appendTo(this.$element);
+        this.process(this.$form);
+
+        this.$form.appendTo(this.$element);
         // OpenERP views spec :
         //      - @width is obsolete ?
-        // TODO: modifiers invisible. Add a special attribute, eg: data-invisible  that should be used in order to create openerp.form.InvisibleWidgetG
 
         this.$element.find('field, button').each(function() {
             var $elem = $(this),
@@ -764,45 +772,112 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
             if (self.view.registry.contains(key)) {
                 var obj = self.view.registry.get_object(key);
                 var w = new (obj)(self.view, openerp.web.xml_to_json($elem[0]));
+                self.alter_field(w);
                 w.replace($elem);
             }
         });
+        $('<button>Debug layout</button>').appendTo(this.$element).click($.proxy(this.toggle_layout_debugging, this));
+    },
+    render_element: function(template, dict) {
+        dict = dict || {};
+        dict.legacy_mode = this.legacy_mode;
+        return $(QWeb.render(template, dict));
+    },
+    alter_field: function(field) {},
+    toggle_layout_debugging: function() {
+        if (!this.$element.has('.oe_layout_debug_cell:first').length) {
+            this.$element.find('.oe_form_group_cell').each(function() {
+                var text = 'W:' + ($(this).attr('width') || '') + ' - C:' + $(this).attr('colspan'),
+                    $span = $('<span class="oe_layout_debug_cell"/>').text(text);
+                $span.prependTo($(this));
+            });
+        }
+        this.$element.toggleClass('oe_layout_debugging');
+
+    },
+    process: function($tag) {
+        var self = this;
+        var tagname = $tag[0].nodeName.toLowerCase();
+        var fn = self['process_' + tagname]; 
+        if (this.registry && this.registry.contains(tagname)) {
+            fn = this.registry.get_object(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));
+            });
+            return $tag;
+        }
     },
-    process_field: function($field, $form) {
+    process_form: function($form) {
+        var $new_form = this.render_element('FormRenderingForm', $form.getAttributes());
+            $dst = this.legacy_mode ? $new_form.find('group:first') : $new_form;
+        $form.children().appendTo($dst);
+        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);
+    },
+    preprocess_field: function($field) {
         var name = $field.attr('name'),
             field_orm = this.fvg.fields[name],
             field_string = $field.attr('string') || field_orm.string || '',
             field_help = $field.attr('help') || field_orm.help || '',
             field_colspan = parseInt($field.attr('colspan'), 10);
-
-        if (!field_orm) {
-            throw new Error("Field '" + name + "' specified in view could not be found.");
-        }
-
         if ($field.attr('nolabel') !== '1') {
-            var $label = $form.find('label[for="' + name + '"]');
+            $field.attr('nolabel', '1');
+            var $label = this.$form.find('label[for="' + name + '"]');
             if (!$label.length) {
                 field_string = $label.attr('string') || $label.text() || field_string;
                 field_help = $label.attr('help') || field_help;
-                $('<label/>').attr({
+                $label = $('<label/>').attr({
                     'for' : name,
                     'string' : field_string,
                     'help' :  field_help
-                }).insertBefore($field).text();
+                });
+                $label.insertBefore($field);
                 if (field_colspan > 1) {
                     $field.attr('colspan', field_colspan - 1);
                 }
             }
+            return $label;
         }
+    },
+    process_field: function($field) {
+        var $label = this.preprocess_field($field);
+        if ($label)
+            this.process($label);
+        var name = $field.attr('name'),
+            field_orm = this.fvg.fields[name],
+            field_string = $field.attr('string') || field_orm.string || '',
+            field_help = $field.attr('help') || field_orm.help || '',
+            field_colspan = parseInt($field.attr('colspan'), 10);
+
+        if (!field_orm) {
+            throw new Error("Field '" + name + "' specified in view could not be found.");
+        }
+
         $field.attr({
             'widget' : $field.attr('widget') || field_orm.type,
             'string' : field_string,
             'help' : field_help
         });
+        return $field;
     },
-    process_group: function($group, $form) {
-        var self = this,
-            $new_group = $(QWeb.render('FormRenderingGroup', $group.getAttributes())),
+    process_group: function($group) {
+        var self = this;
+        $group.children('field').each(function() {
+            self.preprocess_field($(this));
+        });
+        var $new_group = $(QWeb.render('FormRenderingGroup', $group.getAttributes())),
             $table;
         if ($new_group.is('table')) {
             $table = $new_group;
@@ -810,11 +885,12 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
             $table = $new_group.find('table:first');
         }
         $table.addClass('oe_form_group');
-        var $tr,
+        var $tr, $td,
             cols = parseInt($group.attr('col') || 4, 10),
             row_cols = cols;
 
-        $group.children().each(function() {
+        var children = [];
+        $group.children().each(function(a,b,c) {
             var $child = $(this),
                 colspan = parseInt($child.attr('colspan') || 1, 10),
                 tagName = $child[0].tagName.toLowerCase();
@@ -827,14 +903,19 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
                 row_cols = cols;
             }
             row_cols -= colspan;
-            var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
+            $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan);
             $tr.append($td.append($child));
+            children.push($child[0]);
         });
+        if (row_cols) {
+            $td.attr('colspan', parseInt($td.attr('colspan'), 10) + row_cols);
+        }
         $group.before($new_group).remove();
 
         // Now compute width of cells
         $table.find('tbody > tr').each(function() {
             var to_compute = [],
+                row_cols = cols,
                 total = 100;
             $(this).children().each(function() {
                 var $td = $(this),
@@ -844,13 +925,13 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
                         if ($child.attr('orientation') === 'vertical') {
                             $td.addClass('oe_vertical_separator').attr('width', '1');
                             $td.empty();
-                            cols--;
+                            row_cols--;
                         }
                         break;
                     case 'label':
                         if ($child.attr('for')) {
                             $td.attr('width', '1%');
-                            cols--;
+                            row_cols--;
                             total--;
                         }
                         break;
@@ -858,35 +939,46 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
                         to_compute.push($td);
                 }
             });
-            var unit = Math.floor(total / cols);
+            var unit = Math.floor(total / row_cols);
             _.each(to_compute, function($td, i) {
                 var width = parseInt($td.attr('colspan'), 10) * unit;
                 $td.attr('width', ((i == to_compute.length - 1) ? total : width) + '%');
                 total -= width;
             });
         });
+        _.each(children, function(el) {
+            self.process($(el));
+        });
+        return $new_group;
     },
-    process_notebook: function($notebook, $form) {
+    process_notebook: function($notebook) {
+        var self = this;
         var pages = [];
         $notebook.find('> page').each(function() {
             var $page = $(this),
                 page_attrs = $page.getAttributes();
             page_attrs.id = _.uniqueId('notebook_page_');
             pages.push(page_attrs);
-            var $new_page = $(QWeb.render('FormRenderingNotebookPage', page_attrs));
-            $page.children().appendTo($new_page);
+            var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs),
+                $dst = self.legacy_mode ? $new_page.find('group:first') : $new_page;
+            $page.children().appendTo($dst);
             $page.before($new_page).remove();
         });
         var $new_notebook = $(QWeb.render('FormRenderingNotebook', { pages : pages }));
         $notebook.children().appendTo($new_notebook);
         $notebook.before($new_notebook).remove();
+        $new_notebook.children().each(function() {
+            self.process($(this));
+        });
         $new_notebook.tabs();
+        return $new_notebook;
     },
-    process_separator: function($separator, $form) {
+    process_separator: function($separator) {
         var $new_separator = $(QWeb.render('FormRenderingSeparator', $separator.getAttributes()));
         $separator.before($new_separator).remove();
+        return $new_separator;
     },
-    process_label: function($label, $form) {
+    process_label: function($label) {
         var dict = $label.getAttributes();
         var align = parseFloat(dict.align);
         if (isNaN(align) || align === 1) {
@@ -899,6 +991,7 @@ openerp.web.FormRenderingEngine = openerp.web.Widget.extend({
         dict.align = align;
         var $new_label = $(QWeb.render('FormRenderingLabel', dict));
         $label.before($new_label).remove();
+        return $new_label;
     }
 });
 
@@ -1083,9 +1176,13 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.
     },
     process_modifiers: function() {
         var compute_domain = openerp.web.form.compute_domain;
+        var to_set = {};
         for (var a in this.modifiers) {
-            this[a] = compute_domain(this.modifiers[a], this.view.fields);
+            var val = compute_domain(this.modifiers[a], this.view.fields);
+            this[a] = val;
+            to_set[a] = val;
         }
+        this.set(to_set);
     },
     update_dom: function() {
         this.$element.toggle(!this.invisible);
@@ -1243,7 +1340,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
         this.check_disable();
     },
     check_disable: function() {
-        var disabled = (this.readonly || this.force_disabled || !this.view.is_interactible_record());
+        var disabled = (this.force_disabled || !this.view.is_interactible_record());
         this.$element.prop('disabled', disabled);
         this.$element.css('color', disabled ? 'grey' : '');
     }
@@ -1254,8 +1351,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({
  * able to provide the features necessary for the fields to work.
  * 
  * Properties:
- *     - force_readonly: boolean, When it is true, all the fields should always appear
- *      in read only mode, no matter what the value of their "readonly" property can be.
+ *     - ...
  */
 
 openerp.web.form.FieldManagerInterface = {
@@ -1267,7 +1363,8 @@ openerp.web.form.FieldManagerInterface = {
  * 
  * Properties:
  *     - readonly: boolean. If set to true the field should appear in readonly mode.
- * 
+ *     - force_readonly: boolean, When it is true, the field should always appear
+ *      in read only mode, no matter what the value of the "readonly" property can be.
  * Events:
  *     - ...
  * 
@@ -1335,7 +1432,7 @@ openerp.web.form.AbstractField = openerp.web.form.Widget.extend(/** @lends opene
     init: function(field_manager, node) {
         this._super(field_manager, node);
         this.name = this.node.attrs.name;
-        this.value = undefined;
+        this.value = false;
         this.view.fields[this.name] = this;
         this.view.fields_order.push(this.name);
         this.type = this.node.attrs.widget;
@@ -1343,19 +1440,17 @@ openerp.web.form.AbstractField = openerp.web.form.Widget.extend(/** @lends opene
         this.required = this.modifiers['required'] === true;
         this.invalid = this.dirty = false;
         
-        // because I'm lazy to refactor right now
-        this.on("change:readonly", this, function() {this.readonly = this.get("readonly");});
-        
         // some events to make the property "effective_readonly" sync automatically with "readonly" and
         // "force_readonly"
         this.set({"readonly": this.modifiers['readonly'] === true});
         var test_effective_readonly = function() {
-            this.set({"effective_readonly": this.get("readonly") || this.view.get("force_readonly")});
+            this.set({"effective_readonly": this.get("readonly") || this.get("force_readonly")});
         };
         this.on("change:readonly", this, test_effective_readonly);
-        this.view.on("change:force_readonly", this, test_effective_readonly);
+        this.on("change:force_readonly", this, test_effective_readonly);
         _.bind(test_effective_readonly, this)();
 
+        // TODO: do something good about this
         if (this.view) {
             this.$label = this.view.$element.find('label[for="' + this.name + '"]');
             if (this.$label.length) {
@@ -1398,10 +1493,7 @@ openerp.web.form.AbstractField = openerp.web.form.Widget.extend(/** @lends opene
         return !this.invalid;
     },
     is_dirty: function() {
-        return this.dirty && !this.readonly;
-    },
-    get_on_change_value: function() {
-        return this.get_value();
+        return this.dirty && !this.get("effective_readonly");
     },
     update_dom: function(show_invalid) {
         this._super.apply(this, arguments);
@@ -1409,14 +1501,12 @@ openerp.web.form.AbstractField = openerp.web.form.Widget.extend(/** @lends opene
             this.$element.find('.oe_field_translate').toggle(!!this.view.datarecord.id);
         }
         if (!this.disable_utility_classes) {
-            this.$element.toggleClass('disabled', this.readonly);
+            this.$element.toggleClass('disabled', this.get("effective_readonly"));
             this.$element.toggleClass('required', this.required);
             if (show_invalid) {
                 this.$element.toggleClass('invalid', !this.is_valid());
             }
         }
-        // one more shit code to avoid refactoring this.readonly right now
-        this.set({"readonly": this.readonly});
     },
     on_ui_change: function() {
         this.dirty = true;
@@ -1472,6 +1562,7 @@ openerp.web.form.ReinitializeFieldMixin =  {
             this.render_value();
         });
         this.initialize_content();
+        this.render_value();
     },
     /**
      * Called to destroy anything that could have been created previously, called before a
@@ -1590,17 +1681,13 @@ openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({
 openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({
     init: function (view, node) {
         this._super(view, node);
+        this.value = 0;
         if (this.node.attrs.digits) {
-            this.parse_digits(this.node.attrs.digits);
+            this.digits = py.eval(this.node.attrs.digits).toJSON();
         } else {
             this.digits = this.field.digits;
         }
     },
-    parse_digits: function (digits_attr) {
-        // could use a Python parser instead.
-        var match = /^\s*[\(\[](\d+),\s*(\d+)/.exec(digits_attr);
-        return [parseInt(match[1], 10), parseInt(match[2], 10)];
-    },
     set_value: function(value) {
         if (value === false || value === undefined) {
             // As in GTK client, floats default to 0
@@ -1631,7 +1718,7 @@ openerp.web.DateTimeWidget = openerp.web.OldWidget.extend({
             showButtonPanel: true
         });
         this.$element.find('img.oe_datepicker_trigger').click(function() {
-            if (!self.readonly && !self.picker('widget').is(':visible')) {
+            if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) {
                 self.picker('setDate', self.value ? openerp.web.auto_str_to_date(self.value) : new Date());
                 self.$input_picker.show();
                 self.picker('show');
@@ -1844,7 +1931,7 @@ openerp.web.form.FieldProgressBar = openerp.web.form.AbstractField.extend({
         this._super.apply(this, arguments);
         this.$element.progressbar({
             value: this.value,
-            disabled: this.readonly
+            disabled: this.get("effective_readonly")
         });
     },
     set_value: function(value) {
@@ -2940,6 +3027,10 @@ openerp.web.form.SelectCreatePopup = openerp.web.OldWidget.extend(/** @lends ope
             this.new_object();
         }
     },
+    stop: function () {
+        this.$element.dialog('close');
+        this._super();
+    },
     setup_search_view: function(search_defaults) {
         var self = this;
         if (this.searchview) {
@@ -3413,7 +3504,7 @@ openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
     template: 'FieldBinaryImage',
     initialize_content: function() {
         this._super();
-        this.$image = this.$element.find('img.oe-binary-image');
+        this.$placeholder = $(".oe_form_field-binary-image-placeholder", this.$element);
         if (!this.get("effective_readonly"))
             this.$element.find('.oe-binary').show();
         else
@@ -3424,32 +3515,30 @@ openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({
         this.render_value();
     },
     render_value: function() {
-        this.set_image_maxwidth();
-
         var url;
         if (this.value && this.value.substr(0, 10).indexOf(' ') == -1) {
             url = 'data:image/png;base64,' + this.value;
-        } else {
+        } else if (this.value) {
             url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' +
                 this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime());
+        } else {
+            url = "/web/static/src/img/placeholder.png";
         }
-        this.$image.attr('src', url);
-    },
-    set_image_maxwidth: function() {
-        this.$image.css('max-width', this.$element.width());
+        var rendered = QWeb.render("FieldBinaryImage-img", {widget: this, url: url});;
+        this.$placeholder.html(rendered);
     },
     on_file_change: function() {
-        this.set_image_maxwidth();
+        this.render_value();
         this._super.apply(this, arguments);
     },
     on_file_uploaded_and_valid: function(size, name, content_type, file_base64) {
         this.value = file_base64;
         this.binary_value = true;
-        this.$image.attr('src', 'data:' + (content_type || 'image/png') + ';base64,' + file_base64);
+        this.render_value();
     },
     on_clear: function() {
         this._super.apply(this, arguments);
-        this.$image.attr('src', '/web/static/src/img/placeholder.png');
+        this.render_value();
     }
 });