From aa19f2c5f1429c7bd29177b2d706762939bd8d3d Mon Sep 17 00:00:00 2001 From: Akihiro Motoki Date: Wed, 9 May 2018 01:41:51 +0900 Subject: [PATCH] Django 2.0 support Co-Authored-By: Xinni Ge Change-Id: I928156149f7152128e7cfa02d1d6c4849bd0e9a4 --- .zuul.yaml | 6 + .../content/resource_types/views.py | 2 +- heat_dashboard/content/stacks/mappings.py | 3 +- heat_dashboard/content/stacks/tables.py | 12 +- heat_dashboard/content/stacks/views.py | 4 +- .../content/template_versions/views.py | 2 +- .../actions-checked-selected.template.html | 6 + .../stacks/actions/check.service.js | 150 ++++++++++++++++++ .../stacks/actions/create.service.js | 105 ++++++++++++ .../stacks/actions/delete.service.js | 149 +++++++++++++++++ .../test/tests/content/test_resource_types.py | 2 +- .../test/tests/content/test_stacks.py | 30 ++-- .../tests/content/test_template_generator.py | 2 +- .../tests/content/test_template_versions.py | 2 +- tox.ini | 6 + 15 files changed, 451 insertions(+), 30 deletions(-) create mode 100644 heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/actions-checked-selected.template.html create mode 100644 heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/check.service.js create mode 100644 heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/create.service.js create mode 100644 heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/delete.service.js diff --git a/.zuul.yaml b/.zuul.yaml index 67a39c4..58d8a82 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -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 diff --git a/heat_dashboard/content/resource_types/views.py b/heat_dashboard/content/resource_types/views.py index dc6f6bf..2b7b2ed 100644 --- a/heat_dashboard/content/resource_types/views.py +++ b/heat_dashboard/content/resource_types/views.py @@ -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 diff --git a/heat_dashboard/content/stacks/mappings.py b/heat_dashboard/content/stacks/mappings.py index c154180..838c183 100644 --- a/heat_dashboard/content/stacks/mappings.py +++ b/heat_dashboard/content/stacks/mappings.py @@ -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 diff --git a/heat_dashboard/content/stacks/tables.py b/heat_dashboard/content/stacks/tables.py index e189a54..4660a84 100644 --- a/heat_dashboard/content/stacks/tables.py +++ b/heat_dashboard/content/stacks/tables.py @@ -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): diff --git a/heat_dashboard/content/stacks/views.py b/heat_dashboard/content/stacks/views.py index 4540c8a..6d70334 100644 --- a/heat_dashboard/content/stacks/views.py +++ b/heat_dashboard/content/stacks/views.py @@ -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 diff --git a/heat_dashboard/content/template_versions/views.py b/heat_dashboard/content/template_versions/views.py index 3c16838..9039b4e 100644 --- a/heat_dashboard/content/template_versions/views.py +++ b/heat_dashboard/content/template_versions/views.py @@ -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 diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/actions-checked-selected.template.html b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/actions-checked-selected.template.html new file mode 100644 index 0000000..f3e330c --- /dev/null +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/actions-checked-selected.template.html @@ -0,0 +1,6 @@ + + + $text$ + diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/check.service.js b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/check.service.js new file mode 100644 index 0000000..bc40427 --- /dev/null +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/check.service.js @@ -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; + } + } +})(); diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/create.service.js b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/create.service.js new file mode 100644 index 0000000..f1de410 --- /dev/null +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/create.service.js @@ -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 diff --git a/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/delete.service.js b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/delete.service.js new file mode 100644 index 0000000..550feb5 --- /dev/null +++ b/heat_dashboard/static/dashboard/project/heat_dashboard/stacks/actions/delete.service.js @@ -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; + } + } +})(); diff --git a/heat_dashboard/test/tests/content/test_resource_types.py b/heat_dashboard/test/tests/content/test_resource_types.py index b73129f..963bc74 100644 --- a/heat_dashboard/test/tests/content/test_resource_types.py +++ b/heat_dashboard/test/tests/content/test_resource_types.py @@ -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 diff --git a/heat_dashboard/test/tests/content/test_stacks.py b/heat_dashboard/test/tests/content/test_stacks.py index 927bf66..a06ebc6 100644 --- a/heat_dashboard/test/tests/content/test_stacks.py +++ b/heat_dashboard/test/tests/content/test_stacks.py @@ -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 = ('') @@ -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 = ('') @@ -592,11 +591,10 @@ class StackTests(test.TestCase): input_str = ('') - 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 = ('=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