Move congress_dashboard folder to this project
Partially-Implements: blueprint enhance-congress-dashboard Closes-Bug: #1653743 Change-Id: I9b2ae92e125181226130de56b09d5588b4cd1755
This commit is contained in:
parent
2a43ed659a
commit
88ef2e3d5a
45
.gitignore
vendored
Normal file
45
.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
/lib
|
||||
/lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
nosetests.xml
|
||||
.testrepository
|
||||
.venv
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# Sphinx
|
||||
doc/build
|
||||
|
25
congress_dashboard/README.md
Normal file
25
congress_dashboard/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
Congress Dashboard
|
||||
------------------
|
||||
|
||||
Congress Dashboard is an extension for OpenStack Dashboard that provides a UI
|
||||
for Congress. With congress-dashboard, a user is able to easily write the
|
||||
policies and rules for governance of cloud.
|
||||
|
||||
Setup Instructions
|
||||
------------------
|
||||
|
||||
This instruction assumes that Horizon is already installed and its
|
||||
installation folder is <horizon>. Detailed information on how to install
|
||||
Horizon can be found at
|
||||
http://docs.openstack.org/developer/horizon/quickstart.html#setup.
|
||||
|
||||
To integrate congress with horizon, copy the files in
|
||||
<congress_dashboard>/enabled to <horizon>/openstack_dashboard/local/enabled/
|
||||
|
||||
$ cp -b <congress_dashboard>/enabled/_50_policy.py <horizon>/openstack_dashboard/local/enabled/
|
||||
$ cp -b <congress_dashboard>/enabled/_60_policies.py <horizon>/openstack_dashboard/local/enabled/
|
||||
$ cp -b <congress_dashboard>/enabled/_70_datasources.py <horizon>/openstack_dashboard/local/enabled/
|
||||
|
||||
Restart Apache server
|
||||
sudo service apache2 restart
|
||||
|
304
congress_dashboard/api/congress.py
Normal file
304
congress_dashboard/api/congress.py
Normal file
@ -0,0 +1,304 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from congressclient.v1 import client as congress_client
|
||||
from django.conf import settings
|
||||
import keystoneauth1.identity.v2 as v2
|
||||
import keystoneauth1.identity.v3 as v3
|
||||
import keystoneauth1.session as kssession
|
||||
from openstack_dashboard.api import base
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
LITERALS_SEPARATOR = '),'
|
||||
RULE_SEPARATOR = ':-'
|
||||
TABLE_SEPARATOR = ':'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _set_id_as_name_if_empty(apidict, length=0):
|
||||
try:
|
||||
if not apidict._apidict.get('name'):
|
||||
id = apidict._apidict['id']
|
||||
if length:
|
||||
id = id[:length]
|
||||
apidict._apidict['name'] = '(%s)' % id
|
||||
else:
|
||||
apidict._apidict['name'] = id
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class PolicyAPIDictWrapper(base.APIDictWrapper):
|
||||
def set_id_as_name_if_empty(self):
|
||||
_set_id_as_name_if_empty(self)
|
||||
|
||||
def set_id_if_empty(self, id):
|
||||
apidict_id = self._apidict.get('id')
|
||||
if not apidict_id or apidict_id == "None":
|
||||
self._apidict['id'] = id
|
||||
|
||||
def set_value(self, key, value):
|
||||
self._apidict[key] = value
|
||||
|
||||
def delete_by_key(self, key):
|
||||
del self._apidict[key]
|
||||
|
||||
|
||||
class PolicyRule(PolicyAPIDictWrapper):
|
||||
"""Wrapper for a Congress policy's rule."""
|
||||
def set_id_as_name_if_empty(self):
|
||||
pass
|
||||
|
||||
|
||||
class PolicyTable(PolicyAPIDictWrapper):
|
||||
"""Wrapper for a Congress policy's data table."""
|
||||
def set_policy_details(self, policy):
|
||||
self._apidict['policy_name'] = policy['name']
|
||||
self._apidict['policy_owner_id'] = policy['owner_id']
|
||||
|
||||
|
||||
def congressclient(request):
|
||||
"""Instantiate Congress client."""
|
||||
auth_url = getattr(settings, 'OPENSTACK_KEYSTONE_URL')
|
||||
user = request.user
|
||||
session = get_keystone_session(auth_url, user)
|
||||
region_name = user.services_region
|
||||
|
||||
kwargs = {
|
||||
'session': session,
|
||||
'auth': None,
|
||||
'interface': 'publicURL',
|
||||
'service_type': 'policy',
|
||||
'region_name': region_name
|
||||
}
|
||||
return congress_client.Client(**kwargs)
|
||||
|
||||
|
||||
def get_keystone_session(auth_url, user):
|
||||
if auth_url[-3:] == '/v3':
|
||||
auth = v3.Token(auth_url, user.token.id, project_id=user.tenant_id)
|
||||
else:
|
||||
auth = v2.Token(auth_url, user.token.id, tenant_id=user.tenant_id,
|
||||
tenant_name=user.tenant_name)
|
||||
|
||||
session = kssession.Session(auth=auth)
|
||||
return session
|
||||
|
||||
|
||||
def policies_list(request):
|
||||
"""List all policies."""
|
||||
client = congressclient(request)
|
||||
policies_list = client.list_policy()
|
||||
results = policies_list['results']
|
||||
policies = []
|
||||
for p in results:
|
||||
policy = PolicyAPIDictWrapper(p)
|
||||
# Policies currently have a name but not necessarily a non-"None" id.
|
||||
# Use the name to identify the policy, needed to differentiate them in
|
||||
# DataTables.
|
||||
policy.set_id_if_empty(policy.get('name'))
|
||||
policies.append(policy)
|
||||
return policies
|
||||
|
||||
|
||||
def policy_create(request, args):
|
||||
"""Create a policy with the given properties."""
|
||||
client = congressclient(request)
|
||||
policy = client.create_policy(args)
|
||||
return policy
|
||||
|
||||
|
||||
def policy_delete(request, policy_id):
|
||||
"""Delete a policy by id."""
|
||||
client = congressclient(request)
|
||||
policy = client.delete_policy(policy_id)
|
||||
return policy
|
||||
|
||||
|
||||
def policy_get(request, policy_name):
|
||||
"""Get a policy by name."""
|
||||
# TODO(jwy): Use congress.show_policy() once system policies have unique
|
||||
# IDs.
|
||||
policies = policies_list(request)
|
||||
for p in policies:
|
||||
if p['name'] == policy_name:
|
||||
return p
|
||||
|
||||
|
||||
def policy_rule_create(request, policy_name, body=None):
|
||||
"""Create a rule in the given policy, with the given properties."""
|
||||
client = congressclient(request)
|
||||
rule = client.create_policy_rule(policy_name, body=body)
|
||||
return rule
|
||||
|
||||
|
||||
def policy_rule_delete(request, policy_name, rule_id):
|
||||
"""Delete a rule by id, from the given policy."""
|
||||
client = congressclient(request)
|
||||
rule = client.delete_policy_rule(policy_name, rule_id)
|
||||
return rule
|
||||
|
||||
|
||||
def policy_rules_list(request, policy_name):
|
||||
"""List all rules in a policy, given by name."""
|
||||
client = congressclient(request)
|
||||
policy_rules_list = client.list_policy_rules(policy_name)
|
||||
results = policy_rules_list['results']
|
||||
return [PolicyRule(r) for r in results]
|
||||
|
||||
|
||||
def policy_tables_list(request, policy_name):
|
||||
"""List all data tables in a policy, given by name."""
|
||||
client = congressclient(request)
|
||||
policy_tables_list = client.list_policy_tables(policy_name)
|
||||
results = policy_tables_list['results']
|
||||
return [PolicyTable(t) for t in results]
|
||||
|
||||
|
||||
def policy_table_get(request, policy_name, table_name):
|
||||
"""Get a policy table in a policy, given by name."""
|
||||
client = congressclient(request)
|
||||
return client.show_policy_table(policy_name, table_name)
|
||||
|
||||
|
||||
def policy_rows_list(request, policy_name, table_name):
|
||||
"""List all rows in a policy's data table, given by name."""
|
||||
client = congressclient(request)
|
||||
policy_rows_list = client.list_policy_rows(policy_name, table_name)
|
||||
results = policy_rows_list['results']
|
||||
|
||||
policy_rows = []
|
||||
# Policy table rows currently don't have ids. However, the DataTable object
|
||||
# requires an id for the table to get rendered properly. Otherwise, the
|
||||
# same contents are displayed for every row in the table. Assign the rows
|
||||
# ids here.
|
||||
id = 0
|
||||
for row in results:
|
||||
new_row = PolicyAPIDictWrapper(row)
|
||||
new_row.set_id_if_empty(id)
|
||||
id += 1
|
||||
policy_rows.append(new_row)
|
||||
return policy_rows
|
||||
|
||||
|
||||
def policy_table_schema_get(request, policy_name, table_name):
|
||||
"""Get the schema for a policy table, based on the first matching rule."""
|
||||
column_names = []
|
||||
rules = policy_rules_list(request, policy_name)
|
||||
# There might be multiple rules that use the same name in the head. Pick
|
||||
# the first matching one, which is what the policy engine currently does.
|
||||
for rule in rules:
|
||||
rule_def = rule['rule']
|
||||
head, _ = rule_def.split(RULE_SEPARATOR)
|
||||
if head.strip().startswith('%s(' % table_name):
|
||||
start = head.index('(') + 1
|
||||
end = head.index(')')
|
||||
column_names = head[start:end].split(',')
|
||||
break
|
||||
|
||||
schema = {'table_id': table_name}
|
||||
schema['columns'] = [{'name': name.strip(), 'description': None}
|
||||
for name in column_names]
|
||||
return schema
|
||||
|
||||
|
||||
def datasources_list(request):
|
||||
"""List all the data sources."""
|
||||
client = congressclient(request)
|
||||
datasources_list = client.list_datasources()
|
||||
datasources = datasources_list['results']
|
||||
return [PolicyAPIDictWrapper(d) for d in datasources]
|
||||
|
||||
|
||||
def datasource_get(request, datasource_id):
|
||||
"""Get a data source by id."""
|
||||
# TODO(jwy): Need API in congress_client to retrieve data source by id.
|
||||
datasources = datasources_list(request)
|
||||
for d in datasources:
|
||||
if d['id'] == datasource_id:
|
||||
return d
|
||||
|
||||
|
||||
def datasource_get_by_name(request, datasource_name):
|
||||
"""Get a data source by name."""
|
||||
datasources = datasources_list(request)
|
||||
for d in datasources:
|
||||
if d['name'] == datasource_name:
|
||||
return d
|
||||
|
||||
|
||||
def datasource_tables_list(request, datasource_id):
|
||||
"""List all data tables in a data source, given by id."""
|
||||
client = congressclient(request)
|
||||
datasource_tables_list = client.list_datasource_tables(datasource_id)
|
||||
results = datasource_tables_list['results']
|
||||
return [PolicyAPIDictWrapper(t) for t in results]
|
||||
|
||||
|
||||
def datasource_rows_list(request, datasource_id, table_name):
|
||||
"""List all rows in a data source's data table, given by id."""
|
||||
client = congressclient(request)
|
||||
datasource_rows_list = client.list_datasource_rows(datasource_id,
|
||||
table_name)
|
||||
results = datasource_rows_list['results']
|
||||
datasource_rows = []
|
||||
id = 0
|
||||
for row in results:
|
||||
new_row = PolicyAPIDictWrapper(row)
|
||||
new_row.set_id_if_empty(id)
|
||||
id += 1
|
||||
datasource_rows.append(new_row)
|
||||
return datasource_rows
|
||||
|
||||
|
||||
def datasource_schema_get(request, datasource_id):
|
||||
"""Get the schema for all tables in the given data source."""
|
||||
client = congressclient(request)
|
||||
return client.show_datasource_schema(datasource_id)
|
||||
|
||||
|
||||
def datasource_table_schema_get(request, datasource_id, table_name):
|
||||
"""Get the schema for a data source table."""
|
||||
client = congressclient(request)
|
||||
return client.show_datasource_table_schema(datasource_id, table_name)
|
||||
|
||||
|
||||
def datasource_table_schema_get_by_name(request, datasource_name, table_name):
|
||||
"""Get the schema for a data source table."""
|
||||
datasource = datasource_get_by_name(request, datasource_name)
|
||||
client = congressclient(request)
|
||||
return client.show_datasource_table_schema(datasource['id'], table_name)
|
||||
|
||||
|
||||
def datasource_statuses_list(request):
|
||||
client = congressclient(request)
|
||||
datasources_list = client.list_datasources()
|
||||
datasources = datasources_list['results']
|
||||
ds_status = []
|
||||
|
||||
for ds in datasources:
|
||||
try:
|
||||
status = client.list_datasource_status(ds['id'])
|
||||
except Exception:
|
||||
LOG.exception("Exception while getting the status")
|
||||
raise
|
||||
wrapper = PolicyAPIDictWrapper(ds)
|
||||
wrapper.set_value('service', ds['name'])
|
||||
for key in status:
|
||||
value = status[key]
|
||||
wrapper.set_value(key, value)
|
||||
ds_status.append(wrapper)
|
||||
return ds_status
|
0
congress_dashboard/datasources/__init__.py
Normal file
0
congress_dashboard/datasources/__init__.py
Normal file
26
congress_dashboard/datasources/panel.py
Normal file
26
congress_dashboard/datasources/panel.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon
|
||||
from openstack_dashboard.dashboards.admin import dashboard
|
||||
|
||||
|
||||
class DataSources(horizon.Panel):
|
||||
name = _("Data Sources")
|
||||
slug = "datasources"
|
||||
permissions = ('openstack.roles.admin',)
|
||||
|
||||
|
||||
dashboard.Admin.register(DataSources)
|
89
congress_dashboard/datasources/tables.py
Normal file
89
congress_dashboard/datasources/tables.py
Normal file
@ -0,0 +1,89 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import unordered_list
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import tables
|
||||
|
||||
|
||||
def get_resource_url(obj):
|
||||
return reverse('horizon:admin:datasources:datasource_table_detail',
|
||||
args=(obj['datasource_id'], obj['table_id']))
|
||||
|
||||
|
||||
class DataSourcesTablesTable(tables.DataTable):
|
||||
name = tables.Column("name", verbose_name=_("Table Name"),
|
||||
link=get_resource_url)
|
||||
datasource_name = tables.Column("datasource_name",
|
||||
verbose_name=_("Service"))
|
||||
datasource_driver = tables.Column("datasource_driver",
|
||||
verbose_name=_("Driver"))
|
||||
|
||||
class Meta(object):
|
||||
name = "datasources_tables"
|
||||
verbose_name = _("Service Data")
|
||||
hidden_title = False
|
||||
|
||||
|
||||
def get_policy_link(datum):
|
||||
return reverse('horizon:admin:policies:detail',
|
||||
args=(datum['policy_name'],))
|
||||
|
||||
|
||||
def get_policy_table_link(datum):
|
||||
return reverse('horizon:admin:datasources:policy_table_detail',
|
||||
args=(datum['policy_name'], datum['name']))
|
||||
|
||||
|
||||
class PoliciesTablesTable(tables.DataTable):
|
||||
name = tables.Column("name", verbose_name=_("Table Name"),
|
||||
link=get_policy_table_link)
|
||||
policy_name = tables.Column("policy_name", verbose_name=_("Policy"),
|
||||
link=get_policy_link)
|
||||
policy_owner_id = tables.Column("policy_owner_id",
|
||||
verbose_name=_("Owner ID"))
|
||||
|
||||
class Meta(object):
|
||||
name = "policies_tables"
|
||||
verbose_name = _("Policy Data")
|
||||
hidden_title = False
|
||||
|
||||
|
||||
class DataSourceRowsTable(tables.DataTable):
|
||||
class Meta(object):
|
||||
name = "datasource_rows"
|
||||
verbose_name = _("Rows")
|
||||
hidden_title = False
|
||||
|
||||
|
||||
class DataSourceStatusesTable(tables.DataTable):
|
||||
datasource_name = tables.Column("service",
|
||||
verbose_name=_("Service"))
|
||||
last_updated = tables.Column("last_updated",
|
||||
verbose_name=_("Last Updated"))
|
||||
subscriptions = tables.Column("subscriptions",
|
||||
verbose_name=_("Subscriptions"),
|
||||
wrap_list=True, filters=(unordered_list,))
|
||||
last_error = tables.Column("last_error", verbose_name=_("Last Error"))
|
||||
subscribers = tables.Column("subscribers", verbose_name=_("Subscribers"),
|
||||
wrap_list=True, filters=(unordered_list,))
|
||||
initialized = tables.Column("initialized", verbose_name=_("Initialized"))
|
||||
number_of_updates = tables.Column("number_of_updates",
|
||||
verbose_name=_("Number of Updates"))
|
||||
|
||||
class Meta(object):
|
||||
name = "service_status"
|
||||
verbose_name = _("Service Status")
|
||||
hidden_title = False
|
@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans "Service Status" %}</h3>
|
||||
|
||||
<div class="detail">
|
||||
<dl class="dl-horizontal">
|
||||
<dt> {% trans "Service Name" %}</dt>
|
||||
<dd>{{ datasource_name }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>{{ status }}</dd>
|
||||
<dt>{% trans "Subscriptions" %}</dt>
|
||||
<dd>{{ subscriptions }}</dd>
|
||||
<dt>{% trans "Subscribers" %}</dt>
|
||||
<dd>{{ subscribers }}</dd>
|
||||
<dt>{% trans "Number of Updates" %}</dt>
|
||||
<dd>{{ number_of_updates }}</dd>
|
||||
<dt>{% trans "Last Updated" %}</dt>
|
||||
<dd>{{ last_updated }}</dd>
|
||||
<dt>{% trans "Last Error" %}</dt>
|
||||
<dd>{{ last_error }}</dd>
|
||||
</dl>
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans "Table Overview" %}</h3>
|
||||
|
||||
<div class="detail">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ datasource_type }} {% trans "Data Source" %}</dt>
|
||||
<dd>{{ datasource_name }}</dd>
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ table_name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ id|default:table_name }}</dd>
|
||||
</dl>
|
||||
</div>
|
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Datasource Overview" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Datasource Details: ")|add:datasource_name %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/datasources/_datasource_overview.html" %}
|
||||
<div id="datasources_tables">
|
||||
{{ datasources_tables_table.render }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Data Source Table Details" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Data Source Table Details: ")|add:table_name %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/datasources/_detail_overview.html" %}
|
||||
<div id="datasource_table_rows">
|
||||
{{ datasource_rows_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Data Sources" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Data Sources") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div id="policies_tables">
|
||||
{{ policies_tables_table.render }}
|
||||
</div>
|
||||
<div id="service_status_tables">
|
||||
{{ service_status_table.render }}
|
||||
</div>
|
||||
<div id="service_tables">
|
||||
{{ datasources_tables_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
34
congress_dashboard/datasources/urls.py
Normal file
34
congress_dashboard/datasources/urls.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from congress_dashboard.datasources import views
|
||||
|
||||
|
||||
SERVICES = (
|
||||
r'^services/(?P<datasource_id>[^/]+)/(?P<service_table_name>[^/]+)/%s$')
|
||||
POLICIES = (
|
||||
r'^policies/(?P<datasource_id>[^/]+)/(?P<policy_table_name>[^/]+)/%s$')
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(SERVICES % 'detail', views.DetailView.as_view(),
|
||||
name='datasource_table_detail'),
|
||||
url(POLICIES % 'detail', views.DetailView.as_view(),
|
||||
name='policy_table_detail'),
|
||||
)
|
187
congress_dashboard/datasources/utils.py
Normal file
187
congress_dashboard/datasources/utils.py
Normal file
@ -0,0 +1,187 @@
|
||||
# Copyright 2015 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_policy_tables(request):
|
||||
# Return all policy tables.
|
||||
all_tables = []
|
||||
try:
|
||||
# Get all the policies.
|
||||
policies = congress.policies_list(request)
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get list of policies: %s', str(e))
|
||||
else:
|
||||
try:
|
||||
for policy in policies:
|
||||
# Get all the tables in this policy.
|
||||
policy_name = policy['name']
|
||||
policy_tables = congress.policy_tables_list(request,
|
||||
policy_name)
|
||||
# Get the names of the tables.
|
||||
datasource_tables = []
|
||||
for table in policy_tables:
|
||||
table.set_id_as_name_if_empty()
|
||||
table_name = table['name']
|
||||
# Exclude service-derived tables.
|
||||
if congress.TABLE_SEPARATOR not in table_name:
|
||||
datasource_tables.append(table['name'])
|
||||
|
||||
all_tables.append({'datasource': policy_name,
|
||||
'tables': datasource_tables})
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get tables for policy "%s": %s',
|
||||
policy_name, str(e))
|
||||
return all_tables
|
||||
|
||||
|
||||
def _get_service_tables(request):
|
||||
# Return all service tables.
|
||||
all_tables = []
|
||||
try:
|
||||
# Get all the services.
|
||||
services = congress.datasources_list(request)
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get list of data sources: %s', str(e))
|
||||
else:
|
||||
try:
|
||||
for service in services:
|
||||
# Get all the tables in this service.
|
||||
service_id = service['id']
|
||||
service_tables = congress.datasource_tables_list(request,
|
||||
service_id)
|
||||
# Get the names of the tables.
|
||||
datasource_tables = []
|
||||
for table in service_tables:
|
||||
table.set_id_as_name_if_empty()
|
||||
datasource_tables.append(table['name'])
|
||||
|
||||
all_tables.append({'datasource': service['name'],
|
||||
'tables': datasource_tables})
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get tables for data source "%s": %s',
|
||||
service_id, str(e))
|
||||
return all_tables
|
||||
|
||||
|
||||
def get_datasource_tables(request):
|
||||
"""Get names of all data source tables.
|
||||
|
||||
Example:
|
||||
[
|
||||
{
|
||||
'datasource': 'classification',
|
||||
'tables': ['error']
|
||||
},
|
||||
{
|
||||
'datasource': 'neutronv2'
|
||||
'tables': ['networks', 'ports', ...]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
tables = _get_policy_tables(request)
|
||||
tables.extend(_get_service_tables(request))
|
||||
return tables
|
||||
|
||||
|
||||
def get_datasource_columns(request):
|
||||
"""Get of names of columns from all data sources.
|
||||
|
||||
Example:
|
||||
[
|
||||
{
|
||||
'datasource': 'classification',
|
||||
'tables': [
|
||||
{
|
||||
'table': 'error',
|
||||
'columns': ['name']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'datasource': 'neutronv2',
|
||||
'tables': [
|
||||
{
|
||||
'table': 'networks',
|
||||
'columns': ['id', 'tenant_id', ...],
|
||||
},
|
||||
...
|
||||
],
|
||||
...
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
all_columns = []
|
||||
|
||||
# Get all the policy tables.
|
||||
policy_tables = _get_policy_tables(request)
|
||||
try:
|
||||
for policy in policy_tables:
|
||||
# Get all the columns in this policy. Unlike for the services,
|
||||
# there's currently no congress client API to get the schema for
|
||||
# all tables in a policy in a single call.
|
||||
policy_name = policy['datasource']
|
||||
tables = policy['tables']
|
||||
|
||||
datasource_tables = []
|
||||
for table_name in tables:
|
||||
# Get all the columns in this policy table.
|
||||
schema = congress.policy_table_schema_get(request, policy_name,
|
||||
table_name)
|
||||
columns = [c['name'] for c in schema['columns']]
|
||||
datasource_tables.append({'table': table_name,
|
||||
'columns': columns})
|
||||
|
||||
all_columns.append({'datasource': policy_name,
|
||||
'tables': datasource_tables})
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get schema for policy "%s" table "%s": %s',
|
||||
policy_name, table_name, str(e))
|
||||
|
||||
try:
|
||||
# Get all the services.
|
||||
services = congress.datasources_list(request)
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get list of data sources: %s', str(e))
|
||||
else:
|
||||
try:
|
||||
for service in services:
|
||||
# Get the schema for this service.
|
||||
service_id = service['id']
|
||||
service_name = service['name']
|
||||
schema = congress.datasource_schema_get(request, service_id)
|
||||
|
||||
datasource_tables = []
|
||||
for table in schema['tables']:
|
||||
# Get the columns for this table.
|
||||
columns = [c['name'] for c in table['columns']]
|
||||
datasource_table = {'table': table['table_id'],
|
||||
'columns': columns}
|
||||
datasource_tables.append(datasource_table)
|
||||
|
||||
all_columns.append({'datasource': service_name,
|
||||
'tables': datasource_tables})
|
||||
except Exception as e:
|
||||
LOG.error('Unable to get schema for data source "%s": %s',
|
||||
service_id, str(e))
|
||||
|
||||
return all_columns
|
272
congress_dashboard/datasources/views.py
Normal file
272
congress_dashboard/datasources/views.py
Normal file
@ -0,0 +1,272 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
from congress_dashboard.datasources import tables as datasources_tables
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.MultiTableView):
|
||||
"""List service and policy defined data."""
|
||||
table_classes = (datasources_tables.DataSourcesTablesTable,
|
||||
datasources_tables.PoliciesTablesTable,
|
||||
datasources_tables.DataSourceStatusesTable,)
|
||||
template_name = 'admin/datasources/index.html'
|
||||
|
||||
def get_datasources_tables_data(self):
|
||||
try:
|
||||
datasources = congress.datasources_list(self.request)
|
||||
except Exception as e:
|
||||
msg = _('Unable to get services list: %s') % str(e)
|
||||
messages.error(self.request, msg)
|
||||
return []
|
||||
|
||||
ds_temp = []
|
||||
for ds in datasources:
|
||||
ds_id = ds['id']
|
||||
try:
|
||||
ds_tables = congress.datasource_tables_list(self.request,
|
||||
ds_id)
|
||||
except Exception as e:
|
||||
msg_args = {'ds_id': ds_id, 'error': str(e)}
|
||||
msg = _('Unable to get tables list for service "%(ds_id)s": '
|
||||
'%(error)s') % msg_args
|
||||
messages.error(self.request, msg)
|
||||
return []
|
||||
|
||||
for table in ds_tables:
|
||||
table.set_value('datasource_id', ds_id)
|
||||
table.set_value('datasource_name', ds['name'])
|
||||
table.set_value('datasource_driver', ds['driver'])
|
||||
table.set_id_as_name_if_empty()
|
||||
# Object ids within a Horizon table must be unique. Otherwise,
|
||||
# Horizon will cache the column values for the object by id and
|
||||
# use the same column values for all rows with the same id.
|
||||
table.set_value('table_id', table['id'])
|
||||
table.set_value('id', '%s-%s' % (ds_id, table['table_id']))
|
||||
ds_temp.append(table)
|
||||
|
||||
logger.debug("ds_temp %s" % ds_temp)
|
||||
return ds_temp
|
||||
|
||||
def get_service_status_data(self):
|
||||
ds = []
|
||||
try:
|
||||
ds = congress.datasource_statuses_list(self.request)
|
||||
logger.debug("ds status : %s " % ds)
|
||||
except Exception as e:
|
||||
msg = _('Unable to get datasource status list: %s') % str(e)
|
||||
messages.error(self.request, msg)
|
||||
return ds
|
||||
|
||||
def get_policies_tables_data(self):
|
||||
try:
|
||||
policies = congress.policies_list(self.request)
|
||||
except Exception as e:
|
||||
msg = _('Unable to get policies list: %s') % str(e)
|
||||
messages.error(self.request, msg)
|
||||
return []
|
||||
|
||||
policies_tables = []
|
||||
for policy in policies:
|
||||
policy_name = policy['name']
|
||||
try:
|
||||
policy_tables = congress.policy_tables_list(self.request,
|
||||
policy_name)
|
||||
except Exception as e:
|
||||
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||
msg = _('Unable to get tables list for policy '
|
||||
'"%(policy_name)s": %(error)s') % msg_args
|
||||
messages.error(self.request, msg)
|
||||
return []
|
||||
|
||||
for pt in policy_tables:
|
||||
pt.set_id_as_name_if_empty()
|
||||
pt.set_policy_details(policy)
|
||||
# Object ids within a Horizon table must be unique. Otherwise,
|
||||
# Horizon will cache the column values for the object by id and
|
||||
# use the same column values for all rows with the same id.
|
||||
pt.set_value('table_id', pt['id'])
|
||||
pt.set_value('id', '%s-%s' % (policy_name, pt['table_id']))
|
||||
policies_tables.extend(policy_tables)
|
||||
|
||||
return policies_tables
|
||||
|
||||
|
||||
class DetailView(tables.DataTableView):
|
||||
"""List details about and rows from a data source (service or policy)."""
|
||||
table_class = datasources_tables.DataSourceRowsTable
|
||||
template_name = 'admin/datasources/detail.html'
|
||||
|
||||
def get_data(self):
|
||||
datasource_id = self.kwargs['datasource_id']
|
||||
table_name = self.kwargs.get('policy_table_name')
|
||||
is_service = False
|
||||
|
||||
try:
|
||||
if table_name:
|
||||
# Policy data table.
|
||||
rows = congress.policy_rows_list(self.request, datasource_id,
|
||||
table_name)
|
||||
if congress.TABLE_SEPARATOR in table_name:
|
||||
table_name_parts = table_name.split(
|
||||
congress.TABLE_SEPARATOR)
|
||||
maybe_datasource_name = table_name_parts[0]
|
||||
datasources = congress.datasources_list(self.request)
|
||||
for datasource in datasources:
|
||||
if datasource['name'] == maybe_datasource_name:
|
||||
# Serivce-derived policy data table.
|
||||
is_service = True
|
||||
datasource_id = datasource['id']
|
||||
table_name = table_name_parts[1]
|
||||
break
|
||||
else:
|
||||
# Service data table.
|
||||
is_service = True
|
||||
datasource = congress.datasource_get_by_name(
|
||||
self.request, datasource_id)
|
||||
table_name = self.kwargs['service_table_name']
|
||||
rows = congress.datasource_rows_list(
|
||||
self.request, datasource_id, table_name)
|
||||
except Exception as e:
|
||||
msg_args = {
|
||||
'table_name': table_name,
|
||||
'ds_id': datasource_id,
|
||||
'error': str(e)
|
||||
}
|
||||
msg = _('Unable to get rows in table "%(table_name)s", data '
|
||||
'source "%(ds_id)s": %(error)s') % msg_args
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse('horizon:admin:datasources:index')
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
# Normally, in Horizon, the columns for a table are defined as
|
||||
# attributes of the Table class. When the class is instantiated,
|
||||
# the columns are processed during the metaclass initialization. To
|
||||
# add columns dynamically, re-create the class from the metaclass
|
||||
# with the added columns, re-create the Table from the new class,
|
||||
# then reassign the Table stored in this View.
|
||||
column_names = []
|
||||
table_class_attrs = copy.deepcopy(dict(self.table_class.__dict__))
|
||||
# Get schema from the server.
|
||||
try:
|
||||
if is_service:
|
||||
schema = congress.datasource_table_schema_get(
|
||||
self.request, datasource_id, table_name)
|
||||
else:
|
||||
schema = congress.policy_table_schema_get(
|
||||
self.request, datasource_id, table_name)
|
||||
except Exception as e:
|
||||
msg_args = {
|
||||
'table_name': table_name,
|
||||
'ds_id': datasource_id,
|
||||
'error': str(e)
|
||||
}
|
||||
msg = _('Unable to get schema for table "%(table_name)s", '
|
||||
'data source "%(ds_id)s": %(error)s') % msg_args
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse('horizon:admin:datasources:index')
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
columns = schema['columns']
|
||||
row_len = 0
|
||||
if len(rows):
|
||||
row_len = len(rows[0].get('data', []))
|
||||
|
||||
if not row_len or row_len == len(columns):
|
||||
for col in columns:
|
||||
col_name = col['name']
|
||||
# Attribute name for column in the class must be a valid
|
||||
# identifier. Slugify it.
|
||||
col_slug = slugify(col_name)
|
||||
column_names.append(col_slug)
|
||||
table_class_attrs[col_slug] = tables.Column(
|
||||
col_slug, verbose_name=col_name)
|
||||
else:
|
||||
# There could be another table with the same name and different
|
||||
# arity. Divide the rows into unnamed columns. Number them for
|
||||
# internal reference.
|
||||
for i in xrange(0, row_len):
|
||||
col_name = str(i)
|
||||
column_names.append(col_name)
|
||||
table_class_attrs[col_name] = tables.Column(
|
||||
col_name, verbose_name='')
|
||||
|
||||
# Class and object re-creation, using a new class name, the same base
|
||||
# classes, and the new class attributes, which now includes columns.
|
||||
columnized_table_class_name = '%s%sRows' % (
|
||||
slugify(datasource_id).title(), slugify(table_name).title())
|
||||
columnized_table_class = tables.base.DataTableMetaclass(
|
||||
str(columnized_table_class_name), self.table_class.__bases__,
|
||||
table_class_attrs)
|
||||
|
||||
self.table_class = columnized_table_class
|
||||
columnized_table = columnized_table_class(self.request, **self.kwargs)
|
||||
self._tables[columnized_table_class._meta.name] = columnized_table
|
||||
|
||||
# Map columns names to row values.
|
||||
num_cols = len(column_names)
|
||||
for row in rows:
|
||||
try:
|
||||
row_data = row['data']
|
||||
row.delete_by_key('data')
|
||||
for i in xrange(0, num_cols):
|
||||
row.set_value(column_names[i], row_data[i])
|
||||
except Exception as e:
|
||||
msg_args = {
|
||||
'table_name': table_name,
|
||||
'ds_id': datasource_id,
|
||||
'error': str(e)
|
||||
}
|
||||
msg = _('Unable to get data for table "%(table_name)s", data '
|
||||
'source "%(ds_id)s": %(error)s') % msg_args
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse('horizon:admin:datasources:index')
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
return rows
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
if 'policy_table_name' in kwargs:
|
||||
table_name = kwargs.get('policy_table_name')
|
||||
context['datasource_type'] = _('Policy')
|
||||
datasource_name = kwargs['datasource_id']
|
||||
else:
|
||||
table_name = kwargs['service_table_name']
|
||||
context['datasource_type'] = _('Service')
|
||||
try:
|
||||
datasource_id = kwargs['datasource_id']
|
||||
datasource = congress.datasource_get(self.request,
|
||||
datasource_id)
|
||||
datasource_name = datasource['name']
|
||||
except Exception as e:
|
||||
datasource_name = datasource_id
|
||||
logger.info('Failed to get data source "%s": %s' %
|
||||
(datasource_id, str(e)))
|
||||
context['datasource_name'] = datasource_name
|
||||
context['table_name'] = table_name
|
||||
return context
|
3
congress_dashboard/enabled/_50_policy.py
Normal file
3
congress_dashboard/enabled/_50_policy.py
Normal file
@ -0,0 +1,3 @@
|
||||
PANEL_GROUP = 'policy'
|
||||
PANEL_GROUP_NAME = 'Policy'
|
||||
PANEL_GROUP_DASHBOARD = 'admin'
|
9
congress_dashboard/enabled/_60_policies.py
Normal file
9
congress_dashboard/enabled/_60_policies.py
Normal file
@ -0,0 +1,9 @@
|
||||
PANEL = 'policies'
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
PANEL_GROUP = 'policy'
|
||||
ADD_PANEL = 'congress_dashboard.policies.panel.Policies'
|
||||
ADD_INSTALLED_APPS = [
|
||||
'congress_dashboard',
|
||||
]
|
||||
AUTO_DISCOVER_STATIC_FILES = True
|
||||
ADD_SCSS_FILES = ['congress_dashboard/static/admin/css/policies.css']
|
5
congress_dashboard/enabled/_70_datasources.py
Normal file
5
congress_dashboard/enabled/_70_datasources.py
Normal file
@ -0,0 +1,5 @@
|
||||
PANEL = 'datasources'
|
||||
PANEL_DASHBOARD = 'admin'
|
||||
PANEL_GROUP = 'policy'
|
||||
ADD_PANEL = 'congress_dashboard.datasources.panel.DataSources'
|
||||
AUTO_DISCOVER_STATIC_FILES = True
|
0
congress_dashboard/policies/__init__.py
Normal file
0
congress_dashboard/policies/__init__.py
Normal file
69
congress_dashboard/policies/forms.py
Normal file
69
congress_dashboard/policies/forms.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright 2015 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
POLICY_KIND_CHOICES = (
|
||||
('nonrecursive', _('Nonrecursive')),
|
||||
('action', _('Action')),
|
||||
('database', _('Database')),
|
||||
('materialized', _('Materialized')),
|
||||
)
|
||||
|
||||
|
||||
class CreatePolicy(forms.SelfHandlingForm):
|
||||
name = forms.CharField(max_length=255, label=_("Policy Name"))
|
||||
kind = forms.ChoiceField(choices=POLICY_KIND_CHOICES, label=_("Kind"),
|
||||
initial='nonrecursive')
|
||||
description = forms.CharField(label=_("Description"), required=False,
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
failure_url = 'horizon:admin:policies:index'
|
||||
|
||||
def handle(self, request, data):
|
||||
policy_name = data['name']
|
||||
policy_description = data.get('description')
|
||||
policy_kind = data.pop('kind')
|
||||
LOG.info('User %s creating policy "%s" of type %s in tenant %s',
|
||||
request.user.username, policy_name, policy_kind,
|
||||
request.user.tenant_name)
|
||||
try:
|
||||
params = {
|
||||
'name': policy_name,
|
||||
'description': policy_description,
|
||||
'kind': policy_kind,
|
||||
}
|
||||
policy = congress.policy_create(request, params)
|
||||
msg = _('Created policy "%s"') % policy_name
|
||||
LOG.info(msg)
|
||||
messages.success(request, msg)
|
||||
except Exception as e:
|
||||
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||
msg = _('Failed to create policy "%(policy_name)s": '
|
||||
'%(error)s') % msg_args
|
||||
LOG.error(msg)
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse(self.failure_url)
|
||||
raise exceptions.Http302(redirect)
|
||||
return policy
|
26
congress_dashboard/policies/panel.py
Normal file
26
congress_dashboard/policies/panel.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
import horizon
|
||||
from openstack_dashboard.dashboards.admin import dashboard
|
||||
|
||||
|
||||
class Policies(horizon.Panel):
|
||||
name = _("Policies")
|
||||
slug = "policies"
|
||||
permissions = ('openstack.roles.admin',)
|
||||
|
||||
|
||||
dashboard.Admin.register(Policies)
|
0
congress_dashboard/policies/rules/__init__.py
Normal file
0
congress_dashboard/policies/rules/__init__.py
Normal file
112
congress_dashboard/policies/rules/tables.py
Normal file
112
congress_dashboard/policies/rules/tables.py
Normal file
@ -0,0 +1,112 @@
|
||||
# Copyright 2015 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import linebreaksbr
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
from openstack_dashboard import policy
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateRule(tables.LinkAction):
|
||||
name = 'create_rule'
|
||||
verbose_name = _('Create Rule')
|
||||
url = 'horizon:admin:policies:create_rule'
|
||||
classes = ('ajax-modal',)
|
||||
icon = 'plus'
|
||||
policy_rules = (('policy', 'create_rule'),)
|
||||
|
||||
def get_link_url(self, datum=None):
|
||||
policy_name = self.table.kwargs['policy_name']
|
||||
return reverse(self.url, args=(policy_name,))
|
||||
|
||||
|
||||
class DeleteRule(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u'Delete Rule',
|
||||
u'Delete Rules',
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u'Deleted rule',
|
||||
u'Deleted rules',
|
||||
count
|
||||
)
|
||||
|
||||
redirect_url = 'horizon:admin:policies:detail'
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
policy_name = self.table.kwargs['policy_name']
|
||||
LOG.info('User %s deleting policy "%s" rule "%s" in tenant %s',
|
||||
request.user.username, policy_name, obj_id,
|
||||
request.user.tenant_name)
|
||||
try:
|
||||
congress.policy_rule_delete(request, policy_name, obj_id)
|
||||
LOG.info('Deleted policy rule "%s"', obj_id)
|
||||
except Exception as e:
|
||||
msg_args = {'rule_id': obj_id, 'error': str(e)}
|
||||
msg = _('Failed to delete policy rule "%(rule_id)s": '
|
||||
'%(error)s') % msg_args
|
||||
LOG.error(msg)
|
||||
messages.error(request, msg)
|
||||
redirect = reverse(self.redirect_url, args=(policy_name,))
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
|
||||
def _format_rule(rule):
|
||||
"""Make rule's text more human readable."""
|
||||
head_body = rule.split(congress.RULE_SEPARATOR)
|
||||
if len(head_body) < 2:
|
||||
return rule
|
||||
head = head_body[0]
|
||||
body = head_body[1]
|
||||
|
||||
# Add newline after each literal in the body.
|
||||
body_literals = body.split(congress.LITERALS_SEPARATOR)
|
||||
literals_break = congress.LITERALS_SEPARATOR + '\n'
|
||||
new_body = literals_break.join(body_literals)
|
||||
|
||||
# Add newline after the head.
|
||||
rules_break = congress.RULE_SEPARATOR + '\n'
|
||||
return rules_break.join([head, new_body])
|
||||
|
||||
|
||||
class PolicyRulesTable(tables.DataTable):
|
||||
id = tables.Column("id", verbose_name=_("Rule ID"))
|
||||
name = tables.Column("name", verbose_name=_("Name"))
|
||||
comment = tables.Column("comment", verbose_name=_("Comment"))
|
||||
rule = tables.Column("rule", verbose_name=_("Rule"),
|
||||
filters=(_format_rule, linebreaksbr,))
|
||||
|
||||
class Meta(object):
|
||||
name = "policy_rules"
|
||||
verbose_name = _("Rules")
|
||||
table_actions = (CreateRule, DeleteRule,)
|
||||
row_actions = (DeleteRule,)
|
||||
hidden_title = False
|
31
congress_dashboard/policies/rules/views.py
Normal file
31
congress_dashboard/policies/rules/views.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Copyright 2015 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from horizon import workflows
|
||||
|
||||
from congress_dashboard.policies.rules import workflows as rule_workflows
|
||||
|
||||
|
||||
class CreateView(workflows.WorkflowView):
|
||||
workflow_class = rule_workflows.CreateRule
|
||||
ajax_template_name = 'admin/policies/rules/create.html'
|
||||
success_url = 'horizon:admin:policies:detail'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(self.success_url,
|
||||
args=(self.kwargs['policy_name'],))
|
||||
|
||||
def get_initial(self):
|
||||
return {'policy_name': self.kwargs['policy_name']}
|
441
congress_dashboard/policies/rules/workflows.py
Normal file
441
congress_dashboard/policies/rules/workflows.py
Normal file
@ -0,0 +1,441 @@
|
||||
# Copyright 2015 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import template
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import forms
|
||||
from horizon import workflows
|
||||
import six
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
|
||||
|
||||
COLUMN_FORMAT = '<datasource>%s<table> <column>' % congress.TABLE_SEPARATOR
|
||||
COLUMN_PATTERN = r'\s*[\w.]+%s[\w.]+\s+[\w.]+\s*$' % congress.TABLE_SEPARATOR
|
||||
COLUMN_PATTERN_ERROR = 'Column name must be in "%s" format' % COLUMN_FORMAT
|
||||
|
||||
TABLE_FORMAT = '<datasource>%s<table>' % congress.TABLE_SEPARATOR
|
||||
TABLE_PATTERN = r'\s*[\w.]+%s[\w.]+\s*$' % congress.TABLE_SEPARATOR
|
||||
TABLE_PATTERN_ERROR = 'Table name must be in "%s" format' % TABLE_FORMAT
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CreateOutputAction(workflows.Action):
|
||||
policy_name = forms.CharField(widget=forms.HiddenInput(), required=False)
|
||||
rule_name = forms.CharField(label=_('Rule Name'), max_length=255,
|
||||
initial='', required=False)
|
||||
comment = forms.CharField(label=_('Rule Comment'), initial='',
|
||||
required=False)
|
||||
policy_table = forms.CharField(label=_("Policy Table Name"), initial='',
|
||||
max_length=255)
|
||||
policy_columns = forms.CharField(
|
||||
label=_('Policy Table Columns'), initial='',
|
||||
help_text=_('Name the columns in the output table, one per textbox.'))
|
||||
failure_url = 'horizon:admin:policies:detail'
|
||||
|
||||
def __init__(self, request, context, *args, **kwargs):
|
||||
super(CreateOutputAction, self).__init__(request, context, *args,
|
||||
**kwargs)
|
||||
self.fields['policy_name'].initial = context['policy_name']
|
||||
|
||||
class Meta(object):
|
||||
name = _('Output')
|
||||
|
||||
|
||||
class CreateOutput(workflows.Step):
|
||||
action_class = CreateOutputAction
|
||||
contributes = ('policy_name', 'rule_name', 'comment', 'policy_table',
|
||||
'policy_columns')
|
||||
template_name = 'admin/policies/rules/_create_output.html'
|
||||
help_text = _('Information about the rule and the policy table '
|
||||
'being created.')
|
||||
|
||||
def render(self):
|
||||
# Overriding parent method to add extra template context variables.
|
||||
step_template = template.loader.get_template(self.template_name)
|
||||
extra_context = {"form": self.action,
|
||||
"step": self}
|
||||
context = template.RequestContext(self.workflow.request, extra_context)
|
||||
|
||||
# Data needed to re-create policy column inputs after an error occurs.
|
||||
policy_columns = self.workflow.request.POST.get('policy_columns', '')
|
||||
columns_list = policy_columns.split(', ')
|
||||
context['policy_columns_list'] = columns_list
|
||||
context['policy_columns_count'] = len(columns_list)
|
||||
return step_template.render(context)
|
||||
|
||||
|
||||
class CreateConditionsAction(workflows.Action):
|
||||
mappings = forms.CharField(label=_('Policy table columns:'), initial='')
|
||||
|
||||
class Meta(object):
|
||||
name = _('Conditions')
|
||||
|
||||
|
||||
class CreateConditions(workflows.Step):
|
||||
action_class = CreateConditionsAction
|
||||
contributes = ('mappings',)
|
||||
template_name = 'admin/policies/rules/_create_conditions.html'
|
||||
help_text = _('Sources from which the output policy table will get its '
|
||||
'data, plus any constraints.')
|
||||
|
||||
def _compare_mapping_columns(self, x, y):
|
||||
# x = "mapping_column_<int>", y = "mapping_column_<int>"
|
||||
return cmp(int(x.split('_')[-1]), int(y.split('_')[-1]))
|
||||
|
||||
def render(self):
|
||||
# Overriding parent method to add extra template context variables.
|
||||
step_template = template.loader.get_template(self.template_name)
|
||||
extra_context = {"form": self.action,
|
||||
"step": self}
|
||||
context = template.RequestContext(self.workflow.request, extra_context)
|
||||
|
||||
# Data needed to re-create mapping column inputs after an error occurs.
|
||||
post = self.workflow.request.POST
|
||||
mappings = []
|
||||
policy_columns = post.get('policy_columns')
|
||||
policy_columns_list = []
|
||||
# Policy column to data source mappings.
|
||||
if policy_columns:
|
||||
policy_columns_list = policy_columns.split(', ')
|
||||
mapping_columns = []
|
||||
for param, value in post.items():
|
||||
if (param.startswith('mapping_column_') and
|
||||
param != 'mapping_column_0'):
|
||||
mapping_columns.append(param)
|
||||
|
||||
# Mapping columns should be in the same order as the policy columns
|
||||
# above to which they match.
|
||||
sorted_mapping_columns = sorted(mapping_columns,
|
||||
cmp=self._compare_mapping_columns)
|
||||
mapping_columns_list = [post.get(c)
|
||||
for c in sorted_mapping_columns]
|
||||
mappings = zip(policy_columns_list, mapping_columns_list)
|
||||
context['mappings'] = mappings
|
||||
# Add one for the hidden template row.
|
||||
context['mappings_count'] = len(mappings) + 1
|
||||
|
||||
# Data needed to re-create join, negation, and alias inputs.
|
||||
joins = []
|
||||
negations = []
|
||||
aliases = []
|
||||
for param, value in post.items():
|
||||
if param.startswith('join_left_') and value:
|
||||
join_num = param.split('_')[-1]
|
||||
other_value = post.get('join_right_%s' % join_num)
|
||||
join_op = post.get('join_op_%s' % join_num)
|
||||
if other_value and join_op is not None:
|
||||
joins.append((value, join_op, other_value))
|
||||
elif param.startswith('negation_value_') and value:
|
||||
negation_num = param.split('_')[-1]
|
||||
negation_column = post.get('negation_column_%s' %
|
||||
negation_num)
|
||||
if negation_column:
|
||||
negations.append((value, negation_column))
|
||||
elif param.startswith('alias_column_') and value:
|
||||
alias_num = param.split('_')[-1]
|
||||
alias_name = post.get('alias_name_%s' % alias_num)
|
||||
if alias_name:
|
||||
aliases.append((value, alias_name))
|
||||
|
||||
# Make sure there's at least one empty row.
|
||||
context['joins'] = joins or [('', '')]
|
||||
context['joins_count'] = len(joins) or 1
|
||||
context['negations'] = negations or [('', '')]
|
||||
context['negations_count'] = len(negations) or 1
|
||||
context['aliases'] = aliases or [('', '')]
|
||||
context['aliases_count'] = len(aliases) or 1
|
||||
|
||||
# Input validation attributes.
|
||||
context['column_pattern'] = COLUMN_PATTERN
|
||||
context['column_pattern_error'] = COLUMN_PATTERN_ERROR
|
||||
context['table_pattern'] = TABLE_PATTERN
|
||||
context['table_pattern_error'] = TABLE_PATTERN_ERROR
|
||||
return step_template.render(context)
|
||||
|
||||
|
||||
def _underscore_slugify(name):
|
||||
# Slugify given string, except using undesrscores instead of hyphens.
|
||||
return slugify(name).replace('-', '_')
|
||||
|
||||
|
||||
class CreateRule(workflows.Workflow):
|
||||
slug = 'create_rule'
|
||||
name = _('Create Rule')
|
||||
finalize_button_name = _('Create')
|
||||
success_message = _('Created rule%(rule_name)s.%(error)s')
|
||||
failure_message = _('Unable to create rule%(rule_name)s: %(error)s')
|
||||
default_steps = (CreateOutput, CreateConditions)
|
||||
wizard = True
|
||||
|
||||
def get_success_url(self):
|
||||
policy_name = self.context.get('policy_name')
|
||||
return reverse('horizon:admin:policies:detail', args=(policy_name,))
|
||||
|
||||
def get_failure_url(self):
|
||||
policy_name = self.context.get('policy_name')
|
||||
return reverse('horizon:admin:policies:detail', args=(policy_name,))
|
||||
|
||||
def format_status_message(self, message):
|
||||
rule_name = self.context.get('rule_name')
|
||||
name_str = ''
|
||||
if rule_name:
|
||||
name_str = ' "%s"' % rule_name
|
||||
else:
|
||||
rule_id = self.context.get('rule_id')
|
||||
if rule_id:
|
||||
name_str = ' %s' % rule_id
|
||||
return message % {'rule_name': name_str,
|
||||
'error': self.context.get('error', '')}
|
||||
|
||||
def _get_schema_columns(self, request, table):
|
||||
table_parts = table.split(congress.TABLE_SEPARATOR)
|
||||
datasource = table_parts[0]
|
||||
table_name = table_parts[1]
|
||||
try:
|
||||
schema = congress.datasource_table_schema_get_by_name(
|
||||
request, datasource, table_name)
|
||||
except Exception:
|
||||
# Maybe it's a policy table, not a service.
|
||||
try:
|
||||
schema = congress.policy_table_schema_get(
|
||||
request, datasource, table_name)
|
||||
except Exception as e:
|
||||
# Nope.
|
||||
LOG.error('Unable to get schema for table "%s", '
|
||||
'datasource "%s": %s',
|
||||
table_name, datasource, str(e))
|
||||
return str(e)
|
||||
return schema['columns']
|
||||
|
||||
def handle(self, request, data):
|
||||
policy_name = data['policy_name']
|
||||
username = request.user.username
|
||||
project_name = request.user.tenant_name
|
||||
|
||||
# Output data.
|
||||
rule_name = data.get('rule_name')
|
||||
comment = data.get('comment')
|
||||
policy_table = _underscore_slugify(data['policy_table'])
|
||||
if not data['policy_columns']:
|
||||
self.context['error'] = 'Missing policy table columns'
|
||||
return False
|
||||
policy_columns = data['policy_columns'].split(', ')
|
||||
|
||||
# Conditions data.
|
||||
if not data['mappings']:
|
||||
self.context['error'] = ('Missing data source column mappings for '
|
||||
'policy table columns')
|
||||
return False
|
||||
mapping_columns = [c.strip() for c in data['mappings'].split(', ')]
|
||||
if len(policy_columns) != len(mapping_columns):
|
||||
self.context['error'] = ('Missing data source column mappings for '
|
||||
'some policy table columns')
|
||||
return False
|
||||
# Map columns used in rule's head. Every column in the head must also
|
||||
# appear in the body.
|
||||
head_columns = [_underscore_slugify(c).strip() for c in policy_columns]
|
||||
column_variables = dict(zip(mapping_columns, head_columns))
|
||||
|
||||
# All tables needed in the body.
|
||||
body_tables = set()
|
||||
negation_tables = set()
|
||||
|
||||
# Keep track of the tables from the head that need to be in the body.
|
||||
for column in mapping_columns:
|
||||
if re.match(COLUMN_PATTERN, column) is None:
|
||||
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
||||
column)
|
||||
return False
|
||||
table = column.split()[0]
|
||||
body_tables.add(table)
|
||||
|
||||
# Make sure columns that are given a significant variable name are
|
||||
# unique names by adding name_count as a suffix.
|
||||
name_count = 0
|
||||
for param, value in request.POST.items():
|
||||
if param.startswith('join_left_') and value:
|
||||
if re.match(COLUMN_PATTERN, value) is None:
|
||||
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
||||
value)
|
||||
return False
|
||||
value = value.strip()
|
||||
|
||||
# Get operator and other column used in join.
|
||||
join_num = param.split('_')[-1]
|
||||
join_op = request.POST.get('join_op_%s' % join_num)
|
||||
other_value = request.POST.get('join_right_%s' % join_num)
|
||||
other_value = other_value.strip()
|
||||
|
||||
if join_op == '=':
|
||||
try:
|
||||
# Check if static value is a number, but keep it as a
|
||||
# string, to be used later.
|
||||
int(other_value)
|
||||
column_variables[value] = other_value
|
||||
except ValueError:
|
||||
# Pass it along as a quoted string.
|
||||
column_variables[value] = '"%s"' % other_value
|
||||
else:
|
||||
# Join between two columns.
|
||||
if not other_value:
|
||||
# Ignore incomplete pairing.
|
||||
continue
|
||||
if re.match(COLUMN_PATTERN, other_value) is None:
|
||||
self.context['error'] = ('%s: %s' %
|
||||
(COLUMN_PATTERN_ERROR,
|
||||
other_value))
|
||||
return False
|
||||
|
||||
# Tables used in the join need to be in the body.
|
||||
value_parts = value.split()
|
||||
body_tables.add(value_parts[0])
|
||||
body_tables.add(other_value.split()[0])
|
||||
|
||||
# Arbitrarily name the right column the same as the left.
|
||||
column_name = value_parts[1]
|
||||
# Use existing variable name if there is already one for
|
||||
# either column in this join.
|
||||
if other_value in column_variables:
|
||||
column_variables[value] = column_variables[other_value]
|
||||
elif value in column_variables:
|
||||
column_variables[other_value] = column_variables[value]
|
||||
else:
|
||||
variable = '%s_%s' % (column_name, name_count)
|
||||
name_count += 1
|
||||
column_variables[value] = variable
|
||||
column_variables[other_value] = variable
|
||||
|
||||
elif param.startswith('negation_value_') and value:
|
||||
if re.match(COLUMN_PATTERN, value) is None:
|
||||
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
||||
value)
|
||||
return False
|
||||
value = value.strip()
|
||||
|
||||
# Get operator and other column used in negation.
|
||||
negation_num = param.split('_')[-1]
|
||||
negation_column = request.POST.get('negation_column_%s' %
|
||||
negation_num)
|
||||
if not negation_column:
|
||||
# Ignore incomplete pairing.
|
||||
continue
|
||||
if re.match(COLUMN_PATTERN, negation_column) is None:
|
||||
self.context['error'] = '%s: %s' % (COLUMN_PATTERN_ERROR,
|
||||
negation_column)
|
||||
return False
|
||||
negation_column = negation_column.strip()
|
||||
|
||||
# Tables for columns referenced by the negation table must
|
||||
# appear in the body.
|
||||
value_parts = value.split()
|
||||
body_tables.add(value_parts[0])
|
||||
|
||||
negation_tables.add(negation_column.split()[0])
|
||||
# Use existing variable name if there is already one for either
|
||||
# column in this negation.
|
||||
if negation_column in column_variables:
|
||||
column_variables[value] = column_variables[negation_column]
|
||||
elif value in column_variables:
|
||||
column_variables[negation_column] = column_variables[value]
|
||||
else:
|
||||
# Arbitrarily name the negated table's column the same as
|
||||
# the value column.
|
||||
column_name = value_parts[1]
|
||||
variable = '%s_%s' % (column_name, name_count)
|
||||
name_count += 1
|
||||
column_variables[value] = variable
|
||||
column_variables[negation_column] = variable
|
||||
|
||||
LOG.debug('column_variables for rule: %s', column_variables)
|
||||
|
||||
# Form the literals for all the tables needed in the body. Make sure
|
||||
# column that have no relation to any other columns are given a unique
|
||||
# variable name, using column_count.
|
||||
column_count = 0
|
||||
literals = []
|
||||
for table in body_tables:
|
||||
# Replace column names with variable names that join related
|
||||
# columns together.
|
||||
columns = self._get_schema_columns(request, table)
|
||||
if isinstance(columns, six.string_types):
|
||||
self.context['error'] = columns
|
||||
return False
|
||||
|
||||
literal_columns = []
|
||||
if columns:
|
||||
for column in columns:
|
||||
table_column = '%s %s' % (table, column['name'])
|
||||
literal_columns.append(
|
||||
column_variables.get(table_column, 'col_%s' %
|
||||
column_count))
|
||||
column_count += 1
|
||||
literals.append('%s(%s)' % (table, ', '.join(literal_columns)))
|
||||
else:
|
||||
# Just the table name, such as for classification:true.
|
||||
literals.append(table)
|
||||
|
||||
# Form the negated tables.
|
||||
for table in negation_tables:
|
||||
columns = self._get_schema_columns(request, table)
|
||||
if isinstance(columns, six.string_types):
|
||||
self.context['error'] = columns
|
||||
return False
|
||||
|
||||
literal_columns = []
|
||||
num_variables = 0
|
||||
for column in columns:
|
||||
table_column = '%s %s' % (table, column['name'])
|
||||
if table_column in column_variables:
|
||||
literal_columns.append(column_variables[table_column])
|
||||
num_variables += 1
|
||||
else:
|
||||
literal_columns.append('col_%s' % column_count)
|
||||
column_count += 1
|
||||
literal = 'not %s(%s)' % (table, ', '.join(literal_columns))
|
||||
literals.append(literal)
|
||||
|
||||
# Every column in the negated table must appear in a non-negated
|
||||
# literal in the body. If there are some variables that have not
|
||||
# been used elsewhere, repeat the literal in its non-negated form.
|
||||
if num_variables != len(columns) and table not in body_tables:
|
||||
literals.append(literal.replace('not ', ''))
|
||||
|
||||
# All together now.
|
||||
rule = '%s(%s) %s %s' % (policy_table, ', '.join(head_columns),
|
||||
congress.RULE_SEPARATOR, ', '.join(literals))
|
||||
LOG.info('User %s creating policy "%s" rule "%s" in tenant %s: %s',
|
||||
username, policy_name, rule_name, project_name, rule)
|
||||
try:
|
||||
params = {
|
||||
'name': rule_name,
|
||||
'comment': comment,
|
||||
'rule': rule,
|
||||
}
|
||||
rule = congress.policy_rule_create(request, policy_name,
|
||||
body=params)
|
||||
LOG.info('Created rule %s', rule['id'])
|
||||
self.context['rule_id'] = rule['id']
|
||||
except Exception as e:
|
||||
LOG.error('Error creating policy "%s" rule "%s": %s',
|
||||
policy_name, rule_name, str(e))
|
||||
self.context['error'] = str(e)
|
||||
return False
|
||||
return True
|
94
congress_dashboard/policies/tables.py
Normal file
94
congress_dashboard/policies/tables.py
Normal file
@ -0,0 +1,94 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
from horizon import exceptions
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
from openstack_dashboard import policy
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_policy_link(datum):
|
||||
return reverse('horizon:admin:policies:detail', args=(datum['name'],))
|
||||
|
||||
|
||||
class CreatePolicy(tables.LinkAction):
|
||||
name = 'create_policy'
|
||||
verbose_name = _('Create Policy')
|
||||
url = 'horizon:admin:policies:create'
|
||||
classes = ('ajax-modal',)
|
||||
icon = 'plus'
|
||||
|
||||
|
||||
class DeletePolicy(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||
@staticmethod
|
||||
def action_present(count):
|
||||
return ungettext_lazy(
|
||||
u'Delete Policy',
|
||||
u'Delete Policies',
|
||||
count
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def action_past(count):
|
||||
return ungettext_lazy(
|
||||
u'Deleted policy',
|
||||
u'Deleted policies',
|
||||
count
|
||||
)
|
||||
|
||||
redirect_url = 'horizon:admin:policies:index'
|
||||
|
||||
def delete(self, request, obj_id):
|
||||
LOG.info('User %s deleting policy "%s" in tenant %s',
|
||||
request.user.username, obj_id, request.user.tenant_name)
|
||||
try:
|
||||
congress.policy_delete(request, obj_id)
|
||||
LOG.info('Deleted policy "%s"', obj_id)
|
||||
except Exception as e:
|
||||
msg_args = {'policy_id': obj_id, 'error': str(e)}
|
||||
msg = _('Failed to delete policy "%(policy_id)s": '
|
||||
'%(error)s') % msg_args
|
||||
LOG.error(msg)
|
||||
messages.error(request, msg)
|
||||
redirect = reverse(self.redirect_url)
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
def allowed(self, request, policy=None):
|
||||
# Only user policies can be deleted.
|
||||
if policy:
|
||||
return policy['owner_id'] == 'user'
|
||||
return True
|
||||
|
||||
|
||||
class PoliciesTable(tables.DataTable):
|
||||
name = tables.Column("name", verbose_name=_("Name"), link=get_policy_link)
|
||||
description = tables.Column("description", verbose_name=_("Description"))
|
||||
kind = tables.Column("kind", verbose_name=_("Kind"))
|
||||
owner_id = tables.Column("owner_id", verbose_name=_("Owner ID"))
|
||||
|
||||
class Meta(object):
|
||||
name = "policies"
|
||||
verbose_name = _("Policies")
|
||||
table_actions = (CreatePolicy, DeletePolicy)
|
||||
row_actions = (DeletePolicy,)
|
22
congress_dashboard/policies/templates/policies/_create.html
Normal file
22
congress_dashboard/policies/templates/policies/_create.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}create_policy_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:policies:create' %}{% endblock %}
|
||||
|
||||
{% block modal_id %}create_policy_modal{% endblock %}
|
||||
{% block modal-header %}{% trans "Create Policy" %}{% endblock %}
|
||||
|
||||
{% block modal-body %}
|
||||
<div class="left">
|
||||
<fieldset>
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Policy" %}" />
|
||||
<a href="{% url 'horizon:admin:policies:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,18 @@
|
||||
{% load i18n %}
|
||||
|
||||
<h3>{% trans "Policy Overview" %}</h3>
|
||||
|
||||
<div class="detail">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ policy.name|default:policy.id }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>{{ policy.id }}</dd>
|
||||
<dt>{% trans "Description" %}</dt>
|
||||
<dd>{{ policy.description }}</dd>
|
||||
<dt>{% trans "Kind" %}</dt>
|
||||
<dd>{{ policy.kind }}</dd>
|
||||
<dt>{% trans "Owner ID" %}</dt>
|
||||
<dd>{{ policy.owner_id|default:"-" }}</dd>
|
||||
</dl>
|
||||
</div>
|
11
congress_dashboard/policies/templates/policies/create.html
Normal file
11
congress_dashboard/policies/templates/policies/create.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Policy" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Create Policy") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/policies/_create.html" %}
|
||||
{% endblock %}
|
17
congress_dashboard/policies/templates/policies/detail.html
Normal file
17
congress_dashboard/policies/templates/policies/detail.html
Normal file
@ -0,0 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Policy Details" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Policy Details: ")|add:policy.name %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include "admin/policies/_detail_overview.html" %}
|
||||
<hr>
|
||||
<div id="policy_rules">
|
||||
{{ policy_rules_table.render }}
|
||||
</div>
|
||||
<span id="ds_tables" class="hidden">{{ tables }}</span>
|
||||
<span id="ds_columns" class="hidden">{{ columns }}</span>
|
||||
{% endblock %}
|
13
congress_dashboard/policies/templates/policies/index.html
Normal file
13
congress_dashboard/policies/templates/policies/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Policies" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Policies") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
<div id="policies">
|
||||
{{ policies_table.render }}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,174 @@
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
{{ step.get_help_text }}
|
||||
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table id="mappings_table" class="table table-condensed" data-count="{{ mappings_count }}">
|
||||
<tr>
|
||||
<th colspan="5"> <input class="hidden" id="mappings" name="mappings" type="text" value="{{ form.mappings.value }}" /></th>
|
||||
</tr>
|
||||
{% include 'admin/policies/rules/_mapping_row.html' with form=form count=0 column='' value='' %}
|
||||
{% for column, value in mappings %}{% include 'admin/policies/rules/_mapping_row.html' with form=form count=forloop.counter column=column value=value %}{% endfor %}
|
||||
<tr>
|
||||
<td colspan="5" class="borderless input-errors">
|
||||
{% for error in form.mappings.errors %}
|
||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="joins_table" class="table table-condensed">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Only including rows where:</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% for left, op, right in joins %}<tr id="join_{{ forloop.counter0 }}">
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="join_left_{{ forloop.counter0 }}" class="form-control ac-columns" name="join_left_{{ forloop.counter0 }}" type="text" value="{{ left }}" placeholder="e.g. neutronv2:ports device_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||
<div class="form-control-feedback">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless operator-cell">
|
||||
<select id="join_op_{{ forloop.counter0 }}" class="form-control join-op" name="join_op_{{ forloop.counter0 }}">
|
||||
<option value=""{% if op == '' %} selected="selected"{% endif %}>has same value as</option>
|
||||
<option value="="{% if op == '=' %} selected="selected"{% endif %}>equals</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="join_right_{{ forloop.counter0 }}" class="form-control ac-columns join-right" name="join_right_{{ forloop.counter0 }}" type="text" value="{{ right }}" placeholder="e.g. {% if op %}8, active{% else %}nova:servers id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}{% endif %}" data-column-example="e.g. nova:servers id" data-static-example="e.g. 8, active" data-pattern="{{ column_pattern }}" data-pattern-error="{{ column_pattern_error }}" />
|
||||
<div class="form-control-feedback{% if op != '' %} hidden{% endif %}">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless">
|
||||
<a class="{% if forloop.first %}hidden {% endif %}remove-join-button btn btn-xs btn-primary">–</a>
|
||||
</td>
|
||||
</tr>{% endfor %}
|
||||
</table>
|
||||
<a id="add_join_button" class="btn btn-xs btn-primary" data-count="{{ joins_count }}">&</a>
|
||||
<table id="negations_table" class="table table-condensed">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Except for rows where:</label>
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="To exclude a row in the output policy table, select the columns on the left whose values should not exist together in the columns selected on the right."><span class="fa fa-question-circle"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% for value, column in negations %}<tr id="negation_{{ forloop.counter0 }}">
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="negation_value_{{ forloop.counter0 }}" class="form-control ac-columns" name="negation_value_{{ forloop.counter0 }}" value="{{ value }}" type="text" placeholder="e.g. nova:servers tenant_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||
<div class="form-control-feedback">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless operator-cell">value is in</td>
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="negation_column_{{ forloop.counter0 }}" class="form-control ac-columns" name="negation_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. neutronv2:ports tenant_id" pattern="{{ column_pattern }}" title="{{ column_pattern_error }}" />
|
||||
<div class="form-control-feedback">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless">
|
||||
<a class="{% if forloop.first %}hidden {% endif %}remove-negation-button btn btn-xs btn-primary">–</a>
|
||||
</td>
|
||||
</tr>{% endfor %}
|
||||
</table>
|
||||
<a id="add_negation_button" class="btn btn-xs btn-primary" data-count="{{ negations_count }}">&</a>
|
||||
{% comment %}
|
||||
<table id="aliases_table" class="table table-condensed">
|
||||
<tr>
|
||||
<th colspan="5">
|
||||
<div class="form-group">
|
||||
<label class="control-label"> </label>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
{% for column, name in aliases %}<tr id="alias_{{ forloop.counter0 }}">
|
||||
<td class="borderless label-cell">
|
||||
{% if forloop.first %}<div class="form-group">
|
||||
<label class="control-label">Table Aliases:</label>
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="Give an alternate name for a table if more than one instance of it is needed above."><span class="fa fa-question-circle"></span></span>
|
||||
</div>{% endif %}
|
||||
</td>
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="alias_column_{{ forloop.counter0 }}" class="form-control ac-tables" name="alias_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. nova:servers" pattern="{{ table_pattern }}" title="{{ table_pattern_error }}" />
|
||||
<div class="form-control-feedback">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless alias-text">as</td>
|
||||
<td class="borderless input-cell">
|
||||
<input id="alias_name_{{ forloop.counter0 }}" class="form-control alias-name-input" name="alias_name_{{ forloop.counter0 }}" type="text" value="{{ name }}" placeholder="e.g. nova:servers2" />
|
||||
</td>
|
||||
<td class="borderless">
|
||||
<a class="{% if forloop.first %}hidden {% endif %}remove-alias-button btn btn-xs btn-primary">–</a>
|
||||
</td>
|
||||
</tr>{% endfor %}
|
||||
</table>
|
||||
<a id="add_alias_button" class="btn btn-xs btn-primary" data-count="{{ aliases_count }}">+</a>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* Add autocompletion. */
|
||||
$('.ac input.ac-tables').autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_tables').text())
|
||||
});
|
||||
$('.ac input.ac-columns').each(function() {
|
||||
var $input = $(this);
|
||||
var $control = $input.closest('td').find('.form-control-feedback');
|
||||
if (!$control.hasClass('hidden')) {
|
||||
$input.autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_columns').text())
|
||||
});
|
||||
}
|
||||
});
|
||||
$('.ac div.form-control-feedback').click(function() {
|
||||
var $div = $(this);
|
||||
var $input = $div.siblings('.ac-tables, .ac-columns');
|
||||
/* Focus on list now so that clicking outside of it closes it. */
|
||||
$input.autocomplete('search', '').focus();
|
||||
});
|
||||
|
||||
/* Combine mapping columns into single param. */
|
||||
$('#mappings_table').closest('form').submit(function() {
|
||||
var columns = [];
|
||||
var incomplete = false;
|
||||
$('#mappings_table').find('.mapping-column-input').not('#mapping_column_0')
|
||||
.each(function() {
|
||||
/* All values are required. */
|
||||
if (incomplete) {
|
||||
return;
|
||||
}
|
||||
var $input = $(this);
|
||||
var name = $input.val();
|
||||
if (name) {
|
||||
columns.push(name);
|
||||
} else {
|
||||
incomplete = true;
|
||||
columns = [];
|
||||
return;
|
||||
}
|
||||
});
|
||||
$('#mappings').val(columns.join(', '));
|
||||
});
|
||||
</script>
|
@ -0,0 +1,65 @@
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
{{ step.get_help_text }}
|
||||
{% include 'horizon/common/_form_errors.html' with form=form %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="form-group{% if form.rule_name.errors %} has-error{% endif %} {{ form.rule_name.css_classes }}">
|
||||
<label class="control-label{% if form.rule_name.field.required %} {{ form.required_css_class }}{% endif %}" for="rule_name">{{ form.rule_name.label }}</label>
|
||||
{% if form.rule_name.help_text %}
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.rule_name.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||
{% endif %}
|
||||
<input class="form-control" id="rule_name" maxlength="{{ form.rule_name.field.max_length }}" name="rule_name" type="text" value="{{ form.rule_name.value }}" />
|
||||
{% for error in form.rule_name.errors %}
|
||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group{% if form.comment.errors %} has-error{% endif %} {{ form.comment.css_classes }}">
|
||||
<label class="control-label{% if form.comment.field.required %} {{ form.required_css_class }}{% endif %}" for="comment">{{ form.comment.label }}</label>
|
||||
{% if form.comment.help_text %}
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.comment.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||
{% endif %}
|
||||
<textarea class="form-control" cols="40" id="comment" name="comment" rows="4">{{ form.comment.value }}</textarea>
|
||||
{% for error in form.comment.errors %}
|
||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group{% if form.policy_table.errors %} has-error{% endif %} {{ form.policy_table.css_classes }}">
|
||||
<label class="control-label{% if form.policy_table.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_table">{{ form.policy_table.label }}</label>
|
||||
{% if form.policy_table.help_text %}
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.policy_table.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||
{% endif %}
|
||||
<input class="form-control" id="policy_table" maxlength="{{ form.policy_table.field.max_length }}" name="policy_table" type="text" value="{{ form.policy_table.value }}" placeholder="e.g. error" pattern="[^0-9].*" title="Name cannot begin with a number" />
|
||||
{% for error in form.policy_table.errors %}
|
||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="form-group{% if form.policy_columns.errors %} has-error{% endif %} {{ form.policy_columns.css_classes }}">
|
||||
<label class="control-label{% if form.policy_columns.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_columns">{{ form.policy_columns.label }}</label>
|
||||
{% if form.policy_columns.help_text %}
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.policy_columns.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||
{% endif %}
|
||||
<input class="hidden" id="policy_columns" name="policy_columns" type="text" value="{{ form.policy_columns.value }}" />
|
||||
<table id="policy_columns_table" class="table table-condensed">
|
||||
{% for column in policy_columns_list %}<tr id="policy_column_{{ forloop.counter0 }}">
|
||||
<td class="borderless input-cell">
|
||||
<input class="form-control policy-column-input" name="policy_column_{{ forloop.counter0 }}" type="text" value="{{ column }}" placeholder="e.g. name" pattern="[^0-9].*" title="Name cannot begin with a number" />
|
||||
</td>
|
||||
<td class="borderless button-cell">
|
||||
<a class="{% if forloop.first %}hidden {% endif %}remove-policy-column-button btn btn-xs btn-primary">–</a>
|
||||
</td>
|
||||
</tr>{% endfor %}
|
||||
<tr>
|
||||
<td colspan="2" class="borderless input-errors">
|
||||
{% for error in form.policy_columns.errors %}
|
||||
<span class="help-block alert alert-danger {{ form.error_css_class }}">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a id="add_policy_column_button" class="btn btn-xs btn-primary" data-count="{{ policy_columns_count }}">+</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
<tr id="mapping_{{ count }}" class="{% if count == 0 %}hidden{% else %}mapping-row{% endif %}">
|
||||
<td class="borderless label-cell">
|
||||
{% if count <= 1 %}<div class="form-group{% if count == 1 and form.mappings.errors %} has-error{% endif %} {{ form.mappings.css_classes }}">
|
||||
<label class="control-label{% if form.mappings.field.required %} {{ form.required_css_class }}{% endif %}" for="policy_columns">{{ form.mappings.label }}</label>
|
||||
{% if form.mappings.help_text %}
|
||||
<span class="help-icon" data-toggle="tooltip" data-placement="top" title="{{ form.mappings.help_text|safe }}"><span class="fa fa-question-circle"></span></span>
|
||||
{% endif %}
|
||||
</div>{% endif %}
|
||||
</td>
|
||||
<td class="borderless policy-column-name">{{ column }}</td>
|
||||
<td class="borderless mapping-text">maps to</td>
|
||||
<td class="ui-front borderless input-cell">
|
||||
<div class="has-feedback ac">
|
||||
<input id="mapping_column_{{ count }}" class="form-control ac-columns mapping-column-input" name="mapping_column_{{ count }}" type="text" placeholder="e.g. nova:servers name" value="{{ value }}" />
|
||||
<div class="form-control-feedback">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="borderless"></td>
|
||||
</tr>
|
@ -0,0 +1,24 @@
|
||||
{% extends 'horizon/common/_workflow.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block modal-footer %}
|
||||
{% if workflow.wizard %}
|
||||
<div class="row">
|
||||
<div class="col-sm-1">
|
||||
<a href="{% url 'horizon:admin:policies:detail' policy_name %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 back">
|
||||
<button type="button" class="btn btn-default button-previous">« {% trans "Back" %}</button>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 next">
|
||||
<button type="button" class="btn btn-primary button-next">{% trans "Next" %} »</button>
|
||||
<button type="submit" class="btn btn-primary button-final">{{ workflow.finalize_button_name }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{{ workflow.finalize_button_name }}" />
|
||||
{% if modal %}<a class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
32
congress_dashboard/policies/urls.py
Normal file
32
congress_dashboard/policies/urls.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from congress_dashboard.policies.rules import views as rule_views
|
||||
from congress_dashboard.policies import views
|
||||
|
||||
|
||||
POLICY = r'^(?P<policy_name>[^/]+)/%s$'
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(POLICY % 'detail', views.DetailView.as_view(), name='detail'),
|
||||
url(POLICY % 'rules/create',
|
||||
rule_views.CreateView.as_view(), name='create_rule'),
|
||||
)
|
133
congress_dashboard/policies/views.py
Normal file
133
congress_dashboard/policies/views.py
Normal file
@ -0,0 +1,133 @@
|
||||
# Copyright 2014 VMware.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.template.defaultfilters import dictsort
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
from horizon import tables
|
||||
|
||||
from congress_dashboard.api import congress
|
||||
import congress_dashboard.datasources.utils as ds_utils
|
||||
from congress_dashboard.policies import forms as policies_forms
|
||||
from congress_dashboard.policies.rules import tables as rules_tables
|
||||
from congress_dashboard.policies import tables as policies_tables
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
"""List policies."""
|
||||
table_class = policies_tables.PoliciesTable
|
||||
template_name = 'admin/policies/index.html'
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
policies = congress.policies_list(self.request)
|
||||
except Exception as e:
|
||||
msg = _('Unable to get policies list: %s') % str(e)
|
||||
LOG.error(msg)
|
||||
messages.error(self.request, msg)
|
||||
return []
|
||||
return policies
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = policies_forms.CreatePolicy
|
||||
template_name = 'admin/policies/create.html'
|
||||
success_url = reverse_lazy('horizon:admin:policies:index')
|
||||
|
||||
|
||||
class DetailView(tables.DataTableView):
|
||||
"""List details about and rules in a policy."""
|
||||
table_class = rules_tables.PolicyRulesTable
|
||||
template_name = 'admin/policies/detail.html'
|
||||
|
||||
def get_data(self):
|
||||
policy_name = self.kwargs['policy_name']
|
||||
try:
|
||||
policy_rules = congress.policy_rules_list(self.request,
|
||||
policy_name)
|
||||
except Exception as e:
|
||||
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||
msg = _('Unable to get rules in policy "%(policy_name)s": '
|
||||
'%(error)s') % msg_args
|
||||
LOG.error(msg)
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse('horizon:admin:policies:index')
|
||||
raise exceptions.Http302(redirect)
|
||||
|
||||
for r in policy_rules:
|
||||
r.set_id_as_name_if_empty()
|
||||
return policy_rules
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
policy_name = kwargs['policy_name']
|
||||
try:
|
||||
policy = congress.policy_get(self.request, policy_name)
|
||||
except Exception as e:
|
||||
msg_args = {'policy_name': policy_name, 'error': str(e)}
|
||||
msg = _('Unable to get policy "%(policy_name)s": '
|
||||
'%(error)s') % msg_args
|
||||
LOG.error(msg)
|
||||
messages.error(self.request, msg)
|
||||
redirect = reverse('horizon:admin:policies:index')
|
||||
raise exceptions.Http302(redirect)
|
||||
context['policy'] = policy
|
||||
|
||||
# Alphabetize and convert list of data source tables and columns into
|
||||
# JSON formatted string consumable by JavaScript. Do this here instead
|
||||
# of in the Create Rule form so that the tables and columns lists
|
||||
# appear in the HTML document before the JavaScript that uses them.
|
||||
all_tables = ds_utils.get_datasource_tables(self.request)
|
||||
sorted_datasources = dictsort(all_tables, 'datasource')
|
||||
tables = []
|
||||
for ds in sorted_datasources:
|
||||
datasource_tables = ds['tables']
|
||||
datasource_tables.sort()
|
||||
for table in ds['tables']:
|
||||
tables.append('%s%s%s' % (ds['datasource'],
|
||||
congress.TABLE_SEPARATOR, table))
|
||||
context['tables'] = json.dumps(tables)
|
||||
|
||||
datasource_columns = ds_utils.get_datasource_columns(self.request)
|
||||
sorted_datasources = dictsort(datasource_columns, 'datasource')
|
||||
columns = []
|
||||
for ds in sorted_datasources:
|
||||
sorted_tables = dictsort(ds['tables'], 'table')
|
||||
for tbl in sorted_tables:
|
||||
# Ignore service-derived tables, which are already included.
|
||||
if congress.TABLE_SEPARATOR in tbl['table']:
|
||||
continue
|
||||
table_columns = tbl['columns']
|
||||
if table_columns:
|
||||
table_columns.sort()
|
||||
else:
|
||||
# Placeholder name for column when the table has none.
|
||||
table_columns = ['_']
|
||||
|
||||
for column in table_columns:
|
||||
columns.append('%s%s%s %s' % (ds['datasource'],
|
||||
congress.TABLE_SEPARATOR,
|
||||
tbl['table'], column))
|
||||
context['columns'] = json.dumps(columns)
|
||||
return context
|
134
congress_dashboard/static/admin/css/policies.css
Normal file
134
congress_dashboard/static/admin/css/policies.css
Normal file
@ -0,0 +1,134 @@
|
||||
/* tables */
|
||||
#policy_columns_table {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#policy_columns_table td.input-cell {
|
||||
width: 94%;
|
||||
padding-left: 0;
|
||||
}
|
||||
#policy_columns_table td.button-cell {
|
||||
padding: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#policy_columns_table td.input-errors,
|
||||
#mappings_table td.input-errors {
|
||||
padding: 0;
|
||||
}
|
||||
#policy_columns_table td.borderless,
|
||||
#mappings_table td.borderless,
|
||||
#joins_table td.borderless,
|
||||
#negations_table td.borderless,
|
||||
#aliases_table td.borderless {
|
||||
border: none;
|
||||
}
|
||||
#mappings_table td.input-cell,
|
||||
#joins_table td.input-cell,
|
||||
#negations_table td.input-cell,
|
||||
#aliases_table td.input-cell {
|
||||
width: 36%;
|
||||
}
|
||||
|
||||
#mappings_table td.label-cell {
|
||||
width: 22%;
|
||||
}
|
||||
#mappings_table td.policy-column-name {
|
||||
width: 28%;
|
||||
text-align: right;
|
||||
font-style: italic;
|
||||
}
|
||||
#mappings_table td.mapping-text {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#joins_table,
|
||||
#negations_table,
|
||||
#aliases_table {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#joins_table td.operator-cell,
|
||||
#negations_table td.operator-cell {
|
||||
width: 24%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aliases_table td.label-cell {
|
||||
width: 19%;
|
||||
}
|
||||
#aliases_table td.alias-text {
|
||||
width: 5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* forms */
|
||||
#mappings_table div.form-group,
|
||||
#joins_table div.form-group,
|
||||
#negations_table div.form-group,
|
||||
#aliases_table div.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#mappings_table input.form-control,
|
||||
#joins_table input.form-control,
|
||||
#negations_table input.form-control,
|
||||
#aliases_table input.form-control {
|
||||
padding-right: 36px;
|
||||
}
|
||||
#mappings_table div.form-control-feedback,
|
||||
#joins_table div.form-control-feedback,
|
||||
#negations_table div.form-control-feedback,
|
||||
#aliases_table div.form-control-feedback {
|
||||
background-color: #DDDDDD;
|
||||
width: 20px;
|
||||
height: 24px;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
}
|
||||
#mappings_table span.caret,
|
||||
#joins_table span.caret,
|
||||
#negations_table span.caret,
|
||||
#aliases_table span.caret {
|
||||
border-width: 5px;
|
||||
color: #333333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
#add_join_button,
|
||||
#add_negation_button,
|
||||
#add_alias_button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* autocompletion */
|
||||
.ui-autocomplete {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
/* prevent horizontal scrollbar */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* IE 6 doesn't support max-height
|
||||
* we use height instead, but this forces the menu to always be this tall
|
||||
*/
|
||||
* html .ui-autocomplete {
|
||||
height: 200px;
|
||||
}
|
||||
.ui-widget {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
.ui-state-hover,
|
||||
.ui-widget-content .ui-state-hover,
|
||||
.ui-widget-header .ui-state-hover,
|
||||
.ui-state-focus,
|
||||
.ui-widget-content .ui-state-focus,
|
||||
.ui-widget-header .ui-state-focus,
|
||||
.ui-state-active,
|
||||
.ui-widget-content .ui-state-active,
|
||||
.ui-widget-header .ui-state-active {
|
||||
border: 1px solid #285e8e;
|
||||
background: none;
|
||||
background-color: #3276b1;
|
||||
font-weight: normal;
|
||||
color: #ffffff;
|
||||
}
|
288
congress_dashboard/static/admin/js/policies.js
Normal file
288
congress_dashboard/static/admin/js/policies.js
Normal file
@ -0,0 +1,288 @@
|
||||
horizon.policies = {
|
||||
/* Update input attributes for column name autocompletion. */
|
||||
updateColumnAcInput: function($input) {
|
||||
$input.attr({
|
||||
'placeholder': $input.attr('data-column-example'),
|
||||
'pattern': $input.attr('data-pattern'),
|
||||
'title': $input.attr('data-pattern-error')
|
||||
});
|
||||
/* form-control-feedback only hidden, so it still has autocompletion. */
|
||||
$input.closest('td').find('.form-control-feedback')
|
||||
.removeClass('hidden');
|
||||
},
|
||||
|
||||
/* Get column names from conditions mappings. */
|
||||
getMappedColumns: function() {
|
||||
var mappings = [];
|
||||
$('#mappings_table').find('.policy-column-name').each(function() {
|
||||
var $td = $(this);
|
||||
var column = $td.text();
|
||||
if (column) {
|
||||
mappings.push(column);
|
||||
}
|
||||
});
|
||||
return mappings;
|
||||
},
|
||||
|
||||
/* Check if any columns need to be removed from conditions mappings. */
|
||||
scrubMappedColumns: function(columns) {
|
||||
mappings = horizon.policies.getMappedColumns();
|
||||
if (!columns) {
|
||||
columns = [];
|
||||
var $inputs = $('#policy_columns_table').find('.policy-column-input');
|
||||
$inputs.each(function() {
|
||||
var $input = $(this);
|
||||
var name = $input.val();
|
||||
if (name) {
|
||||
columns.push(name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (var i = 0; i < mappings.length; i++) {
|
||||
var name = mappings[i];
|
||||
if ($.inArray(name, columns) == -1) {
|
||||
$('#mappings_table').find('.policy-column-name:contains(' +
|
||||
name + ')').closest('tr').remove();
|
||||
}
|
||||
}
|
||||
/* Put label back if there's only one row left without it. */
|
||||
var $rows = $('#mappings_table').find('.mapping-row');
|
||||
if ($rows.length == 1 && !$rows.find('.label-cell').text()) {
|
||||
var label = $('#mapping_0').find('.label-cell').html();
|
||||
$rows.find('.label-cell').html(label);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
horizon.addInitFunction(horizon.policies.init = function() {
|
||||
/* Add another policy table column name. */
|
||||
$(document).on('click', '#add_policy_column_button', function(evt) {
|
||||
evt.preventDefault();
|
||||
var $button = $(this);
|
||||
var $tr = $('#policy_column_0').clone();
|
||||
|
||||
var count = $button.attr('data-count');
|
||||
var cid = parseInt(count);
|
||||
$button.attr('data-count', cid + 1);
|
||||
|
||||
/* Change ids and reset inputs. */
|
||||
$tr.attr('id', 'policy_column_' + cid);
|
||||
$tr.find('input[name]').val('').each(function() {
|
||||
this.name = this.name.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||
});
|
||||
$tr.find('.remove-policy-column-button').removeClass('hidden');
|
||||
/* Add row before the one reserved for errors. */
|
||||
$('#policy_columns_table').find('tr:last').before($tr);
|
||||
});
|
||||
|
||||
/* Remove policy table column name input. */
|
||||
$(document).on('click',
|
||||
'#policy_columns_table a.remove-policy-column-button',
|
||||
function(evt) {
|
||||
evt.preventDefault();
|
||||
var $a = $(this);
|
||||
var $tr = $a.closest('tr');
|
||||
$tr.remove();
|
||||
horizon.policies.scrubMappedColumns();
|
||||
});
|
||||
|
||||
/* Add policy table columns to conditions and combine into single param. */
|
||||
$(document).on('change',
|
||||
'#policy_columns_table input.policy-column-input',
|
||||
function() {
|
||||
var mappings = horizon.policies.getMappedColumns();
|
||||
var columns = [];
|
||||
|
||||
var $inputs = $('#policy_columns_table').find('.policy-column-input');
|
||||
$inputs.each(function() {
|
||||
var $input = $(this);
|
||||
var name = $input.val();
|
||||
/* Does not make sense to have multiple of the same column. */
|
||||
if (name && $.inArray(name, columns) == -1) {
|
||||
columns.push(name);
|
||||
|
||||
if ($.inArray(name, mappings) == -1) {
|
||||
/* Add mapping inputs for new policy column. */
|
||||
var $tr = $('#mapping_0').clone();
|
||||
var count = $('#mappings_table').attr('data-count');
|
||||
var cid = parseInt(count);
|
||||
$('#mappings_table').attr('data-count', cid + 1);
|
||||
|
||||
/* Change ids. */
|
||||
$tr.attr('id', 'mapping_' + cid).toggleClass('hidden mapping-row');
|
||||
$tr.find('.policy-column-name').text(name);
|
||||
$tr.find('input[id]').each(function() {
|
||||
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||
this.name = this.id;
|
||||
});
|
||||
/* Remove label if there's already a row with it. */
|
||||
if ($('#mappings_table').find('.mapping-row').length) {
|
||||
$tr.find('.label-cell').empty();
|
||||
}
|
||||
$('#mappings_table').find('tr:last').before($tr);
|
||||
|
||||
/* Add autocompletion. */
|
||||
$('#mapping_column_' + cid).autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_columns').text())
|
||||
});
|
||||
$('#mapping_' + cid).find('.ac div.form-control-feedback')
|
||||
.click(function() {
|
||||
/* Focus on list now so that clicking outside of it closes it. */
|
||||
$('#mapping_column_' + cid).autocomplete('search', '').focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* Workflow expects one policy_columns value. */
|
||||
$('#policy_columns').val(columns.join(', '));
|
||||
horizon.policies.scrubMappedColumns(columns);
|
||||
});
|
||||
|
||||
/* Add another join. */
|
||||
$(document).on('click', '#add_join_button', function(evt) {
|
||||
evt.preventDefault();
|
||||
var $button = $(this);
|
||||
var $tr = $('#join_0').clone();
|
||||
|
||||
var count = $button.attr('data-count');
|
||||
var cid = parseInt(count);
|
||||
$button.attr('data-count', cid + 1);
|
||||
|
||||
/* Change ids and reset inputs. */
|
||||
$tr.attr('id', 'join_' + cid);
|
||||
$tr.find('input[id], select[id]').val('').each(function() {
|
||||
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||
this.name = this.id;
|
||||
});
|
||||
$tr.find('select').val($tr.find('option:first').val());
|
||||
$tr.find('.remove-join-button').removeClass('hidden');
|
||||
$('#joins_table').append($tr);
|
||||
|
||||
/* Add autocompletion. */
|
||||
$('#join_left_' + cid + ', #join_right_' + cid).autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_columns').text())
|
||||
});
|
||||
horizon.policies.updateColumnAcInput($('#join_right_' + cid));
|
||||
$('#join_' + cid).find('.ac div.form-control-feedback').click(function() {
|
||||
var $div = $(this);
|
||||
/* Focus on list now so that clicking outside of it closes it. */
|
||||
$div.siblings('.ac-columns').autocomplete('search', '').focus();
|
||||
});
|
||||
});
|
||||
|
||||
/* Remove join input. */
|
||||
$(document).on('click', '#joins_table a.remove-join-button',
|
||||
function(evt) {
|
||||
evt.preventDefault();
|
||||
var $a = $(this);
|
||||
var $tr = $a.closest('tr');
|
||||
$tr.remove();
|
||||
});
|
||||
|
||||
/* Update input attributes based on type selected. */
|
||||
$(document).on('change', '#joins_table select.join-op', function() {
|
||||
var $select = $(this);
|
||||
var $input = $select.closest('tr').find('.join-right').val('');
|
||||
|
||||
if (!$select.val()) {
|
||||
$input.autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_columns').text())
|
||||
});
|
||||
horizon.policies.updateColumnAcInput($input);
|
||||
} else {
|
||||
$input.closest('td').find('.form-control-feedback').addClass('hidden');
|
||||
$input.autocomplete('destroy');
|
||||
$input.attr('placeholder', $input.attr('data-static-example'));
|
||||
$input.removeAttr('pattern').removeAttr('title');
|
||||
}
|
||||
});
|
||||
|
||||
/* Add another negation. */
|
||||
$(document).on('click', '#add_negation_button', function(evt) {
|
||||
evt.preventDefault();
|
||||
var $button = $(this);
|
||||
var $tr = $('#negation_0').clone();
|
||||
|
||||
var count = $button.attr('data-count');
|
||||
var cid = parseInt(count);
|
||||
$button.attr('data-count', cid + 1);
|
||||
|
||||
/* Change ids and reset inputs. */
|
||||
$tr.attr('id', 'negation_' + cid);
|
||||
$tr.find('input[id], select[id]').val('').each(function() {
|
||||
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||
this.name = this.id;
|
||||
});
|
||||
$tr.find('select').val($tr.find('option:first').val());
|
||||
$tr.find('.remove-negation-button').removeClass('hidden');
|
||||
$('#negations_table').append($tr);
|
||||
|
||||
/* Add autocompletion. */
|
||||
$('#negation_value_' + cid + ', #negation_column_' + cid).autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_columns').text())
|
||||
});
|
||||
$('#negation_' + cid).find('.ac div.form-control-feedback')
|
||||
.click(function() {
|
||||
var $div = $(this);
|
||||
/* Focus on list now so that clicking outside of it closes it. */
|
||||
$div.siblings('.ac-columns').autocomplete('search', '').focus();
|
||||
});
|
||||
});
|
||||
|
||||
/* Remove negation input. */
|
||||
$(document).on('click', '#negations_table a.remove-negation-button',
|
||||
function(evt) {
|
||||
evt.preventDefault();
|
||||
var $a = $(this);
|
||||
var $tr = $a.closest('tr');
|
||||
$tr.remove();
|
||||
});
|
||||
|
||||
/* Add another alias. */
|
||||
$(document).on('click', '#add_alias_button', function(evt) {
|
||||
evt.preventDefault();
|
||||
var $button = $(this);
|
||||
var $tr = $('#alias_0').clone();
|
||||
|
||||
var count = $button.attr('data-count');
|
||||
var cid = parseInt(count);
|
||||
$button.attr('data-count', cid + 1);
|
||||
|
||||
/* Change ids and reset inputs. */
|
||||
$tr.attr('id', 'alias_' + cid);
|
||||
$tr.find('td:first').empty();
|
||||
$tr.find('input[id]').val('').each(function() {
|
||||
this.id = this.id.replace(/^(.+_)\d+$/, '$1' + cid);
|
||||
this.name = this.id;
|
||||
});
|
||||
$tr.find('.remove-alias-button').removeClass('hidden');
|
||||
$('#aliases_table').append($tr);
|
||||
|
||||
/* Add autocompletion. */
|
||||
$('#alias_column_' + cid).autocomplete({
|
||||
minLength: 0,
|
||||
source: JSON.parse($('#ds_tables').text())
|
||||
});
|
||||
$('#alias_' + cid).find('.ac div.form-control-feedback')
|
||||
.click(function() {
|
||||
var $div = $(this);
|
||||
/* Focus on list now so that clicking outside of it closes it. */
|
||||
$div.siblings('.ac-tables').autocomplete('search', '').focus();
|
||||
});
|
||||
});
|
||||
|
||||
/* Remove alias input. */
|
||||
$(document).on('click', '#aliases_table a.remove-alias-button',
|
||||
function(evt) {
|
||||
evt.preventDefault();
|
||||
var $a = $(this);
|
||||
var $tr = $a.closest('tr');
|
||||
$tr.remove();
|
||||
});
|
||||
});
|
5
congress_dashboard/templates/admin/_scripts.html
Normal file
5
congress_dashboard/templates/admin/_scripts.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends 'horizon/_scripts.html' %}
|
||||
|
||||
{% block custom_js_files %}
|
||||
<script src='{{ STATIC_URL }}admin/js/policies.js' type='text/javascript' charset='utf-8'></script>
|
||||
{% endblock %}
|
14
congress_dashboard/templates/admin/base.html
Normal file
14
congress_dashboard/templates/admin/base.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block css %}
|
||||
{% include "_stylesheets.html" %}
|
||||
|
||||
{% load compress %}
|
||||
{% compress css %}
|
||||
<link href='{{ STATIC_URL }}admin/css/policies.css' type='text/css' media='screen' rel='stylesheet' />
|
||||
{% endcompress %}
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{% include "admin/_scripts.html" %}
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user