Add stub api calls for Mistral server integration

Store the workbooks being edited inside sqlite database of Horizon
django app. Now it's possible to:
* create a workbook;
* see it in the list of workbooks;
* edit it;
* delete it.

To use the models.py DATABASES variable in openstack_dashboard
settings needs to be set at least to sqlite3.

Change-Id: I9d4c013470e0fc13ef65484c8f6fae69cdad0a05
Implements: blueprint mistral-server-integration
This commit is contained in:
Timur Sufiev 2015-04-29 09:57:12 -07:00
parent 531dc56c64
commit d9f94958c2
8 changed files with 173 additions and 85 deletions

View File

@ -14,56 +14,37 @@
from horizon.test import utils as test_utils from horizon.test import utils as test_utils
from mistral import models
_workbooks = []
def find_max_id(): def create_workbook(request, name, yaml):
max_id = 0 wb = models.Workbook.objects.create(name=name, yaml=yaml)
for workbook in _workbooks: wb.save()
if max_id < int(workbook.id):
max_id = int(workbook.id)
return max_id
def create_workbook(request, json):
name = json['name']
for workbook in _workbooks:
if name == workbook['name']:
raise LookupError('Workbook with that name already exists!')
obj = test_utils.ObjDictWrapper(id=find_max_id()+1, **json)
_workbooks.append(obj)
return True return True
def modify_workbook(request, json): def modify_workbook(request, id, name, yaml):
id = json['id'] try:
for i, workbook in enumerate(_workbooks[:]): wb = models.Workbook.objects.get(id=id)
if unicode(id) == unicode(workbook.id): wb.name = name
_workbooks[i] = test_utils.ObjDictWrapper(**json) wb.yaml = yaml
return True wb.save()
except models.Workbook.DoesNotExist:
return False
return False return True
def remove_workbook(request, id): def remove_workbook(request, id):
for i, workbook in enumerate(_workbooks[:]): models.Workbook.objects.get(id=id).delete()
if unicode(id) == unicode(workbook.id):
del _workbooks[i]
return True
return False
def list_workbooks(request): def list_workbooks(request):
return _workbooks return models.Workbook.objects.values('id', 'name')
def get_workbook(request, id): def get_workbook(request, id):
for workbook in _workbooks: try:
if unicode(id) == unicode(workbook.id): return models.Workbook.objects.get(id=id)
return workbook.__dict__ except models.Workbook.DoesNotExist:
return None
return None

View File

@ -0,0 +1,7 @@
from django.db import models
class Workbook(models.Model):
name = models.CharField(max_length=50, unique=True)
yaml = models.TextField()

View File

@ -8,41 +8,70 @@
.value('baseActionID', 'action') .value('baseActionID', 'action')
.value('baseWorkflowID', 'workflow') .value('baseWorkflowID', 'workflow')
.controller('workbookCtrl', .controller('workbookCtrl',
['$scope', 'mistral.workbook.models', 'baseActionID', 'baseWorkflowID', ['$scope', 'mistral.workbook.models', '$http',
function($scope, models, baseActionId, baseWorkflowId) { 'baseActionID', 'baseWorkflowID',
$scope.workbook = models.Workbook.create({name: 'My Workbook'}); function($scope, models, $http, baseActionId, baseWorkflowId) {
$scope.init = function(id, yaml, commitUrl, discardUrl) {
$scope.workbookID = id;
$scope.commitUrl = commitUrl;
$scope.discardUrl = discardUrl;
if ( id !== undefined ) {
$scope.workbook = models.Workbook.create(jsyaml.safeLoad(yaml));
} else {
$scope.workbook = models.Workbook.create({name: 'My Workbook'});
}
};
function getNextIDSuffix(container, regexp) { function getNextIDSuffix(container, regexp) {
var max = Math.max.apply(Math, container.getIDs().map(function(id) { var max = Math.max.apply(Math, container.getIDs().map(function(id) {
var match = regexp.exec(id); var match = regexp.exec(id);
return match && +match[2]; return match && +match[2];
})); }));
return max > 0 ? max + 1 : 1; return max > 0 ? max + 1 : 1;
}
function getWorkbookNextIDSuffix(base) {
var containerName = base + 's',
regexp = /(workflow|action)([0-9]+)/,
container = $scope.workbook.get(containerName);
if ( !container ) {
throw 'Base should be either "action" or "workflow"!';
} }
return getNextIDSuffix(container, regexp);
}
$scope.addAction = function() { function getWorkbookNextIDSuffix(base) {
var nextSuffix = getWorkbookNextIDSuffix(baseActionId), var containerName = base + 's',
newID = baseActionId + nextSuffix; regexp = /(workflow|action)([0-9]+)/,
$scope.workbook.get('actions').push( container = $scope.workbook.get(containerName);
{name: 'Action ' + nextSuffix}, {id: newID}); if ( !container ) {
}; throw 'Base should be either "action" or "workflow"!';
}
return getNextIDSuffix(container, regexp);
}
$scope.addWorkflow = function() { $scope.addAction = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId), var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseWorkflowId + nextSuffix; newID = baseActionId + nextSuffix;
$scope.workbook.get('workflows').push( $scope.workbook.get('actions').push(
{name: 'Workflow ' + nextSuffix}, {id: newID}); {name: 'Action ' + nextSuffix}, {id: newID});
}; };
}]) $scope.addWorkflow = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId),
newID = baseWorkflowId + nextSuffix;
$scope.workbook.get('workflows').push(
{name: 'Workflow ' + nextSuffix}, {id: newID});
};
$scope.commitWorkbook = function() {
var data = {
name: $scope.workbook.get('name').get(),
yaml: $scope.workbook.toYAML()
};
$http({
url: $scope.commitUrl,
method: 'POST',
data: data
}).success(function(data, status, headers, config) {
document.location = $scope.discardUrl;
});
};
$scope.discardWorkbook = function() {
document.location = $scope.discardUrl;
};
}])
})(); })();

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.core.urlresolvers import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.template import defaultfilters from django.template import defaultfilters
from horizon import tables from horizon import tables
@ -22,10 +23,19 @@ from mistral import api
class CreateWorkbook(tables.LinkAction): class CreateWorkbook(tables.LinkAction):
name = 'create' name = 'create'
verbose_name = _('Create Workbook') verbose_name = _('Create Workbook')
url = 'horizon:project:mistral:create' url = reverse_lazy('horizon:project:mistral:edit', args=())
icon = 'plus' icon = 'plus'
class ModifyWorkbook(tables.LinkAction):
name = 'modify'
verbose_name = _('Modify Workbook')
def get_link_url(self, datum):
return reverse('horizon:project:mistral:edit',
args=(self.table.get_object_id(datum),))
class RemoveWorkbook(tables.DeleteAction): class RemoveWorkbook(tables.DeleteAction):
name = 'remove' name = 'remove'
verbose_name = _('Remove Workbook') verbose_name = _('Remove Workbook')
@ -37,9 +47,11 @@ class RemoveWorkbook(tables.DeleteAction):
class WorkbooksTable(tables.DataTable): class WorkbooksTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Workbook Name')) name = tables.Column('name', verbose_name=_('Workbook Name'))
running = tables.Column('running', verbose_name=_('Running'),
filters=(defaultfilters.yesno,)) def get_object_id(self, datum):
return datum['id']
class Meta: class Meta:
table_actions = (CreateWorkbook,) table_actions = (CreateWorkbook,)
row_actions = (ModifyWorkbook, RemoveWorkbook)
name = 'workbooks' name = 'workbooks'

View File

@ -38,7 +38,8 @@
{% block main %} {% block main %}
<h3>Create Workbook</h3> <h3>Create Workbook</h3>
<div id="create-workbook" class="fluid-container" ng-cloak ng-controller="workbookCtrl"> <div id="create-workbook" class="fluid-container" ng-cloak ng-controller="workbookCtrl"
ng-init="init({{ id|default:'undefined' }}, '{{ yaml }}', '{{ commit_url }}', '{{ discard_url }}')">
<div class="well"> <div class="well">
<div class="two-panels"> <div class="two-panels">
<div class="left-panel"> <div class="left-panel">
@ -54,8 +55,10 @@
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="btn-group btn-toggle pull-right"> <div class="btn-group btn-toggle pull-right">
<button class="btn btn-sm btn-default">Graph</button> <button ng-click="isGraphMode = true" class="btn btn-sm"
<button class="btn btn-sm btn-primary active">YAML</button> ng-class="isGraphMode ? 'active btn-primary' : 'btn-default'">Graph</button>
<button ng-click="isGraphMode = false" class="btn btn-sm"
ng-class="!isGraphMode ? 'active btn-primary' : 'btn-default'">YAML</button>
</div> </div>
</div> </div>
</div> </div>
@ -78,9 +81,12 @@
<!-- YAML Panel --> <!-- YAML Panel -->
<div class="right-panel"> <div class="right-panel">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-body"> <div class="panel-body" ng-show="!isGraphMode">
<pre>{$ workbook.toYAML() $}</pre> <pre>{$ workbook.toYAML() $}</pre>
</div> </div>
<div class="panel-body" ng-show="isGraphMode">
Here will be a fancy Graph View as soon as we implement it!
</div>
</div> </div>
</div> </div>
</div> </div>
@ -88,8 +94,10 @@
<div class="two-panels"> <div class="two-panels">
<div class="full-width"> <div class="full-width">
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-default cancel">Cancel</button> <button ng-click="discardWorkbook()" class="btn btn-default cancel">Cancel</button>
<button class="btn btn-primary">Create</button> <button ng-click="commitWorkbook()" class="btn btn-primary">
{$ workbookID ? 'Modify' : 'Create' $}
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -305,5 +305,15 @@ describe('workbook model logic', function() {
}); });
describe("'Create'/'Modify'/'Cancel' actions", function() {
it('edit causes a request to an api and a return to main page', function() {
});
it('cancel causes just a return to main page', function() {
});
});
}) })
}); });

View File

@ -19,6 +19,10 @@ from mistral import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'), url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create$', views.CreateWorkbookView.as_view(), name='create'), url(r'^edit/(?:(?P<workbook_id>[^/]+))?$',
url(r'^actions/types$', views.ActionTypesView.as_view(), name='action_types') views.EditWorkbookView.as_view(), name='edit'),
url(r'^commit/(?:/(?P<workbook_id>[^/]+))?$',
views.CommitWorkbookView.as_view(), name='commit'),
url(r'^actions/types$', views.ActionTypesView.as_view(),
name='action_types')
) )

View File

@ -14,9 +14,10 @@
import json import json
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy, reverse
from django import http from django import http
from django.views.generic import View from django.views import generic as generic_views
from horizon import messages
from horizon import tables from horizon import tables
from horizon.views import APIView from horizon.views import APIView
import yaml import yaml
@ -26,11 +27,47 @@ from mistral import forms as mistral_forms
from mistral import tables as mistral_tables from mistral import tables as mistral_tables
class CreateWorkbookView(APIView): class EditWorkbookView(APIView):
template_name = 'project/mistral/create.html' template_name = 'project/mistral/create.html'
def get_context_data(self, workbook_id=None, **kwargs):
commit_ns = 'horizon:project:mistral:commit'
if workbook_id is None:
commit_url = reverse(commit_ns, args=())
else:
commit_url = reverse(commit_ns, args=(workbook_id,))
context = {
'commit_url': commit_url,
'discard_url': reverse('horizon:project:mistral:index')
}
if workbook_id is not None:
context['id'] = workbook_id
context['yaml'] = api.get_workbook(self.request, workbook_id).yaml
return context
class ActionTypesView(View):
class CommitWorkbookView(generic_views.View):
def post(self, request, workbook_id=None, **kwargs):
def read_data():
data = json.loads(request.read())
return data['name'], data['yaml']
if workbook_id is None:
name, yaml = read_data()
api.create_workbook(request, name, yaml)
message = "The workbook {0} has been successfully created".format(
name)
else:
name, yaml = read_data()
api.modify_workbook(request, workbook_id, name, yaml)
message = "The workbook {0} has been successfully modified".format(
name)
messages.success(request, message)
return http.HttpResponseRedirect(
reverse_lazy('horizon:project:mistral:index'))
class ActionTypesView(generic_views.View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
key = request.GET.get('key') key = request.GET.get('key')
schema = { schema = {