Add support for extensions.

pull/3/head
Surinder Kumar 2016-02-24 16:45:35 +00:00 committed by Dave Page
parent f466e0169a
commit c950683fa1
10 changed files with 803 additions and 0 deletions

View File

@ -0,0 +1,480 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2016, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
""" Implements Extension Node """
import json
from flask import render_template, make_response, request, jsonify
from flask.ext.babel import gettext
from pgadmin.utils.ajax import make_json_response, \
make_response as ajax_response, internal_server_error
from pgadmin.browser.utils import PGChildNodeView
from pgadmin.browser.collection import CollectionNodeModule
import pgadmin.browser.server_groups.servers.databases as databases
from pgadmin.utils.driver import get_driver
from config import PG_DEFAULT_DRIVER
from functools import wraps
# As unicode type is not available in python3
# If we check a variable is "isinstance(variable, str)
# it breaks in python 3 as variable type is not string its unicode.
# We assign basestring as str type if it is python3, unicode
# if it is python2.
try:
unicode = unicode
except NameError:
# 'unicode' is undefined, must be Python 3
str = str
unicode = str
bytes = bytes
basestring = (str, bytes)
else:
# 'unicode' exists, must be Python 2
str = str
unicode = unicode
bytes = str
basestring = basestring
class ExtensionModule(CollectionNodeModule):
"""
class ExtensionModule(Object):
A collection Node which inherits CollectionNodeModule
class and define methods to get child nodes, to load its own
javascript file.
"""
NODE_TYPE = "extension"
COLLECTION_LABEL = gettext("Extensions")
def __init__(self, *args, **kwargs):
"""
Initialising the base class
"""
super(ExtensionModule, self).__init__(*args, **kwargs)
def get_nodes(self, gid, sid, did):
"""
Generate the collection node
"""
yield self.generate_browser_collection_node(did)
@property
def node_inode(self):
"""
If a node have child return True otherwise False
"""
return False
@property
def script_load(self):
"""
Load the module script for extension, when any of the database nodes are
initialized.
"""
return databases.DatabaseModule.NODE_TYPE
# Create blueprint of extension module
blueprint = ExtensionModule(__name__)
class ExtensionView(PGChildNodeView):
"""
This is a class for extension nodes which inherits the
properties and methods from NodeView class and define
various methods to list, create, update and delete extension.
Variables:
---------
* node_type - tells which type of node it is
* parent_ids - id with its type and name of parent nodes
* ids - id with type and name of extension module being used.
* operations - function routes mappings defined.
"""
node_type = blueprint.node_type
parent_ids = [
{'type': 'int', 'id': 'gid'},
{'type': 'int', 'id': 'sid'},
{'type': 'int', 'id': 'did'}
]
ids = [
{'type': 'int', 'id': 'eid'}
]
operations = dict({
'obj': [
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
{'get': 'list', 'post': 'create'}
],
'delete': [{'delete': 'delete'}],
'nodes': [{'get': 'node'}, {'get': 'nodes'}],
'sql': [{'get': 'sql'}],
'msql': [{'get': 'msql'}, {'get': 'msql'}],
'stats': [{'get': 'statistics'}],
'dependency': [{'get': 'dependencies'}],
'dependent': [{'get': 'dependents'}],
'module.js': [{}, {}, {'get': 'module_js'}],
'avails': [{}, {'get': 'avails'}],
'schemas': [{}, {'get': 'schemas'}],
'children': [{'get': 'children'}]
})
def check_precondition(f):
"""
This function will behave as a decorator which will checks
database connection before running view, it will also attaches
manager,conn & template_path properties to self
"""
@wraps(f)
def wrap(*args, **kwargs):
# Here args[0] will hold self & kwargs will hold gid,sid,did
self = args[0]
self.manager = get_driver(
PG_DEFAULT_DRIVER
).connection_manager(kwargs['sid'])
self.conn = self.manager.connection(did=kwargs['did'])
self.template_path = 'extensions/sql'
return f(*args, **kwargs)
return wrap
@check_precondition
def list(self, gid, sid, did):
"""
Fetches all extensions properties and render into properties tab
"""
SQL = render_template("/".join([self.template_path, 'properties.sql']))
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res['rows'],
status=200
)
@check_precondition
def nodes(self, gid, sid, did):
"""
Lists all extensions under the Extensions Collection node
"""
res = []
SQL = render_template("/".join([self.template_path, 'properties.sql']))
status, rset = self.conn.execute_2darray(SQL)
if not status:
return internal_server_error(errormsg=rset)
for row in rset['rows']:
res.append(
self.blueprint.generate_browser_node(
row['eid'],
did,
row['name'],
'icon-extension'
))
return make_json_response(
data=res,
status=200
)
@check_precondition
def properties(self, gid, sid, did, eid):
"""
Fetch the properties of a single extension and render in properties tab
"""
SQL = render_template("/".join(
[self.template_path, 'properties.sql']), eid=eid)
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
return ajax_response(
response=res['rows'][0],
status=200
)
@check_precondition
def create(self, gid, sid, did):
"""
Create a new extension object
"""
required_args = [
'name'
]
data = request.form if request.form else \
json.loads(request.data.decode())
for arg in required_args:
if arg not in data:
return make_json_response(
status=410,
success=0,
errormsg=gettext(
"Couldn't find the required parameter (%s)." % arg
)
)
status, res = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'create.sql']),
data=data
)
)
if not status:
return internal_server_error(errormsg=res)
status, rset = self.conn.execute_dict(
render_template(
"/".join([self.template_path, 'properties.sql']),
ename=data['name']
)
)
if not status:
return internal_server_error(errormsg=rset)
for row in rset['rows']:
return jsonify(
node=self.blueprint.generate_browser_node(
row['eid'],
did,
row['name'],
'icon-extension'
)
)
@check_precondition
def update(self, gid, sid, did, eid):
"""
This function will update an extension object
"""
data = request.form if request.form else \
json.loads(request.data.decode())
SQL = self.getSQL(gid, sid, data, did, eid)
try:
if SQL and isinstance(SQL, basestring) and \
SQL.strip('\n') and SQL.strip(' '):
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(
success=1,
info="Extension updated",
data={
'id': eid,
'sid': sid,
'gid': gid
}
)
else:
return make_json_response(
success=1,
info="Nothing to update",
data={
'id': did,
'sid': sid,
'gid': gid
}
)
except Exception as e:
return internal_server_error(errormsg=str(e))
@check_precondition
def delete(self, gid, sid, did, eid):
"""
This function will drop/drop cascade a extension object
"""
cascade = True if self.cmd == 'delete' else False
try:
# check if extension with eid exists
SQL = render_template("/".join(
[self.template_path, 'delete.sql']), eid=eid)
status, name = self.conn.execute_scalar(SQL)
if not status:
return internal_server_error(errormsg=name)
# drop extension
SQL = render_template("/".join(
[self.template_path, 'delete.sql']
), name=name, cascade=cascade)
status, res = self.conn.execute_scalar(SQL)
if not status:
return internal_server_error(errormsg=res)
return make_json_response(
success=1,
info=gettext("Extension dropped"),
data={
'id': did,
'sid': sid,
'gid': gid,
}
)
except Exception as e:
return internal_server_error(errormsg=str(e))
@check_precondition
def msql(self, gid, sid, did, eid=None):
"""
This function returns modified SQL
"""
data = request.args.copy()
SQL = self.getSQL(gid, sid, data, did, eid)
if SQL and isinstance(SQL, basestring) and SQL.strip('\n') \
and SQL.strip(' '):
return make_json_response(
data=SQL,
status=200
)
else:
return make_json_response(
data=gettext('-- Modified SQL --'),
status=200
)
def getSQL(self, gid, sid, data, did, eid=None):
"""
This function will generate sql from model data
"""
required_args = [
'name'
]
try:
if eid is not None:
SQL = render_template("/".join(
[self.template_path, 'properties.sql']
), eid=eid)
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
old_data = res['rows'][0]
for arg in required_args:
if arg not in data:
data[arg] = old_data[arg]
SQL = render_template("/".join(
[self.template_path, 'update.sql']
), data=data, o_data=old_data)
else:
SQL = render_template("/".join(
[self.template_path, 'create.sql']
), data=data)
return SQL
except Exception as e:
return internal_server_error(errormsg=str(e))
@check_precondition
def avails(self, gid, sid, did):
"""
This function with fetch all the available extensions
"""
SQL = render_template("/".join([self.template_path, 'extensions.sql']))
status, rset = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=rset)
return make_json_response(
data=rset['rows'],
status=200
)
@check_precondition
def schemas(self, gid, sid, did):
"""
This function with fetch all the schemas
"""
SQL = render_template("/".join([self.template_path, 'schemas.sql']))
status, rset = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=rset)
return make_json_response(
data=rset['rows'],
status=200
)
def module_js(self):
"""
This property defines whether javascript exists for this node.
"""
return make_response(
render_template(
"extensions/js/extensions.js",
_=gettext
),
200, {'Content-Type': 'application/x-javascript'}
)
@check_precondition
def sql(self, gid, sid, did, eid):
"""
This function will generate sql for the sql panel
"""
SQL = render_template("/".join(
[self.template_path, 'properties.sql']
), eid=eid)
status, res = self.conn.execute_dict(SQL)
if not status:
return internal_server_error(errormsg=res)
result = res['rows'][0]
SQL = render_template("/".join(
[self.template_path, 'create.sql']
),
data=result,
conn=self.conn,
display_comments=True
)
return ajax_response(response=SQL)
@check_precondition
def dependents(self, gid, sid, did, eid):
"""
This function gets the dependents and returns an ajax response
for the extension node.
Args:
gid: Server Group ID
sid: Server ID
did: Database ID
eid: Extension ID
"""
dependents_result = self.get_dependents(self.conn, eid)
return ajax_response(
response=dependents_result,
status=200
)
@check_precondition
def dependencies(self, gid, sid, did, eid):
"""
This function gets the dependencies and returns an ajax response
for the extension node.
Args:
gid: Server Group ID
sid: Server ID
did: Database ID
lid: Extension ID
"""
dependencies_result = self.get_dependencies(self.conn, eid)
return ajax_response(
response=dependencies_result,
status=200
)
# Register and add ExtensionView as blueprint
ExtensionView.register_node_view(blueprint)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 B

View File

@ -0,0 +1,254 @@
define(
['jquery', 'underscore', 'underscore.string', 'pgadmin',
'pgadmin.browser', 'pgadmin.browser.collection'],
function($, _, S, pgAdmin, pgBrowser) {
/*
* Create and Add an Extension Collection into nodes
* Params:
* label - Label for Node
* type - Type of Node
* columns - List of columns to show under under properties.
*/
if (!pgBrowser.Nodes['coll-extension']) {
var extensions = pgAdmin.Browser.Nodes['coll-extension'] =
pgAdmin.Browser.Collection.extend({
node: 'extension',
label: '{{ _('Extension') }}',
type: 'coll-extension',
columns: ['name', 'owner', 'comment']
});
};
/*
* Create and Add an Extension Node into nodes
* Params:
* parent_type - Name of parent Node
* type - Type of Node
* hasSQL - True if we need to show SQL query Tab control, otherwise False
* canDrop - True to show "Drop Extension" link under Context menu,
* otherwise False
* canDropCascade - True to show "Drop Cascade" link under Context menu,
* otherwise False
* columns - List of columns to show under under properties tab.
* label - Label for Node
*/
if (!pgBrowser.Nodes['extension']) {
pgAdmin.Browser.Nodes['extension'] =
pgAdmin.Browser.Node.extend({
parent_type: 'database',
type: 'extension',
hasSQL: true,
hasDepends: true,
canDrop: true,
canDropCascade: true,
label: '{{ _('Extension') }}',
Init: function() {
if(this.initialized)
return;
this.initialized = true;
/*
* Add "create extension" menu item into context and object menu
* for the following nodes:
* coll-extension, extension and database.
*/
pgBrowser.add_menus([{
name: 'create_extension_on_coll', node: 'coll-extension', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Extension...') }}',
icon: 'wcTabIcon icon-extension', data: {action: 'create'}
},{
name: 'create_extension', node: 'extension', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Extension...') }}',
icon: 'wcTabIcon icon-extension', data: {action: 'create'}
},{
name: 'create_extension', node: 'database', module: this,
applies: ['object', 'context'], callback: 'show_obj_properties',
category: 'create', priority: 4, label: '{{ _('Extension...') }}',
icon: 'wcTabIcon icon-extension', data: {action: 'create'}
}
]);
},
/*
* Define model for the Node and specify the properties
* of the model in schema.
*/
model: pgAdmin.Browser.Node.Model.extend({
schema: [
{
id: 'name', label: '{{ _('Name')}}', first_empty: true,
type: 'text', mode: ['properties', 'create', 'edit'],
visible: true, url:'avails', disabled: function(m) {
return !m.isNew();
},
transform: function(data) {
var res = [];
var label = this.model.get('name');
if (!this.model.isNew()) {
res.push({label: label, value: label});
}
else {
if (data && _.isArray(data)) {
_.each(data, function(d) {
if (d.installed_version === null)
/*
* d contains json data and sets into
* select's option control
*
* We need to stringify data because formatter will
* convert Array Object as [Object] string
*/
res.push({label: d.name, value: JSON.stringify(d)});
})
}
}
return res;
},
/*
* extends NodeAjaxOptionsControl to override the properties
* getValueFromDOM which takes stringified data from option of
* select control and parse it. And `onChange` takes the stringified
* data from select's option, thus convert it to json format and set the
* data into Model which is used to enable/disable the schema field.
*/
control: Backform.NodeAjaxOptionsControl.extend({
getValueFromDOM: function() {
var data = this.formatter.toRaw(
_.unescape(this.$el.find("select").val()), this.model);
/*
* return null if data is empty to prevent it from
* throwing parsing error. Adds check as name can be empty
*/
if (data === '') {
return null;
}
else if (typeof(data) === 'string') {
data=JSON.parse(data);
}
return data.name;
},
/*
* When name is changed, extract value from its select option and
* set attributes values into the model
*/
onChange: function() {
Backform.NodeAjaxOptionsControl.prototype.onChange.apply(this, arguments);
var selectedValue = this.$el.find("select").val();
if (selectedValue.trim() != "") {
var d = this.formatter.toRaw(selectedValue, this.model);
if(typeof(d) === 'string')
d=JSON.parse(d);
var changes = {
'version' : '',
'relocatable': (
(!_.isNull(d.relocatable[0]) && !_.isUndefined(d.relocatable[0])) ?
d.relocatable[0]: ''),
'schema': ((!_.isNull(d.schema[0]) &&
!_.isUndefined(d.schema[0])) ? d.schema[0]: '')
};
this.model.set(changes);
}
else {
var changes = {'version': '', 'relocatable': true, 'schema': ''};
this.model.set(changes);
}
},
})
},
{
id: 'eid', label: '{{ _('Oid')}}', cell: 'string',
type: 'text', disabled: true, mode: ['properties', 'edit', 'create']
},
{
id: 'owner', label:'{{ _('Owner') }}', control: 'node-list-by-name',
mode: ['properties'], node: 'role', cell: 'string'
},
{
id: 'schema', label: '{{ _('Schema')}}', type: 'text', control: 'node-ajax-options',
mode: ['properties', 'create', 'edit'], group: 'Definition', deps: ['relocatable'],
url: 'schemas', first_empty: true, disabled: function(m) {
/*
* enable or disable schema field if model's relocatable
* attribute is True or False
*/
return (m.has('relocatable') ? !m.get('relocatable') : false);
},
transform: function(data) {
var res = [];
if (data && _.isArray(data)) {
_.each(data, function(d) {
res.push({label: d.schema, value: d.schema});
})
}
return res;
}
},
{
id: 'relocatable', label: '{{ _('Relocatable?')}}', cell: 'switch',
type: 'switch', mode: ['properties'], 'options': {
'onText': 'Yes', 'offText': 'No', 'onColor': 'success',
'offColor': 'default', 'size': 'small'
}
},
{
id: 'version', label: '{{ _('Version')}}', cell: 'string',
mode: ['properties', 'create', 'edit'], group: 'Definition',
control: 'node-ajax-options', url:'avails', first_empty: true,
// Transform the data into version for the selected extension.
transform: function(data) {
res = [];
var extension = this.model.get('name');
_.each(data, function(dt) {
if(dt.name == extension) {
if(dt.version && _.isArray(dt.version)) {
_.each(dt.version, function(v) {
res.push({ label: v, value: v });
});
}
}
});
return res;
}
},
{
id: 'comment', label: '{{ _('Comment')}}', cell: 'string',
type: 'multiline', disabled: true
}
],
validate: function() {
/*
* Triggers error messages for name
* if it is empty/undefined/null
*/
var err = {},
errmsg,
name = this.get('name');
if (_.isUndefined(name) || _.isNull(name) ||
String(name).replace(/^\s+|\s+$/g, '') == '') {
err['name'] = '{{ _('Name can not be empty!') }}';
errmsg = errmsg || err['name'];
this.errorModel.set('name', errmsg);
return errmsg;
}
else {
this.errorModel.unset('name');
}
return null;
}
})
})
};
return pgBrowser.Nodes['coll-extension'];
});

View File

@ -0,0 +1,19 @@
{#=========================Create new extension======================#}
{#===Generates comments and code for SQL tab===#}
{% if display_comments %}
-- Extension: {{ conn|qtIdent(data.name) }}
-- DROP EXTENSION {{ conn|qtIdent(data.name) }};
{% endif %}
{% if data.name %}
CREATE EXTENSION {{ conn|qtIdent(data.name) }}{% if data.schema == '' and data.version == '' %};{% endif %}
{% if data.schema %}
SCHEMA {{ conn|qtIdent(data.schema) }}{% if data.version == '' %};{% endif %}
{% endif %}
{% if data.version %}
VERSION {{ conn|qtIdent(data.version) }};
{% endif %}
{% endif %}

View File

@ -0,0 +1,8 @@
{#============================Drop/Cascade Extension by name=========================#}
{% if eid %}
SELECT x.extname from pg_extension x
WHERE x.oid = {{ eid }}::int
{% endif %}
{% if name %}
DROP EXTENSION {{ conn|qtIdent(name) }} {% if cascade %} CASCADE {% endif %}
{% endif %}

View File

@ -0,0 +1,12 @@
{# ======================Fetch extensions names=====================#}
SELECT
a.name, a.installed_version,
array_agg(av.version) as version,
array_agg(av.schema) as schema,
array_agg(av.superuser) as superuser,
array_agg(av.relocatable) as relocatable
FROM
pg_available_extensions a
LEFT JOIN pg_available_extension_versions av ON (a.name = av.name)
GROUP BY a.name, a.installed_version
ORDER BY a.name

View File

@ -0,0 +1,17 @@
{#===================Fetch properties of each extension by name or oid===================#}
SELECT
x.oid AS eid, pg_get_userbyid(extowner) AS owner,
x.extname AS name, n.nspname AS schema,
x.extrelocatable AS relocatable, x.extversion AS version,
e.comment
FROM
pg_extension x
LEFT JOIN pg_namespace n ON x.extnamespace=n.oid
JOIN pg_available_extensions() e(name, default_version, comment) ON x.extname=e.name
{%- if eid %}
WHERE x.oid = {{eid}}::int
{% elif ename %}
WHERE x.extname = {{ename|qtLiteral}}::text
{% else %}
ORDER BY x.extname
{% endif %}

View File

@ -0,0 +1,3 @@
{#===================fetch all schemas==========================#}
SELECT nspname As schema FROM pg_namespace
ORDER BY nspname

View File

@ -0,0 +1,10 @@
{# =============Update extension schema============= #}
{% if data.schema and data.schema != o_data.schema %}
ALTER EXTENSION {{ conn|qtIdent(o_data.name) }}
SET SCHEMA {{ conn|qtIdent(data.schema) }};
{% endif %}
{# =============Update extension version============= #}
{% if data.version and data.version != o_data.version %}
ALTER EXTENSION {{ conn|qtIdent(o_data.name) }}
UPDATE TO {{ conn|qtIdent(data.version) }};
{% endif %}