Add segment panel

Added segment panel and implemented list and create segment
functionality.Added test cases that actually not covering the
line of code but tested the list and create functionally.

Change-Id: I1366bfdc188f4e0d53fa46f2a6ea3790c9f295fc
This commit is contained in:
nirajsingh 2018-01-30 12:59:31 +05:30
parent 48b361a3a9
commit 42c47241e4
23 changed files with 784 additions and 0 deletions

View File

@ -0,0 +1,96 @@
# Copyright (c) 2018 NTT DATA
#
# 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 itertools
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from horizon.utils import functions as utils
from horizon.utils import memoized
from masakariclient import client as masakari_client
from masakaridashboard.handle_errors import handle_errors
@memoized.memoized
def masakariclient(request):
return masakari_client.Client(1,
username=request.user.username,
password='admin',
user_domain_name='default',
project_domain_name='default',
auth_token=request.user.token.id,
project_id=request.user.tenant_id,
auth_url=getattr(settings,
'OPENSTACK_KEYSTONE_URL'),
endpoint_type=getattr(
settings, 'OPENSTACK_ENDPOINT_TYPE',
'internalURL')
)
@handle_errors(_("Unable to retrieve list"), [])
def pagination_list(request, marker='', paginate=False):
"""Retrieve a listing of specific entity and handles pagination.
:param request: Request data
:param marker: Pagination marker for large data sets: entity id
:param sort_dirs: Sorting Directions (asc/desc). Default:asc
:param paginate: If true will perform pagination based on settings.
Default:False
"""
limit = getattr(settings, 'API_RESULT_LIMIT', 100)
page_size = utils.get_page_size(request)
if paginate:
request_size = page_size + 1
else:
request_size = limit
client = masakariclient(request)
entities_iter = client.service.segments(marker=marker, limit=request_size)
has_prev_data = has_more_data = False
if paginate:
entities = list(itertools.islice(entities_iter, request_size))
# first and middle page condition
if len(entities) > page_size:
entities.pop()
has_more_data = True
# middle page condition
if marker is not None:
has_prev_data = True
elif marker is not None:
has_prev_data = True
else:
entities = list(entities_iter)
return entities, has_more_data, has_prev_data
@handle_errors(_("Unable to retrieve segment."), [])
def segment_list(request):
"""Returns all segments."""
lst = list(masakariclient(request).service.segments())
return lst
@handle_errors(_("Unable to create segment."), [])
def segment_create(request, data):
"""Create segment."""
return masakariclient(request).service.create_segment(**data)
def get_segment(request, segment_id):
"""Returns segment by id"""
return masakariclient(request).service.get_segment(segment_id)

View File

@ -0,0 +1,31 @@
# Copyright (c) 2018 NTT DATA
#
# 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 masakaridashboard.default import panel
class MasakariDashboard(horizon.Dashboard):
slug = "masakaridashboard"
name = _("Masakari Dashboard")
panels = ('default', 'segment')
default_panel = 'default'
roles = ('admin',)
horizon.register(MasakariDashboard)
MasakariDashboard.register(panel.Default)

View File

View File

@ -0,0 +1,24 @@
# Copyright (c) 2018 NTT DATA
#
# 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
class Default(horizon.Panel):
name = _("Default")
slug = 'default'
urls = 'masakaridashboard.segment.urls'
nav = False

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block css %}
{% include "_stylesheets.html" %}
<link href='{{ STATIC_URL }}masakaridashboard/css/style.css' type='text/css' media='screen' rel='stylesheet' />
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'masakaridashboard/default/base.html' %}
{% block main %}
<div class="masakari-wrapper">
{{ table.render }}
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
# Copyright (c) 2018 NTT DATA
#
# 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 masakaridashboard import exceptions
DASHBOARD = 'masakaridashboard'
ADD_INSTALLED_APPS = ['masakaridashboard']
DEFAULT = True
ADD_EXCEPTIONS = {
'recoverable': exceptions.RECOVERABLE,
'not_found': exceptions.NOT_FOUND,
'unauthorized': exceptions.UNAUTHORIZED,
}

View File

@ -0,0 +1,19 @@
# Copyright (c) 2018 NTT DATA
#
# 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 openstack_dashboard import exceptions
NOT_FOUND = exceptions.NOT_FOUND
RECOVERABLE = exceptions.RECOVERABLE
UNAUTHORIZED = exceptions.UNAUTHORIZED

View File

@ -0,0 +1,74 @@
# Copyright (c) 2018 NTT DATA
#
# 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 functools
import inspect
import horizon.exceptions
def handle_errors(error_message, error_default=None, request_arg=None):
"""A decorator for adding default error handling to API calls.
It wraps the original method in a try-except block, with horizon's
error handling added.
Note: it should only be used on functions or methods that take request as
their argument (it has to be named "request", or ``request_arg`` has to be
provided, indicating which argument is the request).
The decorated method accepts a number of additional parameters:
:param _error_handle: whether to handle the errors in this call
:param _error_message: override the error message
:param _error_default: override the default value returned on error
:param _error_redirect: specify a redirect url for errors
:param _error_ignore: ignore known errors
"""
def decorator(func):
if request_arg is None:
_request_arg = 'request'
if _request_arg not in inspect.getargspec(func).args:
raise RuntimeError(
"The handle_errors decorator requires 'request' as "
"an argument of the function or method being decorated")
else:
_request_arg = request_arg
@functools.wraps(func)
def wrapper(*args, **kwargs):
_error_handle = kwargs.pop('_error_handle', True)
_error_message = kwargs.pop('_error_message', error_message)
_error_default = kwargs.pop('_error_default', error_default)
_error_redirect = kwargs.pop('_error_redirect', None)
_error_ignore = kwargs.pop('_error_ignore', False)
if not _error_handle:
return func(*args, **kwargs)
try:
return func(*args, **kwargs)
except Exception as e:
callargs = inspect.getcallargs(func, *args, **kwargs)
request = callargs[_request_arg]
_error_message += ': ' + str(e)
horizon.exceptions.handle(request, _error_message,
ignore=_error_ignore,
redirect=_error_redirect)
return _error_default
wrapper.wrapped = func
return wrapper
return decorator

View File

View File

@ -0,0 +1,107 @@
# Copyright (c) 2018 NTT DATA
#
# 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 six
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 masakaridashboard.api import api
class ValidateSegmentForm(forms.SelfHandlingForm):
name = forms.CharField(
label=_('Segment Name'),
widget=forms.TextInput())
recovery_method = forms.ChoiceField(
label=_('Recovery Method'),
choices=[('auto', 'auto'),
('reserved_host', 'reserved_host'),
('auto_priority', 'auto_priority'),
('rh_priority', 'rh_priority')],
widget=forms.Select(
attrs={'class': 'switchable',
'data-slug': 'recovery_method'})
)
service_type = forms.CharField(
label=_('Service Type'),
widget=forms.TextInput())
def __init__(self, *args, **kwargs):
self.next_view = kwargs.pop('next_view')
super(ValidateSegmentForm, self).__init__(*args, **kwargs)
def clean(self):
cleaned_data = super(ValidateSegmentForm, self).clean()
# validation code
name = cleaned_data.get('name')
service_type = cleaned_data.get('service_type')
if name is not None:
if len(name) < 255 and isinstance(name, six.text_type):
cleaned_data['name'] = name
else:
error_msg = _("validation failed")
self._errors['name'] = self.error_class([error_msg])
return cleaned_data
else:
error_msg = _("You must specify name")
self._errors['name'] = self.error_class([error_msg])
return cleaned_data
if service_type is not None:
if len(service_type) < 255 and isinstance(
service_type, six.text_type):
cleaned_data['service_type'] = service_type
else:
error_msg = _("validation failed")
self._errors['service_type'] = self.error_class([error_msg])
return cleaned_data
else:
error_msg = _("You must specify service_type")
self._errors['service_type'] = self.error_class([error_msg])
return cleaned_data
return cleaned_data
def handle(self, request, data):
kwargs = {'name': data['name'],
'recovery_method': data['recovery_method'],
'service_type': data['service_type']
}
request.method = 'GET'
return self.next_view.as_view()(request, **kwargs)
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
recovery_method = forms.CharField(widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
service_type = forms.CharField(widget=forms.TextInput(
attrs={'readonly': 'readonly'}))
def handle(self, request, data):
try:
api.segment_create(request, data)
msg = _('Successfully created segment.')
messages.success(request, msg)
return True
except Exception:
msg = _('Failed to create segments.')
redirect = reverse('horizon:masakaridashboard:segment:index')
exceptions.handle(request, msg, redirect=redirect)

View File

@ -0,0 +1,27 @@
# Copyright (c) 2018 NTT DATA
#
# 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 masakaridashboard import dashboard
class Segment(horizon.Panel):
name = _("Segment")
slug = 'segment'
dashboard.MasakariDashboard.register(Segment)

View File

@ -0,0 +1,47 @@
# Copyright (c) 2018 NTT DATA
#
# 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 _
from horizon import tables
class CreateSegment(tables.LinkAction):
name = "create"
verbose_name = _("Create Segment")
url = "horizon:masakaridashboard:segment:validate_segment"
classes = ("ajax-modal",)
icon = "plus"
class FailoverSegmentTable(tables.DataTable):
name = tables.Column('name', verbose_name=_("Name"))
uuid = tables.Column('uuid', verbose_name=_("UUID"))
recovery_method = tables.Column(
'recovery_method', verbose_name=_("Recovery Method"))
service_type = tables.Column(
'service_type', verbose_name=_("Service Type"))
created_at = tables.Column(
'created_at', verbose_name=_("Created At"))
def get_object_id(self, datum):
return datum.uuid
class Meta(object):
name = "failover_segment"
verbose_name = _("FailoverSegment")
table_actions = (CreateSegment,
tables.FilterAction
)

View File

@ -0,0 +1,5 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
{% endblock %}

View File

@ -0,0 +1,4 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}

View File

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Segment" %}{% endblock %}
{% block main %}
{% include 'masakaridashboard/segment/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'masakaridashboard/default/table.html' %}
{% load i18n %}
{% block title %}{% trans "Segments" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Segments") %}
{% endblock page_header %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Validate Segment" %}{% endblock %}
{% block main %}
{% include 'masakaridashboard/segment/_validate_segment.html' %}
{% endblock %}

View File

@ -0,0 +1,26 @@
# Copyright (c) 2018 NTT DATA
#
# 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 url
from masakaridashboard.segment import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^validate_segment$',
views.ValidateSegmentView.as_view(),
name='validate_segment'),
url(r'^create$', views.CreateView.as_view(), name='create'),
]

View File

@ -0,0 +1,99 @@
# Copyright (c) 2018 NTT DATA
#
# 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_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from masakaridashboard.api import api
from masakaridashboard.segment import tables as masakari_tab
from horizon import exceptions
from horizon import forms
from masakaridashboard.segment import forms as segment_forms
class IndexView(tables.DataTableView):
table_class = masakari_tab.FailoverSegmentTable
template_name = 'masakaridashboard/segment/index.html'
_more = False
_prev = False
def has_more_data(self, table):
return self._more
def has_prev_data(self, table):
return self._prev
def get_data(self):
segments = []
marker = self.request.GET.get(
masakari_tab.FailoverSegmentTable._meta.pagination_param,
None
)
if marker is not None:
segment = api.get_segment(self.request, marker)
marker = segment.id
try:
segments, self._more, self._prev = api.pagination_list(
request=self.request,
marker=marker,
paginate=True
)
except Exception:
self._prev = False
self._more = False
msg = _('Unable to retrieve segment list.')
exceptions.handle(self.request, msg)
return segments
class ValidateSegmentView(forms.ModalFormView):
template_name = 'masakaridashboard/segment/validate_segment.html'
modal_header = _("Create Segment")
form_id = "validate_segment"
form_class = segment_forms.ValidateSegmentForm
submit_label = _("Validate")
submit_url = reverse_lazy(
"horizon:masakaridashboard:segment:validate_segment")
success_url = reverse_lazy('horizon:masakaridashboard:segment:create')
page_title = _("Validate Segment")
def get_form_kwargs(self):
kwargs = super(ValidateSegmentView, self).get_form_kwargs()
kwargs['next_view'] = CreateView
return kwargs
class CreateView(forms.ModalFormView):
template_name = 'masakaridashboard/segment/create.html'
modal_header = _("Create Segment")
form_id = "create_segment"
form_class = segment_forms.CreateForm
submit_label = _("Create")
submit_url = reverse_lazy("horizon:masakaridashboard:segment:create")
success_url = reverse_lazy("horizon:masakaridashboard:segment:index")
page_title = _("Create Segment")
def get_initial(self):
initial = {}
initial['name'] = self.kwargs.get('name')
initial['recovery_method'] = self.kwargs.get('recovery_method')
initial['service_type'] = self.kwargs.get('service_type')
return initial

View File

@ -0,0 +1,51 @@
.masakari-wrapper.list{
list-style: inherit;
}
.masakari-wrapper #actions{
width:100%;
}
.masakari-wrapper #actions a.btn{
width:initial;
}
.masakari-wrapper.detail-screen .page-breadcrumb ol li{
max-width: inherit;
}
.masakari-wrapper.detail-screen .page-breadcrumb li:last-child{
display:none;
}
.masakari-wrapper .navbar-brand{
padding: 6px 10px;
}
.boolfield{
font-style: italic;
}
.boolfield i{
padding-right: .2em;
}
.boolfield i.green{
color: green;
}
.boolfield i.red{
color: red;
}
.line-space{
margin: .3em 0;
}
.line-space dd{
display:inline-block;
margin-left: 1.5em;
}

View File

@ -0,0 +1,112 @@
# Copyright (c) 2018 NTT DATA
#
# 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 openstack_dashboard.test import helpers as test
import mock
from masakariclient.sdk.ha.v1 import _proxy as proxy_obj
from masakariclient.sdk.ha.v1 import segment
from masakaridashboard.api import api
SEGMENT_LIST = [
segment.Segment(uuid='1', name='test', recovery_method='auto',
service_type='service')
]
INDEX_URL = reverse('horizon:masakaridashboard:segment:index')
CREATE_URL = reverse('horizon:masakaridashboard:segment:create')
class SegmentTest(test.TestCase):
def test_index(self):
with mock.patch('masakaridashboard.api.api.pagination_list',
return_value=SEGMENT_LIST):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'masakaridashboard/segment/index.html')
def test_create_get(self):
res = self.client.get(CREATE_URL)
self.assertTemplateUsed(res, 'masakaridashboard/segment/create.html')
def test_create_post(self):
segment = SEGMENT_LIST[0]
url = reverse('horizon:masakaridashboard:segment:validate_segment')
res = self.client.get(url)
self.assertTemplateUsed(
res,
'masakaridashboard/segment/validate_segment.html'
)
form_data = {
'name': segment.name,
'recovery_method': segment.recovery_method,
'service_type': segment.service_type
}
with mock.patch('masakaridashboard.api.api.segment_create',
return_value=segment) as mocked_create:
res = self.client.post(CREATE_URL, form_data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
mocked_create.assert_called_once_with(
mock.ANY,
form_data
)
@mock.patch.object(proxy_obj.Proxy, 'segments')
def test_segment_list(self, mock_segments):
mock_segments.return_value = SEGMENT_LIST
result = api.segment_list(self.request)
self.assertEqual(SEGMENT_LIST, result)
mock_segments.assert_called_once_with()
@mock.patch.object(proxy_obj.Proxy, 'create_segment')
def test_segment_create(self, mock_segments_create):
segment = SEGMENT_LIST[0]
mock_segments_create.return_value = segment
data = {
'name': segment.name,
'recovery_method': segment.recovery_method,
'service_type': segment.service_type
}
result = api.segment_create(self.request, data)
self.assertEqual(segment, result)
mock_segments_create.assert_called_once_with(**data)
@mock.patch.object(proxy_obj.Proxy, 'get_segment')
def test_get_segment(self, mock_get_segment):
segment = SEGMENT_LIST[0]
mock_get_segment.return_value = segment
result = api.get_segment(self.request, segment.uuid)
self.assertEqual(segment, result)
mock_get_segment.assert_called_once_with(segment.uuid)
@mock.patch.object(proxy_obj.Proxy, 'segments')
def test_pagination_list_with_paginate_false(self, mock_segments):
mock_segments.return_value = SEGMENT_LIST
result = api.pagination_list(self.request, marker='', paginate=False)
self.assertIn(SEGMENT_LIST, result)
mock_segments.assert_called_once_with(limit=100, marker='')
@mock.patch.object(proxy_obj.Proxy, 'segments')
def test_pagination_list_with_paginate_true(self, mock_segments):
mock_segments.return_value = SEGMENT_LIST
result = api.pagination_list(self.request, marker='', paginate=True)
self.assertIn(SEGMENT_LIST, result)
mock_segments.assert_called_once_with(limit=21, marker='')

View File

@ -14,3 +14,4 @@ Django>=1.8,<2.0 # BSD
django-babel>=0.5.1 # BSD
django-compressor>=2.0 # MIT
django-pyscss>=2.0.2 # BSD License (2 clause)
python-masakariclient>=3.0.1 # Apache-2.0