Drop Django <= 1.10 support
After Django 2.0 support, we no longer supports Django 1.10 or older (Actually Django 1.10 seems to work though). The current django.VERSION branches are all related to Django 1.10 or older, so we can drop all conditions. py35dj20 job is now voting. blueprint django2-support Change-Id: Iefc0ab1c62c82f2842ec7761a9b981da9351cbd2
This commit is contained in:
parent
4dd60373dc
commit
a72963bdbd
@ -54,11 +54,11 @@
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- horizon-openstack-tox-py35dj20:
|
||||
voting: false
|
||||
- horizon-openstack-tox-py35dj20
|
||||
- horizon-selenium-headless
|
||||
- horizon-dsvm-tempest-plugin
|
||||
gate:
|
||||
jobs:
|
||||
- horizon-openstack-tox-py35dj20
|
||||
- horizon-selenium-headless
|
||||
- horizon-dsvm-tempest-plugin
|
||||
|
@ -27,7 +27,6 @@ import inspect
|
||||
import logging
|
||||
import os
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.conf.urls import url
|
||||
@ -58,12 +57,7 @@ def _decorate_urlconf(urlpatterns, decorator, *args, **kwargs):
|
||||
for pattern in urlpatterns:
|
||||
if getattr(pattern, 'callback', None):
|
||||
decorated = decorator(pattern.callback, *args, **kwargs)
|
||||
if django.VERSION >= (1, 10):
|
||||
pattern.callback = decorated
|
||||
else:
|
||||
# prior to 1.10 callback was a property and we had
|
||||
# to modify the private attribute behind the property
|
||||
pattern._callback = decorated
|
||||
pattern.callback = decorated
|
||||
if getattr(pattern, 'url_patterns', []):
|
||||
_decorate_urlconf(pattern.url_patterns, decorator, *args, **kwargs)
|
||||
|
||||
|
@ -10,15 +10,11 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import django
|
||||
from django import template
|
||||
from django.template import defaultfilters
|
||||
from django.utils import safestring
|
||||
|
||||
if django.VERSION >= (1, 9):
|
||||
register = template.Library()
|
||||
else:
|
||||
register = template.base.Library()
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
|
@ -22,7 +22,6 @@ from importlib import import_module
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
@ -310,10 +309,7 @@ class HorizonTests(BaseHorizonTests):
|
||||
|
||||
self.client.logout()
|
||||
resp = self.client.get(url)
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertRedirects(resp, settings.TESTSERVER + redirect_url)
|
||||
else:
|
||||
self.assertRedirects(resp, redirect_url)
|
||||
self.assertRedirects(resp, settings.TESTSERVER + redirect_url)
|
||||
|
||||
# Set SSL settings for test server
|
||||
settings.SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL',
|
||||
|
@ -13,7 +13,6 @@
|
||||
|
||||
import uuid
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django import test
|
||||
@ -395,10 +394,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
|
||||
response = self.client.get(url, form_data)
|
||||
|
||||
if next:
|
||||
if django.VERSION >= (1, 9):
|
||||
expected_url = next
|
||||
else:
|
||||
expected_url = 'http://testserver%s' % next
|
||||
expected_url = next
|
||||
self.assertEqual(response['location'], expected_url)
|
||||
else:
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
@ -444,10 +440,7 @@ class OpenStackAuthTestsV2(OpenStackAuthTestsMixin, test.TestCase):
|
||||
response = self.client.get(url, form_data)
|
||||
|
||||
if next:
|
||||
if django.VERSION >= (1, 9):
|
||||
expected_url = next
|
||||
else:
|
||||
expected_url = 'http://testserver%s' % next
|
||||
expected_url = next
|
||||
self.assertEqual(response['location'], expected_url)
|
||||
else:
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
@ -768,10 +761,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
|
||||
response = self.client.get(url, form_data)
|
||||
|
||||
if next:
|
||||
if django.VERSION >= (1, 9):
|
||||
expected_url = next
|
||||
else:
|
||||
expected_url = 'http://testserver%s' % next
|
||||
expected_url = next
|
||||
self.assertEqual(response['location'], expected_url)
|
||||
else:
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
@ -816,10 +806,7 @@ class OpenStackAuthTestsV3(OpenStackAuthTestsMixin,
|
||||
response = self.client.get(url, form_data)
|
||||
|
||||
if next:
|
||||
if django.VERSION >= (1, 9):
|
||||
expected_url = next
|
||||
else:
|
||||
expected_url = 'http://testserver%s' % next
|
||||
expected_url = next
|
||||
self.assertEqual(response['location'], expected_url)
|
||||
else:
|
||||
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
|
||||
|
@ -17,7 +17,6 @@ import logging
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django import http
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
@ -277,14 +276,9 @@ class CreateProjectWorkflowTests(test.BaseAdminViewTests):
|
||||
res = self.client.get(reverse('horizon:identity:projects:create'))
|
||||
|
||||
self.assertTemplateUsed(res, views.WorkflowView.template_name)
|
||||
if django.VERSION >= (1, 10):
|
||||
pattern = ('<input autocomplete="off" class="form-control" '
|
||||
'id="id_subnet" min="-1" '
|
||||
'name="subnet" type="number" value="10" required/>')
|
||||
else:
|
||||
pattern = ('<input autocomplete="off" class="form-control" '
|
||||
'id="id_subnet" min="-1" '
|
||||
'name="subnet" type="number" value="10"/>')
|
||||
pattern = ('<input autocomplete="off" class="form-control" '
|
||||
'id="id_subnet" min="-1" '
|
||||
'name="subnet" type="number" value="10" required/>')
|
||||
self.assertContains(res, pattern, html=True)
|
||||
|
||||
workflow = res.context['workflow']
|
||||
|
@ -18,7 +18,6 @@
|
||||
|
||||
from socket import timeout as socket_timeout
|
||||
|
||||
import django
|
||||
from django import http
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
@ -238,19 +237,18 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
if django.VERSION >= (1, 9):
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -291,19 +289,18 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
if django.VERSION >= (1, 9):
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -347,19 +344,18 @@ class UsersViewTests(test.BaseAdminViewTests):
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
if django.VERSION >= (1, 9):
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
if api.keystone.VERSIONS.active >= 3:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), domain=domain_id).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
else:
|
||||
api.keystone.tenant_list(
|
||||
IgnoreArg(), user=None).AndReturn(
|
||||
[self.tenants.list(), False])
|
||||
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
api.keystone.role_list(IgnoreArg()).AndReturn(self.roles.list())
|
||||
api.keystone.get_default_role(IgnoreArg()) \
|
||||
.AndReturn(self.roles.first())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
|
@ -1783,18 +1783,11 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
|
||||
|
||||
# NOTE(adriant): Django 1.11 changes the checked syntax to use html5
|
||||
# "checked" rather than XHTML's "checked='checked'".
|
||||
if django.VERSION >= (1, 11):
|
||||
checked_box = (
|
||||
'<input type="checkbox" name="network" '
|
||||
'value="82288d84-e0a5-42ac-95be-e6af08727e42" '
|
||||
'id="id_network_0" checked />'
|
||||
)
|
||||
else:
|
||||
checked_box = (
|
||||
'<input type="checkbox" name="network" '
|
||||
'value="82288d84-e0a5-42ac-95be-e6af08727e42" '
|
||||
'id="id_network_0" checked="checked" />'
|
||||
)
|
||||
checked_box = (
|
||||
'<input type="checkbox" name="network" '
|
||||
'value="82288d84-e0a5-42ac-95be-e6af08727e42" '
|
||||
'id="id_network_0" checked />'
|
||||
)
|
||||
if only_one_network:
|
||||
self.assertContains(res, checked_box, html=True)
|
||||
else:
|
||||
|
@ -13,7 +13,6 @@
|
||||
# under the License.
|
||||
import copy
|
||||
|
||||
import django
|
||||
from django import http
|
||||
from django.urls import reverse
|
||||
|
||||
@ -590,14 +589,9 @@ class RouterActionTests(RouterMixin, test.TestCase):
|
||||
|
||||
self.assertTemplateUsed(res, 'project/routers/update.html')
|
||||
self.assertContains(res, 'Router Type')
|
||||
if django.VERSION >= (1, 10):
|
||||
pattern = ('<input class="form-control" id="id_mode" name="mode" '
|
||||
'readonly="readonly" type="text" value="distributed" '
|
||||
'required/>')
|
||||
else:
|
||||
pattern = ('<input class="form-control" id="id_mode" name="mode" '
|
||||
'readonly="readonly" type="text" '
|
||||
'value="distributed" />')
|
||||
pattern = ('<input class="form-control" id="id_mode" name="mode" '
|
||||
'readonly="readonly" type="text" value="distributed" '
|
||||
'required/>')
|
||||
self.assertContains(res, pattern, html=True)
|
||||
self.assertNotContains(res, 'centralized')
|
||||
|
||||
|
@ -21,7 +21,6 @@ import cgi
|
||||
from mox3.mox import IsA
|
||||
import six
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django import http
|
||||
from django.urls import reverse
|
||||
@ -478,9 +477,7 @@ class SecurityGroupsViewTests(test.TestCase):
|
||||
sec_group_list = self.security_groups.list()
|
||||
rule = self.security_group_rules.first()
|
||||
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
if django.VERSION >= (1, 9):
|
||||
for i in range(2):
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
|
||||
@ -503,13 +500,9 @@ class SecurityGroupsViewTests(test.TestCase):
|
||||
sec_group_list = self.security_groups.list()
|
||||
rule = self.security_group_rules.first()
|
||||
|
||||
for i in range(3):
|
||||
for i in range(6):
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
if django.VERSION >= (1, 9):
|
||||
for i in range(3):
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
@ -559,10 +552,7 @@ class SecurityGroupsViewTests(test.TestCase):
|
||||
icmp_rule = self.security_group_rules.list()[1]
|
||||
|
||||
# Call POST 5 times (*2 if Django >= 1.9)
|
||||
call_post = 5
|
||||
if django.VERSION >= (1, 9):
|
||||
call_post *= 2
|
||||
|
||||
call_post = 5 * 2
|
||||
for i in range(call_post):
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
@ -921,9 +911,7 @@ class SecurityGroupsViewTests(test.TestCase):
|
||||
sec_group_list = self.security_groups.list()
|
||||
rule = self.security_group_rules.first()
|
||||
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
if django.VERSION >= (1, 9):
|
||||
for i in range(2):
|
||||
api.neutron.security_group_list(
|
||||
IsA(http.HttpRequest)).AndReturn(sec_group_list)
|
||||
|
||||
|
@ -17,7 +17,6 @@ import copy
|
||||
import mock
|
||||
import six
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.forms import widgets
|
||||
from django.template.defaultfilters import slugify
|
||||
@ -616,12 +615,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.assertFormError(res, 'form', None,
|
||||
"The volume size cannot be less than the "
|
||||
"snapshot size (40GiB)")
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
else:
|
||||
self.assertEqual(2, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(1, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
|
||||
self.mock_volume_snapshot_get.assert_called_with(test.IsHttpRequest(),
|
||||
str(snapshot.id))
|
||||
@ -788,12 +783,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.assertFormError(res, 'form', None, msg)
|
||||
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
else:
|
||||
self.assertEqual(2, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(1, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
|
||||
self.assertEqual(2, self.mock_tenant_limit_usages.call_count)
|
||||
self.mock_image_get.assert_called_with(test.IsHttpRequest(),
|
||||
@ -835,14 +826,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.assertFormError(res, 'form', None,
|
||||
"The volume size cannot be less than the "
|
||||
"image minimum disk size (30GiB)")
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
else:
|
||||
self.assertEqual(2, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(1, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(1, self.mock_availability_zone_list.call_count)
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
|
||||
self.mock_image_get.assert_called_with(test.IsHttpRequest(),
|
||||
str(image.id))
|
||||
@ -904,16 +890,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
' have 20GiB of your quota available.']
|
||||
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
|
||||
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_volume_list.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
else:
|
||||
self.assertEqual(2, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(1, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(1, self.mock_volume_list.call_count)
|
||||
self.assertEqual(1, self.mock_availability_zone_list.call_count)
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_volume_list.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
|
||||
self.assertEqual(2, self.mock_tenant_limit_usages.call_count)
|
||||
self.mock_volume_snapshot_list.assert_called_with(
|
||||
@ -963,14 +943,9 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
' volumes.']
|
||||
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
|
||||
|
||||
if django.VERSION >= (1, 9):
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
else:
|
||||
self.assertEqual(2, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(1, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(1, self.mock_availability_zone_list.call_count)
|
||||
self.assertEqual(3, self.mock_volume_type_list.call_count)
|
||||
self.assertEqual(2, self.mock_volume_type_default.call_count)
|
||||
self.assertEqual(2, self.mock_availability_zone_list.call_count)
|
||||
|
||||
self.mock_volume_snapshot_list.assert_called_with(
|
||||
test.IsHttpRequest(), search_opts=SEARCH_OPTS)
|
||||
|
@ -25,7 +25,6 @@ import os
|
||||
import traceback
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.contrib.messages.storage import default_storage
|
||||
from django.core.handlers import wsgi
|
||||
@ -330,14 +329,10 @@ class TestCase(horizon_helpers.TestCase):
|
||||
Asserts that the given response issued a 302 redirect without
|
||||
processing the view which is redirected to.
|
||||
"""
|
||||
if django.VERSION >= (1, 9):
|
||||
loc = six.text_type(response._headers.get('location', None)[1])
|
||||
loc = http.urlunquote(loc)
|
||||
expected_url = http.urlunquote(expected_url)
|
||||
self.assertEqual(loc, expected_url)
|
||||
else:
|
||||
self.assertEqual(response._headers.get('location', None),
|
||||
('Location', settings.TESTSERVER + expected_url))
|
||||
loc = six.text_type(response._headers.get('location', None)[1])
|
||||
loc = http.urlunquote(loc)
|
||||
expected_url = http.urlunquote(expected_url)
|
||||
self.assertEqual(loc, expected_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def assertNoFormErrors(self, response, context_name="form"):
|
||||
|
6
releasenotes/notes/django-2.0-b37c6e91d20519fa.yaml
Normal file
6
releasenotes/notes/django-2.0-b37c6e91d20519fa.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Django 2.0 support is added as experimental.
|
||||
Support for Django 1.10 or older releases is dropped.
|
||||
Django 1.11 (LTS) is still the primary supported Django version.
|
Loading…
Reference in New Issue
Block a user