Django 2.0 support

Co-Authored-By: Xinni Ge <xinni.ge1990@gmail.com>
Change-Id: I928156149f7152128e7cfa02d1d6c4849bd0e9a4
This commit is contained in:
Akihiro Motoki 2018-05-09 01:41:51 +09:00
parent 71f627702a
commit aa19f2c5f1
15 changed files with 451 additions and 30 deletions

View File

@ -2,6 +2,12 @@
check:
jobs:
- openstack-tox-lower-constraints
- horizon-openstack-tox-py35dj20:
required-projects:
openstack/horizon
gate:
jobs:
- openstack-tox-lower-constraints
- horizon-openstack-tox-py35dj20:
required-projects:
openstack/horizon

View File

@ -13,7 +13,7 @@
import yaml
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions

View File

@ -14,10 +14,11 @@ import json
import logging
from django.conf import settings
from django.core.urlresolvers import reverse
from django.template.defaultfilters import register
from django.urls import reverse
from django.utils import html
from django.utils import safestring
import six
import six.moves.urllib.parse as urlparse

View File

@ -10,19 +10,19 @@
# License for the specific language governing permissions and limitations
# under the License.
from django.core import urlresolvers
from django import urls
from django.http import Http404
from django.template.defaultfilters import title
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from heatclient import exc
from horizon import messages
from horizon import tables
from horizon.utils import filters
from heatclient import exc
from heat_dashboard import api
from heat_dashboard.content.stacks import mappings
@ -135,7 +135,7 @@ class ChangeStackTemplate(tables.LinkAction):
icon = "pencil"
def get_link_url(self, stack):
return urlresolvers.reverse(self.url, args=[stack.id])
return urls.reverse(self.url, args=[stack.id])
class DeleteStack(tables.DeleteAction):
@ -308,8 +308,8 @@ class StacksTable(tables.DataTable):
def get_resource_url(obj):
if obj.physical_resource_id == obj.stack_id:
return None
return urlresolvers.reverse('horizon:project:stacks:resource',
args=(obj.stack_id, obj.resource_name))
return urls.reverse('horizon:project:stacks:resource',
args=(obj.stack_id, obj.resource_name))
class EventsTable(tables.DataTable):

View File

@ -15,9 +15,9 @@ from operator import attrgetter
import yaml
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
import django.views.generic

View File

@ -11,7 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions

View File

@ -0,0 +1,6 @@
<action action-classes="'btn btn-default'"
disabled="tCtrl.selected.length === 0"
item="tCtrl.selected">
<span class="fa fa-check-square"></span>
$text$
</action>

View File

@ -0,0 +1,150 @@
(function() {
'use strict';
angular
.module('horizon.dashboard.project.heat_dashboard.stacks')
.factory('horizon.dashboard.project.heat_dashboard.stacks.actions.check-stack.service', checkStackService);
checkStackService.$inject = [
'$q',
'horizon.dashboard.project.heat_dashboard.service-api.heat',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.util.i18n.gettext',
'horizon.framework.util.q.extensions',
'horizon.framework.widgets.modal.deleteModalService',
'horizon.framework.widgets.toast.service',
'horizon.dashboard.project.heat_dashboard.stacks.resourceType'
];
/*
* @ngdoc factory
* @name horizon.dashboard.project.heat_dashboard.stacks.actions.check-stack.service
*
* @Description
* Brings up the check stacks confirmation modal dialog.
* On submit, check given stacks.
* On cancel, do nothing.
*/
function checkStackService(
$q,
heat,
policy,
actionResultService,
gettext,
$qExtensions,
deleteModal,
toast,
stacksResourceType
) {
var notAllowedMessage = gettext("You are not allowed to check stacks: %s");
var service = {
allowed: allowed,
perform: perform
};
return service;
//////////////
function perform(items, newScope) {
var scope = newScope;
var context = { };
var stacks = angular.isArray(items) ? items : [items];
context.labels = labelize(stacks.length);
context.deleteEntity = checkStack;
return $qExtensions.allSettled(stacks.map(checkPermission)).then(afterCheck);
function checkPermission(stack) {
return {promise: allowed(stack), context: stack};
}
function afterCheck(result) {
var outcome = $q.reject(); // Reject the promise by default
if (result.fail.length > 0) {
toast.add('error', getMessage(notAllowedMessage, result.fail));
outcome = $q.reject(result.fail);
}
if (result.pass.length > 0) {
outcome = deleteModal.open(scope, result.pass.map(getEntity), context).then(createResult);
}
return outcome;
}
}
function allowed(stack) {
// only row actions pass in stack
// otherwise, assume it is a batch action
if (stack) {
return $q.all([
policy.ifAllowed({ rules: [['stack', 'check_stack']] }),
notDeleted(stack)
]);
} else {
return policy.ifAllowed({ rules: [['stack', 'check_stack']] });
}
}
function createResult(deleteModalResult) {
// To make the result of this action generically useful, reformat the return
// from the deleteModal into a standard form
var actionResult = actionResultService.getActionResult();
deleteModalResult.pass.forEach(function markDeleted(item) {
actionResult.deleted(stacksResourceType, getEntity(item).stack_name);
});
deleteModalResult.fail.forEach(function markFailed(item) {
actionResult.failed(stacksResourceType, getEntity(item).stack_name);
});
return actionResult.result;
}
function labelize(count) {
return {
title: ngettext(
'Confirm Check Stack',
'Confirm Check Stacks', count),
message: ngettext(
'You have selected "%s".',
'You have selected "%s".', count),
submit: ngettext(
'Check Stack',
'Check Stacks', count),
success: ngettext(
'Checked Stack: %s.',
'Checked Stacks: %s.', count),
error: ngettext(
'Unable to check Stack: %s.',
'Unable to check Stacks: %s.', count)
};
}
function notDeleted(stack) {
return $qExtensions.booleanAsPromise(stack.stack_status !== 'deleted');
}
function checkStack(stack) {
return heat.checkStack(stack, true);
}
function getMessage(message, entities) {
return interpolate(message, [entities.map(getName).join(", ")]);
}
function getName(result) {
return getEntity(result).name;
}
function getEntity(result) {
return result.context;
}
}
})();

View File

@ -0,0 +1,105 @@
/**
* (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
*
* 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.
*/
(function() {
'use strict';
angular
.module('horizon.dashboard.project.heat_dashboard.stacks')
.factory('horizon.dashboard.project.heat_dashboard.stacks.actions.create-stack.service', createStackService);
createStackService.$inject = [
'$q',
'horizon.dashboard.project.heat_dashboard.service-api.heat',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.widgets.modal.wizard-modal.service',
'horizon.dashboard.project.heat_dashboard.actions.createWorkflow',
'horizon.framework.widgets.toast.service',
'horizon.dashboard.project.heat_dashboard.stacks.resourceType'
];
/**
* @ngDoc factory
* @name horizon.dashboard.project.heat_dashboard.stacks.actions.create-stack.service
* @Description A service to open the user wizard.
*/
function createStackService(
$q,
heat,
policy,
actionResultService,
wizardModalService,
createWorkflow,
toast,
resourceType,
) {
var message = {
success: gettext('Stack %s was successfully created.')
};
var scope;
var service = {
perform: perform,
allowed: allowed
};
return service;
//////////////
function allowed() {
return policy.ifAllowed({ rules: [['stack', 'add_stack']] });
}
function perform(selected, $scope) {
scope = $scope;
return wizardModalService.modal({
workflow: createWorkflow,
submit: submit
}).result;
}
function submit(stepModels) {
var finalModel = angular.extend(
{},
stepModels.selectTemplateForm,
stepModels.stackForm);
if (finalModel.source_type === 'url') {
delete finalModel.data;
} else {
delete finalModel.template_url;
}
function onProgress(progress) {
scope.$broadcast(events.STACK_CREATE_PROGRESS, progress);
}
return glance.createStack(finalModel, onProgress).then(onCreateStack);
}
function onCreateStack(response) {
var newImage = response.data;
toast.add('success', interpolate(message.success, [newStack.name]));
return actionResultService.getActionResult()
.created(resourceType, newStack.id)
.result;
}
} // end of createService
})(); // end of IIFE

View File

@ -0,0 +1,149 @@
(function() {
'use strict';
angular
.module('horizon.dashboard.project.heat_dashboard.stacks')
.factory('horizon.dashboard.project.heat_dashboard.stacks.actions.delete-stack.service', deleteStackService);
deleteStackService.$inject = [
'$q',
'horizon.dashboard.project.heat_dashboard.service-api.heat',
'horizon.app.core.openstack-service-api.policy',
'horizon.framework.util.actions.action-result.service',
'horizon.framework.util.i18n.gettext',
'horizon.framework.util.q.extensions',
'horizon.framework.widgets.modal.deleteModalService',
'horizon.framework.widgets.toast.service',
'horizon.dashboard.project.heat_dashboard.stacks.resourceType'
];
/*
* @ngdoc factory
* @name horizon.dashboard.project.heat_dashboard.stacks.actions.delete-stack.service
*
* @Description
* Brings up the delete stacks confirmation modal dialog.
* On submit, delete given stacks.
* On cancel, do nothing.
*/
function deleteStackService(
$q,
heat,
policy,
actionResultService,
gettext,
$qExtensions,
deleteModal,
toast,
stacksResourceType
) {
var notAllowedMessage = gettext("You are not allowed to delete stacks: %s");
var service = {
allowed: allowed,
perform: perform
};
return service;
//////////////
function perform(items, newScope) {
var scope = newScope;
var context = { };
var stacks = angular.isArray(items) ? items : [items];
context.labels = labelize(stacks.length);
context.deleteEntity = deleteStack;
return $qExtensions.allSettled(stacks.map(checkPermission)).then(afterCheck);
function checkPermission(stack) {
return {promise: allowed(stack), context: stack};
}
function afterCheck(result) {
var outcome = $q.reject(); // Reject the promise by default
if (result.fail.length > 0) {
toast.add('error', getMessage(notAllowedMessage, result.fail));
outcome = $q.reject(result.fail);
}
if (result.pass.length > 0) {
outcome = deleteModal.open(scope, result.pass.map(getEntity), context).then(createResult);
}
return outcome;
}
}
function allowed(stack) {
// only row actions pass in stack
// otherwise, assume it is a batch action
if (stack) {
return $q.all([
policy.ifAllowed({ rules: [['stack', 'delete_stack']] }),
notDeleted(stack)
]);
} else {
return policy.ifAllowed({ rules: [['stack', 'delete_stack']] });
}
}
function createResult(deleteModalResult) {
// To make the result of this action generically useful, reformat the return
// from the deleteModal into a standard form
var actionResult = actionResultService.getActionResult();
deleteModalResult.pass.forEach(function markDeleted(item) {
actionResult.deleted(stacksResourceType, getEntity(item).stack_name);
});
deleteModalResult.fail.forEach(function markFailed(item) {
actionResult.failed(stacksResourceType, getEntity(item).stack_name);
});
return actionResult.result;
}
function labelize(count) {
return {
title: ngettext(
'Confirm Delete Stack',
'Confirm Delete Stacks', count),
message: ngettext(
'You have selected "%s". Deleted stack is not recoverable.',
'You have selected "%s". Deleted stacks are not recoverable.', count),
submit: ngettext(
'Delete Stack',
'Delete Stacks', count),
success: ngettext(
'Deleted Stack: %s.',
'Deleted Stacks: %s.', count),
error: ngettext(
'Unable to delete Stack: %s.',
'Unable to delete Stacks: %s.', count)
};
}
function notDeleted(stack) {
return $qExtensions.booleanAsPromise(stack.stack_status !== 'deleted');
}
function deleteStack(stack) {
return heat.deleteStack(stack, true);
}
function getMessage(message, entities) {
return interpolate(message, [entities.map(getName).join(", ")]);
}
function getName(result) {
return getEntity(result).name;
}
function getEntity(result) {
return result.context;
}
}
})();

View File

@ -11,8 +11,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.core.urlresolvers import reverse
from django import http
from django.urls import reverse
from mox3.mox import IsA

View File

@ -14,26 +14,25 @@ import json
import re
import django
from django.conf import settings
from django.core import exceptions
from django.core.urlresolvers import reverse
from django import http
from django.test.utils import override_settings
from django.utils import html
from mox3.mox import IsA
import six
from heatclient.common import template_format as hc_format
from django import http
from heat_dashboard import api
from heat_dashboard.test import helpers as test
from django.conf import settings
from django.core import exceptions
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import html
from heatclient.common import template_format as hc_format
from mox3.mox import IsA
from openstack_dashboard import api as dashboard_api
from heat_dashboard import api
from heat_dashboard.content.stacks import forms
from heat_dashboard.content.stacks import mappings
from heat_dashboard.content.stacks import tables
from heat_dashboard.test import helpers as test
INDEX_TEMPLATE = 'project/stacks/index.html'
INDEX_URL = reverse('horizon:project:stacks:index')
@ -407,7 +406,7 @@ class StackTests(test.TestCase):
self.assertTemplateUsed(res, 'project/stacks/create.html')
# ensure the fields were rendered correctly
if (1, 10) <= django.VERSION < (2, 0):
if (1, 10) <= django.VERSION < (2, 1):
pattern = ('<input class="form-control" '
'id="id___param_public_string" '
'name="__param_public_string" type="text" required/>')
@ -584,7 +583,7 @@ class StackTests(test.TestCase):
self.assertTemplateUsed(res, 'project/stacks/create.html')
# ensure the fields were rendered correctly
if (1, 10) <= django.VERSION < (2, 0):
if (1, 10) <= django.VERSION < (2, 1):
input_str = ('<input class="form-control" '
'id="id___param_param{0}" '
'name="__param_param{0}" type="{1}" required/>')
@ -592,11 +591,10 @@ class StackTests(test.TestCase):
input_str = ('<input class="form-control" '
'id="id___param_param{0}" '
'name="__param_param{0}" type="{1}"/>')
self.assertContains(res, input_str.format(3, 'text'), html=True)
self.assertContains(res, input_str.format(4, 'text'), html=True)
if (1, 11) <= django.VERSION < (2, 0):
if (1, 11) <= django.VERSION < (2, 1):
input_str_param2 = ('<input type="number" name="__param_param2" '
'autocomplete="off" '
'required class="form-control" '

View File

@ -15,8 +15,8 @@ import json
from mox3.mox import IsA
from django.core.urlresolvers import reverse
from django import http
from django.urls import reverse
from openstack_dashboard import api as dashboard_api
from heat_dashboard import api

View File

@ -11,7 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.core.urlresolvers import reverse
from django.urls import reverse
from heat_dashboard import api
from heat_dashboard.test import helpers as test

View File

@ -88,6 +88,12 @@ commands =
pip install django>=1.11,<2.0
{[unit_tests]commands}
[testenv:py35dj20]
basepython = python3.5
commands =
pip install django>=2.0,<2.1
{[unit_tests]commands}
[testenv:docs]
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/doc/requirements.txt