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