Subject: PATCH: FTS configuration node

From: Sanket Mehta <[email-masked]>
Date: Thu, 5 May 2016 20:18:09 +0530

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
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..4b6f575
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/__init__.py
@@ -0,0 +1,946 @@
+##########################################################################
+#
+# 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
+
+    * tokenize_options(self, option_value):
+    -   This function will tokenize the string stored in database
+        e.g. database store the value as below
+        key1=value1, key2=value2, key3=value3, ....
+        This function will extract key and value from above string
+
+    * 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
+
+    def tokenize_options(self, option_value):
+        """
+        This function will tokenize the string stored in database
+        e.g. database store the value as below
+        key1=value1, key2=value2, key3=value3, ....
+        This function will extract key and value from above string
+
+        Args:
+            option_value: key value option/value pair read from database
+        """
+        if option_value is not None:
+            option_str = option_value.split(',')
+            options = []
+            for fdw_option in option_str:
+                k, v = fdw_option.split('=', 1)
+                options.append({'token': k, 'dictname': v})
+            return options
+
+    @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)
+
+        # for row in res['rows']:
+        #     if row['options'] is not None:
+        #         row['options'] = self.tokenize_options(row['options'])
+
+        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 object
+        :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, '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 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, 'delete.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 = request.args
+        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
+        """
+        data = request.args if request.args else {}
+        try:
+            res = []
+            if cfgid is not None:
+                sql = render_template("/".join([self.template_path, 'tokens.sql']),
+                                      cfgid=cfgid, parser=True)
+                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']),
+                                      cfgid=cfgid, 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..2711204
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/js/fts_configuration.js
@@ -0,0 +1,571 @@
+define(
+        ['jquery', 'underscore', 'underscore.string', 'pgadmin',
+         'pgadmin.browser', 'alertify', 'pgadmin.browser.collection'],
+function($, _, S, pgAdmin, pgBrowser, alertify) {
+
+
+// Model to 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 panel
+  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
+      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 class='pgadmin-control-group backgrid form-group col-xs-12 object subnode'></div>").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,
+          applies: ['object', 'context'], callback: 'show_obj_properties',
+          category: 'create', priority: 4, label: '{{_('FTS Configuration...')}}',
+          icon: 'wcTabIcon icon-fts_configuration', data: {action: 'create'}
+          },{
+          name: 'create_fts_configuration_on_coll', node: 'coll-fts_configuration',
+          module: this, applies: ['object', 'context'],  priority: 4,
+          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...')}}',
+          icon: 'wcTabIcon icon-fts_configuration', data: {action: 'create'}
+          }]);
+      },
+
+      // 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..9e8b820
--- /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,22 @@
+{# FETCH FTS CONFIGURATION NAME Statement #}
+{% 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 %}
+
+{# 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/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..8eca97f
--- /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,13 @@
+{# FETCH parsers for FTS CONFIGURATION #}
+{% 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..038923d
--- /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,23 @@
+{# 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..03c13c3
--- /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,63 @@
+{# 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..5d1d36b
--- /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,23 @@
+{# FETCH TOKEN for FTS CONFIGURATION statement #}
+{% if cfgid %}
+
+{# FETCH parser oid statement #}
+{% if parser %}
+SELECT
+    cfgparser
+FROM
+    pg_ts_config
+where
+    oid = {{cfgid}}::OID
+{% endif %}
+
+{# FETCH TOKEN ALIAS statement #}
+{% if parseroid %}
+SELECT
+    alias
+FROM
+    ts_token_type({{parseroid}}::OID)
+ORDER BY
+    alias
+{% 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/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 6cd3d68..477ec17 100644
--- a/web/pgadmin/browser/static/js/node.ui.js
+++ b/web/pgadmin/browser/static/js/node.ui.js
@@ -487,6 +487,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 ([email-masked])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgadmin-hackers

---

From: Harshal Dhumal <[email-masked]>
Date: Thu, 5 May 2016 20:19:01 +0530

+ patch link

http://www.postgresql.org/message-id/CAFiP3vwkka+=[email-masked]

-- 
*Harshal Dhumal*
*Software Engineer *

EenterpriseDB <http://www.enterprisedb.com>

---

From: Sanket Mehta <[email-masked]>
Date: Thu, 12 May 2016 16:38:43 +0530

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 <
[email-masked]> wrote:

> On Thu, May 5, 2016 at 8:18 PM, Sanket Mehta <
> [email-masked]> wrote:
>
>> Hi,
>>
index 0000000..fa2d3e6
@@ -0,0 +1,937 @@
+                                             'tokenDictList.sql']),
+                                            'schema.sql']),
+                "/".join([self.template_path, 'create.sql']),
+        This function will update FTS Configuration node
+                "/".join([self.template_path, 'get_name.sql']),
+                "/".join([self.template_path, 'delete.sql']),
+                    data=old_data
+                    data=data
+            "/".join([self.template_path, 'parser.sql']),
+            parser=True
+            "/".join([self.template_path, 'copy_config.sql']),
+            copy_config=True
+                    "/".join([self.template_path, 'parser.sql']),
+                    "/".join([self.template_path, 'tokens.sql']),
+                    parseroid=parseroid
+            "/".join([self.template_path,'dictionaries.sql'])
+                    "ERROR: Couldn't generate reversed engineered query for the FTS Configuration!\n{0}"
+                    ).format(

literal 0
HcmV?d00001

literal 0
HcmV?d00001

index 0000000..782d664
@@ -0,0 +1,585 @@
+// Model for tokens control
+// Customized control for token control
+                Backform.NodeAjaxOptionsControl.prototype.initialize.apply(
+                  this,
+                  arguments
+                /* Fetch the tokens/dict list from 'that' node.
+                 * Here 'that' refers to unique collection control where
+                  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.
+                       * 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
+                  transform = (this.field.get('transform')
+                                || self.defaults.transform);
+            first_empty: true,
+      // Creating view from header schema in tokens control
+          $gridBody = $("<div></div>", {
+            class:'pgadmin-control-group backgrid form-group col-xs-12 object subnode'
+          }).append(
+       * Wait for collection to be updated before checking for the button to
+       * be enabled, or not.
+          name: 'create_fts_configuration_on_schema', node: 'schema',
+          module: this, category: 'create', priority: 4,
+          label: '{{_('FTS Configuration...')}}',
+          name: 'create_fts_configuration_on_coll', module: this, priority: 4,
+          node: 'coll-fts_configuration', applies: ['object', 'context'],
+          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'},
+          copy_config: undefined, // FTS configuration list to copy from
index 0000000..0052f61
@@ -0,0 +1,4 @@
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
index 0000000..7238b8c
+++ 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 #}
index 0000000..cbca9c9
@@ -0,0 +1,24 @@
+{# PARSER name from FTS CONFIGURATION OID #}
+{# PARSER list #}
index 0000000..a2f376e
@@ -0,0 +1,25 @@
+    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)
index 0000000..e31307d
@@ -0,0 +1,75 @@
+        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'\n\nCOMMENT ON TEXT SEARCH CONFIGURATION ' ||
+                quote_ident(nspname) || E'.' || quote_ident(cfg.cfgname) ||
+	            '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), ', ') || ';'
+                              tokid,
+                              pg_catalog.ts_token_type(cfg.cfgparser)
+                          ) t ON (t.tokid = map.maptokentype)
index 0000000..ce0c5eb
@@ -0,0 +1,10 @@
+{# Tokens for FTS CONFIGURATION node #}

---

From: Sanket Mehta <[email-masked]>
Date: Fri, 13 May 2016 14:58:50 +0530

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 <[email-masked]

literal 0
HcmV?d00001

literal 0
HcmV?d00001

index de30e48..7d58693 100644
@@ -499,6 +499,35 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
     })
   });

---

From: Akshay Joshi <[email-masked]>
Date: Fri, 13 May 2016 18:20:43 +0530

Hi Sanket

Below are my review comments:

   - Add button should be disabled in Tokens tab while creating.
   - Unable to click on down arrow for token select control.
   - Title should be change from "Create FTS-dictionaries" to "Create FTS
   -Configuration".
   - Tree node is not getting refreshed on name change.
   - Unable to rename FTS Configuration when name contains any capital
   letter.
   - If user tries to add already existing token then respective row in the
   backgrid should be highlighted.
   - "URL not found" when click on SQL help icon from the properties dialog.
   - SQL should not be generated when remove and add the same token again.
   Currently it is creating two sql's one for remove and another is for add.
      - *Fix for the above issue is*: - Add "keys: ['token']," to TokenModel
      in your js file.

On Fri, May 13, 2016 at 2:58 PM, Sanket Mehta <[email-masked]

> On Thu, May 12, 2016 at 4:38 PM, Sanket Mehta <

-- 
*Akshay Joshi*
*Principal Software Engineer *

*Phone: +91 20-3058-9517Mobile: +91 976-788-8246*

---

From: Sanket Mehta <[email-masked]>
Date: Mon, 16 May 2016 19:37:42 +0530

Hi,

Revised patch is attached with this mail.
My response is inline.

Regards,
Sanket Mehta
Sr Software engineer
Enterprisedb

On Fri, May 13, 2016 at 6:20 PM, Akshay Joshi <[email-masked]

> Done

> Done

> Done

> Done

> Done

>
>    - If user tries to add already existing token then respective row in
>    the backgrid should be highlighted.
>
> Done

> Done

>
>    - SQL should not be generated when remove and add the same token
>    again. Currently it is creating two sql's one for remove and another
>    is for add.
>       - *Fix for the above issue is*: - Add "keys: ['token']," to
>       TokenModel in your js file.
>
> Done

>
> On Fri, May 13, 2016 at 2:58 PM, Sanket Mehta <
index 0000000..17130fe
@@ -0,0 +1,951 @@
+    * csssnippets()
+      - Load style sheets for this module
+        return schemas.SchemaModule.NODE_TYPE
+    def csssnippets(self):
+        snippets = [render_template(
+            'fts_configuration/css/fts_configuration.css',
+            node_type = self.node_type,
+        )]
+        snippets.extend(super(SchemaChildModule,self).csssnippets)
+        return snippets
+                    info="FTS Configuration Updated.",

literal 0
HcmV?d00001

literal 0
HcmV?d00001

diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/css/fts_configuration.css b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/css/fts_configuration.css
new file mode 100644
index 0000000..b8ef393
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/fts_configurations/templates/fts_configuration/css/fts_configuration.css
@@ -0,0 +1,3 @@
+.pgadmin-controls {
+    min-width: 0px !important;
+}
index 0000000..b1d73e6
@@ -0,0 +1,579 @@
+        keys: ['token'],
+      // Disable add button in token control in create mode
+      if(data.mode == 'create') {
+        $header.find("button.add").attr('disabled', true);
+          inSelected = (_.isEmpty(data) || _.isUndefined(data)),
+          checkVars = ['token'],
+          idx = -1;
+      // Find if token exists in grid
+          if(val == token) {
+            idx = coll.indexOf(m);
+      if (idx == -1) {
+        idx = coll.indexOf(m);
+      //$(newRow).pgMakeVisible('table-bordered');
+    var fts_configurations = pgAdmin.Browser.Nodes['coll-fts_configuration'] =
+      sqlAlterHelp: 'sql-altertsconfig.html',
+      sqlCreateHelp: 'sql-createtsconfig.html',
+      label: '{{ _('FTS Configuration') }}',
index 0000000..77599f8
+    RENAME TO {{conn|qtIdent(data.name)}};
index 8bf71dc..7e84935 100644
@@ -504,6 +504,35 @@ function($, _, pgAdmin, Backbone, Backform, Alertify, Node) {
     })
   });

---

From: Harshal Dhumal <[email-masked]>
Date: Tue, 17 May 2016 12:54:07 +0530

Hi Sanket,

Please find my review comments below:

1. In create mode it generates wrong sql.

CREATE TEXT SEARCH CONFIGURATION test.asdf (
    COPY=
);

steps to reproduce:
    a] Fill up any necessary fields in general tab.
    b] On definition tab set Copy config filed and navigate to SQL tab.
    c] Come back again to definition tab clear Copy config field and set
Parser field and again navigate to SQL tab

2. Create new FTS configuration with parser throws below error.

provide atleast copy config or parser.

(this is because you have used key name for parser in js as "prsname" and
in python you have applied validations on key "parser")

3. When we clear dictionary for any token using cross (x) button in
select2; JavaScript code for selecct2 fails. (see attached screenshot)

-- 
*Harshal Dhumal*
*Software Engineer *

On Mon, May 16, 2016 at 7:37 PM, Sanket Mehta <[email-masked]

> On Fri, May 13, 2016 at 6:20 PM, Akshay Joshi <
> [email-masked]> wrote:
>
>> Hi Sanket
>>

---

From: Sanket Mehta <[email-masked]>
Date: Wed, 18 May 2016 12:41:01 +0530

Hi,

PFA the revised patch.
Response is lnline.

Regards,
Sanket Mehta
Sr Software engineer
Enterprisedb

> Parser field and again navigate to SQL tab
Fixed

> in python you have applied validations on key "parser")
Fixed

> select2; JavaScript code for selecct2 fails. (see attached screenshot)
This occurs due to issue in select2 control. Its a generalized issue which
will be resolved later.

> On Mon, May 16, 2016 at 7:37 PM, Sanket Mehta <
>>>    - Title should be change from "Create FTS-dictionaries" to "Create
>>>    FTS-Configuration".
>>>
>>> Done
>>
>>>
index 0000000..6b87740
+        if 'copy_config' not in data and 'prsname' not in data:

literal 0
HcmV?d00001

literal 0
HcmV?d00001

index 0000000..88f428b
+{% if 'copy_config' in data and data.copy_config != '' %}
+{% elif 'prsname' in data and data.prsname != '' %}

---

From: Ashesh Vashi <[email-masked]>
Date: Wed, 18 May 2016 12:42:57 +0530

> This occurs due to issue in select2 control. Its a generalized issue which
When you say - it is common issue, can you please point where else you've
seen this behaviour?

--

Thanks & Regards,

Ashesh Vashi
EnterpriseDB INDIA: Enterprise PostgreSQL Company
<http://www.enterprisedb.com/>

*http://www.linkedin.com/in/asheshvashi*
<http://www.linkedin.com/in/asheshvashi>

>>>>    - If user tries to add already existing token then respective row
>>>>    in the backgrid should be highlighted.
>>>>
>>>> Done
>>>
>>>>
>>>>    again. Currently it is creating two sql's one for remove and
>>>>    another is for add.
>>>>>>>> It depends upon backgrid select2cell multi select control, for
>>>>>>>> which Harshal has sent the patch recently.

---

From: Sanket Mehta <[email-masked]>
Date: Wed, 18 May 2016 12:52:37 +0530

On Wed, May 18, 2016 at 12:42 PM, Ashesh Vashi <
[email-masked]> wrote:

>>> (this is because you have used key name for parser in js as "prsname"
>>> and in python you have applied validations on key "parser")
>>>
>> Fixed
>>
>>>
>> This occurs due to issue in select2 control. Its a generalized issue
>> which will be resolved later.
>>
The same behaviour occurs in all nodes where multiselect cell is being used
like, exclusion control, debugger etc.

>>>>>    - Unable to rename FTS Configuration when name contains any
>>>>>    capital letter.
>>>>>
>>>>> Done
>>>>
>>>>>

---

From: Ashesh Vashi <[email-masked]>
Date: Wed, 18 May 2016 12:57:41 +0530

On Wed, May 18, 2016 at 12:52 PM, Sanket Mehta <
[email-masked]> wrote:

>>>>     c] Come back again to definition tab clear Copy config field and
>>>> set Parser field and again navigate to SQL tab
>>>>
>>> Fixed
>>>
>>>>
> The same behaviour occurs in all nodes where multiselect cell is being
> used like, exclusion control, debugger etc.
>
We do not have MultiSelectCell.

--

Thanks & Regards,

Ashesh Vashi

>>>>>>    - Title should be change from "Create FTS-dictionaries" to
>>>>>>    "Create FTS-Configuration".
>>>>>>
>>>>>> Done
>>>>>
>>>>>>

---

From: Akshay Joshi <[email-masked]>
Date: Thu, 19 May 2016 21:05:33 +0530

Thanks - Committed with some modification and fixes.

On Wed, May 18, 2016 at 12:57 PM, Ashesh Vashi <
[email-masked]> wrote:

>>> When you say - it is common issue, can you please point where else
>>> you've seen this behaviour?
>>>
>>>>>>>    - If user tries to add already existing token then respective
>>>>>>>    row in the backgrid should be highlighted.
>>>>>>>
>>>>>>> Done
>>>>>>
>>>>>>>