diff --git a/web/config.py b/web/config.py index c656ebfa3..5dcfc80c9 100644 --- a/web/config.py +++ b/web/config.py @@ -39,6 +39,10 @@ APP_VERSION = '%s.%s.%s-%s' % (APP_MAJOR, APP_MINOR, APP_REVISION, APP_SUFFIX) # List of modules to skip when dynamically loading MODULE_BLACKLIST = [ 'test' ] +# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING! +# List of treeview browser nodes to skip when dynamically loading +NODE_BLACKLIST = [ ] + ########################################################################## # Log settings ########################################################################## diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index b5c47af1d..74620e62d 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -103,10 +103,10 @@ def create_app(app_name=config.APP_NAME): # Looks like a module, 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. Note that we also try to load the 'browser' module for - # the browser integration hooks. + # module here. Note that we also try to load the 'hooks' module for + # the browser integration hooks and other similar functions. app.logger.info('Examining potential module: %s' % d) - module = __import__(f, globals(), locals(), ['browser', 'views'], -1) + module = __import__(f, globals(), locals(), ['hooks', 'views'], -1) # Add the module to the global module list modules.append(module) @@ -118,6 +118,11 @@ def create_app(app_name=config.APP_NAME): app.logger.debug(' - root_path: %s' % module.views.blueprint.root_path) app.logger.debug(' - static_folder: %s' % module.views.blueprint.static_folder) app.logger.debug(' - template_folder: %s' % module.views.blueprint.template_folder) + + # Register any sub-modules + if 'hooks' in dir(module) and 'register_submodules' in dir(module.hooks): + app.logger.info('Registering sub-modules in %s' % f) + module.hooks.register_submodules(app) ########################################################################## # Handle the desktop login diff --git a/web/pgadmin/about/browser.py b/web/pgadmin/about/hooks.py similarity index 100% rename from web/pgadmin/about/browser.py rename to web/pgadmin/about/hooks.py diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index e69de29bb..50115f062 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -0,0 +1,11 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +# Define the global node list +nodes = [ ] \ No newline at end of file diff --git a/web/pgadmin/browser/hooks.py b/web/pgadmin/browser/hooks.py new file mode 100644 index 000000000..b6b90cff8 --- /dev/null +++ b/web/pgadmin/browser/hooks.py @@ -0,0 +1,51 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Browser application hooks""" + +import os, sys +import config + +from . import nodes + +def register_submodules(app): + """Register any child node blueprints""" + path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'nodes') + sys.path.insert(0, path) + 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')): + + if f in config.NODE_BLACKLIST: + app.logger.info('Skipping blacklisted node: %s' % f) + continue + + # 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) + + # Add the node to the global module list + 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 diff --git a/web/pgadmin/browser/nodes/__init__.py b/web/pgadmin/browser/nodes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/nodes/server_groups/__init__.py b/web/pgadmin/browser/nodes/server_groups/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/pgadmin/browser/nodes/server_groups/hooks.py b/web/pgadmin/browser/nodes/server_groups/hooks.py new file mode 100644 index 000000000..b94fb6f24 --- /dev/null +++ b/web/pgadmin/browser/nodes/server_groups/hooks.py @@ -0,0 +1,75 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2014, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Integration hooks for server groups.""" + +from flask import url_for +from flask.ext.security import current_user + +from pgadmin.settings.settings_model import db, ServerGroup + +def get_nodes(): + """Return a JSON document listing the server groups for the user""" + groups = ServerGroup.query.filter_by(user_id=current_user.id) + + value = '' + for group in groups: + value += '{"id":%d,"label":"%s","icon":"icon-server-group","inode":true},' % (group.id, group.name) + value = value[:-1] + + return value + +def get_file_menu_items(): + """Return a (set) of dicts of file menu items, with name, priority and URL.""" + return [ + {'name': 'Add a server group...', 'priority': 10, 'url': '#', 'onclick': 'add_server_group()'} + ] + +def get_script_snippets(): + """Return the script snippets needed to handle treeview node operations.""" + script = """function add_server_group() { + var alert = alertify.prompt( + 'Add a server group', + 'Enter a name for the new server group', + '', + function(evt, value) { $.post("%s", { name: value }) + .done(function(data) { + if (data.success == 0) { + report_error(data.errormsg, data.info); + } else { + var item = { + id: data.data.id, + label: data.data.name, + inode: true, + open: false, + icon: 'icon-server-group' + } + + treeApi.append(null, { + itemData: item + }); + + } + }) + }, + function(evt, value) { } + ); + alert.show(); +} +""" % url_for('NODE-server-group.add') + return script + +def get_css_snippets(): + """Return the CSS needed to display the treeview node image.""" + css = ".icon-server-group {\n" + css += " background: url('%s') 0 0 no-repeat !important;\n" % \ + url_for('NODE-server-group.static', filename='img/server-group.png') + css += "{" + + return css \ No newline at end of file diff --git a/web/pgadmin/browser/nodes/server_groups/static/img/server-group.png b/web/pgadmin/browser/nodes/server_groups/static/img/server-group.png new file mode 100644 index 000000000..bd49bd271 Binary files /dev/null and b/web/pgadmin/browser/nodes/server_groups/static/img/server-group.png differ diff --git a/web/pgadmin/browser/nodes/server_groups/views.py b/web/pgadmin/browser/nodes/server_groups/views.py new file mode 100644 index 000000000..acc82deea --- /dev/null +++ b/web/pgadmin/browser/nodes/server_groups/views.py @@ -0,0 +1,64 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2015, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Defines views for management of server groups""" + +NODE_NAME = 'server-group' + +NODE_PATH = '/browser/' + NODE_NAME + +import traceback +from flask import Blueprint, Response, current_app, request +from flask.ext.security import current_user, login_required + +from utils.ajax import make_json_result +from pgadmin.settings.settings_model import db, ServerGroup +import config + +# Initialise the module +blueprint = Blueprint("NODE-" + NODE_NAME, __name__, static_folder='static', static_url_path='', template_folder='templates', url_prefix=NODE_PATH) + +@blueprint.route('/add/', methods=['POST']) +@login_required +def add(): + """Add a server group node to the settings database""" + success = 1 + errormsg = '' + data = { } + + if request.form['name'] != '': + servergroup = ServerGroup(user_id=current_user.id, name=request.form['name']) + + try: + db.session.add(servergroup) + db.session.commit() + except Exception as e: + success = 0 + errormsg = e.message + + else: + success = 0 + errormsg = "No server group name was specified" + + if success == 1: + data['id'] = servergroup.id + data['name'] = servergroup.name + + value = make_json_result(success=success, + errormsg=errormsg, + info=traceback.format_exc(), + result=request.form, + data=data) + + resp = Response(response=value, + status=200, + mimetype="text/json") + + return resp + \ No newline at end of file diff --git a/web/pgadmin/browser/static/js/utils.js b/web/pgadmin/browser/static/js/utils.js new file mode 100644 index 000000000..7550fa62c --- /dev/null +++ b/web/pgadmin/browser/static/js/utils.js @@ -0,0 +1,39 @@ +function report_error(message, info) { + + text = '
\ +
\ + \ +
\ +
' + message + '
\ +
\ +
' + + if (info != '') { + text += '
\ + \ +
\ +
' + info + '
\ +
\ +
\ +
' + } + + text += '' + + alertify.alert( + 'An error has occurred', + text + ) +} \ No newline at end of file diff --git a/web/pgadmin/browser/templates/browser/body.html b/web/pgadmin/browser/templates/browser/body.html index 282691953..12944cff7 100644 --- a/web/pgadmin/browser/templates/browser/body.html +++ b/web/pgadmin/browser/templates/browser/body.html @@ -153,17 +153,18 @@ $('#dependents a').click(function (e) { // Syntax highlight the SQL Pane var editor = CodeMirror.fromTextArea(document.getElementById("sql-textarea"), { - lineNumbers: true, - mode: "text/x-sql", - readOnly: true, + lineNumbers: true, + mode: "text/x-sql", + readOnly: true, }); // Initialise the treeview $('#tree').aciTree({ - ajax: { - url: '/static/tree.json' - } + ajax: { + url: '{{ url_for('browser.get_nodes') }}' + }, }); +var treeApi = $('#tree').aciTree('api'); diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index 4f0508bda..15dd0f86a 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -66,8 +66,4 @@ {% include 'browser/body.html' %} {% include 'browser/messages.html' %} - - {% endblock %} diff --git a/web/pgadmin/browser/views.py b/web/pgadmin/browser/views.py index c9ecc73fd..bd3868e10 100644 --- a/web/pgadmin/browser/views.py +++ b/web/pgadmin/browser/views.py @@ -10,12 +10,13 @@ """A blueprint module implementing the core pgAdmin browser.""" MODULE_NAME = 'browser' -from flask import Blueprint, current_app, render_template, url_for +from flask import Blueprint, Response, current_app, render_template, url_for from flaskext.gravatar import Gravatar from flask.ext.security import login_required from flask.ext.login import current_user from inspect import getmoduleinfo, getmembers +from . import nodes from pgadmin import modules from pgadmin.settings import get_setting @@ -24,9 +25,6 @@ import config # Initialise the module blueprint = Blueprint(MODULE_NAME, __name__, static_folder='static', template_folder='templates', url_prefix='/' + MODULE_NAME) -########################################################################## -# A test page -########################################################################## @blueprint.route("/") @login_required def index(): @@ -47,42 +45,48 @@ def index(): help_items = [ ] stylesheets = [ ] scripts = [ ] + + modules_and_nodes = modules + nodes # Add browser stylesheets stylesheets.append(url_for('static', filename='css/codemirror/codemirror.css')) stylesheets.append(url_for('browser.static', filename='css/browser.css')) stylesheets.append(url_for('browser.static', filename='css/aciTree/css/aciTree.css')) + stylesheets.append(url_for('browser.browser_css')) # Add browser scripts scripts.append(url_for('static', filename='js/codemirror/codemirror.js')) scripts.append(url_for('static', filename='js/codemirror/mode/sql.js')) + scripts.append(url_for('browser.static', filename='js/utils.js')) scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciPlugin.min.js')) + scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.dom.js')) scripts.append(url_for('browser.static', filename='js/aciTree/jquery.aciTree.min.js')) + scripts.append(url_for('browser.browser_js')) - for module in modules: + for module in modules_and_nodes: # Get the edit menu items - if 'browser' in dir(module) and 'get_file_menu_items' in dir(module.browser): - file_items.extend(module.browser.get_file_menu_items()) + if 'hooks' in dir(module) and 'get_file_menu_items' in dir(module.hooks): + file_items.extend(module.hooks.get_file_menu_items()) # Get the edit menu items - if 'browser' in dir(module) and 'get_edit_menu_items' in dir(module.browser): - edit_items.extend(module.browser.get_edit_menu_items()) + if 'hooks' in dir(module) and 'get_edit_menu_items' in dir(module.hooks): + edit_items.extend(module.hooks.get_edit_menu_items()) # Get the tools menu items - if 'browser' in dir(module) and 'get_tools_menu_items' in dir(module.browser): - tools_items.extend(module.browser.get_tools_menu_items()) + if 'hooks' in dir(module) and 'get_tools_menu_items' in dir(module.hooks): + tools_items.extend(module.hooks.get_tools_menu_items()) # Get the help menu items - if 'browser' in dir(module) and 'get_help_menu_items' in dir(module.browser): - help_items.extend(module.browser.get_help_menu_items()) + if 'hooks' in dir(module) and 'get_help_menu_items' in dir(module.hooks): + help_items.extend(module.hooks.get_help_menu_items()) # Get any stylesheets - if 'browser' in dir(module) and 'get_stylesheets' in dir(module.browser): - stylesheets += module.browser.get_stylesheets() + if 'hooks' in dir(module) and 'get_stylesheets' in dir(module.hooks): + stylesheets += module.hooks.get_stylesheets() # Get any scripts - if 'browser' in dir(module) and 'get_scripts' in dir(module.browser): - scripts += module.browser.get_scripts() + if 'hooks' in dir(module) and 'get_scripts' in dir(module.hooks): + scripts += module.hooks.get_scripts() file_items = sorted(file_items, key=lambda k: k['priority']) edit_items = sorted(edit_items, key=lambda k: k['priority']) @@ -105,3 +109,61 @@ def index(): stylesheets = stylesheets, scripts = scripts, layout_settings = layout_settings) + +@blueprint.route("/browser.js") +@login_required +def browser_js(): + """Render and return JS snippets from the nodes and modules.""" + snippets = '' + modules_and_nodes = modules + nodes + + for module in modules_and_nodes: + if 'hooks' in dir(module) and 'get_script_snippets' in dir(module.hooks): + snippets += module.hooks.get_script_snippets() + + resp = Response(response=snippets, + status=200, + mimetype="application/javascript") + + return resp + +@blueprint.route("/browser.css") +@login_required +def browser_css(): + """Render and return CSS snippets from the nodes and modules.""" + snippets = '' + modules_and_nodes = modules + nodes + + for module in modules_and_nodes: + if 'hooks' in dir(module) and 'get_css_snippets' in dir(module.hooks): + snippets += module.hooks.get_css_snippets() + + resp = Response(response=snippets, + status=200, + mimetype="text/css") + + return resp + +@blueprint.route("/root-nodes.json") +@login_required +def get_nodes(): + """Build a list of treeview nodes from the child modules.""" + value = '[' + + for node in nodes: + if 'hooks' in dir(node) and 'get_nodes' in dir(node.hooks): + value += node.hooks.get_nodes() + ',' + + if value[-1:] == ',': + value = value[:-1] + + value += ']' + + resp = Response(response=value, + status=200, + mimetype="text/json") + + return resp + + + \ No newline at end of file diff --git a/web/pgadmin/settings/browser.py b/web/pgadmin/settings/hooks.py similarity index 100% rename from web/pgadmin/settings/browser.py rename to web/pgadmin/settings/hooks.py diff --git a/web/pgadmin/settings/views.py b/web/pgadmin/settings/views.py index db392ae73..0421694ab 100644 --- a/web/pgadmin/settings/views.py +++ b/web/pgadmin/settings/views.py @@ -10,10 +10,12 @@ """Views for setting and storing configuration options.""" MODULE_NAME = 'settings' -import config +import traceback from flask import Blueprint, Response, abort, request, render_template from flask.ext.security import login_required +import config +from utils.ajax import make_json_result from . import get_setting, store_setting # Initialise the module @@ -33,16 +35,30 @@ def script(): def store(setting=None, value=None): """Store a configuration setting, or if this is a POST request and a count value is present, store multiple settings at once.""" - if request.method == 'POST': - if 'count' in request.form: - for x in range(int(request.form['count'])): - store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)]) - else: - store_setting(request.form['setting'], request.form['value']) - else: - store_setting(setting, value) + success = 1 + errorcode = 0 + errormsg = '' - return '' + try: + if request.method == 'POST': + if 'count' in request.form: + for x in range(int(request.form['count'])): + store_setting(request.form['setting%d' % (x+1)], request.form['value%d' % (x+1)]) + else: + store_setting(request.form['setting'], request.form['value']) + else: + store_setting(setting, value) + except Exception as e: + success = 0 + errormsg = e.message + + value = make_json_result(success=success, errormsg=errormsg, info=traceback.format_exc(), result=request.form) + + resp = Response(response=value, + status=200, + mimetype="text/json") + + return resp @blueprint.route("/get", methods=['POST']) @blueprint.route("/get/", methods=['GET']) @@ -53,14 +69,21 @@ def get(setting=None, default=None): if request.method == 'POST': setting = request.form['setting'] default = request.form['default'] - + + success = 1 + errorcode = 0 + errormsg = '' + try: value = get_setting(setting, default) - except: - return '' - + except Exception as e: + success = 0 + errormsg = e.message + + value = make_json_result(success=success, errormsg=errormsg, info=traceback.format_exc(), result=request.form) + resp = Response(response=value, status=200, - mimetype="text/plain") + mimetype="text/json") return resp diff --git a/web/pgadmin/static/css/overrides.css b/web/pgadmin/static/css/overrides.css index 20f4fea51..cb9be4ce3 100644 --- a/web/pgadmin/static/css/overrides.css +++ b/web/pgadmin/static/css/overrides.css @@ -15,6 +15,11 @@ color: #fff; } +.alertify .ajs-content { + padding-left: 0 !important; + padding-right: 0 !important; +} + /* iFrames should have no border */ iframe { border-width: 0; @@ -30,6 +35,13 @@ iframe { padding-left: 0; } -.icon-servers { - background: url('/static/servers.png') 0 0 no-repeat !important; -} +/* Alert info panel */ +.alert-info-panel { + border: 2px solid #a1a1a1; + margin-top: 2em; + padding: 5px 5px; + background: #dddddd; + border-radius: 5px; + height: 8em; + overflow: scroll; +} \ No newline at end of file diff --git a/web/pgadmin/test/browser.py b/web/pgadmin/test/hooks.py similarity index 100% rename from web/pgadmin/test/browser.py rename to web/pgadmin/test/hooks.py diff --git a/web/utils/__init__.py b/web/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/web/utils/ajax.py b/web/utils/ajax.py new file mode 100644 index 000000000..ae5249ca0 --- /dev/null +++ b/web/utils/ajax.py @@ -0,0 +1,24 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2015, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Utility functions for dealing with AJAX.""" + +import json + +def make_json_result(success=1, errormsg='', info='', result={}, data={}): + """Create a JSON response document describing the results of a request and + containing the data.""" + doc = { } + doc['success'] = success + doc['errormsg'] = errormsg + doc['info'] = info + doc['result'] = result + doc['data'] = data + + return json.dumps(doc) \ No newline at end of file