From ed8600ef89218d5fb04cf00c97b17e06cbe369b8 Mon Sep 17 00:00:00 2001 From: Khushboo Vashi Date: Tue, 17 Nov 2015 11:51:09 +0530 Subject: [PATCH] Adding support for collection browser node. --- web/pgadmin/browser/__init__.py | 10 +- web/pgadmin/browser/collection.py | 208 ++++++++++++++++++ .../browser/server_groups/servers/__init__.py | 2 +- .../templates/browser/css/collection.css | 7 + .../browser/templates/browser/js/browser.js | 5 +- .../templates/browser/js/collection.js | 119 ++++++++++ .../browser/templates/browser/js/node.js | 111 +--------- web/pgadmin/browser/utils.py | 10 +- web/pgadmin/static/js/backform.pgadmin.js | 61 ++--- 9 files changed, 394 insertions(+), 139 deletions(-) create mode 100644 web/pgadmin/browser/collection.py create mode 100644 web/pgadmin/browser/templates/browser/css/collection.css create mode 100644 web/pgadmin/browser/templates/browser/js/collection.js diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index 876e93224..b91a777c8 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -119,7 +119,8 @@ class BrowserModule(PgAdminModule): for name, script in [ ['pgadmin.browser', 'js/browser'], ['pgadmin.browser.error', 'js/error'], - ['pgadmin.browser.node', 'js/node']]: + ['pgadmin.browser.node', 'js/node'], + ['pgadmin.browser.collection', 'js/collection']]: scripts.append({ 'name': name, 'path': url_for('browser.index') + script, @@ -297,6 +298,13 @@ def node_js(): render_template('browser/js/node.js', _=gettext), 200, {'Content-Type': 'application/x-javascript'}) +@blueprint.route("/js/collection.js") +@login_required +def collection_js(): + return make_response( + render_template('browser/js/collection.js', _=gettext), + 200, {'Content-Type': 'application/x-javascript'}) + @blueprint.route("/browser.css") @login_required diff --git a/web/pgadmin/browser/collection.py b/web/pgadmin/browser/collection.py new file mode 100644 index 000000000..ab902f054 --- /dev/null +++ b/web/pgadmin/browser/collection.py @@ -0,0 +1,208 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2015, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import six +from abc import ABCMeta, abstractmethod +from flask import url_for, render_template +from flask.ext.babel import gettext +from pgadmin.browser.utils import PgAdminModule, PGChildModule +from pgadmin.browser import BrowserPluginModule +from pgadmin.browser.utils import NodeView, PGChildNodeView + +@six.add_metaclass(ABCMeta) +class CollectionNodeModule(PgAdminModule, PGChildModule): + """ + Base class for collection node submodules. + """ + browser_url_prefix = BrowserPluginModule.browser_url_prefix + + def __init__(self, import_name, **kwargs): + kwargs.setdefault("url_prefix", self.node_path) + kwargs.setdefault("static_url_path", '/static') + super(CollectionNodeModule, self).__init__( + "NODE-%s" % self.node_type, + import_name, + **kwargs + ) + + @property + def jssnippets(self): + """ + Returns a snippet of javascript to include in the page + """ + return [] + + def get_own_javascripts(self): + scripts = [] + + scripts.extend([{ + 'name': 'pgadmin.node.%s' % self.node_type, + 'path': url_for('browser.index') + '%s/module' % self.node_type, + 'when': self.script_load + }]) + + for module in self.submodules: + scripts.extend(module.get_own_javascripts()) + + return scripts + + def generate_browser_node( + self, node_id, parents, label, icon, **kwargs + ): + obj = { + "id": "%s/%s" % (self.node_type, node_id), + "label": label, + "icon": self.node_icon if not icon else icon, + "inode": self.node_inode, + "_type": self.node_type, + "_id": node_id, + "refid": parents, + "module": 'pgadmin.node.%s' % self.node_type + } + for key in kwargs: + obj.setdefault(key, kwargs[key]) + return obj + + def generate_browser_collection_node(self, sid, parents, **kwargs): + obj = { + "id": "coll-%s/%d" % (self.node_type, sid), + "label": self.collection_label, + "icon": self.collection_icon, + "inode": True, + "_type": 'coll-%s' % (self.node_type), + "_id": sid, + "refid": parents, + "module": 'pgadmin.node.%s' % self.node_type + } + + for key in kwargs: + obj.setdefault(key, kwargs[key]) + + return obj + + @property + def node_type(self): + return '%s-%s' % (self.SERVER_TYPE, self.NODE_TYPE) + + @property + def csssnippets(self): + """ + Returns a snippet of css to include in the page + """ + snippets = [ + render_template( + "browser/css/collection.css", + node_type=self.node_type, + _=gettext + ), + render_template( + "browser/css/node.css", + node_type=self.node_type, + _=gettext + ) + ] + + for submodule in self.submodules: + snippets.extend(submodule.csssnippets) + + print "snipp==",snippets + return snippets + + @property + def collection_label(self): + """ + Label to be shown for the collection node, do not forget to set the + class level variable COLLECTION_LABEL. + """ + return self.COLLECTION_LABEL + + @property + def collection_icon(self): + """ + icon to be displayed for the browser collection node + """ + return 'icon-coll-%s' % (self.node_type) + + @property + def node_icon(self): + """ + icon to be displayed for the browser nodes + """ + return 'icon-%s' % (self.node_type) + + @property + def node_inode(self): + """ + Override this property to make the node as leaf node. + """ + return True + + @abstractmethod + def get_nodes(self, sid, *args, **kwargs): + """ + Generate the collection node + You need to override this method because every node will have different + url pattern for the parent. + """ + pass + + @property + def script_load(self): + """ + This property defines, when to load this script. + In order to allow creation of an object, we need to load script for any + node at the parent level. + + i.e. + - In order to allow creating a server object, it should be loaded at + server-group node. + """ + pass + + @property + def node_path(self): + return self.browser_url_prefix + self.node_type + + @property + def javascripts(self): + return [] + + +class CollectionNodeView(NodeView, PGChildNodeView): + """ + A PostgreSQL Collection node has specific functions needs to address + i.e. + - List the nodes + - Get the list of nodes objects (model) + + This class can be inherited to achieve the diffrent routes for each of the + object types/collections. + + OPERATION | URL | Method + ---------------+------------------------+-------- + List | /coll/[Parent URL]/ | GET + Children Nodes | /node/[Parent URL]/ | GET + + NOTE: + Parent URL can be seen as the path to identify the particular node. + """ + operations = dict({ + }) + operations = dict({ + 'obj': [ + {'get': 'properties', 'delete': 'delete', 'put': 'update'}, + {'get': 'list', 'post': 'create'} + ], + 'nodes': [{'get': 'nodes'}], + 'sql': [{'get': 'sql', 'post': 'modified_sql'}], + 'stats': [{'get': 'statistics'}], + 'deps': [{'get': 'dependencies', 'post': 'dependents'}], + 'module.js': [{}, {}, {'get': 'module_js'}], + 'coll': [{}, {'get': 'collections'}] + }) diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 22f3c4d52..323f2e3d1 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -584,7 +584,7 @@ class ServerNode(NodeView): if e.message: return internal_server_error(errormsg=e.message) else: - return internal_server_error(errormsg=str(e)) + return internal_server_error(errormsg=str(e)) if not status: current_app.logger.error( diff --git a/web/pgadmin/browser/templates/browser/css/collection.css b/web/pgadmin/browser/templates/browser/css/collection.css new file mode 100644 index 000000000..84325a153 --- /dev/null +++ b/web/pgadmin/browser/templates/browser/css/collection.css @@ -0,0 +1,7 @@ +.icon-coll-{{node_type}} { + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/coll-%s.png' % node_type )}}') !important; + background-repeat: no-repeat; + align-content: center; + vertical-align: middle; + height: 1.3em; +} \ No newline at end of file diff --git a/web/pgadmin/browser/templates/browser/js/browser.js b/web/pgadmin/browser/templates/browser/js/browser.js index c2ba09633..ee4984d64 100644 --- a/web/pgadmin/browser/templates/browser/js/browser.js +++ b/web/pgadmin/browser/templates/browser/js/browser.js @@ -356,7 +356,9 @@ OWNER TO helpdesk;\n'; ajaxHook: function(item, settings) { if (item != null) { var d = this.itemData(item); - settings.url = '{{ url_for('browser.index') }}' + d._type + '/nodes/' + (d.refid ? d.refid + '/' : '') + d._id + n = obj.Nodes[d._type]; + if (n) + settings.url = n.generate_url('nodes', d, true); } } }); @@ -549,7 +551,6 @@ OWNER TO helpdesk;\n'; add_menus: function(menus) { var pgMenu = this.menus; var MenuItem = pgAdmin.Browser.MenuItem; - _.each(menus, function(m) { _.each(m.applies, function(a) { /* We do support menu type only from this list */ diff --git a/web/pgadmin/browser/templates/browser/js/collection.js b/web/pgadmin/browser/templates/browser/js/collection.js new file mode 100644 index 000000000..96c49fc59 --- /dev/null +++ b/web/pgadmin/browser/templates/browser/js/collection.js @@ -0,0 +1,119 @@ +define( + ['jquery', 'underscore', 'underscore.string', 'pgadmin', + 'backbone', 'alertify', 'backform', 'pgadmin.backform', + 'pgadmin.backgrid', 'pgadmin.browser.node' + ], +function($, _, S, pgAdmin, Backbone, Alertify, Backform) { + + var pgBrowser = pgAdmin.Browser = pgAdmin.Browser || {}; + + // It has already been defined. + // Avoid running this script again. + if (pgBrowser.Collection) + return pgBrowser.Collection; + + pgBrowser.Collection = _.extend(_.clone(pgBrowser.Node), { + /////// + // Initialization function + // Generally - used to register the menus for this type of node. + // + // Also, look at pgAdmin.Browser.add_menus(...) function. + // + // Collection will not have 'Properties' menu. + // + // NOTE: Override this for each node for initialization purpose + Init: function() { + if (this.node_initialized) + return; + this.node_initialized = true; + pgAdmin.Browser.add_menus([{ + name: 'refresh', node: this.type, module: this, + applies: ['object', 'context'], callback: 'refresh_node', + priority: 1, label: '{{ _("Refresh...") }}', + icon: 'fa fa-refresh' + }]); + }, + showProperties: function(item, data, panel) { + var that = this, + j = panel.$container.find('.obj_properties').first(), + view = j.data('obj-view'), + content = $('
') + .addClass('pg-prop-content col-xs-12'), + node = pgBrowser.Nodes[that.node], + // This will be the URL, used for object manipulation. + urlBase = this.generate_url('properties', data), + collections = new (node.Collection.extend({ + url: urlBase, + model: node.model + }))(), + gridSchema = Backform.generateGridColumnsFromModel( + node.model, 'prorperties', that.columns + ), + // Initialize a new Grid instance + grid = new Backgrid.Grid({ + columns: gridSchema.columns, + collection: collections, + className: "backgrid table-bordered" + }), + gridView = { + 'remove': function() { + if (this.grid) { + delete (this.grid); + this.grid = null; + } + } + }; + gridView.grid = grid; + + if (view) { + // Release the view + view.remove(); + // Deallocate the view + delete view; + view = null; + // Reset the data object + j.data('obj-view', null); + } + + // Make sure the HTML element is empty. + j.empty(); + j.data('obj-view', gridView); + + // Render subNode grid + content.append(grid.render().$el); + j.append(content); + + // Fetch Data + collections.fetch({reset: true}); + }, + generate_url: function(type, d) { + var url = pgAdmin.Browser.URL + '{TYPE}/{REDIRECT}{REF}', + ref = S('/%s/').sprintf(d._id).value(), + /* + * Using list, and collections functions of a node to get the nodes + * under the collection, and properties of the collection respectively. + */ + opURL = { + 'nodes': 'obj', 'properties': 'coll' + }; + if (d._type == this.type) { + if (d.refid) + ref = S('/%s/').sprintf(d.refid).value(); + } + + var TYPE = d.module.split("."); + var args = {'TYPE': TYPE[TYPE.length-1], 'REDIRECT': '', 'REF': ref}; + if (type in opURL) { + args.REDIRECT = opURL[type]; + } else { + args.REDIRECT = type; + } + + return url.replace(/{(\w+)}/g, function(match, arg) { + return args[arg]; + }); + } + }); + + return pgAdmin.Browser.Collection; +}); diff --git a/web/pgadmin/browser/templates/browser/js/node.js b/web/pgadmin/browser/templates/browser/js/node.js index d959ec88d..0c6f7d119 100644 --- a/web/pgadmin/browser/templates/browser/js/node.js +++ b/web/pgadmin/browser/templates/browser/js/node.js @@ -803,9 +803,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) { opURL = { 'create': 'obj', 'drop': 'obj', 'edit': 'obj', 'properties': 'obj', 'depends': 'deps', - 'statistics': 'stats', 'collections': 'nodes' + 'statistics': 'stats', 'nodes': 'nodes' }; - if (d._type == this.type) { ref = ''; if (d.refid) @@ -964,6 +963,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) { self.on('add', self.onModelAdd); self.on('remove', self.onModelRemove); self.on('change', self.onModelChange); + + return self; }, startNewSession: function() { var self = this; @@ -1171,8 +1172,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) { self.onChangeData = options.onChangeData; self.onChangeCallback = options.onChangeCallback; - if (this.schema && _.isArray(this.schema)) { - _.each(this.schema, function(s) { + if (self.schema && _.isArray(self.schema)) { + _.each(self.schema, function(s) { var obj = null; switch(s.type) { case 'collection': @@ -1208,6 +1209,8 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) { self.set(s.id, obj, {silent: true}); }); } + + return self; }, onChange: function() { var self = this; @@ -1337,105 +1340,5 @@ function($, _, S, pgAdmin, Menu, Backbone, Alertify, Backform) { }) }); - pgBrowser.Collection = _.extend(_.clone(pgAdmin.Browser.Node), { - /////// - // Initialization function - // Generally - used to register the menus for this type of node. - // - // Also, look at pgAdmin.Browser.add_menus(...) function. - // - // Collection will not have 'Properties' menu. - // - // NOTE: Override this for each node for initialization purpose - Init: function() { - if (this.node_initialized) - return; - this.node_initialized = true; - - pgAdmin.Browser.add_menus([{ - name: 'refresh', node: this.type, module: this, - applies: ['object', 'context'], callback: 'refresh_collection', - priority: 2, label: '{{ _("Refresh...") }}', - icon: 'fa fa-refresh' - }]); - }, - callbacks: { - refresh_collection: function() { - // TODO:: Refresh the collection node - console.log(arguments); - }, - selected: function(o) { - // Show (Node information on these panels, which one is visible.) - // + Properties (list down the children nodes in pages) - // + Query (Remove existing SQL) - // + Dependents (Remove dependents) - // + Dependencies (Remove dependencies) - // + Statistics (TODO:: Check the current implementation in pgAdmin 3) - - // Update the menu items - pgAdmin.Browser.enable_disable_menus.apply(o.browser, [o.item]); - - if (o && o.data && o.browser) { - var br = o.browser; - if ('properties' in br.panels && - br.panels['properties'] && - br.panels['properties'].panel && - br.panels['properties'].panel.isVisible()) { - // Show object properties (only when the 'properties' tab - // is active). - this.showProperties(o.item, o.data, - pgBrowser.panels['properties'].panel); - } - if ('sql' in br.panels && - br.panels['sql'] && - br.panels['sql'].panel && - br.panels['sql'].panel.isVisible()) { - // TODO:: - // Remove the information from the sql pane - } - if ('statistics' in br.panels && - br.panels['statistics'] && - br.panels['statistics'].panel && - br.panels['statistics'].panel.isVisible()) { - // TODO:: - // Remove information from the statistics pane - } - if ('dependencies' in br.panels && - br.panels['dependencies'] && - br.panels['dependencies'].panel && - br.panels['dependencies'].panel.isVisible()) { - // TODO:: - // Remove information from the dependencies pane - } - if ('dependents' in br.panels && - br.panels['dependents'] && - br.panels['dependents'].panel && - br.panels['dependents'].panel.isVisible()) { - // TODO:: - // Remove information from the dependents pane - } - } - } - }, - /********************************************************************** - * A hook (not a callback) to show object properties in given HTML - * element. - * - * This has been used for the showing, editing properties of the node. - * This has also been used for creating a node. - **/ - showProperties: function(item, data, panel) { - var that = this, - tree = pgAdmin.Browser.tree, - j = panel.$container.find('.obj_properties').first(), - view = j.data('obj-view'), - content = $('
') - .addClass('pg-prop-content col-xs-12'); - - // TODO:: Show list of children in paging mode - return; - } - }); - return pgAdmin.Browser.Node; }); diff --git a/web/pgadmin/browser/utils.py b/web/pgadmin/browser/utils.py index 89e2ff03a..4b5669564 100644 --- a/web/pgadmin/browser/utils.py +++ b/web/pgadmin/browser/utils.py @@ -63,7 +63,6 @@ class PGChildModule(object): def BackendSupported(self, mangaer): sversion = getattr(mangaer, 'sversion', None) - if (sversion is None or not isinstance(sversion, int)): return False @@ -180,7 +179,6 @@ class NodeView(with_metaclass(MethodViewType, View)): 'with_id': (idx != 2), 'methods': meths }) idx += 1 - return cmds # Inherited class needs to modify these parameters @@ -321,9 +319,9 @@ class NodeView(with_metaclass(MethodViewType, View)): return make_json_response(data=nodes) -class PGChildNodeView(NodeView): +class PGChildNodeView(object): - def nodes(self, sid=None, **kwargs): + def nodes(self, sid, **kwargs): """Build a list of treeview nodes from the child nodes.""" from pgadmin.utils.driver import get_driver @@ -334,8 +332,8 @@ class PGChildNodeView(NodeView): if isinstance(module, ServerChildModule): if sid is not None and manager is not None and \ module.BackendSupported(manager): - nodes.extend(module.get_nodes(*args, **kwargs)) + nodes.extend(module.get_nodes(sid=sid, **kwargs)) else: - nodes.extend(module.get_nodes(*args, **kwargs)) + nodes.extend(module.get_nodes(sid=sid, **kwargs)) return make_json_response(data=nodes) diff --git a/web/pgadmin/static/js/backform.pgadmin.js b/web/pgadmin/static/js/backform.pgadmin.js index f460f074c..2de7cc86f 100644 --- a/web/pgadmin/static/js/backform.pgadmin.js +++ b/web/pgadmin/static/js/backform.pgadmin.js @@ -296,6 +296,33 @@ events: {} }); + var generateGridColumnsFromModel = Backform.generateGridColumnsFromModel = function(m, type, cols) { + var groups = Backform.generateViewSchema(m, type), + schema = [], + columns = [], + addAll = _.isUndefined(cols) || _.isNull(cols); + + // Prepare columns for backgrid + _.each(groups, function(fields, key) { + _.each(fields, function(f) { + if (!f.control && !f.cell) { + return; + } + f.cell_priority = _.indexOf(cols, f.name); + if (addAll || f.cell_priority != -1) { + columns.push(f); + } + }); + schema.push({label: key, fields: fields}); + }); + return { + 'columns': _.sortBy(columns, function(c) { + return c.cell_priority; + }), + 'schema': schema + }; + } + var SubNodeCollectionControl = Backform.SubNodeCollectionControl = Backform.Control.extend({ render: function() { var field = _.defaults(this.field.toJSON(), this.defaults), @@ -339,24 +366,9 @@ gridBody = $("
").append(gridHeader); var subnode = data.subnode.schema ? data.subnode : data.subnode.prototype, - columns = [], - gridColumns = [], - groups = Backform.generateViewSchema(subnode, this.field.get('mode')), - schema = []; - - // Prepare columns for backgrid - _.each(groups, function(fields, key) { - _.each(fields, function(f) { - if (!f.control && !f.cell) { - return; - } - f.cel_priority = _.indexOf(data.columns, f.name); - if (f.cel_priority != -1) { - columns.push(f); - } - }); - schema.push({label: key, fields: fields}); - }); + gridSchema = Backform.generateGridColumnsFromModel( + subnode, this.field.get('mode'), data.columns + ); // Set visibility of Add button if (data.disabled || data.canAdd == false) { @@ -365,30 +377,29 @@ // Insert Delete Cell into Grid if (data.disabled == false && data.canDelete) { - columns.unshift({ + gridSchema.columns.unshift({ name: "pg-backform-delete", label: "", cell: Backgrid.Extension.DeleteCell, - editable: false, priority: -1 + editable: false, cell_priority: -1 }); } // Insert Edit Cell into Grid if (data.disabled == false && data.canEdit) { var editCell = Backgrid.Extension.ObjectCell.extend({ - schema: schema + schema: gridSchema.schema }); - columns.unshift({ + gridSchema.columns.unshift({ name: "pg-backform-edit", label: "", cell : editCell, - priority: -2 + cell_priority: -2 }); } var collections = this.model.get(data.name); - // Initialize a new Grid instance var grid = new Backgrid.Grid({ - columns: _.sortBy(columns, function(c) { return c.cell_priority; }), + columns: gridSchema.columns, collection: collections, className: "backgrid table-bordered" });