Subject: [pgAdmin4] [Patch]: Extension Module

From: Surinder Kumar <[email-masked]>
Date: Tue, 12 Jan 2016 12:44:12 +0530

Hi,

Please find attached patch for the extension module.
Please review it and Let me know for any comments.

Thanks,
Surinder Kumar
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/extensions/__init__.py
new file mode 100644
index 0000000..33f3fba
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/__init__.py
@@ -0,0 +1,356 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2015, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+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 NodeView
+from pgadmin.browser.collection import CollectionNodeModule
+import pgadmin.browser.server_groups.servers.databases as databases
+from pgadmin.utils.ajax import precondition_required
+from pgadmin.utils.driver import get_driver
+from config import PG_DEFAULT_DRIVER
+from functools import wraps
+
+
+class ExtensionModule(CollectionNodeModule):
+    NODE_TYPE = "extension"
+    COLLECTION_LABEL = gettext("Extensions")
+
+    def __init__(self, *args, **kwargs):
+        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 script_load(self):
+        """
+        Load the module script for server, when any of the server-group node is
+        initialized.
+        """
+        return databases.DatabaseModule.NODE_TYPE
+
+
+blueprint = ExtensionModule(__name__)
+
+
+class ExtensionView(NodeView):
+    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):
+        """
+        It 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):
+        """
+        It lists down the 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'],
+                        row['name'],
+                        'icon-extension'
+                    ))
+
+        return make_json_response(
+                data=res,
+                status=200
+                )
+
+    @check_precondition
+    def properties(self, gid, sid, did, eid):
+        """
+        It fetches 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):
+        """
+        This function will creates new the extension object
+        """
+        required_args = [
+            'name',
+            'version'
+        ]
+
+        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'],
+                    row['name'],
+                    'icon-extension'
+                    )
+            )
+
+    @check_precondition
+    def update(self, gid, sid, did, eid):
+        """
+        This function will update 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 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 delete drop/drop cascade the 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 to return modified SQL
+        """
+        data = request.args.copy()
+        SQL = self.getSQL(gid, sid, data, did, eid)
+        if SQL.strip('\n') and SQL.strip(' '):
+            return make_json_response(
+                    data=SQL,
+                    status=200
+                    )
+        else:
+            return make_json_response(
+                    data='-- Modified SQL --',
+                    status=200
+                    )
+
+    def getSQL(self, gid, sid, data, did, eid=None):
+        """
+        This function will generate sql from model data
+        """
+        required_args = [
+            'name',
+            'schema',
+            'version'
+        ]
+        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 (if javascript) exists for this node.
+        Override this property for your own logic.
+        """
+        return make_response(
+                render_template(
+                    "extensions/js/extensions.js",
+                    _=gettext
+                    ),
+                200, {'Content-Type': 'application/x-javascript'}
+                )
+
+ExtensionView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/static/img/coll-extension.png b/web/pgadmin/browser/server_groups/servers/databases/extensions/static/img/coll-extension.png
new file mode 100644
index 0000000000000000000000000000000000000000..eed7ca97a33ef595f448b8621168d531f86d51d5
GIT binary patch
literal 1017
zcmV<V0|xwwP)<h;3K|Lk000e1NJLTq000mG000mO0{{R3C@l|D00001b5ch_0Itp)
z=>Px%_fSk!MS^xZ(#^N3p?R&Od+_Yu$ibj%T^6RorB#bmn!1_5x}2JiUH9|m=i$p{
zRt>7esZx(rkHMB&m|kp}YkF-i+S9qHoNTYCfx^|oK7c>H>Bx7neMEjjfO9vZnQgzg
znaszo-`vc-ww7K_0rBeAwyS=gm1jPCJeJI$Gj=n&vyq*XV~~VSeQ+?GkzLu)v+(KC
z?&Zpui%@D?5_o7K5L*te<Gd|$E&TiU`}p$t_U!fW;PdR+>*2!a-MZ%4vg6gM;L)L=
zk53w479M3BDQ+lfsBo&|xoE3!8)F$jd_LXGn5dUmHEJcE$)m~b(xK3*v*EqB=fkk&
zzN6u@o!_pI*P(X7g>k=tf76xP$dIj?SVe9ugSLpn>&}+lsF2#DiP)Wj)|h<Mlh449
zm4!uCPy}dL4Vs8UtCv!Hv4M5cjedPPC}$ujZ6{xNJBocXBx4(;)vk8Mhd_@=5nK-#
zWf(t|O0=YEF=rrzvxn93+^yBLT$f*Ao@PIeN0+#kWR+urcQMcJ*SF-sT&rnflVFjw
zkaKM%G=n;@$*_-qITl_M!syGG&7n||RE^1*D|;}$)4tfmhrz6AIbIZAnqq{yi*T%V
zHhVWtV;&u18I#tdIE_NpzkbrYc9VfMLtPX<ZYx1&B5Z9Om3}8VZ7FiPes9o-TDx%1
zw{6X{Wx%FXorW{5k~_krMv{3VMsze|t!`+}gJR5mHj+XnY9(-;Zb*VhCx$c1uU*Hh
zRmG=Gz@R^havV*1JY33nR>*Toyk;|+MLMZbK)_+XojHAH5m<yvR;FT5#&1i+YDL0k
zJHK6_k4uhwGlp|1i+C)!pi;J+M~ZYIf@~RtZ5D4_30jF#R;gq*y;+2KJ*J&=$it|K
zbtb^1PrI~}#=)h;zM@}|T^wm05?&D+Y8^J0MJ<m!6k`>&nL&4HC>d%SP=`*4d_$3X
zCoGaZfT(|KS__tZB7UZQKD%8OXc&KL7J+LO8+Rm0f=C!{9T#mJ7UDFE00001bW%=J
z06^y0W&i*H0b)x>L;#2d9Y_EG010qNS#tmY3ljhU3ljkVnw%H_000McNliru+XEXB
z6Cp(Rbcp}}0B%V{K~xyiU68R2z#t3+nS(tt!3~hY^vGg42FL=`EENNm0{M!6-y1;)
zQxFazvL_cK*b<_okFg0d8Hc#V=Eh`)h+yZmplehnlNZEdtgQ@hX0CNeIWEpxw!oyN
ndc~tsp9TlidOzOC&-)*|KH)3pD2ngr00000NkvXXu0mjf<KNUz

literal 0
HcmV?d00001

diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/static/img/extension.png b/web/pgadmin/browser/server_groups/servers/databases/extensions/static/img/extension.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3c533347883fc1dec73bcb21b32fc54e3c31732
GIT binary patch
literal 996
zcmV<A0~`E_P)<h;3K|Lk000e1NJLTq000mG000mO0{{R3C@l|D00001b5ch_0Itp)
z=>Px%`cO<%MQ2zLf_6I6&9|zdd99>-@a*2m!Junh7N)|bRf|-bx|zVboSKha_w(lG
z;mc-L4XVVcQjb)R!IoN>UTm6cdTlP+)48aeY_F(+!qvh)fIq$I$ak@QM1Demb2p-y
zZNIpg%*U?Z+|0eUmR?Q)@#@yLtA3u9XFhv8mdv0tb~C!Ok)4xckc3Wsa4?*aUD?pH
z@afX-<;s|gP-<HecxWLITMn+{ye)Dq{QLL&`11Mo?Dg>A^X%E{;lk+My5`xk<JGC)
z(V?J^Pa0qr9%URUZYXJ}aH`|EXsd7=V;MkvKHbcisFznYY9*e@qsi^kq0p+c;k~!#
z!?5MPqv5li->#6?p?1NAale3n)0NuDkgb|nMQ$vEwur;)&X(P%klLe(*qwsbn0(Ze
z&%lqBg+*3S1ZY?dnutTImr{GNfpyZ2etkM9XCNqTCtr9wihVOAV;iN_u6D(TK#xcf
zTn`v!7(bOtw4`b=XCQ>Lht=`it<|(#mtSF?W<QQcm$;T>m1BZ;G0*SUx8%WGt7&4B
zV3D+tb8RIwgF3Luu#bN^7G4v==*yVRp-_@kjmeoSdoaG!zSzWv!K`UHUKC!MVuZSj
zaIAJVdpAyF9vxyClh&g+jY8GGe$u;klYuouT@*iVD?w)>Y;7EsekVC?DRR1gZ_tQZ
zyKv99ZOyY~z@}84hBK{_JHn(!l6fLVbTnhFZfMSfV$6Lul0qhGC2*Z?NP<TvhBL~q
zUB|0c#ivccpg)Ro98G#WT*`P>$a6}(W;2>aI;l}Wz+t|fIeljlScFSfreaXWZ%f2#
zMZ#t~zg?k^OOAUphI1*4cr3V}Qns8&igY1@Y#D@Y7H?b$T8UCtsbn_2S%i2!rk!)h
z!>EaMCcvamyR?(W!KK8$qF<3+9BCaAUJ)8<9X6LmEss1DV->ZTL3e2=8EP9)hfaum
zLy>wXERsEdsDEo(3zmE$ex`jsyImG&7=LOOfol~TcO*%INEmJ%7i}B|YRn1%0004W
zQchC<K<3zH00001VoOIv0Eh)0NB{r;32;bRa{vGf6951U69E94oEQKA00(qQO+^RW
z0~-xG4!&HBF8}}lR!KxbR2b7^U?3j2xS;VvLXbtwTz~>0W=QhVA)yH_FkL{}1;huk
zpaKCQKz@=7s`{i97i0@vl2TS8w1C7?R&G7y;)1Nm<<OZkC{A-h<6}A<CjbDaP8qDe
SR_4e60000<MNUMnLSTaX-^L37

literal 0
HcmV?d00001

diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/js/extensions.js b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/js/extensions.js
new file mode 100644
index 0000000..a21bf7b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/js/extensions.js
@@ -0,0 +1,188 @@
+define(
+        ['jquery', 'underscore', 'underscore.string', 'pgadmin', 'pgadmin.browser', 'pgadmin.browser.collection'],
+function($, _, S, pgAdmin, pgBrowser) {
+
+  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']
+      });
+  };
+
+  if (!pgBrowser.Nodes['extension']) {
+    pgAdmin.Browser.Nodes['extension'] =
+    pgAdmin.Browser.Node.extend({
+      parent_type: 'database',
+      type: 'extension',
+      hasSQL: true,
+      canDrop: true,
+      canDropCascade: true,
+      label: '{{ _('Extension') }}',
+
+      Init: function(){
+        if(this.initialized)
+            return;
+
+        this.initialized = true;
+
+        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 pg-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 pg-icon-extension', data: {action: 'create'}
+        }
+        ]);
+      },
+      model: pgAdmin.Browser.Node.Model.extend({
+        schema: [
+            {
+                id: 'name', label: '{{ _('Name')}}',
+                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)) {
+                            res.push({label: '', value: ''});
+                            _.each(data, function(d) {
+                                if (d.installed_version == null)
+                                    /* d contains json data and sets into
+                                    * select's option control
+                                    */
+                                    res.push({label: d.name, value: 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(this.$el.find("select").val(), this.model);
+                        return data.name;
+                    },
+                    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);
+                            var changes = {
+                                'version': (!_.isNull(d.version[0]) ? d.version[0]: ''),
+                                'relocatable': (!_.isNull(d.relocatable[0]) ? d.relocatable[0]: ''),
+                                'schema': (!_.isNull(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')}}', cell: 'string',
+                type: 'text', mode: ['properties']
+            },
+            {
+                id: 'schema', label: '{{ _('Schema')}}', type: 'text', control: 'node-ajax-options',
+                mode: ['properties', 'create', 'edit'], group: 'Definition', deps: ['relocatable'],
+                url: 'schemas', 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',
+                type: 'options', mode: ['properties', 'create', 'edit'], group: 'Definition',
+                disabled: false, control: 'node-ajax-options', url:'avails',
+                /*
+                * 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
+            },
+            {
+                id: 'slony', label: '{{ _('Use Slony')}}', cell: 'string',
+                type: 'text', disabled: true, mode: ['create', 'edit']
+            }
+            ],
+        validate: function() {
+            /*
+            * Triggers specific error messages for name and
+            * version if it is empty
+            */
+            var changedAttrs = this.changed;
+            if (_.has(changedAttrs, this.get('name')) && _.isUndefined(this.get('name')) || String(this.get('name')).replace(/^\s+|\s+$/g, '') == '') {
+                var msg = '{{ _('Name can not be empty!') }}';
+                this.trigger('on-status',{msg:msg});
+                return msg;
+            }
+            if (_.has(changedAttrs, this.get('version')) && _.isUndefined(this.get('version')) || String(this.get('version')).replace(/^\s+|\s+$/g, '') == '') {
+                var msg = '{{ _('Version can not be empty!') }}';
+                this.trigger('on-status',{msg:msg});
+                return msg;
+            }
+            this.trigger('on-status-clear');
+            return true;
+        }
+      })
+    })
+  };
+
+  return pgBrowser.Nodes['coll-extension'];
+});
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/create.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/create.sql
new file mode 100644
index 0000000..a68b7ce
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/create.sql
@@ -0,0 +1,10 @@
+{#=========================Create new extension======================#}
+{% if data.name %}
+CREATE EXTENSION {{ data.name }}
+{% if data.schema %}
+  SCHEMA {{ data.schema }}
+{% endif %}
+{% if data.version %}
+  VERSION '{{ data.version }}';
+{% endif %}
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/delete.sql
new file mode 100644
index 0000000..12741fb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/delete.sql
@@ -0,0 +1,8 @@
+{#============================Drop Extension by name=========================#}
+{% if eid %}
+SELECT x.extname from pg_extension x
+    WHERE x.oid = {{ eid }}::int
+{% endif %}
+{% if name %}
+DROP EXTENSION {{ name }} {% if cascade %} CASCADE {% endif %}
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/extensions.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/extensions.sql
new file mode 100644
index 0000000..bf3979d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/extensions.sql
@@ -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
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/properties.sql
new file mode 100644
index 0000000..b609883
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/properties.sql
@@ -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}}'::text
+{% else %}
+ ORDER BY x.extname
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/schemas.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/schemas.sql
new file mode 100644
index 0000000..b8e3c97
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/schemas.sql
@@ -0,0 +1,3 @@
+{#===================fetch all schemas==========================#}
+SELECT nspname As schema FROM pg_namespace
+ ORDER BY nspname
diff --git a/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/update.sql b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/update.sql
new file mode 100644
index 0000000..cbdaa60
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/extensions/templates/extensions/sql/update.sql
@@ -0,0 +1,10 @@
+{# =============Update extension schema============= #}
+{% if data.schema != o_data.schema %}
+ALTER EXTENSION {{ o_data.name }}
+    SET SCHEMA {{ data.schema|e }};
+{% endif %}
+{# =============Update extension version============= #}
+{% if data.version and data.version != o_data.version %}
+ALTER EXTENSION {{ o_data.name }}
+    UPDATE TO '{{ data.version }}';
+{% endif %}

-- 
Sent via pgadmin-hackers mailing list ([email-masked])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgadmin-hackers

---

From: Surinder Kumar <[email-masked]>
Date: Tue, 12 Jan 2016 13:15:31 +0530

Please find the updated patch with following changes:

   1. corrected copyright.
   2. Added proper comment for script_module function in __init__.py file.
   3. Renamed collection Node's label to Extensions in extensions.js file.

+# Copyright (C) 2013 - 2016, The pgAdmin Development Team
+        Load the module script for extension, when any of the database node is

literal 0
HcmV?d00001

literal 0
HcmV?d00001

+        label: '{{ _('Extensions') }}',
+{#============================Drop/Cascade Extension by name=========================#}

---

From: Neel Patel <[email-masked]>
Date: Mon, 18 Jan 2016 16:52:50 +0530

Hi Surinder,

We have applied/tested the patch and below are the review comments.

1. When we select the extension "plpython3u", "plperl", "plperu" etc. then
it gives 'TypeError' in Javascript.
TypeError: d.version is undefined
'version': (!_.isNull(d.version[0]) ? d.version[0]: '')

We are getting this error while selecting many extensions so please test
with all types of extensions, it should not give any error at client side.

2.  Use 2 space indentation instead of 4 space in javascript file.

3. In "validate" function in "extension.js" file, validate only the changed
values not all, and "this.get('name') - should be called only one time not
multiple
    time".

4. When we pass object identifier, use the function 'qtIdent', and for the
values, use function  'qtLiteral' in all the sql files.

5. By default, when we create the extension, "schema_name" and "version"
should not be be set with value. It should be set blank by default.

6. When we create any extension like "citext" then we are not able to
create the same extension again after deleting the same extension. May be
issue
    with caching mechanism.

7. When we remove the schema_name during the "Edit" operation then wrong
SQL is getting generated.

8. Remove "Use Slony" option. As discussed with Ashesh, we will implement
it as separate module.

Please fix the above issues. Let us know if you want more information.

Thanks,
Neel Patel

Thanks,
Neel Patel

---

From: Surinder Kumar <[email-masked]>
Date: Mon, 18 Jan 2016 17:44:59 +0530

Thanks Neel for reviewing. I'll send the patch with the fixes suggested.

On Mon, Jan 18, 2016 at 4:52 PM, Neel Patel <[email-masked]>
wrote:

> 3. In "validate" function in "extension.js" file, validate only the
> changed values not all, and "this.get('name') - should be called only one
> time not multiple
>     time".
>
> 4. When we pass object identifier, use the function 'qtIdent', and for
> the values, use function  'qtLiteral' in all the sql files.
>

---

From: Surinder Kumar <[email-masked]>
Date: Thu, 21 Jan 2016 20:04:30 +0530

Hi,

I've fixed the issues given in review comments.

Please find the attached updated patch for extension module, review it and
let me know for any comments.

On Mon, Jan 18, 2016 at 5:44 PM, Surinder Kumar <
[email-masked]> wrote:

>> 1. When we select the extension "plpython3u", "plperl", "plperu" etc.
>> then it gives 'TypeError' in Javascript.
> Fixed.

> Done

> Fixed.

> Done.

> Kept blank while creating extension.

> It is an architecture change. we'll fix it later.

> Fixed, Now it generates right SQL.

> Removed.

---

From: Surinder Kumar <[email-masked]>
Date: Fri, 22 Jan 2016 12:25:06 +0530

Hi

Please find the updated patch with following fixes:
1. Missing `owner` column under properties for `extensions collection`. Add
cell: 'string' property for owner fixed it
2. Schema object identifier should be wrapped with in function qtIdent .
Using function `qtIdent` for schema in create.sql fixed it.

Thanks
Surinder Kumar

---

From: Neel Patel <[email-masked]>
Date: Thu, 4 Feb 2016 10:16:48 +0530

Hi Surinder,

While applying the patch, we are getting below warnings.

extension_v3.patch:362: trailing whitespace.
        This function will generate sql for sql panel
extension_v3.patch:646: trailing whitespace.
-- Extension: {{ conn|qtIdent(data.name) }}
warning: 2 lines add whitespace errors.

Can you please resend the patch file, after fixing above warnings ?

Thanks,
Neel Patel

On Fri, Jan 22, 2016 at 12:25 PM, Surinder Kumar <
[email-masked]> wrote:

> 1. Missing `owner` column under properties for `extensions collection`.
> Add cell: 'string' property for owner fixed it
>> Please find the attached updated patch for extension module, review it
>> and let me know for any comments.
>>
>>> On Mon, Jan 18, 2016 at 4:52 PM, Neel Patel <[email-masked]
>>> > wrote:
>>>
>>>> Hi Surinder,
>>>>
>>>> We are getting this error while selecting many extensions so please
>>>> test with all types of extensions, it should not give any error at client
>>>> side.
>>>>
>>> Fixed.
>>
>>>
>>>> 5. By default, when we create the extension, "schema_name" and
>>>> "version" should not be be set with value. It should be set blank by
>>>> default.
>>>>
>>>> 7. When we remove the schema_name during the "Edit" operation then
>>>> wrong SQL is getting generated.
>>>>
>>> 8. Remove "Use Slony" option. As discussed with Ashesh, we will
>>>> implement it as separate module.
>>>>
>>> Removed.
>>
>>>

---

From: Surinder Kumar <[email-masked]>
Date: Thu, 4 Feb 2016 11:40:49 +0530

Hi Neel,

Please find the patch with following changes:
1. Removed Whitespace from files.
2. Fixed an issue in which json object converted into [object object]
string.
3. Fixed python3 issue of unicode type where code breaks at
"isinstance(SQL, str)" in python3 if str is unicode,
because python no longer support unicode type of string.

Please review the patch and Let me know for any comments.

Thanks,
Surinder Kumar

On Thu, Feb 4, 2016 at 10:16 AM, Neel Patel <[email-masked]>
wrote:

>>>> On Mon, Jan 18, 2016 at 4:52 PM, Neel Patel <
>>>> [email-masked]> wrote:
>>>>
>>>>> Hi Surinder,
>>>>>
>>>>> 4. When we pass object identifier, use the function 'qtIdent', and
>>>>> for the values, use function  'qtLiteral' in all the sql files.
>>>>>
>>>> Done.
>>>
>>>>

---

From: Neel Patel <[email-masked]>
Date: Thu, 4 Feb 2016 11:59:11 +0530

Hi,

New patch is working fine.
Ashesh - Can you please review it? If it looks good then we can commit the
extension module.

Thanks,
Neel Patel

---

From: Surinder Kumar <[email-masked]>
Date: Mon, 15 Feb 2016 15:25:12 +0530

Hi,

PFA patch with following changes:

   1. Added "Create Extension" menu item in context menu of Database node.
   2. Added a new method "node_node" in ExtensionModule class. If a node
   has child, returns True, otherwise False.
   3. Fixed an issue in which icon won't display in create extension link
   in context menu.
   4. Added Docstring for the class and methods in python file and proper
   commenting in js file.
   5. Followed PEP-08 coding conventions.

Please review the patch and let me know for any comments.

On Thu, Feb 4, 2016 at 11:59 AM, Neel Patel <[email-masked]>
wrote:

>>>> 2. Schema object identifier should be wrapped with in function qtIdent
>>>> . Using function `qtIdent` for schema in create.sql fixed it.
>>>>
>>>>
>>>> Thanks
>>>> Surinder Kumar
>>>>
>>>>>>> 1. When we select the extension "plpython3u", "plperl", "plperu"
>>>>>>> etc. then it gives 'TypeError' in Javascript.
>>>>>>> 6. When we create any extension like "citext" then we are not able
>>>>>>> to create the same extension again after deleting the same extension. May
>>>>>>> be issue
>>>>>>>     with caching mechanism.
>>>>>>>

---

From: Dave Page <[email-masked]>
Date: Mon, 15 Feb 2016 11:07:26 +0000

Hi

>    5. Followed PEP-08 coding conventions.
I haven't tested this, but a few initial comments:

- The commenting of the JS code is better than I've seen in other patches
\o/, but the commenting style is inconsistent. We should use /* */ for
multi-line comments, and // for single line.
-  The JS code could use some carefully introduced blank lines to help make
it more readable.
- s/}else{/} else {/
- Dependency/depends display is missing (see previous email to Akshay).
This is essential for this node!
- There's no pydoc comment introducing __init__.py
- Shouldn't "data='-- Modified SQL --'," be "data=gettext('-- Modified SQL
--'),"?

-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EnterpriseDB UK: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: Surinder Kumar <[email-masked]>
Date: Tue, 23 Feb 2016 16:37:22 +0530

Hi,

PFA patch with changes suggested by Dave

On Mon, Feb 15, 2016 at 4:37 PM, Dave Page <[email-masked]> wrote:

>>    3. Fixed an issue in which icon won't display in create extension
>>    link in context menu.
>>    4. Added Docstring for the class and methods in python file and
>>    proper commenting in js file.
> -  The JS code could use some carefully introduced blank lines to help
> make it more readable.
>
Done

Done

> This is essential for this node!
Implemented dependency and depends

> - There's no pydoc comment introducing __init__.py
Added pydoc

> - Shouldn't "data='-- Modified SQL --'," be "data=gettext('-- Modified SQL
Yes, it should be. Fixed.

---

From: Dave Page <[email-masked]>
Date: Wed, 24 Feb 2016 16:47:12 +0000

Thanks - committed.

On Tue, Feb 23, 2016 at 11:07 AM, Surinder Kumar <
[email-masked]> wrote:

>>>    2. Added a new method "node_node" in ExtensionModule class. If a
>>>    node has child, returns True, otherwise False.
>> - Shouldn't "data='-- Modified SQL --'," be "data=gettext('-- Modified
>> SQL --'),"?
>>