Added support for FTS configuration node

pull/3/head
Sanket Mehta 2016-05-19 21:03:26 +05:30 committed by Akshay Joshi
parent 7b2e4fb467
commit 6895da9cbc
19 changed files with 1851 additions and 1 deletions

View File

@ -0,0 +1,939 @@
##########################################################################
#
# 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
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 schemas.SchemaModule.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 'prsname' 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 make_json_response(
success=1,
info="FTS Configuration Updated.",
data={
'id': cfgid,
'sid': sid,
'gid': gid,
'did': did,
'scid': scid
}
)
# 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,579 @@
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
},
keys: ['token'],
// 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-3">',
' <label class="control-label"><%-token_label%></label>',
' </div>',
' <div class="col-xs-6" header="token"></div>',
' <div class="col-xs-2">',
' <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();
}
// Disable add button in token control in create mode
if(data.mode == 'create') {
$header.find("button.add").attr('disabled', true);
}
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 = (_.isEmpty(data) || _.isUndefined(data)),
checkVars = ['token'];
if (!self.$header) {
return;
}
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
}),
checkVars = ['token'],
idx = -1;
// Find if token exists in grid
self.collection.each(function(m) {
_.each(checkVars, function(v) {
val = m.get(v);
if(val == token) {
idx = coll.indexOf(m);
}
});
});
// remove 'm' if duplicate value found.
if (idx == -1) {
coll.add(m);
idx = coll.indexOf(m);
}
self.$grid.find('.new').removeClass('new');
var newRow = self.grid.body.rows[idx].$el;
newRow.addClass("new");
//$(newRow).pgMakeVisible('table-bordered');
$(newRow).pgMakeVisible('backform-tab');
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_configurations = 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-altertsconfig.html',
sqlCreateHelp: 'sql-createtsconfig.html',
canDrop: true,
canDropCascade: true,
label: '{{ _('FTS Configuration') }}',
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, select2: { allowClear: false }
},{
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'];
});

View File

@ -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 %}

View File

@ -0,0 +1,19 @@
{# 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 and data.copy_config != '' %}
COPY={{ data.copy_config }}
{% elif 'prsname' in data and data.prsname != '' %}
PARSER = {{ data.prsname }}
{% endif %}
);
{% if 'owner' in data and data.owner != '' %}
ALTER TEXT SEARCH CONFIGURATION {{ conn|qtIdent(data.schema, data.name) }} OWNER TO {{ conn|qtIdent(data.owner) }};
{% 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 %}

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{# FETCH DICTIONARIES statement #}
SELECT
dictname
FROM
pg_ts_dict
ORDER BY
dictname

View File

@ -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 %}

View File

@ -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

View File

@ -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 %}

View File

@ -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

View File

@ -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 %}

View File

@ -0,0 +1,76 @@
{# 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 || E'\n' ||
array_to_string(
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)
, E'\n') 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 %}

View File

@ -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 %}

View File

@ -0,0 +1,10 @@
{# Tokens for FTS CONFIGURATION node #}
{% if parseroid %}
SELECT
alias
FROM
ts_token_type({{parseroid}}::OID)
ORDER BY
alias
{% endif %}

View File

@ -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 {{conn|qtIdent(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 != '' 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 %}

View File

@ -59,7 +59,7 @@ function($, _, S, pgAdmin, pgBrowser, alertify) {
sqlCreateHelp: 'sql-createtsdictionary.html',
canDrop: true,
canDropCascade: true,
label: '{{ _('FTS dictionaries') }}',
label: '{{ _('FTS dictionary') }}',
hasSQL: true,
hasDepends: true,
Init: function() {

View File

@ -504,6 +504,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.
*/