From 763da046ef766d0e78c614d647674af545917552 Mon Sep 17 00:00:00 2001
From: Renaud Le Gac <legac@cppm.in2p3.fr>
Date: Fri, 8 Mar 2013 15:05:45 +0100
Subject: [PATCH] Polish dbsvc service. It can process several records in
 create and update transactions. In addition all exceptions are logged to the
 web2py ticket system. Fix a bug in App.form.Panel.onStoreException and
 improve it.

---
 modules/plugin_dbui/dbsvc.py   | 286 ++++++++++++++++++++++-----------
 static/plugin_dbui/CHANGELOG   |   3 +
 static/plugin_dbui/src/form.js | 205 ++++++++++++-----------
 3 files changed, 302 insertions(+), 192 deletions(-)

diff --git a/modules/plugin_dbui/dbsvc.py b/modules/plugin_dbui/dbsvc.py
index 3018f58e..59d43a4b 100644
--- a/modules/plugin_dbui/dbsvc.py
+++ b/modules/plugin_dbui/dbsvc.py
@@ -7,6 +7,7 @@ import re
 from basesvc import BaseSvc
 from converter import ROOT, SUCCESS, TOTAL
 from gluon.contrib import simplejson as json
+from gluon.storage import Storage
 from helper import (encode_field, 
                     decode_field,
                     get_foreign_field,
@@ -16,10 +17,10 @@ from helper import (encode_field,
 
 FIELD_NOT_IN_DB = "the field '%s.%s' doesn't exist in the database."
 KEYWORD_MISSING = "The keyword '%s' is missing."
-RECORD_DELETED = "Record %s is deleted."
-RECORD_INSERTED = "Record is inserted with id %i."
 RECORD_NOT_IN_DB = "The record '%s' doesn't exist in the table %s."
-RECORD_UPDATED = "Record %s is updated."
+RECORDS_DELETED = "Record(s) %s is deleted."
+RECORDS_INSERTED = "Record(s) is inserted with id(s) %s."
+RECORDS_UPDATED = "Record(s) %s is updated."
 TABLE_NOT_IN_DB = "The table '%s' doesn't exist in the database."
 TO_MANY_RECORDS = "To many records !!!"
 
@@ -134,54 +135,6 @@ class DbSvc(BaseSvc):
         return eval(query, {}, {"db": self.environment['db']})    
 
     
-    def _get_fields(self, arg):
-        """Helper method to get fields and their values in order to
-        create or update a record. The dictionary arg contains the following 
-        keys:
-
-            tablename
-                the name of the table in the database
-                
-            dbFields
-                The list of table fields.
-                It also include pointing field resolving foreign keys.
-                [(table1, field1), (table1, field2), (table2, field3), ...]
-
-            records (ROOT)
-                List of dictionary containing the new / update values for field
-                as well as the identifier of the record to be updated (id)
-
-        Return a dictionary {fieldName: value, ...}
-        or {"success": False, "errors": {...}, ..} in case of validation errors
-        
-        """
-        fields = {}
-        table = arg['tableName']
-
-        # protection
-        if len(arg[ROOT]) > 1:
-            raise DbSvcException(TO_MANY_RECORDS)
-        
-        # Remove fields in the record which do not belong to the table.
-        # The JsonStore send the foreign key and the the pointing field
-        # when manipulating table with foreign keys. the latter belongs
-        # to another table.
-        record = arg[ROOT][0]
-        for table_field in record:
-            tablename, fieldname = decode_field(table_field)
-            if tablename == table:
-                fields[fieldname.encode('utf8')] = record[table_field]
-
-        # Validate field contents
-        di = self._is_fields_values_valid(table, fields)
-        if di:
-            table_id = encode_field(table, 'id')
-            record[table_id] = ""
-            return {SUCCESS: False, "errors": di, ROOT: record}
-
-        return fields
-
-
     def _get_record(self, table, id):
         """Helper function to get the record id in a form which can be decoded
         by the JsonReader running on the client side. Foreign key are resolved.
@@ -239,8 +192,76 @@ class DbSvc(BaseSvc):
             raise DbSvcException(TABLE_NOT_IN_DB % table)
 
 
+    def _prepare_records(self, arg):
+        """Helper method to prepare the records for their insertion
+        in the database (create or update).
+
+        The current transaction is defined in the dictionary arg.
+        It is associate to one table and can contain several records.
+
+            tablename
+                the name of the table in the database
+                
+            dbFields
+                The list of table fields.
+                It also include pointing field resolving foreign keys.
+                [(table1, field1), (table1, field2), (table2, field3), ...]
+
+            records (ROOT)
+                List of dictionary containing the new / update values for field
+                as well as the identifier of the record to be updated (id)
+        
+        The method removes fields in the record which do not belong to the
+        table since the JsonStore send the foreign key and the pointing field.
+        Pointing fields are not used here.
+        
+        The method also validates values.
+
+        Return a Storage:
+        
+            {
+                "errors": [None,...], 
+                "records": [{TableField: value,...}, ..]
+            }
+        
+        There is one to one correspondence between the errors 
+        and the records lists. Error is None when fields are validated.
+        Otherwise the error is a dictionary:
+        
+            {TableField: "error message", ...}
+        
+        """
+        data = Storage(errors=[], records=[])
+        table = arg['tableName']
+
+        for record in arg[ROOT]:        
+            fields = {}
+            
+            # Remove fields in the record which do not belong to the table.
+            # The JsonStore send the foreign key and the the pointing field
+            # when manipulating table with foreign keys. the latter belongs
+            # to another table.
+            for table_field in record:
+                tablename, fieldname = decode_field(table_field)
+                if tablename == table:
+                    fields[fieldname.encode('utf8')] = record[table_field]
+
+            data.errors.append(None)
+            data.records.append(fields)
+            
+            # Validate field contents
+            di = self._is_fields_values_valid(table, fields)
+            if di:
+                table_id = encode_field(table, 'id')
+                record[table_id] = ""
+                data.records[-1] = record
+                data.errors[-1] = di
+
+        return data
+
+    
     def create(self, arg):
-        """Create a new record defined in arg.
+        """Create new records defined in the transaction arg.
         The dictionary arg contains the following keys:
 
             tableName
@@ -252,35 +273,58 @@ class DbSvc(BaseSvc):
                 [(table1, field1), (table1, field2), (table2, field3), ...]
 
             records (ROOT)
-                A dictionary containing the update values for field
+                A list of dictionary containing the new values for fields
                 as well as the identifier of the record to be updated (id)
                 
         Return a dictionary with status, message and the update row:
-        {success: True, msg: 'blalbla', records:{TableId:xx,....}}
-
+        
+            {
+                success: True, 
+                msg: 'blalbla', 
+                records: [{TableId: xxx,....}, ...]
+            }
+
+        When at least a field value is not validated, abort the full transaction
+        and return a dictionary with the error messages for the first bad record:
+        
+            {
+                success: False, 
+                errors: {TableField: error message, ..}, 
+                records:{TableField: xxx, ...}
+            }
+            
         """
         self.dbg("Start DbSvc.create")
 
         self._check_request(arg)
-        fields = self._get_fields(arg)
+        data = self._prepare_records(arg)
 
-        if SUCCESS in fields and fields[SUCCESS] == False:
-            return fields
-
-        # insert new record        
-        table = arg['tableName']
-        id = self.environment['db'][table].insert (**fields)
+        # Abort the full transaction if at least one record is in error
+        for error in data.errors:
+            if error:
+                i = data.errors.index(error)
+                return {SUCCESS: False, "errors": error, ROOT: data.records[i]}
         
-        # return the complete record
-        # mandatory, since this is the one which is display in the grid.
-        record = self._get_record(table, id)
-
-        self.dbg("End DbSvc.create.", RECORD_INSERTED % id)
-        return {SUCCESS: True, 'msg': RECORD_INSERTED % id, ROOT: record}
+        # insert new records  
+        ids, table, records = [], arg['tableName'], [] 
+        for fields in data.records:
+            
+            id = self.environment['db'][table].insert (**fields)
+            
+            # return the complete record
+            # mandatory to display the new record in the grid.
+            record = self._get_record(table, id)
+            
+            ids.append(str(id))
+            records.append(record)
+        
+        txt = ', '.join(ids)
+        self.dbg("End DbSvc.create.", RECORDS_INSERTED % txt)
+        return {SUCCESS: True, 'msg': RECORDS_INSERTED % txt, ROOT: records}
 
     
     def destroy(self, arg):
-        """Destroy the record defined in arg.
+        """Destroy the record defined in the transaction arg.
         The dictionary arg contains the following keys:
 
             tableName
@@ -294,8 +338,22 @@ class DbSvc(BaseSvc):
             records (ROOT)
                 A list with id numbers
                 
-        Return a dictionary with status, message and id of the deleted row:
-        {success: True, msg: 'blalbla', records:{TableId:xx}}
+        Return a dictionary with status, message and ids of the deleted row:
+            
+            {
+                success: True, 
+                msg: 'blalbla', 
+                records:[{TableId:xx}, ..]
+            }
+
+        When at least one record does not exist, abort the full transaction
+        and return a dictionary with the error messages for the first bad record:
+        
+            {
+                success: False, 
+                msg: 'blalbla', 
+                records:{TableId:xx}
+            }
 
         """
         self.dbg("Start DbSvc.destroy")
@@ -303,24 +361,30 @@ class DbSvc(BaseSvc):
         self._check_request(arg)
         
         db = self.environment['db']
-        table = arg['tableName']
+        ids, table, records = [], arg['tableName'], []
         table_id = encode_field(table, 'id')
         
+        # Abort the transaction is at least one record does not exists
         for id in arg[ROOT]:
             if not db[table][id]:
-                return {"success": False, 
-                        "msg": RECORD_NOT_IN_DB % (id, table),  
-                        ROOT: {table_id: id}}
-            
+                txt = RECORD_NOT_IN_DB % (id, table)
+                return {"success": False, "msg": txt, ROOT: {table_id: id}}
+
+        # delete records        
+        for id in arg[ROOT]:
             del db[table][id]
-        
+            
+            ids.append(str(id))
+            records.append({table_id: id})
+            
+        txt = ', '.join(ids)
         self.dbg("End DbSvc.destroy")
-        return {"success": True, "msg": RECORD_DELETED % id,  ROOT: {table_id: id}}
+        return {"success": True, "msg": RECORDS_DELETED % txt,  ROOT: records}
     
     
     def read(self, arg):
-        """Read the content of a table as specified in the arg dictionary.
-        The latter contains the following keys:
+        """Read the content of a table as specified in the transaction arg.
+        The arg dictionary contains the following keys:
         
             tableName
                 name of the database table
@@ -350,7 +414,11 @@ class DbSvc(BaseSvc):
             dir
 
         The method return a list of records:
-        {success: True, records: [{TableField: value, ...}, {...}, ...], ...}
+            
+            {
+                success: True, 
+                records: [{TableField: value, ...}, ...]
+            }
         
         """
         self.dbg("Start DbSvc.read")
@@ -404,8 +472,8 @@ class DbSvc(BaseSvc):
 
 
     def update(self, arg):
-        """Update a record according to the arg dictionary.
-        The latter contains the following keys:
+        """Update records defined in the transaction arg.
+        The arg dictionary contains the following keys:
         
             tableName
                 the name of the table in the database
@@ -416,30 +484,54 @@ class DbSvc(BaseSvc):
                 [(table1, field1), (table1, field2), (table2, field3), ...]
 
             records (ROOT)
-                A dictionary containing the update values for field
+                A list of dictionary containing the update values for field
                 as well as the identifier of the record to be updated (id)
 
         Return a dictionary with status, message and the update row:
-        {success: True, msg: 'blalbla', records:{TableId:xx,...}}
 
+            {
+                success: True, 
+                msg: 'blalbla', 
+                records: [{TableId: xxx,....}, ...]
+            }
+
+        When at least a field value is not validated, abort the full transaction
+        and return a dictionary with the error messages for the first bad record:
+        
+            {
+                success: False, 
+                errors: {TableField: error message, ..}, 
+                records:{TableField: xxx, ...}
+            }
+            
         """
         self.dbg("Start DbSvc.update.")
 
         self._check_request(arg)
-        fields = self._get_fields(arg)
+        data = self._prepare_records(arg)
 
-        if SUCCESS in fields and fields[SUCCESS] == False:
-            return fields
+        # Abort the full transaction if at least one record is in error
+        for error in data.errors:
+            if error:
+                i = data.errors.index(error)
+                return {SUCCESS: False, "errors": error, ROOT: data.records[i]}
 
-        # the client send modified fields only
-        # update the database accordingly        
-        id = fields['id']
-        table = arg['tableName']
-        self.environment['db'][table][id] = fields
+        # update records  
+        ids, table, records = [], arg['tableName'], [] 
+        for fields in data.records:
 
-        # return the complete record
-        # mandatory, since this is the one which is display in the grid.
-        record = self._get_record(table, id) 
+            # the client send modified fields only
+            # update the database accordingly        
+            id = fields['id']
+            self.environment['db'][table][id] = fields
 
+            # return the complete record
+            # mandatory to display the new record in the grid.
+            record = self._get_record(table, id) 
+
+            ids.append(str(id))
+            records.append(record)
+            
+        txt = ', '.join(ids)
         self.dbg("End DbSvc.update.")
-        return {SUCCESS: True, "msg": RECORD_UPDATED % id, ROOT: record}
+        return {SUCCESS: True, "msg": RECORDS_UPDATED % txt, ROOT: records}
diff --git a/static/plugin_dbui/CHANGELOG b/static/plugin_dbui/CHANGELOG
index c8b046aa..56a61d33 100644
--- a/static/plugin_dbui/CHANGELOG
+++ b/static/plugin_dbui/CHANGELOG
@@ -2,6 +2,9 @@
 
 HEAD
   - DirectSvc exceptions are logged in the ticket system.
+  - Polish dbsvc service which can now process several records
+    in create and update transactions.
+  - Fix a bug and improve App.form.Panel.onStoreException.
 
 0.4.10.1 (Dec 2012)
   - Bug fixed
diff --git a/static/plugin_dbui/src/form.js b/static/plugin_dbui/src/form.js
index 070920f1..2aa8f8c8 100644
--- a/static/plugin_dbui/src/form.js
+++ b/static/plugin_dbui/src/form.js
@@ -1,19 +1,19 @@
 /**
- * The FormPanel is an Ext.form.formPanel with an App.data.DirectStore. 
- * It comes with two button 'Action' and 'Reset' and a set of method to 
+ * The FormPanel is an Ext.form.formPanel with an App.data.DirectStore.
+ * It comes with two button 'Action' and 'Reset' and a set of method to
  * create, duplicate, destroy and update record in the direct store.
- * 
- * This object can be used as a classic form, a row editor for a grid, 
+ *
+ * This object can be used as a classic form, a row editor for a grid,
  * a record browser, ....
- * 
+ *
  * The action associated to the button is set via the setAction method.
  * the create action is set by default.
- * 
+ *
  * The type of this component is xform.
- * 
+ *
  * @extends Ext.form.FormPanel
  * @version
- * 
+ *
  */
 Ext.namespace('App.form');
 
@@ -24,7 +24,7 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
      * The store has to be defined since the actions rely on it.
      */
     store: null,
-    
+
     /**
      * Predefined configuration options
      */
@@ -42,13 +42,13 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
     defaultType: 'textfield',
     frame: true,
     monitorValid: true,
-    
+
     /**
      * private attribute to keep track of current action parameters
      */
     currentAction: null,
     currentRecord: null,
-    
+
     /**
      * private attributes for internationalization
      */
@@ -57,29 +57,29 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
     textDuplicate: 'Duplicate',
     textReset: 'Reset',
     textUpdate: 'Update',
-    
+
     /**
      * private method require by the ExtJs component model
-     */	
+     */
     initComponent: function () {
-        
+
         // helper function to reset the form
         // the scope is Ext.data.BasicStore
         function resetForm() {
             this.reset();
         }
-        
+
         // construct the underlying class. DON'T MOVE
         App.form.FormPanel.superclass.initComponent.call(this);
-        
+
         // button events listeners
         this.buttonAction.on('click', this.doAction, this);
         this.buttonReset.on('click', resetForm, this.getForm());
-        
+
         // link the form to the data store
         this.store = App.getStore(this.store);
         this.store.on('exception', this.onStoreException, this);
-        this.store.on('write', resetForm, this.getForm());    
+        this.store.on('write', resetForm, this.getForm());
 
         // set the default action: create
         this.setAction('create');
@@ -91,9 +91,9 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
      * @param {Object} boolean
      */
     disableFields: function (bool) {
-        
+
         var form = this.getForm();
-        
+
         form.items.each(function (field) {
             field.setDisabled(bool);
         });
@@ -110,33 +110,33 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
 
         var form = this.getForm(),
             newRecord;
-        
+
         if (!form.isValid()) {
             return;
         }
-        
+
         if (!this.store) {
             throw new Error('the store is undefined !!!');
         }
-        
+
         switch (this.currentAction) {
 
         case 'create':
             newRecord = new this.store.recordType();
             this.updateRecord(newRecord);
             this.store.add(newRecord);
-            break;  
-            
+            break;
+
         case 'destroy':
             this.store.remove(this.currentRecord);
             break;
-            
+
         case 'duplicate':
             newRecord = new this.store.recordType();
             this.updateRecord(newRecord);
             this.store.add(newRecord);
             break;
-            
+
         case 'update':
             this.currentRecord.beginEdit();
             this.updateRecord(this.currentRecord);
@@ -144,29 +144,29 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
             break;
         }
         if (this.store.autoSave === false) {
-            this.store.save();    
+            this.store.save();
         }
     },
-    
+
     /**
      * Private method to hardreset the form.
-     * 
+     *
      * The reset method resets the field value to the originally loaded.
-     * 
+     *
      * The hardreset erases the originally loaded values and reset the form.
      * Hardreset is required to clean form in sequence like update, create,...
      * It handle properly default value for combobox.
-     * 
+     *
      */
     hardReset: function () {
         var form = this.getForm();
-        
+
         form.items.each(function (field) {
             field.originalValue = Ext.value(field.initialConfig.value, '');
             field.setValue(field.originalValue);
         });
     },
-    
+
     /**
      * Handler to process store exception
      * the scope of this function is App.form.FormPanel
@@ -184,52 +184,67 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
             field,
             fieldName,
             form = this.getForm(),
-            msg,
+            msg = '',
             records;
 
-        // mark fields invalid
-        for (fieldName in response.errors) {
-            if (response.errors.hasOwnProperty(fieldName)) {
-                field = form.findField(fieldName);
-                field.markInvalid(response.errors[fieldName]);           
-            }
-        }
-        
-        // report the exception to the user
-        // The first case is when the server replies
-        if (type === 'remote' && response.xhr.status !== 200) {
-
-            msg = response.xhr.statusText.toLowerCase() + '. ';
-
-            // remove the new records from the store
-            if (action === 'create') {
-                records = this.store.getModifiedRecords();
-                for (i=0; i < records.length; i += 1) {
-                    this.store.remove(records[i]);        
-                } 
-                
-                msg += '<br><br>';
-                msg += 'The record was removed from the internal storage.';
-            }
-            
-            msg += 'The database is not modified.';
-            
-            Ext.Msg.show({
-                title: action + ' the record failed',
-                msg: msg,
-                buttons: Ext.Msg.OK,
-                icon: Ext.Msg.ERROR,
-                width: 300
-            });
-            
+        switch (type) {
+
+            // invalid response from the server, HTTP 400, 500
+            // inform the user
+            case 'response':
+                Ext.Msg.alert('Error...', 'Internal server error.');
+                break;
+
+            // valid answer from the server, HTTP 200
+            // something went wrong in the server validation process, ...
+            case 'remote':
+
+                // mark fields invalid
+                for (fieldName in response.errors) {
+                    if (response.errors.hasOwnProperty(fieldName)) {
+                        field = form.findField(fieldName);
+                        field.markInvalid(response.errors[fieldName]);
+
+                        msg += response.errors[fieldName];
+                        msg += '<br>';
+                    }
+                }
+
+                // Keep the store in synchronization with the database
+                // remove the new records from the store
+                records = arg;
+
+                switch (action) {
+                    case 'create':
+                        for (i=0; i < records.length; i += 1) {
+                            this.store.remove(records[i]);
+                        }
+
+                        msg += 'The new record(s) is removed from the internal storage.<br>';
+                        break;
+
+                    case 'update':
+                        for (i=0; i < records.length; i += 1) {
+                            records[i].reject();
+                        }
+
+                        msg += 'The update record(s) is revert to its original values.<br>';
+                        break;
+                }
+
+                msg += 'The database is not modified.';
+
+                // inform the user
+                Ext.Msg.alert(action + ' failed...', msg);
+
+                break;
         }
-        
     },
-    
+
     /**
-     * Public method to set the action associated to the form. 
-     * The record is load into the form and the text of the button is 
-     * properly set. Understood action are: create, destroy, duplicate, 
+     * Public method to set the action associated to the form.
+     * The record is load into the form and the text of the button is
+     * properly set. Understood action are: create, destroy, duplicate,
      * update and view.
      * @param {Object} action
      * @param {Object} record
@@ -239,27 +254,27 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
         var form = this.getForm(),
             table,
             tableId;
-        
+
         this.buttonReset.show();
         this.buttonAction.show();
-        
+
         this.disableFields(false);
-        
+
         this.currentAction = action;
         this.currentRecord = record;
-        
+
         switch (action) {
 
         case 'create':
             this.hardReset();
             this.buttonAction.setText(this.textCreate);
-            break;  
-            
+            break;
+
         case 'destroy':
             this.buttonAction.setText(this.textDestroy);
             form.loadRecord(record);
             break;
-            
+
         case 'duplicate':
             this.buttonAction.setText(this.textDuplicate);
             this.hardReset();
@@ -268,12 +283,12 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
             delete record.data[tableId];
             form.loadRecord(record);
             break;
-            
+
         case 'update':
             this.buttonAction.setText(this.textUpdate);
             form.loadRecord(record);
             break;
-            
+
         case 'view':
             this.buttonReset.hide();
             this.buttonAction.hide();
@@ -282,7 +297,7 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
             break;
         }
     },
-    
+
     /**
      * Private method to update the selected record with the value of the form
      * This method have been designed to handle foreign keys, date object, ....
@@ -292,13 +307,13 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
 
         var combo,
             field,
-            fields = [], 
+            fields = [],
             i,
             items,
             rec,
             value;
 
-        // get the list of dirty fields 
+        // get the list of dirty fields
         items = this.findByType('field');
         for (i = 0; i < items.length; i += 1) {
             field = items[i];
@@ -307,7 +322,7 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
             }
         }
 
-        // include dirty fields embedded in composite fields 
+        // include dirty fields embedded in composite fields
         items = this.findByType('compositefield');
         for (i = 0; i < items.length; i += 1) {
             items[i].items.eachKey(function(key, subfield) {
@@ -318,17 +333,17 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
         // update the record
         // take care of special treatment required by date and combobox
         for (i = 0; i < fields.length; i += 1) {
-            
+
             field = fields[i];
             value = field.getValue();
 
             switch (field.getXType()) {
-                
+
                 // We convert the date object according to a string defined
                 // by the Ext.form.DateField property format.
                 // NOTE: by default in the json encoding, the date object
                 // is converted as string using iso format YYYY-MM-DDTHH:MM:SS.
-                // However, the string expected by the database depends on 
+                // However, the string expected by the database depends on
                 // the date type: date, datetime or time. The format of the
                 // Ext.form.DateField, used by the interface, is defined in the
                 // property format.
@@ -339,21 +354,21 @@ App.form.FormPanel = Ext.extend(Ext.form.FormPanel, {
                     }
                     break;
 
-                // For foreign key, the record contains the valueField 
+                // For foreign key, the record contains the valueField
                 // as well as the displayField. The default action update
-                // the valueField but note the display field. 
+                // the valueField but note the display field.
                 // The next lines append the displayField
 
                 case 'xcombobox':
                     combo = field;
                     rec = combo.findRecord(combo.valueField, combo.getValue());
-                    record.set(combo.displayField, rec.get(combo.displayField));            
+                    record.set(combo.displayField, rec.get(combo.displayField));
                     break;
             }
 
             record.set(field.getName(), value);
         }
-               
+
     }
 
 });
-- 
GitLab