From 392ecb21fb990635d73dd50c0b118bf9a4719c7f Mon Sep 17 00:00:00 2001 From: Renaud Le Gac <renaud.legac@free.fr> Date: Wed, 24 Feb 2010 21:32:28 +0000 Subject: [PATCH] Deploy the new web service cfgSvc. It is base on the Ext.Direct technology. In this approach the configuration of the form or grid are performed on demand. --- controllers/configuration.py | 111 ++++++---- modules/cfgsvc.py | 354 +++++++++++------------------- modules/foreignfield.py | 69 ++++++ static/appjs/appviewportintree.js | 113 ++++++---- static/script/viewport_1.js | 53 ++--- views/applayout.html | 2 +- 6 files changed, 365 insertions(+), 337 deletions(-) create mode 100644 modules/foreignfield.py diff --git a/controllers/configuration.py b/controllers/configuration.py index 86487b93..8c7abfc7 100644 --- a/controllers/configuration.py +++ b/controllers/configuration.py @@ -1,19 +1,10 @@ """ configuration.py - Controllers to interface the javascript application and database model. - It mainly return configuration object adapted to the UI. - - url - http://localhost:8000/mdvPostInstall/configuration - - debug - post argument. - Print a lot of information when true - - exclude - post argument. - A list of tables for which configuration object are nor return. - + Controllers/router for the web service cfgSVc + + Relies on Ext.Direct see specification: + http://www.extjs.com/products/extjs/direct.php + Author: R. Le Gac $Id$ @@ -21,37 +12,75 @@ __version__ = "$Revision$" -import pprint +from applications.mdvPostInstall.modules.cfgsvc import CfgSvc from gluon.contrib import simplejson as json -from gluon.http import HTTP +from pprint import pprint -from applications.mdvPostInstall.modules.cfgsvc import AppCfg def index(): - """ controller handling application configuration object + """ controller handling application configuration object - Return a JSON string when the action is successful. - Otherwise return and HTTP error. + Act as a router for the cfgSvc service (see Ext;Direct Extjs 3.1.1) + Decode the raw HTTP post request: + {action: "cfgSvc", method: x, data: y, tip: i, type: "rpc"} + + Return a JSON string with the result of the request: + {action: "cfgSvc", method: x, result: y, tip: i, type: "rpc"} + + Otherwise return and HTTP error. - """ - - debug = "debug" in request.vars and (request.vars.debug == 'true') - - if debug: - print "\nSTART CONFIGURATION CONTROLLER" - print "POST arguments:", request.vars - - try: - di = AppCfg(db).proceed(request.vars) - resp = json.dumps(di) - - except BaseException, e: - raise HTTP(500, e) - - if debug: - print "\nCONFIGURATION CONTROLLER RESPONSE IS:" - pprint.pprint(di) - print "\nEND OF CONFIGURATION CONTROLLER\n" - - return resp + For more detail on Ext.Direct see specification: + http://www.extjs.com/products/extjs/direct.php + + """ + + debug = True + + # NOTE: the server cfgSvc is start at the beginning + # when building the model (db.py) + + # decode the raw HTTP post + # ExtJs.3.1.1 {action: "xx", method: "y", data: [], type:"rpc", tid:2} + + req = json.loads(request.body.read()) + + if debug: + print "\nSTART CONFIGURATION CONTROLLER:" + pprint(req) + + # receive a list of dictionary for a multi-request + li = [] + if isinstance(req, list): + li.extend(req) + else: + li.append(req) + + for di in li: + # construct the command to be executed + args = di["data"] + cmd = "%s.%s(*args)" % (di["action"], di["method"]) + + if args == None: + cmd = "%s.%s()" % (di["action"], di["method"]) + + # execute the command and return HTTP exception + # when an internal error occurred + try: + res = eval(cmd) + except BaseException, e: + raise HTTP(500, e) + + # NOTE: the client expect the following object: + # ExtJs.3.1.1 {action: "xx", method: "y", result: [], type:"rpc", tid:2} + + del di["data"] + di["result"] = res + + if debug: + print "\nCONFIGURATION CONTROLLER RESPONSE IS:" + pprint(li) + print "\nEND OF CONFIGURATION CONTROLLER\n" + + # encode the data as a json string + return json.dumps(li) \ No newline at end of file diff --git a/modules/cfgsvc.py b/modules/cfgsvc.py index 8d6fe869..11f71b92 100644 --- a/modules/cfgsvc.py +++ b/modules/cfgsvc.py @@ -5,8 +5,8 @@ __version__ = "$Revision$" -from basesvc import BaseSvc, KEYWORD_MISSING -from gluon.validators import IS_IN_DB +from foreignfield import ForeignField + # Convert a field type into a widget xtype FTYPE_TO_XTYPE = {'boolean': 'checkbox',\ @@ -32,262 +32,170 @@ def first_is_upper(field): return "%s%s" % (field[0].upper(), field[1:]) -class AppCfg(BaseSvc): +class CfgSvc: - foreign = None - form_models = {} - grid_models = {} - json_stores = {} - tables = [] - + _db = None + _foreign = None def __init__(self, db): - BaseSvc.__init__(self, db) - self.foreign = ForeignField(db) - def generate_forms(self): - """ Return a dictionary with configuration for entry form. + self._db = db + self._foreign = ForeignField(db) + + + def _getColumnsModel(self, table): + """ Generate columns model required by the gridPanel widget. + The method return a list. """ - di = {} - for table in self.tables: - di[table] = {"xtype": "xentry", - "title": first_is_upper(table), - "table": table, - "items": self.form_models[table]} - return di + + li = [] + + for field in self._db[table].fields: + col = {} + + # replace foreign key by the pointing column + if self._foreign.is_foreign_field(table, field): + k_field = self._foreign[table][field]["k_field"] + col["header"] = first_is_upper(k_field) + col["dataIndex"] = k_field + + # standard column + else: + col["header"] = first_is_upper(field) + col["dataIndex"] = field + + col["sortable"] = True + + # hide the primary key + if field == "id": + col["hidden"] = True + li.append(col) - def generate_formModels(self): - """ Fill the attribute _form_models with formModels for selected table. - These configuration objects are used by the entryFormPanel and by the - gridPanel widgets. - - To avoid complication form_model will be copied in each widget in - due time. It is why there are kept in memory. + return li + + + def _getFormModelItems(self, table): + """ Return the list of items appearing in a Form model. """ - di = {} - for table in self.tables: - di[table] = [] - - for el in self._db[table].fields: - cfg = {} - field = self._db[table][el] + li = [] + + for el in self._db[table].fields: + cfg = {} + field = self._db[table][el] - # foreign key - if self.foreign.is_foreign_field(table, field.name): - - k = self.foreign.get_foreign_data(table, field.name) + # foreign key + if self._foreign.is_foreign_field(table, field.name): - cfg["xtype"] = "xcombobox" - cfg["hiddenName"] = field.name - cfg["displayField"] = k["k_field"] - cfg["fieldLabel"] = first_is_upper(k["k_field"]) - cfg["table"] = k["k_table"] - cfg["valueField"] = k["k_key"] + k = self._foreign.get_foreign_data(table, field.name) - # standard field - else: - cfg["xtype"] = FTYPE_TO_XTYPE[field.type] - cfg["fieldLabel"] = first_is_upper(field.name) - cfg["name"] = field.name - - # default value and not null - # NOTE: the entryform doesn't work when both default and - # notnull condition are defined. - if field.default: - cfg["emptyText"] = str(field.default) - elif field.notnull: - cfg["allowBlank"] = False + cfg["xtype"] = "xcombobox" + cfg["hiddenName"] = field.name + cfg["displayField"] = k["k_field"] + cfg["fieldLabel"] = first_is_upper(k["k_field"]) + cfg["table"] = k["k_table"] + cfg["valueField"] = k["k_key"] - # hide primary key - if field.name == "id": - cfg["hidden"] = True - cfg["hideLabel"] = True - cfg["readOnly"] = True + # standard field + else: + cfg["xtype"] = FTYPE_TO_XTYPE[field.type] + cfg["fieldLabel"] = first_is_upper(field.name) + cfg["name"] = field.name + + # default value and not null + # NOTE: the entryform doesn't work when both default and + # notnull condition are defined. + if field.default: + cfg["emptyText"] = str(field.default) + elif field.notnull: + cfg["allowBlank"] = False - # format - if field.type == "date": - cfg["format"] = "Y-m-d" - + # hide primary key + if field.name == "id": + cfg["hidden"] = True + cfg["hideLabel"] = True + cfg["readOnly"] = True + + # format + if field.type == "date": + cfg["format"] = "Y-m-d" if field.type == "datetime": cfg["format"] = "Y-m-d H:i" - di[table].append(cfg) - - self.form_models = di - - - def generate_gridModels(self): - """ Generate gridModel requires by the gridPanel widget. - Foreign key are replace by the pointing columns. - - """ - di = {} - - for table in self.tables: - di[table] = [] - for field in self._db[table].fields: - col = {} - - # replace foreign key by the pointing column - if self.foreign.is_foreign_field(table, field): - k_field = self.foreign[table][field]["k_field"] - col["header"] = first_is_upper(k_field) - col["dataIndex"] = k_field - - # standard column - else: - col["header"] = first_is_upper(field) - col["dataIndex"] = field - - col["sortable"] = True - - # hide the primary key - if field == "id": - col["hidden"] = True - - di[table].append(col) - - self.grid_models = di - - - def generate_jsonStores(self): - """ Generate configuration object for JSON stores. - They are require by the gridPanel widget. - - """ - di = {} - - for table in self.tables: - di[table] = {"table": table} - di[table]["debug"] = False - di[table]["tableFields"] = self._db[table].fields + li.append(cfg) - if table in self.foreign: - di[table]["foreignFields"] = [] - for field in self.foreign[table]: - k = self.foreign[table][field] - li = [field, k["k_table"], k["k_field"]] - di[table]["foreignFields"].append(li) - - self.json_stores = di - + return li - def generate_grids(self): - """ Generate the configuration object for the gridPanel widgets. - The configuration of JSON store, formModel and gridModel have - to be generated before. - - """ - di = {} - for table in self.tables: - di[table] = {} - di[table]["xtype"] = "xgrid" - di[table]["colModel"] = self.grid_models[table] - di[table]["formModel"] = self.form_models[table] - di[table]["store"] = self.json_stores[table] - di[table]["table"] = table - di[table]["title"] = first_is_upper(table) - - return di - + def _getJsonStore(self, table): + """ Generate configuration object for JSON stores + required by the gridPanel widget. - def proceed(self, vars): - """Proceed the request sends via the POST/GET method. - Return a dictionary with the configuration object - """ - BaseSvc.proceed(self, vars) - - self.select_tables() - self.generate_formModels() - self.generate_jsonStores() - self.generate_gridModels() + di = {"table": table} - di = {} - di["form"] = self.generate_forms() - di["grid"] = self.generate_grids() + di["debug"] = False + di["tableFields"] = self._db[table].fields - return di - - - def select_tables(self): - """ build the list of selected table. - Some tables can be removed from the UI using the keyword exclude. - - """ - excluded_tables = [] - if "exclude" in self._vars: - excluded_tables = self._list(self._vars["exclude"]) + if table in self._foreign: + di["foreignFields"] = [] + for field in self._foreign[table]: + k = self._foreign[table][field] + li = [field, k["k_table"], k["k_field"]] + di["foreignFields"].append(li) - self.tables = list(self._db.tables) - for table in self._db.tables: - if table in excluded_tables: - self.tables.pop(self.tables.index(table)) + return di - -class ForeignField(dict): - - def __init__(self, db): - """ Scan the database model to identify foreign key, - Store the pointing table and field. + def _getMethods(self): + """ Return the list of methods available and + their number of arguments. """ - - for table in db.tables: - fields = [db[table][el] for el in db[table].fields] - - for field in fields: - # - # FOREIGN FIELD - # - # The reference to the foreign table is defined in the - # type: "reference foreignTable". - # The foreign table and the associated column are also - # defined in the validator IS_IN_DB. - # - if field.type.startswith("reference"): - - validators = field.requires - if not isinstance(validators, (list, tuple)): - validators = [field.requires] - - for vdt in validators: - if isinstance(vdt, IS_IN_DB): - - if table not in self: - self[table] = {} - - self[table][field.name] = {} - self[table][field.name]["k_table"] = vdt.ktable - self[table][field.name]["k_field"] = vdt.ks[0] - self[table][field.name]["k_key"] = vdt.ks[1] - + li = [] + for el in dir(self): + if el.startswith("_"): continue + di = {"name": el,\ + "len": eval("self.%s.func_code.co_argcount" % el)-1} + li.append(di) + + print li + return li - def is_foreign_field(self, table, field): - """ Return true is the table.field is a foreign key. - """ - if table in self: - return field in self[table] + def getFormModel(self, table): + """ Return the configuration dictionary for an entryForm widget. - else: - return False + """ + di = {"xtype": "xentry", + "title": first_is_upper(table), + "table": table, + "items": self._getFormModelItems(table)} + return di + - - def get_foreign_data(self, table, field): - """ Return a dictionary containing the pointing table, field and key. - Otherwise return None. + def getGridModel(self, table): + """ Return the configuration dictionary for a gridPanel widget. """ - if self.is_foreign_field(table, field): - return self[table][field] - - else: - return None + di = {} + + di["xtype"] = "xgrid" + di["colModel"] = self._getColumnsModel(table) + di["formModel"] = self._getFormModelItems(table) + di["store"] = self._getJsonStore(table) + di["table"] = table + di["title"] = first_is_upper(table) + return di + + def getTables(self, *args): + """ Return a the list of tables. + + """ + li = list(self._db.tables) + li.sort() + return li \ No newline at end of file diff --git a/modules/foreignfield.py b/modules/foreignfield.py new file mode 100644 index 00000000..895541f4 --- /dev/null +++ b/modules/foreignfield.py @@ -0,0 +1,69 @@ +""" $Id$ + Author: R. Le Gac +""" + +__version__ = "$Revision$" + + +from gluon.validators import IS_IN_DB + +class ForeignField(dict): + + def __init__(self, db): + """ Scan the database model to identify foreign key, + Store the pointing table and field. + + """ + + for table in db.tables: + fields = [db[table][el] for el in db[table].fields] + + for field in fields: + # + # FOREIGN FIELD + # + # The reference to the foreign table is defined in the + # type: "reference foreignTable". + # The foreign table and the associated column are also + # defined in the validator IS_IN_DB. + # + if field.type.startswith("reference"): + + validators = field.requires + if not isinstance(validators, (list, tuple)): + validators = [field.requires] + + for vdt in validators: + if isinstance(vdt, IS_IN_DB): + + if table not in self: + self[table] = {} + + self[table][field.name] = {} + self[table][field.name]["k_table"] = vdt.ktable + self[table][field.name]["k_field"] = vdt.ks[0] + self[table][field.name]["k_key"] = vdt.ks[1] + + + def is_foreign_field(self, table, field): + """ Return true is the table.field is a foreign key. + + """ + if table in self: + return field in self[table] + + else: + return False + + + def get_foreign_data(self, table, field): + """ Return a dictionary containing the pointing table, field and key. + Otherwise return None. + + """ + if self.is_foreign_field(table, field): + return self[table][field] + + else: + return None + diff --git a/static/appjs/appviewportintree.js b/static/appjs/appviewportintree.js index 46f99ba4..819e9abc 100644 --- a/static/appjs/appviewportintree.js +++ b/static/appjs/appviewportintree.js @@ -5,25 +5,28 @@ * This plugin add a topNode in the tree which contains child. * When a child is clicked a new tab appears in the tab panel. * - * The plugin requires that the property model is set. - * It is a dictionary containing the configuration for the object which - * will be created in the tab. + * The name of the child nodes are defined by the property 'nodes'. + * For example the name of the tables for a database application. * - * The key is the name of the child node appearing in the tree, - * i.e name of the table in a database applications + * The plugin request to the server the model of widget embedded + * in the tab using the cfgSvc web service. + * The type of the widget has to be defined through the property wdgType. + * In the current implementation 'xentry' and 'xgrid' are recognized: . * * The ptype of this component is pintree. * * MANDATORY PROPERTIES: * - models * - topNodeName + * - nodes + * - wdgType * * NOTE: * - the id of leaf node are defined as: - * /App.name/TopNodeName/key + * /App.name/TopNodeName/node * * - the id of create tab is equal to the id of the node: - * /App.name/TopNodeName/key + * /App.name/TopNodeName/node * * @author legac * @version $Id$ @@ -32,19 +35,24 @@ Ext.namespace('App.viewport'); App.viewport.InTreePanel = Ext.extend(Object, { - /** - * @cfg {Object} models configuration object describing - * the object encapsulated in the tab panel. - * This object can be provided by the server. - */ - models: null, - /** * @cfg {String} topNodeName name of the top node associate to this plugin. * */ topNodeName: null, + /** + * @cfg {Array} nodes list of names for the children nodes + + */ + nodes: null, + + /** + * @cfg {String} wdgType type of the widget embeded in the tab. + * The current version of this pulgin understand 'xentry' and 'xgrid' + */ + wdgType: null, + /** * Private parameters referring to the viewport */ @@ -77,11 +85,11 @@ App.viewport.InTreePanel = Ext.extend(Object, { root.appendChild(node); //add the children and the associated handler - for(item in this.models){ + for (var i = 0; i < this.nodes.length; i++){ var child = new Ext.tree.TreeNode({ - id: "/"+App.name+"/"+this.topNodeName+"/"+item, - text: this.models[item].title, + id: "/"+App.name+"/"+this.topNodeName+"/"+this.nodes[i], + text: this.nodes[i], leaf: true, }); @@ -89,52 +97,77 @@ App.viewport.InTreePanel = Ext.extend(Object, { child.on('click', this.onCreateTab, this); } }, - + /** - * Handler to create a tab when a node is click - * @param {Object} node Ext.treeTreeNode + * Private function to create the widget embedded in the tab + * Its configuration is requested to the server via the cfgSvc service + * + * NOTE: + * The underlying technic relies on Ext.Direct (Extjs 3.1.1) + * + * @param {Object} provider + * @param {Object} response */ - onCreateTab: function(node){ - - // is the tab already exists - var tabId = node.id; - if(this.viewport.tabPanel.getItem(tabId)){ - this.viewport.tabPanel.setActiveTab(tabId); - return; - } - - // retrieve the name of the table - // retrieve the name of the top node - var li = tabId.split("/").reverse(); - var table = li[0]; - var topNode = li[1]; + createNewTab: function(provider, response){ + var model = response.result; + + // The id of the node is equal to the id of the tab + var tabId = "/"+App.name+"/"+this.topNodeName+"/"+model.table + // instantiate the object to be load in this tab. // Special processing is require for xgrid - if (this.models[table].xtype == "xgrid") { + if (model.xtype == "xgrid") { var gridConfigurator = new App.grid.Configurator({ debug: App.debug, dbUrl: App.dburl, - gridModel: this.models[table], + gridModel: model, }); var wdg = gridConfigurator.getGrid(); } else { - var cfg = this.models[table]; - var wdg = Ext.ComponentMgr.create(cfg); + var wdg = Ext.ComponentMgr.create(model); } - + // create a panel with the user widget and the tab var pan = new Ext.Panel({ id: tabId, closable: true, - title: topNode+" "+table, + title: this.topNodeName+" "+model.table, items: [wdg], }); var tab = this.viewport.tabPanel.add(pan); this.viewport.tabPanel.setActiveTab(tab.id); + }, + /** + * Handler to create a tab when a node is click + * @param {Object} node Ext.treeTreeNode + */ + onCreateTab: function(node){ + + // is the tab already exists + var tabId = node.id; + if(this.viewport.tabPanel.getItem(tabId)){ + this.viewport.tabPanel.setActiveTab(tabId); + return; + } + + // retrieve the name of the table from the tabId + // /App.name/TopNodeName/table + var li = tabId.split("/").reverse(); + var table = li[0]; + + // request the configuration object and instanciate it + if (this.wdgType == 'xentry') + cfgSvc.getFormModel(table, this.createNewTab, this); + + else if (this.wdgType == 'xgrid') + cfgSvc.getGridModel(table, this.createNewTab, this); + + else + throw new Error("the poperty wdgType for App.viewport.InTreePanel is not defined.") }, }); diff --git a/static/script/viewport_1.js b/static/script/viewport_1.js index 739354bb..c81ce440 100644 --- a/static/script/viewport_1.js +++ b/static/script/viewport_1.js @@ -1,52 +1,48 @@ /** * viewport_1.js - * To test and debug application viewport. + * To test and debug application viewport using remote service. * * $Id$ * - * TODO: the editform widget does not dialog with the database ? why ? - * TODO: create configuration object with layout - * */ /** * main function - * The scope, this, contains the models return by the server + * */ -var main = function(){ - // configuration model - var model = this; - -// console.log("Models"); -// console.dir(model); +var main = function(provider, response){ -// console.log("Model form"); -// console.dir(model.form); + // list of tables removing excluded tables + var tables = response.result.filter(function(element, index, array){ + return App.excludedTables.indexOf(element) == -1 ? true : false + }); + /* * Plugin to handle entryform */ var formInTree = { - models: model.form, topNodeName: 'Forms', + nodes: tables, + wdgType: 'xentry', ptype: 'pintree', }; + /* * Plugin to handle grid */ var tableInTree = { - models: model.grid, topNodeName: 'Tables', + nodes: tables, + wdgType: 'xgrid', ptype: 'pintree', }; /* * Pre-configured viewport with a treepanel and a tab panel - * The id of the root node is App.name - * the id of the first tab is /tab/welcome */ var cfg = Ext.apply({ @@ -61,20 +57,13 @@ Ext.onReady(function(){ Ext.QuickTips.init(); - // Request widget models from the server - // It is strongly relate'd to the database model - var models = new App.Cfg({ - url: App.cfgurl, - }); - - // The response of the server is asynchronous. - // When the model is load launch the main function - models.on('cfgloaded', main, models); - - //request the models from the server - models.load({ - debug: App.debug, - exclude: ["systemsAddRpms", "systemsRemoveRpms"], - }) + // Expose the remote service cfgSvc + Ext.Direct.addProvider(App.cfgSvcMethods); + // Define list of tables not shown in the user interface + App.excludedTables = ["systemsAddRpms", "systemsRemoveRpms"]; + + // Request the list of tables and launch the main application + cfgSvc.getTables(main); + }); \ No newline at end of file diff --git a/views/applayout.html b/views/applayout.html index d0b8daac..6354958b 100644 --- a/views/applayout.html +++ b/views/applayout.html @@ -25,7 +25,7 @@ App.name = '{{=request.application}}'; App.debug = {{=str(appdebug).lower()}}; App.cfgSvcMethods = { - "url": '/{{=request.application}}/foo', + "url": '/{{=request.application}}/configuration', "type":"remoting", "actions": { "cfgSvc": {{=cfgSvcMethods}} -- GitLab