Hi All,

Last patch for FTS configuration does not contain node.ui.js file
Kindly ignore it.

Here is the new revised patch attached with this mail.
Please do review it and let me know if any changes required

Regards,
Sanket Mehta
Sr Software engineer
Enterprisedb

On Thu, May 12, 2016 at 4:38 PM, Sanket Mehta <sanket.me...@enterprisedb.com
> wrote:

> Hi,
>
> PFA the revised patch.
> Please do review it and let me know if anything is not proper.
>
> Regards,
> Sanket Mehta
> Sr Software engineer
> Enterprisedb
>
> On Thu, May 5, 2016 at 8:19 PM, Harshal Dhumal <
> harshal.dhu...@enterprisedb.com> wrote:
>
>> + patch link
>>
>>
>> http://www.postgresql.org/message-id/CAFiP3vwkka+=1foj7kr2zbc4azecoca9eo9dz34-oyy_9ge...@mail.gmail.com
>>
>> --
>> *Harshal Dhumal*
>> *Software Engineer *
>>
>>
>>
>> EenterpriseDB <http://www.enterprisedb.com>
>>
>> On Thu, May 5, 2016 at 8:18 PM, Sanket Mehta <
>> sanket.me...@enterprisedb.com> wrote:
>>
>>> Hi,
>>>
>>> PFA first patch for FTS configuration node.
>>>
>>> It depends upon backgrid select2cell multi select control, for which
>>> Harshal has sent the patch recently.
>>> Please do apply his patch first and then apply this patch.
>>>
>>> Please do review it and let me know if any changes are required.
>>>
>>>
>>> Regards,
>>> Sanket Mehta
>>> Sr Software engineer
>>> Enterprisedb
>>>
>>>
>>> --
>>> Sent via pgadmin-hackers mailing list (pgadmin-hackers@postgresql.org)
>>> To make changes to your subscription:
>>> http://www.postgresql.org/mailpref/pgadmin-hackers
>>>
>>>
>>
>
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/__init__.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/__init__.py
new file mode 100644
index 0000000..fa2d3e6
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/__init__.py
@@ -0,0 +1,937 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2016, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Defines views for management of Fts Configuration node"""
+
+import json
+from flask import render_template, make_response, current_app, request, jsonify
+from flask.ext.babel import gettext as _
+from pgadmin.utils.ajax import make_json_response, \
+    make_response as ajax_response, internal_server_error, gone
+from pgadmin.browser.utils import PGChildNodeView
+from pgadmin.browser.server_groups.servers.databases.schemas.utils \
+    import SchemaChildModule
+from pgadmin.browser.server_groups.servers.databases import DatabaseModule
+import pgadmin.browser.server_groups.servers.databases.schemas as schemas
+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 FtsConfigurationModule(SchemaChildModule):
+    """
+     class FtsConfigurationModule(SchemaChildModule)
+
+        A module class for FTS Configuration node derived from SchemaChildModule.
+
+    Methods:
+    -------
+    * __init__(*args, **kwargs)
+      - Method is used to initialize the FtsConfigurationModule and
+        it's base module.
+
+    * get_nodes(gid, sid, did, scid)
+      - Method is used to generate the browser collection node.
+
+    * node_inode()
+      - Method is overridden from its base class to make the node as leaf node
+
+    * script_load()
+      - Load the module script for FTS Configuration, when any of the schema
+      node is initialized.
+    """
+    NODE_TYPE = 'fts_configuration'
+    COLLECTION_LABEL = _('FTS Configurations')
+
+    def __init__(self, *args, **kwargs):
+        self.min_ver = None
+        self.max_ver = None
+        self.manager = None
+        super(FtsConfigurationModule, self).__init__(*args, **kwargs)
+
+    def get_nodes(self, gid, sid, did, scid):
+        """
+        Generate the collection node
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        """
+        yield self.generate_browser_collection_node(scid)
+
+    @property
+    def node_inode(self):
+        """
+        Override the property to make the node as leaf node
+        """
+        return False
+
+    @property
+    def script_load(self):
+        """
+        Load the module script for fts template, when any of the schema
+        node is initialized.
+        """
+        return DatabaseModule.NODE_TYPE
+
+
+blueprint = FtsConfigurationModule(__name__)
+
+
+class FtsConfigurationView(PGChildNodeView):
+    """
+    class FtsConfigurationView(PGChildNodeView)
+
+        A view class for FTS Configuration node derived from PGChildNodeView.
+        This class is responsible for all the stuff related to view like
+        create/update/delete FTS Configuration,
+        showing properties of node, showing sql in sql pane.
+
+    Methods:
+    -------
+    * __init__(**kwargs)
+      - Method is used to initialize the FtsConfigurationView and it's base view.
+
+    * module_js()
+      - This property defines (if javascript) exists for this node.
+        Override this property for your own logic
+
+    * check_precondition()
+      - 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
+
+    * list()
+      - This function is used to list all the  nodes within that collection.
+
+    * nodes()
+      - This function will be used to create all the child node within collection.
+        Here it will create all the FTS Configuration nodes.
+
+    * node()
+      - This function will be used to create a node given its oid
+        Here it will create the FTS Template node based on its oid
+
+    * properties(gid, sid, did, scid, cfgid)
+      - This function will show the properties of the selected FTS Configuration node
+
+    * create(gid, sid, did, scid)
+      - This function will create the new FTS Configuration object
+
+    * update(gid, sid, did, scid, cfgid)
+      - This function will update the data for the selected FTS Configuration node
+
+    * delete(self, gid, sid, did, scid, cfgid):
+      - This function will drop the FTS Configuration object
+
+    * msql(gid, sid, did, scid, cfgid)
+      - This function is used to return modified SQL for the selected node
+
+    * get_sql(data, cfgid)
+      - This function will generate sql from model data
+
+    * sql(gid, sid, did, scid, cfgid):
+      - This function will generate sql to show in sql pane for node.
+
+    * parsers(gid, sid, did, scid):
+      - This function will fetch all ftp parsers from the same schema
+
+    * copyConfig():
+      - This function will fetch all existed fts configurations from same schema
+
+    * tokens():
+      - This function will fetch all tokens from fts parser related to node
+
+    * dictionaries():
+      - This function will fetch all dictionaries related to node
+
+    * dependents(gid, sid, did, scid, cfgid):
+      - This function get the dependents and return ajax response for the node.
+
+    * dependencies(self, gid, sid, did, scid, cfgid):
+      - This function get the dependencies and return ajax response for node.
+
+    """
+
+    node_type = blueprint.node_type
+
+    parent_ids = [
+        {'type': 'int', 'id': 'gid'},
+        {'type': 'int', 'id': 'sid'},
+        {'type': 'int', 'id': 'did'},
+        {'type': 'int', 'id': 'scid'}
+    ]
+    ids = [
+        {'type': 'int', 'id': 'cfgid'}
+    ]
+
+    operations = dict({
+        'obj': [
+            {'get': 'properties', 'delete': 'delete', 'put': 'update'},
+            {'get': 'list', 'post': 'create'}
+        ],
+        'children': [{
+            'get': 'children'
+        }],
+        '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'}],
+        'parsers': [{'get': 'parsers'},
+                          {'get': 'parsers'}],
+        'copyConfig': [{'get': 'copyConfig'},
+                              {'get': 'copyConfig'}],
+        'tokens': [{'get': 'tokens'}, {'get': 'tokens'}],
+        'dictionaries': [{}, {'get': 'dictionaries'}],
+    })
+
+    def _init_(self, **kwargs):
+        self.conn = None
+        self.template_path = None
+        self.manager = None
+        super(FtsConfigurationView, self).__init__(**kwargs)
+
+    def module_js(self):
+        """
+        Load JS file (fts_configuration.js) for this module.
+        """
+        return make_response(
+            render_template(
+                "fts_configuration/js/fts_configuration.js",
+                _=_
+            ),
+            200, {'Content-Type': 'application/x-javascript'}
+        )
+
+    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'])
+            # If DB not connected then return error to browser
+            if not self.conn.connected():
+                return precondition_required(
+                    _("Connection to the server has been lost!")
+                )
+            # we will set template path for sql scripts depending upon server version
+            ver = self.manager.version
+            if ver >= 90100:
+                self.template_path = 'fts_configuration/sql/9.1_plus'
+            return f(*args, **kwargs)
+
+        return wrap
+
+    @check_precondition
+    def list(self, gid, sid, did, scid):
+        """
+        List all FTS Configuration nodes.
+
+        Args:
+            gid: Server Group Id
+            sid: Server Id
+            did: Database Id
+            scid: Schema Id
+        """
+
+        sql = render_template(
+                "/".join([self.template_path, 'properties.sql']),
+                scid=scid
+             )
+        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, scid):
+        """
+        Return all FTS Configurations to generate nodes.
+
+        Args:
+            gid: Server Group Id
+            sid: Server Id
+            did: Database Id
+            scid: Schema Id
+        """
+
+        res = []
+        sql = render_template(
+            "/".join([self.template_path, 'nodes.sql']),
+            scid=scid
+        )
+        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['oid'],
+                    did,
+                    row['name'],
+                    icon="icon-fts_configuration"
+                ))
+
+        return make_json_response(
+            data=res,
+            status=200
+        )
+
+    @check_precondition
+    def node(self, gid, sid, did, scid, cfgid):
+        """
+        Return FTS Configuration node to generate node
+
+        Args:
+            gid: Server Group Id
+            sid: Server Id
+            did: Database Id
+            scid: Schema Id
+            cfgid: fts Configuration id
+        """
+
+        sql = render_template(
+            "/".join([self.template_path, 'nodes.sql']),
+            cfgid=cfgid
+        )
+        status, rset = self.conn.execute_2darray(sql)
+        if not status:
+            return internal_server_error(errormsg=rset)
+
+        if len(rset['rows']) == 0:
+            return gone(_("""
+                Could not find the FTS Configuration node.
+                """))
+
+        for row in rset['rows']:
+            return make_json_response(
+                data=self.blueprint.generate_browser_node(
+                    row['oid'],
+                    did,
+                    row['name'],
+                    icon="icon-fts_configuration"
+                ),
+                status=200
+            )
+
+    @check_precondition
+    def properties(self, gid, sid, did, scid, cfgid):
+        """
+        Show properties of FTS Configuration node
+
+        Args:
+            gid: Server Group Id
+            sid: Server Id
+            did: Database Id
+            scid: Schema Id
+            cfgid: fts Configuration id
+        """
+
+        sql = render_template(
+                "/".join([self.template_path, 'properties.sql']),
+                scid=scid,
+                cfgid=cfgid
+              )
+        status, res = self.conn.execute_dict(sql)
+
+        if not status:
+            return internal_server_error(errormsg=res)
+
+        if len(res['rows']) == 0:
+            return gone(_("""
+                Could not find the FTS Configuration node.
+                """))
+
+        # In edit mode fetch token/dictionary list also
+        if cfgid:
+            sql = render_template("/".join([self.template_path,
+                                             'tokenDictList.sql']),
+                                cfgid=cfgid)
+
+            status, rset = self.conn.execute_dict(sql)
+
+            if not status:
+                return internal_server_error(errormsg=rset)
+
+            res['rows'][0]['tokens'] = rset['rows']
+
+        return ajax_response(
+            response=res['rows'][0],
+            status=200
+        )
+
+    @check_precondition
+    def create(self, gid, sid, did, scid):
+        """
+        This function will creates new the FTS Configuration object
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        """
+
+        # Mandatory fields to create a new FTS Configuration
+        required_args = [
+            'schema',
+            '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=_(
+                        "Couldn't find the required parameter (%s)." % arg
+                    )
+                )
+
+        # Either copy config or parser must be present in data
+        if 'copy_config' not in data and 'parser' not in data:
+            return make_json_response(
+                status=410,
+                success=0,
+                errormsg=_(
+                    "provide atleast copy config or parser"
+                )
+            )
+
+        try:
+            # Fetch schema name from schema oid
+            sql = render_template("/".join([self.template_path,
+                                            'schema.sql']),
+                                  data=data,
+                                  conn=self.conn,
+                                  )
+
+            status, schema = self.conn.execute_scalar(sql)
+            if not status:
+                return internal_server_error(errormsg=schema)
+
+            # Replace schema oid with schema name before passing to create.sql
+            # To generate proper sql query
+            new_data = data.copy()
+            new_data['schema'] = schema
+
+            sql = render_template(
+                "/".join([self.template_path, 'create.sql']),
+                data=new_data,
+                conn=self.conn,
+            )
+            status, res = self.conn.execute_scalar(sql)
+            if not status:
+                return internal_server_error(errormsg=res)
+
+            # We need cfgid to add object in tree at browser,
+            # Below sql will give the same
+            sql = render_template(
+                "/".join([self.template_path, 'properties.sql']),
+                name=data['name']
+            )
+            status, cfgid = self.conn.execute_scalar(sql)
+            if not status:
+                return internal_server_error(errormsg=cfgid)
+
+            return jsonify(
+                node=self.blueprint.generate_browser_node(
+                    cfgid,
+                    did,
+                    data['name'],
+                    icon="icon-fts_configuration"
+                )
+            )
+        except Exception as e:
+            current_app.logger.exception(e)
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def update(self, gid, sid, did, scid, cfgid):
+        """
+        This function will update FTS Configuration node
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: fts Configuration id
+        """
+        data = request.form if request.form else json.loads(
+            request.data.decode())
+
+        # Fetch sql query to update fts Configuration
+        sql = self.get_sql(gid, sid, did, scid, data, cfgid)
+        try:
+            if sql and sql.strip('\n') and sql.strip(' '):
+                status, res = self.conn.execute_scalar(sql)
+                if not status:
+                    return internal_server_error(errormsg=res)
+
+                if cfgid is not None:
+                    sql = render_template(
+                        "/".join([self.template_path, 'nodes.sql']),
+                        cfgid=cfgid,
+                        scid=scid
+                    )
+
+                status, res = self.conn.execute_dict(sql)
+                if not status:
+                    return internal_server_error(errormsg=res)
+
+                if len(res['rows']) == 0:
+                    return gone(_("""
+                        Could not find the FTS Configuration node to update.
+                    """))
+
+                data = res['rows'][0]
+                return jsonify(
+                    node=self.blueprint.generate_browser_node(
+                        cfgid,
+                        did,
+                        data['name'],
+                        icon="icon-fts_configuration"
+                    )
+                )
+            # In case FTS Configuration node is not present
+            else:
+                return make_json_response(
+                    success=1,
+                    info="Nothing to update",
+                    data={
+                        'id': cfgid,
+                        'sid': sid,
+                        'gid': gid,
+                        'did': did,
+                        'scid': scid
+                    }
+                )
+
+        except Exception as e:
+            current_app.logger.exception(e)
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def delete(self, gid, sid, did, scid, cfgid):
+        """
+        This function will drop the FTS Configuration object
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: FTS Configuration id
+        """
+        # Below will decide if it's simple drop or drop with cascade call
+        if self.cmd == 'delete':
+            # This is a cascade operation
+            cascade = True
+        else:
+            cascade = False
+
+        try:
+            # Get name for FTS Configuration from cfgid
+            sql = render_template(
+                "/".join([self.template_path, 'get_name.sql']),
+                cfgid=cfgid
+                )
+            status, res = self.conn.execute_dict(sql)
+            if not status:
+                return internal_server_error(errormsg=res)
+
+            if len(res['rows']) == 0:
+                return gone(_("""
+                        Could not find the FTS Configuration node to delete.
+                    """))
+
+            # Drop FTS Configuration
+            result = res['rows'][0]
+            sql = render_template(
+                "/".join([self.template_path, 'delete.sql']),
+                name=result['name'],
+                schema=result['schema'],
+                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=_("FTS Configuration dropped"),
+                data={
+                    'id': cfgid,
+                    'sid': sid,
+                    'gid': gid,
+                    'did': did,
+                    'scid': scid
+                }
+            )
+
+        except Exception as e:
+            current_app.logger.exception(e)
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def msql(self, gid, sid, did, scid, cfgid=None):
+        """
+        This function returns modified SQL
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: FTS Configuration id
+        """
+        data = {}
+        for k, v in request.args.items():
+            try:
+                data[k] = json.loads(v)
+            except ValueError:
+                data[k] = v
+
+        # Fetch sql query for modified data
+        sql = self.get_sql(gid, sid, did, scid, data, cfgid)
+
+        if isinstance(sql, str) and sql and 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 get_sql(self, gid, sid, did, scid, data, cfgid=None):
+        """
+        This function will return SQL for model data
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: fts Configuration id
+        """
+        try:
+            # Fetch sql for update
+            if cfgid is not None:
+                sql = render_template(
+                    "/".join([self.template_path, 'properties.sql']),
+                    cfgid=cfgid,
+                    scid=scid
+                )
+
+                status, res = self.conn.execute_dict(sql)
+                if not status:
+                    return internal_server_error(errormsg=res)
+
+                if len(res['rows']) == 0:
+                    return gone(_("""
+                        Could not find the FTS Configuration node.
+                    """))
+
+                old_data = res['rows'][0]
+
+                # If user has changed the schema then fetch new schema directly
+                # using its oid otherwise fetch old schema name using its oid
+                sql = render_template(
+                    "/".join([self.template_path, 'schema.sql']),
+                    data=data)
+
+                status, new_schema = self.conn.execute_scalar(sql)
+                if not status:
+                    return internal_server_error(errormsg=new_schema)
+
+                # Replace schema oid with schema name
+                new_data = data.copy()
+                if 'schema' in new_data:
+                    new_data['schema'] = new_schema
+
+                # Fetch old schema name using old schema oid
+                sql = render_template(
+                    "/".join([self.template_path, 'schema.sql']),
+                    data=old_data
+                    )
+
+                status, old_schema = self.conn.execute_scalar(sql)
+                if not status:
+                    return internal_server_error(errormsg=old_schema)
+
+                # Replace old schema oid with old schema name
+                old_data['schema'] = old_schema
+
+                sql = render_template(
+                    "/".join([self.template_path, 'update.sql']),
+                    data=new_data, o_data=old_data
+                    )
+                # Fetch sql query for modified data
+            else:
+                # Fetch schema name from schema oid
+                sql = render_template(
+                    "/".join([self.template_path, 'schema.sql']),
+                    data=data
+                    )
+
+                status, schema = self.conn.execute_scalar(sql)
+                if not status:
+                    return internal_server_error(errormsg=schema)
+
+                # Replace schema oid with schema name
+                new_data = data.copy()
+                new_data['schema'] = schema
+
+                if 'name' in new_data and \
+                        'schema' in new_data:
+                    sql = render_template("/".join([self.template_path,
+                                                    'create.sql']),
+                                          data=new_data,
+                                          conn=self.conn
+                                          )
+                else:
+                    sql = "-- incomplete definition"
+            return str(sql.strip('\n'))
+
+        except Exception as e:
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def parsers(self, gid, sid, did, scid):
+        """
+        This function will return fts parsers list for FTS Configuration
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        """
+        # Fetch last system oid
+        datlastsysoid = self.manager.db_info[did]['datlastsysoid']
+
+        sql = render_template(
+            "/".join([self.template_path, 'parser.sql']),
+            parser=True
+            )
+        status, rset = self.conn.execute_dict(sql)
+
+        if not status:
+            return internal_server_error(errormsg=rset)
+
+        # Empty set is added before actual list as initially it will be visible
+        # at parser control while creating a new FTS Configuration
+        res = [{'label':'', 'value':''}]
+        for row in rset['rows']:
+            if row['schemaoid'] > datlastsysoid:
+                row['prsname'] = row['nspname'] + '.' + row['prsname']
+
+            res.append({'label': row['prsname'],
+                        'value': row['prsname']})
+        return make_json_response(
+            data=res,
+            status=200
+        )
+
+    @check_precondition
+    def copyConfig(self, gid, sid, did, scid):
+        """
+        This function will return copy config list for FTS Configuration
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        """
+        # Fetch last system oid
+        datlastsysoid = self.manager.db_info[did]['datlastsysoid']
+
+        sql = render_template(
+            "/".join([self.template_path, 'copy_config.sql']),
+            copy_config=True
+            )
+        status, rset = self.conn.execute_dict(sql)
+
+        if not status:
+            return internal_server_error(errormsg=rset)
+
+        # Empty set is added before actual list as initially it will be visible
+        # at copy_config control while creating a new FTS Configuration
+        res = [{'label': '', 'value': ''}]
+        for row in rset['rows']:
+            if row['oid'] > datlastsysoid:
+                row['cfgname'] = row['nspname'] + '.' + row['cfgname']
+
+            res.append({'label': row['cfgname'],
+                        'value': row['cfgname']})
+        return make_json_response(
+            data=res,
+            status=200
+        )
+
+    @check_precondition
+    def tokens(self, gid, sid, did, scid, cfgid=None):
+        """
+        This function will return token list of fts parser node related to
+        current FTS Configuration node
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: fts configuration id
+        """
+        try:
+            res = []
+            if cfgid is not None:
+                sql = render_template(
+                    "/".join([self.template_path, 'parser.sql']),
+                    cfgid=cfgid
+                    )
+                status, parseroid = self.conn.execute_scalar(sql)
+
+                if not status:
+                    return internal_server_error(errormsg=parseroid)
+
+                sql = render_template(
+                    "/".join([self.template_path, 'tokens.sql']),
+                    parseroid=parseroid
+                    )
+                status, rset = self.conn.execute_dict(sql)
+
+                for row in rset['rows']:
+                    res.append({'label': row['alias'],
+                                'value': row['alias']})
+
+            return make_json_response(
+                data=res,
+                status=200
+            )
+
+        except Exception as e:
+            current_app.logger.exception(e)
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def dictionaries(self, gid, sid, did, scid, cfgid=None):
+        """
+        This function will return dictionary list for FTS Configuration
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        """
+        sql = render_template(
+            "/".join([self.template_path,'dictionaries.sql'])
+            )
+        status, rset = self.conn.execute_dict(sql)
+
+        if not status:
+            return internal_server_error(errormsg=rset)
+
+        res = []
+        for row in rset['rows']:
+            res.append({'label': row['dictname'],
+                        'value': row['dictname']})
+        return make_json_response(
+            data=res,
+            status=200
+        )
+
+    @check_precondition
+    def sql(self, gid, sid, did, scid, cfgid):
+        """
+        This function will reverse generate sql for sql panel
+        :param gid: group id
+        :param sid: server id
+        :param did: database id
+        :param scid: schema id
+        :param cfgid: FTS Configuration id
+        """
+        try:
+            sql = render_template(
+                "/".join([self.template_path, 'sql.sql']),
+                cfgid=cfgid,
+                scid=scid,
+                conn=self.conn
+            )
+            status, res = self.conn.execute_scalar(sql)
+            if not status:
+                return internal_server_error(
+                    _(
+                    "ERROR: Couldn't generate reversed engineered query for the FTS Configuration!\n{0}"
+                    ).format(
+                        res
+                    )
+                )
+
+            if res is None:
+                return gone(
+                    _(
+                        "ERROR: Couldn't generate reversed engineered query for FTS Configuration node!")
+                )
+
+            return ajax_response(response=res)
+
+        except Exception as e:
+            current_app.logger.exception(e)
+            return internal_server_error(errormsg=str(e))
+
+    @check_precondition
+    def dependents(self, gid, sid, did, scid, cfgid):
+        """
+        This function get the dependents and return ajax response
+        for the FTS Configuration node.
+
+        Args:
+            gid: Server Group ID
+            sid: Server ID
+            did: Database ID
+            scid: Schema ID
+            cfgid: FTS Configuration ID
+        """
+        dependents_result = self.get_dependents(self.conn, cfgid)
+        return ajax_response(
+            response=dependents_result,
+            status=200
+        )
+
+    @check_precondition
+    def dependencies(self, gid, sid, did, scid, cfgid):
+        """
+        This function get the dependencies and return ajax response
+        for the FTS Configuration node.
+
+        Args:
+            gid: Server Group ID
+            sid: Server ID
+            did: Database ID
+            scid: Schema ID
+            cfgid: FTS Configuration ID
+        """
+        dependencies_result = self.get_dependencies(self.conn, cfgid)
+        return ajax_response(
+            response=dependencies_result,
+            status=200
+        )
+
+
+FtsConfigurationView.register_node_view(blueprint)
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/img/coll-fts_configuration.png b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/img/coll-fts_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..01bf4ee5bbcc3f7c8598a4fa9d9a76e061bf60e0
GIT binary patch
literal 369
zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAm`Z~Df*BafCZDwc@shmVT^JZv
z^(q?yd7K3vk;OpT1B~5HX4?T7OFdm2LnJOIConW}78-sOmJ~?%c}Bax{<t#d71=#|
zHPqCWN$5@Z@lwFUm#62x_~-Sqk-qU~?)RU_`jzx=&r9~zKjry1W&57Ff8930^nd;L
z<IHbY?b^4AY0<OSr`yBd9c5tswJ<Ah{>^>As$>q&(poX=^>V-YH|>j=m=Dhq*)Xe{
zJ^#4MHC^^d=CI8letex_H{)<aR+wI0^MmFqG5h<@u)mICPGw@SSyf=o@G(9S=wQ_n
z*NBpo#FA92<f2p{#b9J$XrOCoq-$UpVq{=tVr*q%qHSPmWnf@2Q&kQ{LvDUbW?Cgg
ggMlSj14y-%ff+=@sp+9>fEpM)UHx3vIVCg!0N_P|FaQ7m

literal 0
HcmV?d00001

diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/img/fts_configuration.png b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/static/img/fts_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..0a5caf5373670cc684605d42c9eb21724fd75523
GIT binary patch
literal 346
zcmeAS@N?(olHy`uVBq!ia0vp^0wBx*Bp9q_EZ7UAm`Z~Df*BafCZDwc@shmVT^JZv
z^(q?yd7K3vk;OpT1B~5HX4?T7eV#6kArhC96ArLWyRPcz_RN1y<+t?5ekwm_`v3pt
zpXShUW$y3)=KnrDaoe%($o&2Lz8?Sn->2o%@1(l@)iv**{hVA{@b9+t^71?jt76M3
z|6d>7dilAY%nVDxQ~!THIdrkwu(MR)r>SB6yt#Ml)tB+7IbQubmD}%*%Lhj$?|Vug
zC9j3V?(aLp-Wjqs@c=Kwu}j{!7{ZG80bQnA;u=wsl30>zm0Xkxq!^403=MP*jdTqR
zLyQcpOpL8eOtcM5tqcq-W~$1eXvob^$xN$+XfUt@YXGUXGBAT^I5j<V4NwDvr>mdK
II;Vst0AK%q4gdfE

literal 0
HcmV?d00001

diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/js/fts_configuration.js b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/js/fts_configuration.js
new file mode 100644
index 0000000..782d664
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/js/fts_configuration.js
@@ -0,0 +1,585 @@
+define(
+        ['jquery', 'underscore', 'underscore.string', 'pgadmin',
+         'pgadmin.browser', 'alertify', 'pgadmin.browser.collection'],
+function($, _, S, pgAdmin, pgBrowser, alertify) {
+
+
+// Model for tokens control
+  var TokenModel = pgAdmin.Browser.Node.Model.extend({
+        defaults: {
+          token: undefined,
+          dictname: undefined
+        },
+        // Define the schema for the token/dictionary list
+        schema: [
+          {
+            id: 'token', label:'Token', type:'text', group: null,
+            cellHeaderClasses:'width_percent_50', editable: true,
+            editable: false, cell: 'string', url: 'tokens'
+          },{
+            id: 'dictname', label: 'Dictionaries', type: 'text', group:null,
+            cellHeaderClasses:'width_percent_50', editable: true,
+            cell:Backgrid.Extension.MultiSelectAjaxCell, url: 'dictionaries'
+          }
+        ],
+        // Validation for token and dictionary list
+        validate: function() {
+            // Clear any existing errors.
+            var msg;
+            this.errorModel.clear();
+            var token = this.get('token');
+            var dictionary = this.get('dictname');
+
+            if (_.isNull(token) ||
+                _.isUndefined(token) ||
+                String(token).replace(/^\s+|\s+$/g, '') == '') {
+                msg = '{{ _('Token can not be empty!') }}';
+                this.errorModel.set('token',msg);
+                return msg;
+            }
+
+            if (_.isNull(dictionary) ||
+                _.isUndefined(dictionary) ||
+                String(dictionary).replace(/^\s+|\s+$/g, '') == '') {
+                msg = '{{ _('Dictionary name can not be empty!') }}';
+                this.errorModel.set('dictname',msg);
+                return msg;
+            }
+            return null;
+        }
+  });
+
+// Customized control for token control
+  var TokenControl =  Backform.TokenControl =
+    Backform.UniqueColCollectionControl.extend({
+
+    initialize: function(opts) {
+      Backform.UniqueColCollectionControl.prototype.initialize.apply(
+        this, arguments
+      );
+
+      var self = that = this,
+          node = 'fts_configuration',
+          headerSchema = [{
+            id: 'token', label:'', type:'text', url: 'tokens',
+            node:'fts_configuration', canAdd: true, 'url_with_id': true,
+
+            // Defining control for tokens dropdown control in header
+            control: Backform.NodeAjaxOptionsControl.extend({
+              formatter: Backform.NodeAjaxOptionsControl.prototype.formatter,
+              initialize: function() {
+                Backform.NodeAjaxOptionsControl.prototype.initialize.apply(
+                  this,
+                  arguments
+                 );
+                var self = this,
+                  url = self.field.get('url') || self.defaults.url,
+                  m = self.model.top || self.model;
+
+                /* Fetch the tokens/dict list from 'that' node.
+                 * Here 'that' refers to unique collection control where
+                 * 'self' refers to nodeAjaxOptions control for dictionary
+                 */
+                var cfgid = that.model.get('oid');
+                if (url) {
+                  var node = this.field.get('schema_node'),
+                    node_info = this.field.get('node_info'),
+                    full_url = node.generate_url.apply(
+                      node, [
+                        null, url, this.field.get('node_data'),
+                        this.field.get('url_with_id') || false,
+                        node_info
+                      ]),
+                    cache_level = this.field.get('cache_level') || node.type,
+                    cache_node = this.field.get('cache_node');
+
+                  cache_node = (cache_node &&
+                                    pgAdmin.Browser.Nodes['cache_node'])
+                               || node;
+
+                  /*
+                   * We needs to check, if we have already cached data
+                   * for this url. If yes - use it, and do not bother about
+                   * fetching it again.
+                   */
+                  var data = cache_node.cache(url, node_info, cache_level);
+
+                  // Fetch token/dictionary list
+                  if (this.field.get('version_compatible') &&
+                    (_.isUndefined(data) || _.isNull(data))) {
+                    m.trigger('pgadmin:view:fetching', m, self.field);
+                    $.ajax({
+                      async: false,
+                      url: full_url,
+                      success: function(res) {
+                      /*
+                       * We will cache this data for short period of time for
+                       * avoiding same calls.
+                       */
+                        data = cache_node.cache(url,
+                                 node_info,
+                                 cache_level,
+                                 res.data
+                               );
+                      },
+                      error: function() {
+                        m.trigger('pgadmin:view:fetch:error', m, self.field);
+                      }
+                    });
+                    m.trigger('pgadmin:view:fetched', m, self.field);
+                  }
+
+                  // It is feasible that the data may not have been fetched.
+                  data = (data && data.data) || [];
+
+                  /*
+                   * Transform the data
+                   */
+                  transform = (this.field.get('transform')
+                                || self.defaults.transform);
+                  if (transform && _.isFunction(transform)) {
+                    self.field.set('options', transform.bind(self, data));
+                  } else {
+                    self.field.set('options', data);
+                  }
+                }
+              }
+            }),
+            // Select2 control for adding new tokens
+            select2: {
+                allowClear: true, width: 'style',
+                placeholder: 'Select token'
+              },
+            first_empty: true,
+            disabled: function(m) {
+              return _.isUndefined(self.model.get('oid'));
+            }
+          }],
+          headerDefaults = {token: null},
+          // Grid columns backgrid
+          gridCols = ['token', 'dictname'];
+
+      // Creating model for header control which is used to add new tokens
+      self.headerData = new (Backbone.Model.extend({
+        defaults: headerDefaults,
+        schema: headerSchema
+      }))({});
+
+      // Creating view from header schema in tokens control
+      var headerGroups = Backform.generateViewSchema(
+          self.field.get('node_info'), self.headerData, 'create',
+          self.field.get('schema_node'), self.field.get('node_data')
+          ),
+          fields = [];
+
+      _.each(headerGroups, function(o) {
+        fields = fields.concat(o.fields);
+      });
+      self.headerFields = new Backform.Fields(fields);
+
+      // creating grid using grid columns
+      self.gridSchema = Backform.generateGridColumnsFromModel(
+          self.field.get('node_info'), self.field.get('model'),
+          'edit', gridCols, self.field.get('schema_node')
+          );
+
+      // Providing behaviour control functions to header and grid control
+      self.controls = [];
+      self.listenTo(self.headerData, "change", self.headerDataChanged);
+      self.listenTo(self.headerData, "select2", self.headerDataChanged);
+      self.listenTo(self.collection, "add", self.onAddorRemoveTokens);
+      self.listenTo(self.collection, "remove", self.onAddorRemoveTokens);
+    },
+
+    // Template for creating header view
+    generateHeader: function(data) {
+      var header = [
+        '<div class="subnode-header-form">',
+        ' <div class="container-fluid">',
+        '  <div class="row">',
+        '   <div class="col-xs-4">',
+        '    <label class="control-label"><%-token_label%></label>',
+        '   </div>',
+        '   <div class="col-xs-4" header="token"></div>',
+        '   <div class="col-xs-4">',
+        '     <button class="btn-sm btn-default add" <%=canAdd ? "" : "disabled=\'disabled\'"%> ><%-add_label%></buttton>',
+        '   </div>',
+        '  </div>',
+        ' </div>',
+        '</div>',].join("\n")
+
+      _.extend(data, {
+        token_label: '{{ _('Tokens')}}',
+        add_label: '{{ _('ADD')}}'
+      });
+
+      var self = this,
+          headerTmpl = _.template(header),
+          $header = $(headerTmpl(data)),
+          controls = this.controls;
+
+      self.headerFields.each(function(field) {
+        var control = new (field.get("control"))({
+          field: field,
+          model: self.headerData
+        });
+
+        $header.find('div[header="' + field.get('name') + '"]').append(
+          control.render().$el
+        );
+
+        control.$el.find('.control-label').remove();
+        controls.push(control);
+      });
+
+      // We should not show add button in properties mode
+      if (data.mode == 'properties') {
+        $header.find("button.add").remove();
+      }
+      self.$header = $header;
+      return $header;
+    },
+
+    // Providing event handler for add button in header
+    events: _.extend(
+              {}, Backform.UniqueColCollectionControl.prototype.events,
+              {'click button.add': 'addTokens'}
+            ),
+
+    // Show token/dictionary grid
+    showGridControl: function(data) {
+
+      var self = this,
+          titleTmpl = _.template("<div class='subnode-header'></div>"),
+          $gridBody = $("<div></div>", {
+            class:'pgadmin-control-group backgrid form-group col-xs-12 object subnode'
+          }).append(
+              titleTmpl({label: data.label})
+            );
+
+      $gridBody.append(self.generateHeader(data));
+
+      var gridColumns = _.clone(this.gridSchema.columns);
+
+      // Insert Delete Cell into Grid
+      if (data.disabled == false && data.canDelete) {
+          gridColumns.unshift({
+            name: "pg-backform-delete", label: "",
+            cell: Backgrid.Extension.DeleteCell,
+            editable: false, cell_priority: -1
+          });
+      }
+
+      if (self.grid) {
+        self.grid.remove();
+        self.grid.null;
+      }
+      // Initialize a new Grid instance
+      var grid = self.grid = new Backgrid.Grid({
+        columns: gridColumns,
+        collection: self.collection,
+        className: "backgrid table-bordered"
+      });
+      self.$grid = grid.render().$el;
+
+      $gridBody.append(self.$grid);
+
+      // Find selected dictionaries in grid and show it all together
+      setTimeout(function() {
+        self.headerData.set({
+          'token': self.$header.find(
+            'div[header="token"] select'
+            ).val()
+            }, {silent:true}
+          );
+      }, 10);
+
+      // Render node grid
+      return $gridBody;
+    },
+
+    // When user change the header control to add a new token
+    headerDataChanged: function() {
+      var self = this, val,
+          data = this.headerData.toJSON(),
+          inSelected = false,
+          checkVars = ['token'];
+
+      if (!self.$header) {
+        return;
+      }
+
+      if (self.control_data.canAdd) {
+        self.collection.each(function(m) {
+          if (!inSelected) {
+            _.each(checkVars, function(v) {
+              if (!inSelected) {
+                val = m.get(v);
+                inSelected = ((
+                  (_.isUndefined(val) || _.isNull(val)) &&
+                  (_.isUndefined(data[v]) || _.isNull(data[v]))
+                  ) ||
+                  (val == data[v]));
+              }
+            });
+          }
+        });
+      }
+      else {
+        inSelected = true;
+      }
+
+      self.$header.find('button.add').prop('disabled', inSelected);
+    },
+
+    // Get called when user click on add button header
+    addTokens: function(ev) {
+      ev.preventDefault();
+      var self = this,
+          token = self.headerData.get('token');
+
+      if (!token || token == '') {
+        return false;
+      }
+
+      var coll = self.model.get(self.field.get('name')),
+          m = new (self.field.get('model'))(
+                self.headerData.toJSON(), {
+                  silent: true, top: self.model.top,
+                  collection: coll, handler: coll
+                });
+
+      // Add new row in grid
+      coll.add(m);
+
+      // Index of newly created row in grid
+      var idx = coll.indexOf(m);
+
+      // idx may not be always > -1 because our UniqueColCollection may
+      // remove 'm' if duplicate value found.
+      if (idx > -1) {
+        self.$grid.find('.new').removeClass('new');
+        var newRow = self.grid.body.rows[idx].$el;
+        newRow.addClass("new");
+        $(newRow).pgMakeVisible('backform-tab');
+      } else {
+        delete m;
+      }
+
+      return false;
+    },
+
+    // When user delete token/dictionary entry from grid
+    onAddorRemoveTokens: function() {
+      var self = this;
+
+      /*
+       * Wait for collection to be updated before checking for the button to
+       * be enabled, or not.
+       */
+      setTimeout(function() {
+          self.collection.trigger('pgadmin:tokens:updated', self.collection);
+        self.headerDataChanged();
+      }, 10);
+    },
+
+    // When control is about to destroy
+    remove: function() {
+      /*
+       * Stop listening the events registered by this control.
+       */
+      this.stopListening(this.headerData, "change", this.headerDataChanged);
+      this.listenTo(this.headerData, "select2", this.headerDataChanged);
+      this.listenTo(this.collection, "remove", this.onAddorRemoveTokens);
+
+      TokenControl.__super__.remove.apply(this, arguments);
+
+      // Remove the header model
+      delete (this.headerData);
+
+    }
+  });
+
+
+  // Extend the collection class for FTS Configuration
+  if (!pgBrowser.Nodes['coll-fts_configuration']) {
+    var fts_dictionaries = pgAdmin.Browser.Nodes['coll-fts_configuration'] =
+      pgAdmin.Browser.Collection.extend({
+        node: 'fts_configuration',
+        label: '{{ _('FTS Configurations') }}',
+        type: 'coll-fts_configuration',
+        columns: ['name', 'description']
+      });
+  };
+
+  // Extend the node class for FTS Configuration
+  if (!pgBrowser.Nodes['fts_configuration']) {
+    pgAdmin.Browser.Nodes['fts_configuration'] = pgAdmin.Browser.Node.extend({
+      parent_type: ['schema', 'catalog'],
+      type: 'fts_configuration',
+      sqlAlterHelp: 'sql-altertsconfiguration.html',
+      sqlCreateHelp: 'sql-createtsconfiguration.html',
+      canDrop: true,
+      canDropCascade: true,
+      label: '{{ _('FTS dictionaries') }}',
+      hasSQL: true,
+      hasDepends: true,
+      Init: function() {
+
+        // Avoid multiple registration of menus
+        if (this.initialized)
+          return;
+
+        this.initialized = true;
+
+        // Add context menus for FTS Configuration
+        pgBrowser.add_menus([{
+          name: 'create_fts_configuration_on_schema', node: 'schema',
+          module: this, category: 'create', priority: 4,
+          applies: ['object', 'context'], callback: 'show_obj_properties',
+          label: '{{_('FTS Configuration...')}}',
+          icon: 'wcTabIcon icon-fts_configuration', data: {action: 'create'}
+          },{
+          name: 'create_fts_configuration_on_coll', module: this, priority: 4,
+          node: 'coll-fts_configuration', applies: ['object', 'context'],
+          callback: 'show_obj_properties', category: 'create',
+          label: '{{ _('FTS Configuration...') }}', data: {action: 'create'},
+          icon: 'wcTabIcon icon-fts_configuration'
+          },{
+          name: 'create_fts_configuration', node: 'fts_configuration',
+          module: this, applies: ['object', 'context'],
+          callback: 'show_obj_properties', category: 'create', priority: 4,
+          label: '{{_('FTS Configuration...')}}', data: {action: 'create'},
+          icon: 'wcTabIcon icon-fts_configuration'
+          }]);
+      },
+
+      // Defining model for FTS Configuration node
+      model: pgAdmin.Browser.Node.Model.extend({
+        defaults: {
+          name: undefined,        // FTS Configuration name
+          owner: undefined,       // FTS Configuration owner
+          description: undefined, // Comment on FTS Configuration
+          schema: undefined,      // Schema name FTS Configuration belongs to
+          prsname: undefined,    // FTS parser list for FTS Configuration node
+          copy_config: undefined, // FTS configuration list to copy from
+          tokens: undefined      // token/dictionary pair list for node
+        },
+        initialize: function(attrs, args) {
+          var isNew = (_.size(attrs) === 0);
+
+          if (isNew) {
+            var userInfo = pgBrowser.serverInfo[args.node_info.server._id].user;
+            this.set({'owner': userInfo.name}, {silent: true});
+          }
+          pgAdmin.Browser.Node.Model.prototype.initialize.apply(this, arguments);
+          if (_.isUndefined(this.get('schema'))) {
+              this.set('schema', this.node_info.schema._id);
+          }
+        },
+        // Defining schema for FTS Configuration
+        schema: [{
+          id: 'name', label: '{{ _('Name') }}', cell: 'string',
+          type: 'text', cellHeaderClasses: 'width_percent_50'
+        },{
+          id: 'oid', label:'{{ _('OID') }}', cell: 'string',
+          editable: false, type: 'text', disabled: true, mode:['properties']
+        },{
+          id: 'owner', label:'{{ _('Owner') }}', cell: 'string',
+          type: 'text', mode: ['properties', 'edit','create'], node: 'role',
+          control: Backform.NodeListByNameControl
+        },{
+          id: 'schema', label: '{{ _('Schema')}}', cell: 'string',
+          type: 'text', mode: ['create','edit'], node: 'schema',
+          control: 'node-list-by-id'
+        },{
+          id: 'description', label:'{{ _('Comment') }}', cell: 'string',
+          type: 'multiline', cellHeaderClasses: 'width_percent_50'
+        },{
+          id: 'prsname', label: '{{ _('Parser')}}',type: 'text',
+          url: 'parsers', first_empty: true,
+          group: '{{ _('Definition') }}', control: 'node-ajax-options',
+          deps: ['copy_config'],
+          //disable parser when user select copy_config manually and vica-versa
+          disabled: function(m) {
+            var copy_config = m.get('copy_config');
+            return m.isNew() &&
+                    (_.isNull(copy_config) ||
+                    _.isUndefined(copy_config) ||
+                    copy_config === '') ? false : true;
+          }
+        },{
+          id: 'copy_config', label: '{{ _('Copy Config')}}',type: 'text',
+          mode: ['create'], group: '{{ _('Definition') }}',
+          control: 'node-ajax-options', url: 'copyConfig', deps: ['prsname'],
+
+          //disable copy_config when user select parser manually and vica-versa
+          disabled: function(m) {
+            var parser = m.get('prsname');
+            return m.isNew() &&
+                    (_.isNull(parser) ||
+                    _.isUndefined(parser) ||
+                    parser === '') ? false : true;
+          }
+        },{
+          id: 'tokens', label: '{{ _('Tokens') }}', type: 'collection',
+          group: '{{ _('Tokens') }}', control: TokenControl,
+          model: TokenModel, columns: ['token', 'dictionary'],
+          uniqueCol : ['token'], mode: ['create','edit'],
+          canAdd: true, canEdit: false, canDelete: true
+         }],
+
+        /*
+         * Triggers control specific error messages for name,
+         * copy_config/parser and schema, if any one of them is not specified
+         * while creating new fts configuration
+         */
+        validate: function(keys){
+          var msg;
+          var name = this.get('name');
+          var parser = this.get('prsname');
+          var copy_config_or_parser = !(parser === '' ||
+                                        _.isUndefined(parser) ||
+                                        _.isNull(parser)) ?
+                                        this.get('prsname') : this.get('copy_config');
+          var schema = this.get('schema');
+
+          // Clear the existing error model
+          this.errorModel.clear();
+          this.trigger('on-status-clear');
+
+          // Validate the name
+          if (_.isUndefined(name) ||
+              _.isNull(name) ||
+              String(name).replace(/^\s+|\s+$/g, '') == '') {
+            msg = '{{ _('Name must be specified!') }}';
+            this.errorModel.set('name', msg);
+            return msg;
+          }
+
+          // Validate parser or copy_config
+          else if (_.isUndefined(copy_config_or_parser) ||
+                   _.isNull(copy_config_or_parser) ||
+                   String(copy_config_or_parser).replace(/^\s+|\s+$/g, '') == '') {
+            msg = '{{ _('Select parser or configuration to copy!') }}';
+            this.errorModel.set('parser', msg);
+            return msg;
+          }
+
+          // Validate schema
+          else if (_.isUndefined(schema) ||
+                   _.isNull(schema) ||
+                   String(schema).replace(/^\s+|\s+$/g, '') == '') {
+            msg = '{{ _('Schema must be selected!') }}';
+            this.errorModel.set('schema', msg);
+            return msg;
+          }
+
+          return null;
+        }
+      })
+    });
+  }
+
+return pgBrowser.Nodes['coll-fts_configuration'];
+});
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/copy_config.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/copy_config.sql
new file mode 100644
index 0000000..584fc00
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/copy_config.sql
@@ -0,0 +1,15 @@
+{# FETCH copy config for FTS CONFIGURATION #}
+{% if copy_config %}
+SELECT
+    cfg.oid,
+    cfgname,
+    nspname,
+    n.oid as schemaoid
+FROM
+    pg_ts_config cfg
+    JOIN pg_namespace n
+    ON n.oid=cfgnamespace
+ORDER BY
+    nspname,
+    cfgname
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/create.sql
new file mode 100644
index 0000000..26d79f3
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/create.sql
@@ -0,0 +1,15 @@
+{# CREATE FTS CONFIGURATION Statement #}
+{% if data and data.schema and data.name %}
+CREATE TEXT SEARCH CONFIGURATION {{ conn|qtIdent(data.schema, data.name) }} (
+{% if 'copy_config' in data %}
+    COPY={{ data.copy_config }}
+{% elif 'prsname' in data %}
+    PARSER = {{ data.prsname }}
+{% endif %}
+);
+{# Description for FTS_CONFIGURATION #}
+
+{% if data.description %}
+COMMENT ON TEXT SEARCH CONFIGURATION {{ conn|qtIdent(data.schema, data.name) }}
+    IS {{ data.description|qtLiteral }};
+{% endif %}{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/delete.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/delete.sql
new file mode 100644
index 0000000..0052f61
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/delete.sql
@@ -0,0 +1,4 @@
+{# DROP FTS CONFIGURATION Statement #}
+{% if schema and name %}
+DROP TEXT SEARCH CONFIGURATION {{conn|qtIdent(schema)}}.{{conn|qtIdent(name)}} {% if cascade %}CASCADE{%endif%};
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/dictionaries.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/dictionaries.sql
new file mode 100644
index 0000000..f0e7b0f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/dictionaries.sql
@@ -0,0 +1,7 @@
+{# FETCH DICTIONARIES statement #}
+SELECT
+    dictname
+FROM
+    pg_ts_dict
+ORDER BY
+    dictname
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/get_name.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/get_name.sql
new file mode 100644
index 0000000..7238b8c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/get_name.sql
@@ -0,0 +1,17 @@
+{# GET FTS CONFIGURATION name #}
+{% if cfgid %}
+SELECT
+    cfg.cfgname as name,
+    (
+    SELECT
+        nspname
+    FROM
+        pg_namespace
+    WHERE
+        oid = cfg.cfgnamespace
+    ) as schema
+FROM
+    pg_ts_config cfg
+WHERE
+    cfg.oid = {{cfgid}}::OID;
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/nodes.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/nodes.sql
new file mode 100644
index 0000000..ec50acb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/nodes.sql
@@ -0,0 +1,13 @@
+{# FETCH FTS CONFIGURATION NAME statement #}
+SELECT
+    oid, cfgname as name
+FROM
+    pg_ts_config cfg
+WHERE
+{% if scid %}
+    cfg.cfgnamespace = {{scid}}::OID
+{% elif cfgid %}
+    cfg.oid = {{cfgid}}::OID
+{% endif %}
+
+ORDER BY name
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/parser.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/parser.sql
new file mode 100644
index 0000000..cbca9c9
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/parser.sql
@@ -0,0 +1,24 @@
+{# PARSER name from FTS CONFIGURATION OID #}
+{% if cfgid %}
+SELECT
+    cfgparser
+FROM
+    pg_ts_config
+where
+    oid = {{cfgid}}::OID
+{% endif %}
+
+
+{# PARSER list #}
+{% if parser %}
+SELECT
+    prsname,
+    nspname,
+    n.oid as schemaoid
+FROM
+    pg_ts_parser
+    JOIN pg_namespace n
+    ON n.oid=prsnamespace
+ORDER BY
+    prsname;
+{% endif %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/properties.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/properties.sql
new file mode 100644
index 0000000..a2f376e
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/properties.sql
@@ -0,0 +1,25 @@
+{# FETCH properties for FTS CONFIGURATION #}
+SELECT
+    cfg.oid,
+    cfg.cfgname as name,
+    pg_get_userbyid(cfg.cfgowner) as owner,
+    cfg.cfgparser as parser,
+    cfg.cfgnamespace as schema,
+    parser.prsname as prsname,
+    description
+FROM
+    pg_ts_config cfg
+    LEFT OUTER JOIN pg_ts_parser parser
+    ON parser.oid=cfg.cfgparser
+    LEFT OUTER JOIN pg_description des
+    ON (des.objoid=cfg.oid AND des.classoid='pg_ts_config'::regclass)
+WHERE
+{% if scid %}
+    cfg.cfgnamespace = {{scid}}::OID
+{% elif name %}
+    cfg.cfgname = {{name|qtLiteral}}
+{% endif %}
+{% if cfgid %}
+    AND cfg.oid = {{cfgid}}::OID
+{% endif %}
+ORDER BY cfg.cfgname
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/schema.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/schema.sql
new file mode 100644
index 0000000..b56ceb8
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/schema.sql
@@ -0,0 +1,19 @@
+{# FETCH statement for SCHEMA name #}
+{% if data.schema %}
+SELECT
+    nspname
+FROM
+    pg_namespace
+WHERE
+    oid = {{data.schema}}::OID
+
+{% elif data.id %}
+SELECT
+    nspname
+FROM
+    pg_namespace nsp
+    LEFT JOIN pg_ts_config cfg
+    ON cfg.cfgnamespace = nsp.oid
+WHERE
+    cfg.oid = {{data.id}}::OID
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/sql.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/sql.sql
new file mode 100644
index 0000000..e31307d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/sql.sql
@@ -0,0 +1,75 @@
+{# REVERSED ENGINEERED SQL FOR FTS CONFIGURATION #}
+{% if cfgid and scid %}
+SELECT
+    array_to_string(array_agg(sql), E'\n\n') as sql
+FROM
+    (
+    SELECT
+        E'-- Text Search CONFIGURATION: ' || quote_ident(nspname) || E'.'
+        || quote_ident(cfg.cfgname) ||
+        E'\n\n-- DROP TEXT SEARCH CONFIGURATION ' || quote_ident(nspname) ||
+        E'.' || quote_ident(cfg.cfgname) ||
+        E'\n\nCREATE TEXT SEARCH CONFIGURATION ' || quote_ident(nspname) ||
+        E'.' ||  quote_ident(cfg.cfgname) || E' (\n' ||
+        E'\tPARSER = ' || parsername ||
+        E'\n);' ||
+        CASE
+            WHEN description IS NOT NULL THEN
+                E'\n\nCOMMENT ON TEXT SEARCH CONFIGURATION ' ||
+                quote_ident(nspname) || E'.' || quote_ident(cfg.cfgname) ||
+                E' IS ' || pg_catalog.quote_literal(description) || E';'
+            ELSE ''
+        END ||
+        (
+        SELECT array(
+	        SELECT
+	            'ALTER TEXT SEARCH CONFIGURATION ' || quote_ident(nspname) ||
+	            E'.' || quote_ident(cfg.cfgname) || ' ADD MAPPING FOR ' ||
+	            t.alias  || ' WITH ' ||
+	            array_to_string(array_agg(dict.dictname), ', ') || ';'
+            FROM
+                pg_ts_config_map map
+                LEFT JOIN (
+                          SELECT
+                              tokid,
+                              alias
+                          FROM
+                              pg_catalog.ts_token_type(cfg.cfgparser)
+                          ) t ON (t.tokid = map.maptokentype)
+                LEFT OUTER JOIN pg_ts_dict dict ON (map.mapdict = dict.oid)
+            WHERE
+                map.mapcfg = cfg.oid
+            GROUP BY t.alias
+            ORDER BY t.alias)
+        ) as sql
+    FROM
+        pg_ts_config cfg
+    LEFT JOIN (
+        SELECT
+            des.description as description,
+            des.objoid as descoid
+        FROM
+            pg_description des
+        WHERE
+            des.objoid={{cfgid}}::OID AND des.classoid='pg_ts_config'::regclass
+    ) a ON (a.descoid = cfg.oid)
+    LEFT JOIN (
+        SELECT
+            nspname,
+            nsp.oid as noid
+        FROM
+            pg_namespace nsp
+        WHERE
+            oid = {{scid}}::OID
+    ) b ON (b.noid = cfg.cfgnamespace)
+    LEFT JOIN(
+        SELECT
+            prs.prsname as parsername,
+            prs.oid as oid
+        FROM
+            pg_ts_parser prs
+    )c ON (c.oid = cfg.cfgparser)
+    WHERE
+       cfg.oid={{cfgid}}::OID
+    ) e;
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokenDictList.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokenDictList.sql
new file mode 100644
index 0000000..e8a2d5b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokenDictList.sql
@@ -0,0 +1,23 @@
+{# Fetch token/dictionary list for FTS CONFIGURATION #}
+{% if cfgid %}
+SELECT
+    (
+    SELECT
+        t.alias
+    FROM
+        pg_catalog.ts_token_type(cfgparser) AS t
+    WHERE
+        t.tokid = maptokentype
+    ) AS token,
+    array_agg(dictname) AS dictname
+FROM
+    pg_ts_config_map
+    LEFT OUTER JOIN pg_ts_config ON mapcfg = pg_ts_config.oid
+    LEFT OUTER JOIN pg_ts_dict ON mapdict = pg_ts_dict.oid
+WHERE
+    mapcfg={{cfgid}}::OID
+GROUP BY
+    token
+ORDER BY
+    1
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokens.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokens.sql
new file mode 100644
index 0000000..ce0c5eb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/tokens.sql
@@ -0,0 +1,10 @@
+{# Tokens for FTS CONFIGURATION node #}
+
+{% if parseroid %}
+SELECT
+    alias
+FROM
+    ts_token_type({{parseroid}}::OID)
+ORDER BY
+    alias
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/update.sql
new file mode 100644
index 0000000..e497bee
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/sql/9.1_plus/update.sql
@@ -0,0 +1,51 @@
+{# UPDATE statement for FTS CONFIGURATION #}
+{% if data %}
+{% set name = o_data.name %}
+{% set schema = o_data.schema %}
+{% if data.name and data.name != o_data.name %}
+{% set name = data.name %}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(o_data.name)}}
+    RENAME TO {{data.name}};
+
+{% endif %}
+{% if 'tokens' in data %}
+{% if'changed' in data.tokens %}
+{% for tok in data.tokens.changed %}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(name)}}
+    ALTER MAPPING FOR {{tok.token}}
+    WITH {% for dict in tok.dictname %}{{dict}}{% if not loop.last %}, {% endif %}{% endfor %};
+
+{% endfor %}
+{% endif %}
+{% if'added' in data.tokens %}
+{% for tok in data.tokens.added %}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(name)}}
+    ADD MAPPING FOR {{tok.token}}
+    WITH {% for dict in tok.dictname %}{{dict}}{% if not loop.last %}, {% endif %}{% endfor %};
+
+{% endfor %}
+{% endif %}
+{% if'deleted' in data.tokens %}
+{% for tok in data.tokens.deleted %}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(name)}}
+    DROP MAPPING FOR {{tok.token}};
+
+{% endfor %}
+{% endif %}
+{% endif %}
+{% if 'owner' in data and data.owner != o_data.owner %}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(name)}}
+    OWNER TO {{data.owner}};
+
+{% endif %}
+{% if 'schema' in data and data.schema != o_data.schema %}
+{% set schema = data.schema%}
+ALTER TEXT SEARCH CONFIGURATION {{conn|qtIdent(o_data.schema)}}.{{conn|qtIdent(name)}}
+    SET SCHEMA {{data.schema}};
+
+{% endif %}
+{% if 'description' in data and data.description != o_data.description %}
+COMMENT ON TEXT SEARCH CONFIGURATION {{conn|qtIdent(schema)}}.{{conn|qtIdent(name)}}
+    IS {{ data.description|qtLiteral }};
+{% endif %}
+{% endif %}
\ No newline at end of file
diff --git a/web/pgadmin/browser/static/js/node.ui.js b/web/pgadmin/browser/static/js/node.ui.js
index de30e48..7d58693 100644
--- a/web/pgadmin/browser/static/js/node.ui.js
+++ b/web/pgadmin/browser/static/js/node.ui.js
@@ -499,6 +499,35 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
     })
   });
 
+    // Extend the browser's node model class to create a option/value pair
+  var MultiSelectAjaxCell = Backgrid.Extension.MultiSelectAjaxCell = Backgrid.Extension.NodeAjaxOptionsCell.extend({
+    defaults: _.extend({}, NodeAjaxOptionsCell.prototype.defaults, {
+      transform: undefined,
+      url_with_id: false,
+      select2: {
+        allowClear: true,
+        placeholder: 'Select from the list',
+        width: 'style',
+        multiple: true
+      },
+      opt: {
+        label: null,
+        value: null,
+        image: null,
+        selected: false
+       }
+    }),
+    getValueFromDOM: function() {
+      var res = [];
+
+      this.$el.find("select").find(':selected').each(function() {
+        res.push($(this).attr('value'));
+      });
+
+      return res;
+    },
+  });
+
   /*
    * Control to select multiple columns.
    */
-- 
Sent via pgadmin-hackers mailing list (pgadmin-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgadmin-hackers

Reply via email to