diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py new file mode 100644 index 00000000..002f07d9 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/__init__.py @@ -0,0 +1,163 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +"""Implements External Tables Node""" +from functools import wraps +from gettext import gettext + +from flask import render_template + +from config import PG_DEFAULT_DRIVER +from pgadmin.browser.collection import CollectionNodeModule +from pgadmin.browser.server_groups.servers import databases +from pgadmin.browser.utils import PGChildNodeView +from pgadmin.utils.ajax import make_json_response +from pgadmin.utils.driver import get_driver +from pgadmin.utils.compile_template_name import compile_template_name + + +class ExternalTablesModule(CollectionNodeModule): + """ + class ExternalTablesModule(CollectionNodeModule) + + A module class for External Tables node derived from + CollectionNodeModule. + + Methods: + ------- + * __init__(*args, **kwargs) + - Method is used to initialize the External Tables module + and it's base module. + + * get_nodes(gid, sid, did) + - Method is used to generate the browser collection node. + + * script_load() + - Load the module script for External Tables, when any of + the database node is initialized. + """ + + NODE_TYPE = 'external_tables' + COLLECTION_LABEL = gettext("External Tables") + + def __init__(self, *args, **kwargs): + """ + Method is used to initialize the External tables module and + it's base module. + + Args: + *args: + **kwargs: + """ + + super(ExternalTablesModule, self).__init__(*args, **kwargs) + self.max_ver = 0 + + def get_nodes(self, gid, sid, did): + yield self.generate_browser_collection_node(did) + + @property + def script_load(self): + """ + Load the module script for External tables, + when any of the database node is initialized. + + Returns: node type of the database module. + """ + return databases.DatabaseModule.NODE_TYPE + + @property + def module_use_template_javascript(self): + """ + Returns whether Jinja2 template is used for generating the javascript + module. + """ + return False + + +blueprint = ExternalTablesModule(__name__) + + +class ExternalTablesView(PGChildNodeView): + node_type = blueprint.node_type + + parent_ids = [ + {'type': 'int', 'id': 'server_group_id'}, + {'type': 'int', 'id': 'server_id'}, + {'type': 'int', 'id': 'database_id'} + ] + + ids = [ + {'type': 'int', 'id': 'fid'} + ] + + operations = dict({ + 'nodes': [{'get': 'node'}, {'get': 'nodes'}], + 'children': [{'get': 'children'}] + }) + + def check_precondition(function_wrapped): + """ + 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(function_wrapped) + 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['server_id'] + ) + self.connection = self.manager.connection( + did=kwargs['database_id'] + ) + + return function_wrapped(*args, **kwargs) + + return wrap + + @check_precondition + def nodes(self, server_group_id, server_id, database_id): + """ + This function will used to create all the child node within that + collection. + Here it will create all the foreign data wrapper node. + + Args: + server_group_id: Server Group ID + server_id: Server ID + database_id: Database ID + """ + template_name = compile_template_name( + 'sql/', + 'list.sql', + self.manager.server_type, + self.manager.sversion + ) + template = render_template(template_name) + status, external_tables = self.connection.execute_2darray(template) + icon_css_class = 'icon-external_tables' + result = [] + for external_table in external_tables['rows']: + result.append(self.blueprint.generate_browser_node( + external_table['oid'], + database_id, + external_table['name'], + inode=False, + icon=icon_css_class + )) + return make_json_response( + data=result, + status=200 + ) + + +ExternalTablesView.register_node_view(blueprint) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/reverse_engineer_ddl.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/reverse_engineer_ddl.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_tables.svg b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_tables.svg new file mode 100644 index 00000000..bcce32d6 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/coll-external_tables.svg @@ -0,0 +1 @@ +coll-foreign_data_wrapper \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_tables.svg b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_tables.svg new file mode 100644 index 00000000..90228c96 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/static/img/external_tables.svg @@ -0,0 +1 @@ +coll-table \ No newline at end of file diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql new file mode 100644 index 00000000..19fd0953 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/get_columns.sql @@ -0,0 +1,12 @@ +SELECT + a.attname AS name, format_type(a.atttypid, NULL) AS cltype, + quote_ident(n.nspname)||'.'||quote_ident(c.relname) as inheritedfrom, + c.oid as inheritedid +FROM + pg_class c +JOIN + pg_namespace n ON c.relnamespace=n.oid +JOIN + pg_attribute a ON a.attrelid = c.oid AND NOT a.attisdropped AND a.attnum > 0 +WHERE + c.oid = {{tid}}::OID diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql new file mode 100644 index 00000000..7490583f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/templates/sql/gpdb_5.0_plus/list.sql @@ -0,0 +1,6 @@ +SELECT pg_class.oid, relname as name +FROM pg_class +LEFT JOIN pg_namespace ON pg_namespace.oid=pg_class.relnamespace::oid +WHERE relkind = 'r' + AND relstorage = 'x' + AND pg_namespace.nspname not like 'gp_toolkit'; diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py new file mode 100644 index 00000000..f8c47ab3 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_module.py @@ -0,0 +1,99 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +import six + +from pgadmin.browser.server_groups.servers\ + .databases.external_tables import ExternalTablesModule +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, Mock +else: + from unittest.mock import MagicMock, Mock + + +class TestExternalTablesModule(BaseTestGenerator): + scenarios = [ + ('#BackendSupported When access the on a Postgresql Database, ' + 'it returns false', + dict( + test_type='backend-support', + manager=dict( + server_type='pg', + sversion=90100 + ), + expected_result=False, + )), + ('#BackendSupported When access the on a GreenPlum Database, ' + 'it returns true', + dict( + test_type='backend-support', + manager=dict( + server_type='gpdb', + sversion=82303 + ), + expected_result=True + )), + ('#get_nodes when trying to retrieve the node, ' + 'it should return true', + dict( + test_type='get-nodes', + function_parameters=dict( + gid=10, + sid=11, + did=12, + ), + expected_generate_browser_collection_node_called_with=12 + )), + ('#get_module_use_template_javascript when checking if need to ' + 'generate javascript from template, ' + 'it should return false', + dict( + test_type='template-javascript', + expected_result=False + )) + ] + + def runTest(self): + if self.test_type == 'backend-support': + self.__test_backend_support() + elif self.test_type == 'get-nodes': + self.__test_get_nodes() + elif self.test_type == 'template-javascript': + self.__test_template_javascript() + + def __test_backend_support(self): + manager = MagicMock() + manager.sversion = self.manager['sversion'] + manager.server_type = self.manager['server_type'] + module = ExternalTablesModule('something') + self.assertEquals( + self.expected_result, + module.BackendSupported(manager) + ) + + def __test_get_nodes(self): + module = ExternalTablesModule('something') + module.generate_browser_collection_node = Mock() + + result = module.get_nodes(**self.function_parameters) + six.next(result) + + module.generate_browser_collection_node.assert_called_with( + self.expected_generate_browser_collection_node_called_with + ) + + def __test_template_javascript(self): + module = ExternalTablesModule('something') + self.assertEquals( + self.expected_result, + module.module_use_template_javascript) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py new file mode 100644 index 00000000..f0272bd2 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_external_tables_view.py @@ -0,0 +1,159 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +from pgadmin.browser.server_groups.servers.databases.external_tables import \ + ExternalTablesView +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, patch +else: + from unittest.mock import MagicMock, patch + + +class TestExternalTablesView(BaseTestGenerator): + scenarios = [ + ('#check_precondition When executing any http call, ' + 'it saves stores the connection and the manager in the class object', + dict( + test_type='check-precondition', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + expected_manager_connection_to_be_called_with=dict( + did=2 + ), + )), + ('#nodes When retrieving the nodes ' + 'and the database does not have external tables, ' + 'it return no child nodes ' + 'and status 200', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_make_json_response_called_with=dict( + data=[], + status=200 + ), + )), + ('#nodes When retrieving the nodes ' + 'and the database has 2 external tables' + 'it return 2 child nodes ' + 'and status 200', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict( + rows=[ + dict( + oid='oid1', + name='table_one' + ), + dict( + oid='oid2', + name='table_two' + ), + ] + )), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_make_json_response_called_with=dict( + data=[ + { + 'id': "external_tables/oid1", + 'label': 'table_one', + 'icon': 'icon-external_tables', + 'inode': False, + '_type': 'external_tables', + '_id': 'oid1', + '_pid': 2, + 'module': 'pgadmin.node.external_tables' + }, + { + 'id': "external_tables/oid2", + 'label': 'table_two', + 'icon': 'icon-external_tables', + 'inode': False, + '_type': 'external_tables', + '_id': 'oid2', + '_pid': 2, + 'module': 'pgadmin.node.external_tables' + } + ], + status=200 + ), + )), + ] + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.get_driver') + def runTest(self, get_driver_mock): + self.__before_all(get_driver_mock) + + if self.test_type == 'check-precondition': + self.__test_backend_support() + elif self.test_type == 'nodes': + self.__test_nodes() + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + def __test_backend_support(self, _): + self.manager.connection = MagicMock(return_value=self.connection) + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.nodes(**self.function_parameters) + self.manager.connection.assert_called_with( + **self.expected_manager_connection_to_be_called_with + ) + self.assertEquals(self.manager, external_tables_view.manager) + self.assertEquals(self.connection, external_tables_view.connection) + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.make_json_response') + def __test_nodes(self, make_json_response_mock, render_template_mock): + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.nodes(**self.function_parameters) + make_json_response_mock.assert_called_with( + **self.expected_make_json_response_called_with + ) + render_template_mock.assert_called_with( + self.expect_render_template_called_with + ) + + def __before_all(self, get_driver_mock): + self.connection.execute_2darray.return_value = \ + self.execute_2darray_return_value + self.manager.connection = MagicMock(return_value=self.connection) + get_driver_mock.return_value = MagicMock( + connection_manager=MagicMock(return_value=self.manager) + ) diff --git a/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py new file mode 100644 index 00000000..49ebdfa4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/databases/external_tables/tests/test_reverse_engineer_ddl.py @@ -0,0 +1,62 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2018, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import sys + +from pgadmin.browser.server_groups.servers.databases.external_tables import \ + ExternalTablesView +from pgadmin.utils.route import BaseTestGenerator + +if sys.version_info < (3, 3): + from mock import MagicMock, patch +else: + from unittest.mock import MagicMock, patch + + +class TestReverseEngineerDDL(BaseTestGenerator): + scenarios = [ + ('#format When a , ' + 'it return no child nodes ' + 'and status 200', + dict( + test_type='nodes', + function_parameters=dict( + server_group_id=0, + server_id=1, + database_id=2, + ), + manager=MagicMock(server_type='gpdb', sversion=80323), + connection=MagicMock(execute_2darray=MagicMock()), + execute_2darray_return_value=(True, dict(rows=[])), + + expect_render_template_called_with='sql/#gpdb#80323#/list.sql', + expected_make_json_response_called_with=dict( + data=[], + status=200 + ), + )), + + ] + + def runTest(self): + + if self.test_type == 'check-precondition': + self.__test_backend_support() + + @patch('pgadmin.browser.server_groups.servers.databases.external_tables' + '.render_template') + def __test_backend_support(self, _): + self.manager.connection = MagicMock(return_value=self.connection) + external_tables_view = ExternalTablesView(cmd='') + external_tables_view.nodes(**self.function_parameters) + self.manager.connection.assert_called_with( + **self.expected_manager_connection_to_be_called_with + ) + self.assertEquals(self.manager, external_tables_view.manager) + self.assertEquals(self.connection, external_tables_view.connection) diff --git a/web/pgadmin/static/bundle/browser.js b/web/pgadmin/static/bundle/browser.js index d39ce22b..3fcc69d8 100644 --- a/web/pgadmin/static/bundle/browser.js +++ b/web/pgadmin/static/bundle/browser.js @@ -1,5 +1,6 @@ define('bundled_browser',[ 'pgadmin.browser', + 'sources/browser/server_groups/servers/databases/external_tables/index', ], function(pgBrowser) { pgBrowser.init(); }); diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js new file mode 100644 index 00000000..d0655c52 --- /dev/null +++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/external_tables.js @@ -0,0 +1,35 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +export function initialize(pgBrowser, gettext) { + if (!pgBrowser.Nodes['coll-external_tables']) { + pgBrowser.Nodes['coll-external_tables'] = + pgBrowser.Collection.extend({ + node: 'external_tables', + label: gettext('External Tables'), + type: 'coll-external_tables', + columns: ['name', 'fdwowner', 'description'], + }); + } + + if (!pgBrowser.Nodes['external_table']) { + pgBrowser.Nodes['external_table'] = pgBrowser.Node.extend({ + parent_type: 'database', + type: 'external_table', + label: gettext('External Table'), + hasSQL: true, + hasDepends: true, + canDrop: true, + canDropCascade: true, + }); + } + + return pgBrowser; +} + diff --git a/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js new file mode 100644 index 00000000..54cd14bc --- /dev/null +++ b/web/pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index.js @@ -0,0 +1,18 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import pgBrowser from 'top/browser/static/js/browser'; +import gettext from 'sources/gettext'; +import {initialize} from './external_tables'; + +let pgBrowserOut = initialize(pgBrowser, gettext); + +module.exports = { + pgBrowser: pgBrowserOut, +}; diff --git a/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js b/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js new file mode 100644 index 00000000..4236438a --- /dev/null +++ b/web/regression/javascript/browser/server_groups/servers/databases/external_tables/external_tables_spec.js @@ -0,0 +1,55 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2018, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import {initialize} from 'sources/browser/server_groups/servers/databases/external_tables/external_tables'; + +describe('when external tables is loaded', () => { + let pgBrowser; + let gettext; + let result; + beforeEach(() => { + pgBrowser = { + Nodes: {}, + }; + pgBrowser.Collection = jasmine.createSpyObj('Collection', ['extend']); + pgBrowser.Node = jasmine.createSpyObj('Node', ['extend']); + pgBrowser.Collection.extend.and.returnValue('extended object'); + pgBrowser.Node.extend.and.returnValue('extended node object'); + gettext = jasmine.createSpy('gettext').and.callFake((text) => text); + }); + + describe('when external tables is already defined', () => { + beforeEach(() => { + pgBrowser.Nodes['coll-external_tables'] = {}; + result = initialize(pgBrowser, gettext); + }); + + it('does not reinitialize it', () => { + expect(pgBrowser.Collection.extend).not.toHaveBeenCalled(); + }); + + it('returns the not updated version of pgBrowser', () => { + expect(result).toBe(pgBrowser); + }); + }); + + describe('when external tables is not defined', () => { + beforeEach(() => { + result = initialize(pgBrowser, gettext); + }); + + it('initializes "coll-external_tables"', () => { + expect(pgBrowser.Collection.extend).toHaveBeenCalled(); + }); + + it('returns the updated version of pgBrowser', () => { + expect(result.Nodes['coll-external_tables']).not.toBeUndefined(); + }); + }); +}); diff --git a/web/webpack.config.js b/web/webpack.config.js index 84f8466e..4dbf1357 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -146,12 +146,21 @@ module.exports = { presets: ['es2015', 'react'], }, }, + }, { + test: /external_table.*\.js/, + use: { + loader: 'babel-loader', + options: { + presets: ['es2015'], + }, + }, }, { // Transforms the code in a way that it works in the webpack environment. // It uses imports-loader internally to load dependency. Its // configuration is specified in webpack.shim.js // Ref: https://www.npmjs.com/package/shim-loader test: /\.js/, + exclude: [/external_table/], loader: 'shim-loader', query: webpackShimConfig, include: path.join(__dirname, '/pgadmin/browser'), diff --git a/web/webpack.shim.js b/web/webpack.shim.js index b0cf5fd6..25c2b519 100644 --- a/web/webpack.shim.js +++ b/web/webpack.shim.js @@ -120,6 +120,7 @@ var webpackShimConfig = { // Map module id to file path used in 'define(['baseurl', 'misc']). It is // used by webpack while creating bundle resolveAlias: { + 'top': path.join(__dirname, './pgadmin'), 'bundled_codemirror': path.join(__dirname, './pgadmin/static/bundle/codemirror'), 'bundled_browser': path.join(__dirname, './pgadmin/static/bundle/browser'), 'sources': path.join(__dirname, './pgadmin/static/js'), @@ -212,6 +213,8 @@ var webpackShimConfig = { 'pgadmin.node.catalog_object': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/catalog_objects/static/js/catalog_object'), 'pgadmin.dashboard': path.join(__dirname, './pgadmin/dashboard/static/js/dashboard'), 'pgadmin.node.foreign_data_wrapper': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/foreign_data_wrappers/static/js/foreign_data_wrapper'), + 'pgadmin.node.external_table': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'), + 'pgadmin.node.external_tables': path.join(__dirname, './pgadmin/static/js/browser/server_groups/servers/databases/external_tables/index'), 'pgadmin.node.foreign_key': path.join(__dirname, './pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/foreign_key/static/js/foreign_key'), 'pgadmin.browser.server.variable': path.join(__dirname, './pgadmin/browser/server_groups/servers/static/js/variable'), 'pgadmin.tools.grant_wizard': path.join(__dirname, './pgadmin/tools/grant_wizard/static/js/grant_wizard'),