Address RemovedInDjango40Warning (7)

HttpRequest.is_ajax() was marked as deprecated since Django 3.1 and will be
removed in Django 4.0 [1].

While the current implementation of is_ajax() relies on a jQuery-specific way
of signifying AJAX as noted in the Django relnotes, horizon works with this.
Thus this commit copies the existing logic of HttpRequest.is_ajax() to the
horizon repo (as horizon.utils.http.is_ajax()) and consumes it.

https: //docs.djangoproject.com/en/4.0/releases/3.1/#features-deprecated-in-3-1
Change-Id: I3def53033524985818a891a1b9d4659fad4ba2ba
This commit is contained in:
Akihiro Motoki
2022-01-31 11:45:27 +09:00
parent 00def145de
commit 7052b7f065
22 changed files with 96 additions and 37 deletions

View File

@@ -20,6 +20,7 @@ from django import http
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon.utils import http as http_utils
from horizon import views from horizon import views
@@ -59,7 +60,7 @@ class ModalBackdropMixin(object):
class ModalFormMixin(ModalBackdropMixin): class ModalFormMixin(ModalBackdropMixin):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
if not hasattr(self, "ajax_template_name"): if not hasattr(self, "ajax_template_name"):
# Transform standard template name to ajax name (leading "_") # Transform standard template name to ajax name (leading "_")
bits = list(os.path.split(self.template_name)) bits = list(os.path.split(self.template_name))
@@ -74,7 +75,7 @@ class ModalFormMixin(ModalBackdropMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
context['hide'] = True context['hide'] = True
if ADD_TO_FIELD_HEADER in self.request.META: if ADD_TO_FIELD_HEADER in self.request.META:
context['add_to_field'] = self.request.META[ADD_TO_FIELD_HEADER] context['add_to_field'] = self.request.META[ADD_TO_FIELD_HEADER]

View File

@@ -22,10 +22,12 @@ from django.contrib.messages import constants
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.safestring import SafeData from django.utils.safestring import SafeData
from horizon.utils import http as http_utils
def horizon_message_already_queued(request, message): def horizon_message_already_queued(request, message):
_message = force_str(message) _message = force_str(message)
if request.is_ajax(): if http_utils.is_ajax(request):
for tag, msg, extra in request.horizon['async_messages']: for tag, msg, extra in request.horizon['async_messages']:
if _message == msg: if _message == msg:
return True return True
@@ -39,7 +41,7 @@ def horizon_message_already_queued(request, message):
def add_message(request, level, message, extra_tags='', fail_silently=False): def add_message(request, level, message, extra_tags='', fail_silently=False):
"""Attempts to add a message to the request using the 'messages' app.""" """Attempts to add a message to the request using the 'messages' app."""
if not horizon_message_already_queued(request, message): if not horizon_message_already_queued(request, message):
if request.is_ajax(): if http_utils.is_ajax(request):
tag = constants.DEFAULT_TAGS[level] tag = constants.DEFAULT_TAGS[level]
# if message is marked as safe, pass "safe" tag as extra_tags so # if message is marked as safe, pass "safe" tag as extra_tags so
# that client can skip HTML escape for the message when rendering # that client can skip HTML escape for the message when rendering

View File

@@ -36,6 +36,7 @@ from django.utils import timezone
from horizon import exceptions from horizon import exceptions
from horizon.utils import functions as utils from horizon.utils import functions as utils
from horizon.utils import http as http_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -77,7 +78,7 @@ class HorizonMiddleware(object):
session_time = min(timeout, int(token_life.total_seconds())) session_time = min(timeout, int(token_life.total_seconds()))
request.session.set_expiry(session_time) request.session.set_expiry(session_time)
if request.is_ajax(): if http_utils.is_ajax(request):
# if the request is Ajax we do not want to proceed, as clients can # if the request is Ajax we do not want to proceed, as clients can
# 1) create pages with constant polling, which can create race # 1) create pages with constant polling, which can create race
# conditions when a page navigation occurs # conditions when a page navigation occurs
@@ -140,7 +141,7 @@ class HorizonMiddleware(object):
return shortcuts.render(request, 'not_authorized.html', return shortcuts.render(request, 'not_authorized.html',
status=403) status=403)
if request.is_ajax(): if http_utils.is_ajax(request):
response_401 = http.HttpResponse(status=401) response_401 = http.HttpResponse(status=401)
response_401['X-Horizon-Location'] = response['location'] response_401['X-Horizon-Location'] = response['location']
return response_401 return response_401
@@ -166,7 +167,7 @@ class HorizonMiddleware(object):
This is to allow ajax request to redirect url. This is to allow ajax request to redirect url.
""" """
if request.is_ajax() and hasattr(request, 'horizon'): if http_utils.is_ajax(request) and hasattr(request, 'horizon'):
queued_msgs = request.horizon['async_messages'] queued_msgs = request.horizon['async_messages']
if type(response) == http.HttpResponseRedirect: if type(response) == http.HttpResponseRedirect:
# Drop our messages back into the session as per usual so they # Drop our messages back into the session as per usual so they

View File

@@ -46,6 +46,7 @@ from horizon.tables.actions import BatchAction
from horizon.tables.actions import FilterAction from horizon.tables.actions import FilterAction
from horizon.tables.actions import LinkAction from horizon.tables.actions import LinkAction
from horizon.utils import html from horizon.utils import html
from horizon.utils import http as http_utils
from horizon.utils import settings as utils_settings from horizon.utils import settings as utils_settings
@@ -1684,7 +1685,7 @@ class DataTable(object, metaclass=DataTableMetaclass):
except Exception: except Exception:
datum = None datum = None
error = exceptions.handle(request, ignore=True) error = exceptions.handle(request, ignore=True)
if request.is_ajax(): if http_utils.is_ajax(request):
if not error: if not error:
return HttpResponse(new_row.render()) return HttpResponse(new_row.render())
return HttpResponse(status=error.status_code) return HttpResponse(status=error.status_code)
@@ -1744,7 +1745,7 @@ class DataTable(object, metaclass=DataTableMetaclass):
except Exception: except Exception:
datum = None datum = None
error = exceptions.handle(request, ignore=True) error = exceptions.handle(request, ignore=True)
if request.is_ajax(): if http_utils.is_ajax(request):
if not error: if not error:
return HttpResponse(cell.render()) return HttpResponse(cell.render())
return HttpResponse(status=error.status_code) return HttpResponse(status=error.status_code)

View File

@@ -15,6 +15,7 @@ from django import http
from horizon import exceptions from horizon import exceptions
from horizon import tables from horizon import tables
from horizon.tabs.base import TableTab from horizon.tabs.base import TableTab
from horizon.utils import http as http_utils
from horizon import views from horizon import views
@@ -60,7 +61,7 @@ class TabView(views.HorizonTemplateView):
Otherwise renders the response as normal. Otherwise renders the response as normal.
""" """
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
if tab_group.selected: if tab_group.selected:
return http.HttpResponse(tab_group.selected.render()) return http.HttpResponse(tab_group.selected.render())
return http.HttpResponse(tab_group.render()) return http.HttpResponse(tab_group.render())

28
horizon/utils/http.py Normal file
View File

@@ -0,0 +1,28 @@
# 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.
def is_ajax(request):
"""Check if the request is AJAX-based.
:param request: django.http.HttpRequest object
:return: True if the request is AJAX-based.
"""
# NOTE: Django 3.1 or later deprecates request.is_ajax() as it relied
# on a jQuery-specific way of signifying AJAX calls,
# but at the moment checking X-Requested-With header works in horizon.
# If we adopt modern frameworks with JavaScript Fetch API,
# we need to consider checking Accepts header as suggested in the
# Django 3.1 release notes.
# https://docs.djangoproject.com/en/4.0/releases/3.1/#id2
# https://docs.djangoproject.com/en/3.1/ref/request-response/#django.http.HttpRequest.is_ajax
return request.headers.get('x-requested-with') == 'XMLHttpRequest'

View File

@@ -35,6 +35,7 @@ from horizon import base
from horizon import exceptions from horizon import exceptions
from horizon.templatetags.horizon import has_permissions from horizon.templatetags.horizon import has_permissions
from horizon.utils import html from horizon.utils import html
from horizon.utils import http as http_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -904,7 +905,7 @@ class Workflow(html.HTMLElement, metaclass=WorkflowMetaclass):
"""Renders the workflow.""" """Renders the workflow."""
workflow_template = template.loader.get_template(self.template_name) workflow_template = template.loader.get_template(self.template_name)
extra_context = {"workflow": self} extra_context = {"workflow": self}
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
extra_context['modal'] = True extra_context['modal'] = True
return workflow_template.render(extra_context, self.request) return workflow_template.render(extra_context, self.request)

View File

@@ -25,6 +25,7 @@ from horizon import exceptions
from horizon.forms import views as hz_views from horizon.forms import views as hz_views
from horizon.forms.views import ADD_TO_FIELD_HEADER from horizon.forms.views import ADD_TO_FIELD_HEADER
from horizon import messages from horizon import messages
from horizon.utils import http as http_utils
class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView): class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
@@ -115,7 +116,7 @@ class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
The returned classes are determied based on The returned classes are determied based on
the workflow characteristics. the workflow characteristics.
""" """
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
layout = ['modal', ] layout = ['modal', ]
else: else:
layout = ['static_page', ] layout = ['static_page', ]
@@ -127,7 +128,7 @@ class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
def get_template_names(self): def get_template_names(self):
"""Returns the template name to use for this request.""" """Returns the template name to use for this request."""
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
template = self.ajax_template_name template = self.ajax_template_name
else: else:
template = self.template_name template = self.template_name

View File

@@ -71,6 +71,13 @@ def set_logout_reason(res, msg):
res.set_cookie('logout_reason', msg, max_age=10) res.set_cookie('logout_reason', msg, max_age=10)
def is_ajax(request):
# See horizon.utils.http.is_ajax() for more detail.
# NOTE: openstack_auth does not import modules from horizon to avoid
# import loops, so we copy the logic from horizon.utils.http.
return request.headers.get('x-requested-with') == 'XMLHttpRequest'
# TODO(stephenfin): Migrate to CBV # TODO(stephenfin): Migrate to CBV
@sensitive_post_parameters() @sensitive_post_parameters()
@csrf_protect @csrf_protect
@@ -102,7 +109,7 @@ def login(request):
url = utils.get_websso_url(request, auth_url, auth_type) url = utils.get_websso_url(request, auth_url, auth_type)
return shortcuts.redirect(url) return shortcuts.redirect(url)
if not request.is_ajax(): if not is_ajax(request):
# If the user is already authenticated, redirect them to the # If the user is already authenticated, redirect them to the
# dashboard straight away, unless the 'next' parameter is set as it # dashboard straight away, unless the 'next' parameter is set as it
# usually indicates requesting access to a page that requires different # usually indicates requesting access to a page that requires different
@@ -143,7 +150,7 @@ def login(request):
'logout_status': logout_status, 'logout_status': logout_status,
} }
if request.is_ajax(): if is_ajax(request):
template_name = 'auth/_login.html' template_name = 'auth/_login.html'
extra_context['hide'] = True extra_context['hide'] = True
else: else:

View File

@@ -21,6 +21,7 @@ from django import http
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from horizon import exceptions from horizon import exceptions
from horizon.utils import http as http_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -107,7 +108,7 @@ def ajax(authenticated=True, data_required=False,
def _wrapped(self, request, *args, **kw): def _wrapped(self, request, *args, **kw):
if authenticated and not request.user.is_authenticated: if authenticated and not request.user.is_authenticated:
return JSONResponse('not logged in', 401) return JSONResponse('not logged in', 401)
if not request.is_ajax(): if not http_utils.is_ajax(request):
return JSONResponse('request must be AJAX', 400) return JSONResponse('request must be AJAX', 400)
# decode the JSON body if present # decode the JSON body if present

View File

@@ -33,6 +33,7 @@ from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import tables from horizon import tables
from horizon import tabs from horizon import tabs
from horizon.utils import http as http_utils
from horizon.utils import memoized from horizon.utils import memoized
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
@@ -566,7 +567,7 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
else: else:
context['show_attach'] = False context['show_attach'] = False
context['volume'] = volume context['volume'] = volume
if self.request.is_ajax(): if http_utils.is_ajax(self.request):
context['hide'] = True context['hide'] = True
return context return context

View File

@@ -389,7 +389,6 @@ class TestCase(horizon_helpers.TestCase):
def mock_rest_request(**args): def mock_rest_request(**args):
mock_args = { mock_args = {
'user.is_authenticated': True, 'user.is_authenticated': True,
'is_ajax.return_value': True,
'policy.check.return_value': True, 'policy.check.return_value': True,
'body': '' 'body': ''
} }
@@ -483,6 +482,12 @@ class APITestCase(TestCase):
utils.patch_middleware_get_user() utils.patch_middleware_get_user()
class RestAPITestCase(TestCase):
def setUp(self):
super().setUp()
mock.patch('horizon.utils.http.is_ajax', return_value=True).start()
# APIMockTestCase was introduced to support mox to mock migration smoothly # APIMockTestCase was introduced to support mox to mock migration smoothly
# but it turns we have still users of APITestCase. # but it turns we have still users of APITestCase.
# We keep both for a while. # We keep both for a while.

View File

@@ -20,7 +20,7 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
class CinderRestTestCase(test.TestCase): class CinderRestTestCase(test.RestAPITestCase):
# #
# Volumes # Volumes

View File

@@ -18,7 +18,7 @@ from openstack_dashboard import api
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class ConfigRestTestCase(test.TestCase): class ConfigRestTestCase(test.RestAPITestCase):
@mock.patch.object(api.glance, 'get_image_schemas') @mock.patch.object(api.glance, 'get_image_schemas')
def test_settings_config_get(self, mock_schemas_list): def test_settings_config_get(self, mock_schemas_list):

View File

@@ -19,7 +19,7 @@ from openstack_dashboard.api.rest import glance
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class ImagesRestTestCase(test.ResetImageAPIVersionMixin, test.TestCase): class ImagesRestTestCase(test.ResetImageAPIVersionMixin, test.RestAPITestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()

View File

@@ -22,7 +22,7 @@ from openstack_dashboard.api.rest import keystone
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class KeystoneRestTestCase(test.TestCase): class KeystoneRestTestCase(test.RestAPITestCase):
# #
# Version # Version

View File

@@ -18,7 +18,7 @@ from openstack_dashboard.api.rest import network
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class RestNetworkApiSecurityGroupTests(test.TestCase): class RestNetworkApiSecurityGroupTests(test.RestAPITestCase):
@test.create_mocks({api.neutron: ['security_group_list']}) @test.create_mocks({api.neutron: ['security_group_list']})
def test_security_group_detailed(self): def test_security_group_detailed(self):
@@ -34,7 +34,7 @@ class RestNetworkApiSecurityGroupTests(test.TestCase):
self.mock_security_group_list.assert_called_once_with(request) self.mock_security_group_list.assert_called_once_with(request)
class RestNetworkApiFloatingIpTests(test.TestCase): class RestNetworkApiFloatingIpTests(test.RestAPITestCase):
@test.create_mocks({api.neutron: ['tenant_floating_ip_list']}) @test.create_mocks({api.neutron: ['tenant_floating_ip_list']})
def test_floating_ip_list(self): def test_floating_ip_list(self):

View File

@@ -24,7 +24,7 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
class NeutronNetworksTestCase(test.TestCase): class NeutronNetworksTestCase(test.RestAPITestCase):
def _dictify_network(self, network): def _dictify_network(self, network):
net_dict = network.to_dict() net_dict = network.to_dict()
@@ -109,7 +109,7 @@ class NeutronNetworksTestCase(test.TestCase):
mock_is_service_enabled.assert_called_once_with(request, 'network') mock_is_service_enabled.assert_called_once_with(request, 'network')
class NeutronSubnetsTestCase(test.TestCase): class NeutronSubnetsTestCase(test.RestAPITestCase):
@mock.patch.object(api.neutron, 'subnet_list') @mock.patch.object(api.neutron, 'subnet_list')
def test_get(self, mock_subnet_list): def test_get(self, mock_subnet_list):
@@ -141,7 +141,7 @@ class NeutronSubnetsTestCase(test.TestCase):
network_id=network_id) network_id=network_id)
class NeutronPortsTestCase(test.TestCase): class NeutronPortsTestCase(test.RestAPITestCase):
@mock.patch.object(api.neutron, 'port_list_with_trunk_types') @mock.patch.object(api.neutron, 'port_list_with_trunk_types')
def test_get(self, mock_port_list_with_trunk_types): def test_get(self, mock_port_list_with_trunk_types):
@@ -155,7 +155,7 @@ class NeutronPortsTestCase(test.TestCase):
request, network_id=network_id) request, network_id=network_id)
class NeutronTrunkTestCase(test.TestCase): class NeutronTrunkTestCase(test.RestAPITestCase):
@mock.patch.object(api.neutron, 'trunk_delete') @mock.patch.object(api.neutron, 'trunk_delete')
def test_trunk_delete(self, mock_trunk_delete): def test_trunk_delete(self, mock_trunk_delete):
@@ -189,7 +189,7 @@ class NeutronTrunkTestCase(test.TestCase):
) )
class NeutronTrunksTestCase(test.TestCase): class NeutronTrunksTestCase(test.RestAPITestCase):
@mock.patch.object(api.neutron, 'trunk_list') @mock.patch.object(api.neutron, 'trunk_list')
def test_trunks_get(self, mock_trunk_list): def test_trunks_get(self, mock_trunk_list):
@@ -216,7 +216,7 @@ class NeutronTrunksTestCase(test.TestCase):
port_id='1') port_id='1')
class NeutronExtensionsTestCase(test.TestCase): class NeutronExtensionsTestCase(test.RestAPITestCase):
@mock.patch.object(api.neutron, 'list_extensions') @mock.patch.object(api.neutron, 'list_extensions')
def test_list_extensions(self, mock_list_extensions): def test_list_extensions(self, mock_list_extensions):
@@ -228,7 +228,7 @@ class NeutronExtensionsTestCase(test.TestCase):
mock_list_extensions.assert_called_once_with(request) mock_list_extensions.assert_called_once_with(request)
class NeutronDefaultQuotasTestCase(test.TestCase): class NeutronDefaultQuotasTestCase(test.RestAPITestCase):
@test.create_mocks({api.base: ['is_service_enabled'], @test.create_mocks({api.base: ['is_service_enabled'],
api.neutron: ['tenant_quota_get']}) api.neutron: ['tenant_quota_get']})
@@ -268,7 +268,7 @@ class NeutronDefaultQuotasTestCase(test.TestCase):
mock_is_service_enabled.assert_called_once_with(request, 'network') mock_is_service_enabled.assert_called_once_with(request, 'network')
class NeutronQuotaSetsTestCase(test.TestCase): class NeutronQuotaSetsTestCase(test.RestAPITestCase):
@test.create_mocks({api.base: ['is_service_enabled'], @test.create_mocks({api.base: ['is_service_enabled'],
api.neutron: ['is_extension_supported', api.neutron: ['is_extension_supported',

View File

@@ -39,7 +39,7 @@ class FakeFlavor(object):
return {"id": self.id} return {"id": self.id}
class NovaRestTestCase(test.TestCase): class NovaRestTestCase(test.RestAPITestCase):
# #
# Snapshots # Snapshots

View File

@@ -11,6 +11,7 @@
# limitations under the License. # limitations under the License.
import json import json
from unittest import mock
from django.test.utils import override_settings from django.test.utils import override_settings
@@ -18,7 +19,7 @@ from openstack_dashboard.api.rest import policy
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class PolicyRestTestCase(test.TestCase): class PolicyRestTestCase(test.RestAPITestCase):
@override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check')
def _test_policy(self, body, expected=True): def _test_policy(self, body, expected=True):
@@ -77,6 +78,14 @@ class PolicyRestTestCase(test.TestCase):
class AdminPolicyRestTestCase(test.BaseAdminViewTests): class AdminPolicyRestTestCase(test.BaseAdminViewTests):
# NOTE: BaseAdminViewTests is used by other unit tests too,
# so mock for is_ajax() is prepared explicitly here.
# It should match horizon.test.helpers.RestAPITestCase.setUp().
def setUp(self):
super().setUp()
mock.patch('horizon.utils.http.is_ajax', return_value=True).start()
@override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check')
def test_rule_with_target(self): def test_rule_with_target(self):
body = json.dumps( body = json.dumps(

View File

@@ -18,7 +18,7 @@ from openstack_dashboard.api.rest import swift
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class SwiftRestTestCase(test.TestCase): class SwiftRestTestCase(test.RestAPITestCase):
# #
# Version # Version

View File

@@ -16,7 +16,7 @@ from openstack_dashboard.api.rest import utils
from openstack_dashboard.test import helpers as test from openstack_dashboard.test import helpers as test
class RestUtilsTestCase(test.TestCase): class RestUtilsTestCase(test.RestAPITestCase):
def test_api_success(self): def test_api_success(self):
@utils.ajax() @utils.ajax()
@@ -164,7 +164,7 @@ class RestUtilsTestCase(test.TestCase):
self.assertDictEqual({}, output_filters) self.assertDictEqual({}, output_filters)
class JSONEncoderTestCase(test.TestCase): class JSONEncoderTestCase(test.RestAPITestCase):
# NOTE(tsufiev): NaN numeric is "conventional" in a sense that the custom # NOTE(tsufiev): NaN numeric is "conventional" in a sense that the custom
# NaNJSONEncoder encoder translates it to the same token that the standard # NaNJSONEncoder encoder translates it to the same token that the standard