From 35d01bea3ebda80f0e52e55446b34e3950452986 Mon Sep 17 00:00:00 2001 From: Ashesh Vashi Date: Mon, 29 Jun 2015 13:41:56 +0530 Subject: [PATCH] Introduced a class - NodeView to achieve REST API required by the PostgreSQL node(s). Browser Tree Node (PostgreSQL object) requires more than just CRUD. i.e. - CRUD (Create, Read, Update & Delete) - Reversed Engineered SQL for the object - Modified Query in edit mode i.e. ALTER TABLE ... - Statistics - List of dependents - List of dependencies - Children node list This class can be inherited to achieve the different routes for each of the object types/collections. OPERATION | URL | Method ---------------+------------------------+-------- List | /obj/[Parent URL]/ | GET Properties | /obj/[Parent URL]/id | GET Create | /obj/[Parent URL]/ | POST Delete | /obj/[Parent URL]/id | DELETE Update | /obj/[Parent URL]/id | PUT SQL (Reversed | /sql/[Parent URL]/id | GET Engineering) | SQL (Modified | /sql/[Parent URL]/id | POST Properties) | Statistics | /stats/[Parent URL]/id | GET Dependencies | /deps/[Parent URL]/id | GET Dependents | /deps/[Parent URL]/id | POST Children Nodes | /nodes/[Parent URL]/id | GET NOTE: Parent URL can be seen as the path to identify the particular node. i.e. In order to identify the TABLE object, we requires information about the server -> database -> schema objects. Hence, the Parent URL for the TABLE object will be something like this as below: // Inherited a new classes ServerGroupView and ServerView, which are inherited from the NodeView for the implementation of above operations. --- web/pgAdmin4.py | 4 +- web/pgadmin/browser/__init__.py | 37 ++- web/pgadmin/browser/server_groups/__init__.py | 201 +++++++----- .../browser/server_groups/servers/__init__.py | 293 +++++++++++++----- .../servers/templates/servers/servers.js | 32 +- .../templates/server_groups/server_groups.js | 23 +- .../browser/templates/browser/css/node.css | 2 +- .../browser/templates/browser/js/browser.js | 6 + web/pgadmin/browser/utils.py | 202 +++++++++--- web/pgadmin/settings/settings_model.py | 26 +- web/pgadmin/templates/base.html | 24 +- web/setup.py | 13 + 12 files changed, 603 insertions(+), 260 deletions(-) diff --git a/web/pgAdmin4.py b/web/pgAdmin4.py index 9d2cff2dc..237cb3212 100644 --- a/web/pgAdmin4.py +++ b/web/pgAdmin4.py @@ -50,8 +50,8 @@ if not os.path.isfile(config.SQLITE_PATH): # Create the app! app = create_app() -#if config.DEBUG: -# app.debug = True +if config.DEBUG: + app.debug = True # Start the web server. The port number should have already been set by the # runtime if we're running in desktop mode, otherwise we'll just use the diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index e340c836d..ae4210449 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -21,25 +21,23 @@ MODULE_NAME = 'browser' class BrowserModule(PgAdminModule): - def get_own_stylesheets(self): stylesheets = [] # Add browser stylesheets for (endpoint, filename) in [ ('static', 'css/codemirror/codemirror.css'), - ('static', 'css/wcDocker/theme.css'), ('static', 'css/jQuery-contextMenu/jquery.contextMenu.css'), - ('browser.static', 'css/browser.css'), - ('browser.static', 'css/aciTree/css/aciTree.css') + ('static', 'css/wcDocker/wcDockerSkeleton.css' if \ + current_app.debug else \ + 'css/wcDocker/wcDockerSkeleton.min.css'), + ('static', 'css/wcDocker/theme.css'), + ('browser.static', 'css/aciTree/css/aciTree.css'), ]: stylesheets.append(url_for(endpoint, filename=filename)) stylesheets.append(url_for('browser.browser_css')) - if current_app.debug: - stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.css')) - else: - stylesheets.append(url_for('static', filename='css/wcDocker/wcDockerSkeleton.min.css')) return stylesheets + def get_own_javascripts(self): scripts = [] for (endpoint, filename) in [ @@ -74,7 +72,7 @@ class BrowserPluginModule(PgAdminModule): def __init__(self, import_name, **kwargs): kwargs.setdefault("url_prefix", self.node_path) - kwargs.setdefault("static_url_path", '') + kwargs.setdefault("static_url_path", 'static') super(BrowserPluginModule, self).__init__("NODE-%s" % self.node_type, import_name, **kwargs) @@ -95,8 +93,8 @@ class BrowserPluginModule(PgAdminModule): Returns a snippet of css to include in the page """ # TODO: move those methods to BrowserModule subclass ? - return render_template("browser/css/node.css", - node_type=self.node_type) + return [render_template("browser/css/node.css", + node_type=self.node_type)] @abstractmethod def get_nodes(self): @@ -137,11 +135,12 @@ def browser_js(): snippets = [] for submodule in current_blueprint.submodules: snippets.extend(submodule.jssnippets) - return make_response(render_template( - 'browser/js/browser.js', - layout=layout, - jssnippets=snippets), - 200, {'Content-Type': 'application/x-javascript'}) + return make_response( + render_template( + 'browser/js/browser.js', + layout=layout, + jssnippets=snippets), + 200, {'Content-Type': 'application/x-javascript'}) @blueprint.route("/browser.css") @login_required @@ -150,9 +149,9 @@ def browser_css(): snippets = [] for submodule in current_blueprint.submodules: snippets.extend(submodule.csssnippets) - return make_response(render_template('browser/css/browser.css', - snippets=snippets), - 200, {'Content-Type': 'text/css'}) + return make_response( + render_template('browser/css/browser.css', snippets=snippets), + 200, {'Content-Type': 'text/css'}) @blueprint.route("/nodes/") diff --git a/web/pgadmin/browser/server_groups/__init__.py b/web/pgadmin/browser/server_groups/__init__.py index bfcc7f333..b5a4167a9 100644 --- a/web/pgadmin/browser/server_groups/__init__.py +++ b/web/pgadmin/browser/server_groups/__init__.py @@ -18,10 +18,10 @@ from pgadmin.utils.ajax import make_json_response from pgadmin.browser import BrowserPluginModule from pgadmin.utils.menu import MenuItem from pgadmin.settings.settings_model import db, ServerGroup +from pgadmin.browser.utils import generate_browser_node import config - class ServerGroupModule(BrowserPluginModule): NODE_TYPE = "server-group" @@ -65,18 +65,20 @@ class ServerGroupModule(BrowserPluginModule): # TODO: Move this JSON generation to a Server method # this code is duplicated somewhere else for group in groups: - yield { - "id": "%s/%d" % (self.node_type, group.id), - "label": group.name, - "icon": "icon-%s" % self.node_type, - "inode": True, - "_type": self.node_type - } + yield generate_browser_node( + "%d" % (group.id), + group.name, + "icon-%s" % self.node_type, + True, + self.node_type) @property def node_type(self): return self.NODE_TYPE + @property + def node_path(self): + return '/browser/' + self.node_type class ServerGroupMenuItem(MenuItem): @@ -99,109 +101,142 @@ class ServerGroupPluginModule(BrowserPluginModule): pass -# Initialise the module + @property + def node_path(self): + return '/browser/' + self.node_type + + blueprint = ServerGroupModule( __name__, static_url_path='') -@blueprint.route("/") -@login_required -def get_nodes(server_group): - """Build a list of treeview nodes from the child nodes.""" - nodes = [] - for module in current_blueprint.submodules: - nodes.extend(module.get_nodes(server_group=server_group)) - return make_json_response(data=nodes) +# Initialise the module +from pgadmin.browser.utils import NodeView -@blueprint.route('/add/', methods=['POST']) -@login_required -def add(): - """Add a server group node to the settings database""" - success = 1 - errormsg = '' - data = { } +class ServerGroupView(NodeView): - if request.form['name'] != '': - servergroup = ServerGroup(user_id=current_user.id, name=request.form['name']) + node_type = ServerGroupModule.NODE_TYPE + parent_ids = [] + ids = [{'type':'int', 'id':'gid'}] - try: - db.session.add(servergroup) - db.session.commit() - except Exception as e: - success = 0 - errormsg = e.message - else: - success = 0 - errormsg = gettext('No server group name was specified') + def list(self): + res = [] + for g in blueprint.get_nodes(): + res.append(g) + return make_json_response(result=res) - if success == 1: - data['id'] = servergroup.id - data['name'] = servergroup.name - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form, - data=data) + def delete(self, gid): + """Delete a server group node in the settings database""" -@blueprint.route('/delete/', methods=['POST']) -@login_required -def delete(): - """Delete a server group node in the settings database""" - success = 1 - errormsg = '' - - if request.form['id'] != '': # There can be only one record at most - servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first() + servergroup = ServerGroup.query.filter_by( + user_id=current_user.id, + id=gid) if servergroup is None: - success = 0 - errormsg = gettext('The specified server group could not be found.') + return make_json_response( + success=0, + errormsg=gettext('The specified server group could not be found.')) else: try: db.session.delete(servergroup) db.session.commit() except Exception as e: - success = 0 - errormsg = e.message + return make_json_response(success=0, errormsg=e.message) - else: - success = 0 - errormsg = gettext('No server group was specified.') + return make_json_response(result=request.form) - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form) -@blueprint.route('/rename/', methods=['POST']) -@login_required -def rename(): - """Rename a server group node in the settings database""" - success = 1 - errormsg = '' + def update(self, gid): + """Update the server-group properties""" - if request.form['id'] != '': # There can be only one record at most - servergroup = ServerGroup.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first() + servergroup = ServerGroup.query.filter_by( + user_id=current_user.id, + id=gid).first() if servergroup is None: - success = 0 - errormsg = gettext('The specified server group could not be found.') + return make_json_response( + success=0, + errormsg=gettext('The specified server group could not be found.')) else: try: - servergroup.name = request.form['name'] + if 'name' in request.form: + servergroup.name = request.form['name'] db.session.commit() except Exception as e: - success = 0 - errormsg = e.message + return make_json_response(success=0, errormsg=e.message) - else: - success = 0 - errormsg = gettext('No server group was specified.') + return make_json_response(result=request.form) - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form) + + def properties(self, gid): + """Update the server-group properties""" + + # There can be only one record at most + sg = ServerGroup.query.filter_by( + user_id=current_user.id, + id=gid).first() + data = {} + + if sg is None: + return make_json_response( + success=0, + errormsg=gettext('The specified server group could not be found.')) + else: + return make_json_response(data={'id': sg.id, 'name': sg.name}) + + + def create(self): + data = [] + if request.form['name'] != '': + servergroup = ServerGroup( + user_id=current_user.id, + name=request.form['name']) + try: + db.session.add(servergroup) + db.session.commit() + + data['id'] = servergroup.id + data['name'] = servergroup.name + except Exception as e: + return make_json_response(success=0, errormsg=e.message) + + else: + return make_json_response( + success=0, + errormsg=gettext('No server group name was specified')) + + return make_json_response(data=data) + + + def nodes(self, gid): + """Build a list of treeview nodes from the child nodes.""" + nodes = [] + for module in blueprint.submodules: + nodes.extend(module.get_nodes(server_group=gid)) + return make_json_response(data=nodes) + + + def sql(self, gid): + return make_json_response(data='') + + + def modified_sql(self, gid): + return make_json_response(data='') + + + def statistics(self, gid): + return make_json_response(data='') + + + def dependencies(self, gid): + return make_json_response(data='') + + + def dependents(self, gid): + return make_json_response(data='') + + +ServerGroupView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 855bb19b2..68e501c8e 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -9,9 +9,10 @@ from flask import render_template, request from pgadmin.browser.server_groups import ServerGroupPluginModule from flask.ext.security import login_required, current_user -from pgadmin.settings.settings_model import db, Server +from pgadmin.settings.settings_model import db, Server, ServerGroup from pgadmin.utils.menu import MenuItem from pgadmin.utils.ajax import make_json_response +from pgadmin.browser.utils import generate_browser_node, NodeView import traceback from flask.ext.babel import gettext @@ -30,13 +31,13 @@ class ServerModule(ServerGroupPluginModule): # TODO: Move this JSON generation to a Server method for server in servers: - yield { - "id": "%s/%d" % (NODE_TYPE, server.id), - "label": server.name, - "icon": "icon-%s" % NODE_TYPE, - "inode": True, - "_type": NODE_TYPE - } + yield generate_browser_node( + "%d" % server.id, + server.name, + "icon-%s" % self.NODE_TYPE, + True, + self.NODE_TYPE + ) def get_own_menuitems(self): return { @@ -49,13 +50,13 @@ class ServerModule(ServerGroupPluginModule): name="create_server", label=gettext('Server...'), priority=50, - function='create_server') + function='create_server(item)') ], 'context_items': [ ServerMenuItem(name='delete_server', label=gettext('Delete server'), priority=50, - onclick='drop_server'), + onclick='drop_server(item)'), ServerMenuItem(name='rename_server', label=gettext('Rename server...'), priority=60, @@ -75,98 +76,224 @@ class ServerMenuItem(MenuItem): kwargs.setdefault("type", ServerModule.NODE_TYPE) super(ServerMenuItem, self).__init__(**kwargs) + blueprint = ServerModule(__name__) -@blueprint.route('/add/', methods=['POST']) -@login_required -def add(): - """Add a server node to the settings database""" - success = 1 - errormsg = '' - data = {} - success = False - errormsg = '' - if request.form['name'] != '': - server = Server(user_id=current_user.id, name=request.form['name']) - try: - db.session.add(server) - db.session.commit() - success = True - except Exception as e: - errormsg = e.message - else: - errormsg = gettext('No server name was specified') +class ServerNode(NodeView): - if success: - data['id'] = server.id - data['name'] = server.name + node_type = ServerModule.NODE_TYPE + parent_ids = [{'type':'int', 'id':'gid'}] + ids = [{'type':'int', 'id':'sid'}] - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form, - data=data) -@blueprint.route('/delete/', methods=['POST']) -@login_required -def delete(): - """Delete a server node in the settings database""" - success = 1 - errormsg = '' + def list(self, gid): + res = [] + """Return a JSON document listing the server groups for the user""" + servers = Server.query.filter_by(user_id=current_user.id, + servergroup_id=gid) - if request.form['id'] != '': - # There can be only one record at most - servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first() + for server in servers: + res.append( + generate_browser_node( + "%d/%d" % (gid, server.id), + server.name, + "icon-%s" % NODE_TYPE, + True, + NODE_TYPE + ) + ) + return make_json_response(result=res) + + def delete(self, gid, sid): + """Delete a server node in the settings database""" + server = Server.query.filter_by(user_id=current_user.id, id=sid) + + # TODO:: A server, which is connected, can not be deleted if server is None: - success = 0 - errormsg = gettext('The specified server could not be found.') + return make_json_response( + success=0, + errormsg=gettext( + 'The specified server could not be found.\n' + 'Does the user have permission to access the ' + 'server?' + ) + ) else: try: db.session.delete(server) db.session.commit() except Exception as e: - success = 0 - errormsg = e.message + return make_json_response( + success=0, + errormsg=e.message) - else: - success = 0 - errormsg = gettext('No server was specified.') + return make_json_response(success=success, + errormsg=errormsg, + info=traceback.format_exc()) - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form) -@blueprint.route('/rename/', methods=['POST']) -@login_required -def rename(): - """Rename a server node in the settings database""" - success = 1 - errormsg = '' - - if request.form['id'] != '': - # There can be only one record at most - servergroup = Server.query.filter_by(user_id=current_user.id, id=int(request.form['id'])).first() + def update(self, gid, sid): + """Update the server settings""" + server = Server.query.filter_by(user_id=current_user.id, id=sid).first() if server is None: - success = 0 - errormsg = gettext('The specified server could not be found.') - else: - try: - server.name = request.form['name'] - db.session.commit() - except Exception as e: - success = 0 - errormsg = e.message + return make_json_response( + success=0, + errormsg=gettext("Couldn't find the given server.") + ) - else: - success = 0 - errormsg = gettext('No server was specified.') + # TODO:: + # Not all parameters can be modified, while the server is connected + possible_args = { + 'name': 'name', + 'host': 'host', + 'port': 'port', + 'db': 'maintenance_db', + 'username': 'username', + 'sslmode': 'sslmode', + 'gid': 'servergroup_id' + } - return make_json_response(success=success, - errormsg=errormsg, - info=traceback.format_exc(), - result=request.form) + idx = 0 + for arg in possible_args: + if arg in request.form: + server[possible_args[arg]] = request.form[arg] + idx += 1 + if idx == 0: + return make_json_response( + success=0, + errormsg=gettext('No parameters were chagned!') + ) + + try: + db.session.commit() + except Exception as e: + return make_json_response( + success=0, + errormsg=e.message + ) + + return make_json_response( + success=1, + data={ + 'id': server.id, + 'gid': server.servergroup_id + } + ) + + + def properties(self, gid, sid): + """Return list of attributes of a server""" + server = Server.query.filter_by( + user_id=current_user.id, + id=sid).first() + + if server is None: + return make_json_response( + success=0, + errormsg=gettext("Couldn't find the given server") + ) + + sg = ServerGroup.query.filter_by( + user_id=current_user.id, + id=server.servergroup_id + ).first() + + return make_json_response( + success=1, + data={ + 'id':server.id, + 'name':server.name, + 'host':server.host, + 'port':server.port, + 'db':server.maintenance_db, + 'username':server.username, + 'gid':server.servergroup_id, + 'group-name':sg.name + } + ) + + + def create(self, gid): + """Add a server node to the settings database""" + required_args = [ + 'name', + 'host', + 'port', + 'db', + 'username', + 'sslmode' + ] + + for arg in required_args: + if arg not in request.form: + return make_json_response( + success=0, + errormsg=gettext( + "Couldn't find the required parameter (%s)." % arg + ) + ) + + server = Server( + user_id=current_user.id, + servergroup_id=gid, + name=request.form['name'], + host=request.form['host'], + port=request.form['port'], + maintenance_db=request.form['db'], + username=request.form['username'], + sslmode=request.form['username'] + ) + + try: + db.session.add(server) + db.session.commit() + except Exception as e: + return make_json_response( + success=0, + errormsg=e.message + ) + + return make_json_response(success=1, + data={ + 'id': server.id, + 'name': server.name, + 'gid': gid + }) + + + def nodes(self, gid, sid): + """Build a list of treeview nodes from the child nodes.""" + nodes = [] + # TODO:: + # We can have nodes for the server object, only when + # the server is connected at the moment. + for module in blueprint.submodules: + nodes.extend(module.get_nodes(server=sid)) + return make_json_response(data=nodes) + + + def sql(self, gid, sid): + return make_json_response(data='') + + + def modified_sql(self, gid, sid): + return make_json_response(data='') + + + def statistics(self, gid, sid): + return make_json_response(data='') + + + def dependencies(self, gid, sid): + return make_json_response(data='') + + + def dependents(self, gid, sid): + return make_json_response(data='') + + +ServerNode.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/templates/servers/servers.js b/web/pgadmin/browser/server_groups/servers/templates/servers/servers.js index 822441d04..5361229da 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/servers/servers.js +++ b/web/pgadmin/browser/server_groups/servers/templates/servers/servers.js @@ -1,11 +1,18 @@ // Add a server -function create_server() { +function create_server(item) { var alert = alertify.prompt( '{{ _('Create a server') }}', '{{ _('Enter a name for the new server') }}', '', function(evt, value) { - $.post("{{ url_for('NODE-server.add') }}", { name: value }) + var d = tree.itemData(item); + if (d._type != 'server-group') { + d = tree.itemData(tree.parent(item)); + } + $.post( + "{{ url_for('browser.index') }}server/obj/" + d.refid + '/', + { name: value } + ) .done(function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); @@ -38,8 +45,10 @@ function drop_server(item) { '{{ _('Are you sure you wish to drop the server "{0}"?') }}'.replace('{0}', tree.getLabel(item)), function() { var id = tree.getId(item).split('/').pop() - $.post("{{ url_for('NODE-server.delete') }}", { id: id }) - .done(function(data) { + $.ajax({ + url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid, + type:'DELETE', + success: function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); } else { @@ -53,7 +62,7 @@ function drop_server(item) { } } } - ) + }) }, null ) @@ -66,17 +75,20 @@ function rename_server(item) { '{{ _('Enter a new name for the server') }}', tree.getLabel(item), function(evt, value) { - var id = tree.getId(item).split('/').pop() - $.post("{{ url_for('NODE-server.rename') }}", { id: id, name: value }) - .done(function(data) { + var d = tree.itemData(item); + $.ajax({ + url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid, + type:'PUT', + params: {name: value}, + success: function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); } else { tree.setLabel(item, { label: value }); } } - ) + }) }, null ) -} \ No newline at end of file +} diff --git a/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js b/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js index f1fa85d8f..a15401276 100644 --- a/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js +++ b/web/pgadmin/browser/server_groups/templates/server_groups/server_groups.js @@ -5,7 +5,7 @@ function create_server_group() { '{{ _('Enter a name for the new server group') }}', '', function(evt, value) { - $.post("{{ url_for('NODE-server-group.add') }}", { name: value }) + $.post("{{ url_for('browser.index') }}server-group/obj/", { name: value }) .done(function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); @@ -37,9 +37,11 @@ function drop_server_group(item) { '{{ _('Delete server group?') }}', '{{ _('Are you sure you wish to delete the server group "{0}"?') }}'.replace('{0}', tree.getLabel(item)), function() { - var id = tree.getId(item).split('/').pop() - $.post("{{ url_for('NODE-server-group.delete') }}", { id: id }) - .done(function(data) { + var d = tree.itemData(item); + $.ajax({ + url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid, + type:'DELETE', + success: function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); } else { @@ -53,7 +55,7 @@ function drop_server_group(item) { } } } - ) + }) }, null ) @@ -66,16 +68,19 @@ function rename_server_group(item) { '{{ _('Enter a new name for the server group') }}', tree.getLabel(item), function(evt, value) { - var id = tree.getId(item).split('/').pop() - $.post("{{ url_for('NODE-server-group.rename') }}", { id: id, name: value }) - .done(function(data) { + var d = tree.itemData(item); + $.ajax({ + url:"{{ url_for('browser.index') }}" + d._type + "/obj/" + d.refid, + type:'PUT', + params: { name: value }, + success: function(data) { if (data.success == 0) { report_error(data.errormsg, data.info); } else { tree.setLabel(item, { label: value }); } } - ) + }) }, null ) diff --git a/web/pgadmin/browser/templates/browser/css/node.css b/web/pgadmin/browser/templates/browser/css/node.css index 3fea07422..da56d6910 100644 --- a/web/pgadmin/browser/templates/browser/css/node.css +++ b/web/pgadmin/browser/templates/browser/css/node.css @@ -1,3 +1,3 @@ .icon-{{node_type}} { - background: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') 0 0 no-repeat; + background-image: url('{{ url_for('NODE-%s.static' % node_type, filename='img/%s.png' % node_type )}}') !important; } diff --git a/web/pgadmin/browser/templates/browser/js/browser.js b/web/pgadmin/browser/templates/browser/js/browser.js index e1aa05b5a..dbdf0343d 100644 --- a/web/pgadmin/browser/templates/browser/js/browser.js +++ b/web/pgadmin/browser/templates/browser/js/browser.js @@ -298,6 +298,12 @@ ALTER TABLE tickets_detail \n\ return $.parseJSON(payload).data; } } + }, + ajaxHook: function(item, settings) { + if (item != null) { + var d = this.itemData(item); + settings.url = '{{ url_for('browser.index') }}' + d._type + '/nodes/' + d.refid + } } }); tree = $('#tree').aciTree('api'); diff --git a/web/pgadmin/browser/utils.py b/web/pgadmin/browser/utils.py index 4cf955f41..db33120e8 100644 --- a/web/pgadmin/browser/utils.py +++ b/web/pgadmin/browser/utils.py @@ -9,45 +9,173 @@ """Browser helper utilities""" -import os, sys -import config +from flask import request +from flask.views import View, MethodViewType, with_metaclass -def register_modules(app, file, all_nodes, sub_nodes, prefix): - """Register any child node blueprints for the specified file""" - path = os.path.dirname(os.path.realpath(file)) - files = os.listdir(path) - for f in files: - d = os.path.join(path, f) - if os.path.isdir(d) and os.path.isfile(os.path.join(d, '__init__.py')): +def generate_browser_node(node_id, label, icon, inode, node_type): + return { + "id": "%s/%s" % (node_type, node_id), + "label": label, + "icon": icon, + "inode": inode, + "_type": node_type, + "refid": node_id + } - if f in config.NODE_BLACKLIST: - app.logger.info('Skipping blacklisted node: %s' % f) - continue - # Construct the 'real' module name - if prefix != '': - f = prefix + '.' + f - - # Looks like a node, so import it, and register the blueprint if present - # We rely on the ordering of syspath to ensure we actually get the right - # module here. - app.logger.info('Examining potential node: %s' % d) - node = __import__(f, globals(), locals(), ['hooks', 'views'], -1) +class NodeView(with_metaclass(MethodViewType, View)): + """ + A PostgreSQL Object has so many operaions/functions apart from CRUD + (Create, Read, Update, Delete): + i.e. + - Reversed Engineered SQL + - Modified Query for parameter while editing object attributes + i.e. ALTER TABLE ... + - Statistics of the objects + - List of dependents + - List of dependencies + - Listing of the children object types for the certain node + It will used by the browser tree to get the children nodes - # Add the node to the node lists - all_nodes.append(node) - sub_nodes.append(node) - - # Register the blueprint if present - if 'views' in dir(node) and 'blueprint' in dir(node.views): - app.logger.info('Registering blueprint node: %s' % f) - app.register_blueprint(node.views.blueprint) - app.logger.debug(' - root_path: %s' % node.views.blueprint.root_path) - app.logger.debug(' - static_folder: %s' % node.views.blueprint.static_folder) - app.logger.debug(' - template_folder: %s' % node.views.blueprint.template_folder) - - # Register any sub-modules - if 'hooks' in dir(node) and 'register_submodules' in dir(node.hooks): - app.logger.info('Registering sub-modules in %s' % f) - node.hooks.register_submodules(app) \ No newline at end of file + This class can be inherited to achieve the diffrent routes for each of the + object types/collections. + + OPERATION | URL | Method + ---------------+------------------------+-------- + List | /obj/[Parent URL]/ | GET + Properties | /obj/[Parent URL]/id | GET + Create | /obj/[Parent URL]/ | POST + Delete | /obj/[Parent URL]/id | DELETE + Update | /obj/[Parent URL]/id | PUT + + SQL (Reversed | /sql/[Parent URL]/id | GET + Engineering) | + SQL (Modified | /sql/[Parent URL]/id | POST + Properties) | + + Statistics | /stats/[Parent URL]/id | GET + Dependencies | /deps/[Parent URL]/id | GET + Dependents | /deps/[Parent URL]/id | POST + + Children Nodes | /nodes/[Parent URL]/id | GET + + NOTE: + Parent URL can be seen as the path to identify the particular node. + + i.e. + In order to identify the TABLE object, we need server -> database -> schema + information. + """ + operations = { + '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'}, {}] + } + + + @classmethod + def generate_ops(cls): + cmds = [] + for op in cls.operations: + idx=0 + for ops in cls.operations[op]: + meths = [] + for meth in ops: + meths.append(meth.upper()) + if len(meths) > 0: + cmds.append({'cmd': op, 'req':idx==0, 'methods': meths}) + idx+=1 + + return cmds + + + # Inherited class needs to modify these parameters + node_type = None + # This must be an array object with attributes (type and id) + parent_ids = [] + # This must be an array object with attributes (type and id) + ids = [] + + + @classmethod + def get_node_urls(cls): + assert cls.node_type is not None, "Please set the node_type for this class (%r)" % cls + common_url = '/' + for p in cls.parent_ids: + common_url += '<' + p['type'] + ":" + p['id'] + '>/' + + id_url = common_url + idx = 0 + for p in cls.ids: + id_url += '/<' if idx == 1 else '<' + p['type'] + ":" + p['id'] + '>' + idx += 1 + + return id_url, common_url + + + def __init__(self, cmd): + self.cmd = cmd; + + + # Check the existance of all the required arguments from parent_ids + # and return combination of has parent arguments, and has id arguments + def check_args(self, *args, **kwargs): + has_id = has_args = True + for p in self.parent_ids: + if p['id'] not in kwargs: + has_args = False + break + + for p in self.ids: + if p['id'] not in kwargs: + has_id = False + break + + return has_args, has_id and has_args + + + def dispatch_request(self, *args, **kwargs): + meth = request.method.lower() + if meth == 'head': + meth = 'get' + + assert self.cmd in NodeView.operations, \ + "Unimplemented Command (%s) for Node View" % self.cmd + has_args, has_id = self.check_args(*args, **kwargs) + + assert (has_id and meth in NodeView.operations[self.cmd][0]) \ + or (not has_id and meth in NodeView.operations[self.cmd][1]), \ + "Unimplemented method (%s) for command (%s), which %s an id" \ + % (meth, self.cmd, 'requires' if has_id else 'does not require') + + meth = NodeView.operations[self.cmd][0][meth] if has_id else \ + NodeView.operations[self.cmd][1][meth] + + method = getattr(self, meth, None) + + assert method is not None, \ + "Unimplemented method (%s) for this url (%u)" % \ + (meth, request.path) + + return method(*args, **kwargs) + + + @classmethod + def register_node_view(cls, blueprint): + id_url, url = cls.get_node_urls() + + commands = cls.generate_ops() + + for c in commands: + blueprint.add_url_rule( + '/%s%s' % (c['cmd'], id_url if c['req'] else url), + view_func=cls.as_view( + '%s%s' % (c['cmd'], '_id' if c['req'] else ''), + cmd=c['cmd']), + methods=c['methods']) diff --git a/web/pgadmin/settings/settings_model.py b/web/pgadmin/settings/settings_model.py index 3dc82c19d..377272848 100644 --- a/web/pgadmin/settings/settings_model.py +++ b/web/pgadmin/settings/settings_model.py @@ -65,6 +65,7 @@ class ServerGroup(db.Model): __tablename__ = 'servergroup' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + name = db.Column(db.String(128), nullable=False) __table_args__ = (db.UniqueConstraint('user_id', 'name'),) @@ -73,11 +74,28 @@ class Server(db.Model): """Define a registered Postgres server""" __tablename__ = 'server' id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - servergroup_id = db.Column(db.Integer, db.ForeignKey('servergroup.id'), nullable=False) + user_id = db.Column( + db.Integer, + db.ForeignKey('user.id'), + nullable=False + ) + servergroup_id = db.Column( + db.Integer, + db.ForeignKey('servergroup.id'), + nullable=False + ) + name = db.Column(db.String(128), nullable=False) host = db.Column(db.String(128), nullable=False) - port = db.Column(db.Integer(), db.CheckConstraint('port >= 1024 AND port <= 65534'), nullable=False) + port = db.Column( + db.Integer(), + db.CheckConstraint('port >= 1024 AND port <= 65534'), + nullable=False) maintenance_db = db.Column(db.String(64), nullable=False) username = db.Column(db.String(64), nullable=False) - ssl_mode = db.Column(db.String(16), nullable=False) + ssl_mode = db.Column( + db.String(16), + db.CheckConstraint( + "ssl_mode IN ('allow', 'prefer', 'require', 'disable', 'verify-ca', 'verify-full')" + ), + nullable=False) diff --git a/web/pgadmin/templates/base.html b/web/pgadmin/templates/base.html index 5a36182b5..300e888f9 100755 --- a/web/pgadmin/templates/base.html +++ b/web/pgadmin/templates/base.html @@ -15,25 +15,25 @@ - {% if config.DEBUG %}{% else %}{% endif %} - {% if config.DEBUG %}{% else %}{% endif %} - {% if config.DEBUG %}{% else %}{% endif %} - {% if config.DEBUG %}{% else %}{% endif %} - + + + + + {% for stylesheet in current_app.stylesheets %} - + {% endfor %} - - {% if config.DEBUG %} - {% if config.DEBUG %} - {% if config.DEBUG %} - + + + + + {% for script in current_app.javascripts %} - + {% endfor %} diff --git a/web/setup.py b/web/setup.py index c7398f53d..1b16dcc5a 100644 --- a/web/setup.py +++ b/web/setup.py @@ -66,6 +66,19 @@ def do_setup(): server_group = ServerGroup(user_id=user.id, name="Servers") db.session.merge(server_group) + # TODO:: Remove this server later + # It is here to demo the server listing is workig in + # browser tree. + server_group = ServerGroup.query.filter_by(name='Servers').first() + + server = Server( + user_id=user.id, servergroup_id=server_group.id, + name='PostgreSQL 9.3', host='localhost', port=3930, + maintenance_db='postgres', username='asheshvashi', + ssl_mode='prefer' + ) + db.session.merge(server) + # Set the schema version version = Version(name='ConfigDB', value=config.SETTINGS_SCHEMA_VERSION) db.session.merge(version)