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
_workbooks = []
from mistral import models
def find_max_id():
max_id = 0
for workbook in _workbooks:
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)
def create_workbook(request, name, yaml):
wb = models.Workbook.objects.create(name=name, yaml=yaml)
wb.save()
return True
def modify_workbook(request, json):
id = json['id']
for i, workbook in enumerate(_workbooks[:]):
if unicode(id) == unicode(workbook.id):
_workbooks[i] = test_utils.ObjDictWrapper(**json)
return True
def modify_workbook(request, id, name, yaml):
try:
wb = models.Workbook.objects.get(id=id)
wb.name = name
wb.yaml = yaml
wb.save()
except models.Workbook.DoesNotExist:
return False
return False
return True
def remove_workbook(request, id):
for i, workbook in enumerate(_workbooks[:]):
if unicode(id) == unicode(workbook.id):
del _workbooks[i]
return True
return False
models.Workbook.objects.get(id=id).delete()
def list_workbooks(request):
return _workbooks
return models.Workbook.objects.values('id', 'name')
def get_workbook(request, id):
for workbook in _workbooks:
if unicode(id) == unicode(workbook.id):
return workbook.__dict__
return None
try:
return models.Workbook.objects.get(id=id)
except models.Workbook.DoesNotExist:
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('baseWorkflowID', 'workflow')
.controller('workbookCtrl',
['$scope', 'mistral.workbook.models', 'baseActionID', 'baseWorkflowID',
function($scope, models, baseActionId, baseWorkflowId) {
$scope.workbook = models.Workbook.create({name: 'My Workbook'});
['$scope', 'mistral.workbook.models', '$http',
'baseActionID', 'baseWorkflowID',
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) {
var max = Math.max.apply(Math, container.getIDs().map(function(id) {
var match = regexp.exec(id);
return match && +match[2];
}));
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"!';
function getNextIDSuffix(container, regexp) {
var max = Math.max.apply(Math, container.getIDs().map(function(id) {
var match = regexp.exec(id);
return match && +match[2];
}));
return max > 0 ? max + 1 : 1;
}
return getNextIDSuffix(container, regexp);
}
$scope.addAction = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseActionId + nextSuffix;
$scope.workbook.get('actions').push(
{name: 'Action ' + nextSuffix}, {id: newID});
};
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.addWorkflow = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseWorkflowId),
newID = baseWorkflowId + nextSuffix;
$scope.workbook.get('workflows').push(
{name: 'Workflow ' + nextSuffix}, {id: newID});
};
$scope.addAction = function() {
var nextSuffix = getWorkbookNextIDSuffix(baseActionId),
newID = baseActionId + nextSuffix;
$scope.workbook.get('actions').push(
{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
# under the License.
from django.core.urlresolvers import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from django.template import defaultfilters
from horizon import tables
@ -22,10 +23,19 @@ from mistral import api
class CreateWorkbook(tables.LinkAction):
name = 'create'
verbose_name = _('Create Workbook')
url = 'horizon:project:mistral:create'
url = reverse_lazy('horizon:project:mistral:edit', args=())
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):
name = 'remove'
verbose_name = _('Remove Workbook')
@ -37,9 +47,11 @@ class RemoveWorkbook(tables.DeleteAction):
class WorkbooksTable(tables.DataTable):
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:
table_actions = (CreateWorkbook,)
row_actions = (ModifyWorkbook, RemoveWorkbook)
name = 'workbooks'

View File

@ -38,7 +38,8 @@
{% block main %}
<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="two-panels">
<div class="left-panel">
@ -54,8 +55,10 @@
</div>
<div class="right-panel">
<div class="btn-group btn-toggle pull-right">
<button class="btn btn-sm btn-default">Graph</button>
<button class="btn btn-sm btn-primary active">YAML</button>
<button ng-click="isGraphMode = true" class="btn btn-sm"
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>
@ -78,9 +81,12 @@
<!-- YAML Panel -->
<div class="right-panel">
<div class="panel panel-default">
<div class="panel-body">
<div class="panel-body" ng-show="!isGraphMode">
<pre>{$ workbook.toYAML() $}</pre>
</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>
@ -88,8 +94,10 @@
<div class="two-panels">
<div class="full-width">
<div class="pull-right">
<button class="btn btn-default cancel">Cancel</button>
<button class="btn btn-primary">Create</button>
<button ng-click="discardWorkbook()" class="btn btn-default cancel">Cancel</button>
<button ng-click="commitWorkbook()" class="btn btn-primary">
{$ workbookID ? 'Modify' : 'Create' $}
</button>
</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('',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create$', views.CreateWorkbookView.as_view(), name='create'),
url(r'^actions/types$', views.ActionTypesView.as_view(), name='action_types')
url(r'^edit/(?:(?P<workbook_id>[^/]+))?$',
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
from django.core.urlresolvers import reverse_lazy
from django.core.urlresolvers import reverse_lazy, reverse
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.views import APIView
import yaml
@ -26,11 +27,47 @@ from mistral import forms as mistral_forms
from mistral import tables as mistral_tables
class CreateWorkbookView(APIView):
class EditWorkbookView(APIView):
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):
key = request.GET.get('key')
schema = {