2015-06-18 16:38:29 +03:00
|
|
|
import re
|
|
|
|
|
Replace all usage ugettext functions with the non-u versions (#6634)
On Python 3, the ugettext functions are a simple aliases of their non-u
counterparts (the 'u' represents Python 2 unicode type). Starting with
Django 3.0, the u versions will be deprecated.
https://docs.djangoproject.com/en/dev/releases/3.0/#id2
> django.utils.translation.ugettext(), ugettext_lazy(), ugettext_noop(),
> ungettext(), and ungettext_lazy() are deprecated in favor of the
> functions that they’re aliases for:
> django.utils.translation.gettext(), gettext_lazy(), gettext_noop(),
> ngettext(), and ngettext_lazy().
2019-05-01 08:49:54 +03:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2015-06-18 16:38:29 +03:00
|
|
|
|
2014-12-17 15:41:46 +03:00
|
|
|
from rest_framework import exceptions
|
2015-06-25 23:55:51 +03:00
|
|
|
from rest_framework.compat import unicode_http_header
|
2014-12-16 18:34:19 +03:00
|
|
|
from rest_framework.reverse import _reverse
|
2014-12-17 15:41:46 +03:00
|
|
|
from rest_framework.settings import api_settings
|
2015-06-18 16:38:29 +03:00
|
|
|
from rest_framework.templatetags.rest_framework import replace_query_param
|
2015-06-25 23:55:51 +03:00
|
|
|
from rest_framework.utils.mediatypes import _MediaType
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
|
2019-04-30 18:53:44 +03:00
|
|
|
class BaseVersioning:
|
2014-12-17 15:41:46 +03:00
|
|
|
default_version = api_settings.DEFAULT_VERSION
|
|
|
|
allowed_versions = api_settings.ALLOWED_VERSIONS
|
|
|
|
version_param = api_settings.VERSION_PARAM
|
|
|
|
|
2014-12-16 18:34:19 +03:00
|
|
|
def determine_version(self, request, *args, **kwargs):
|
|
|
|
msg = '{cls}.determine_version() must be implemented.'
|
2015-01-19 18:22:38 +03:00
|
|
|
raise NotImplementedError(msg.format(
|
2014-12-16 18:34:19 +03:00
|
|
|
cls=self.__class__.__name__
|
|
|
|
))
|
|
|
|
|
|
|
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
|
|
|
return _reverse(viewname, args, kwargs, request, format, **extra)
|
|
|
|
|
2014-12-17 15:41:46 +03:00
|
|
|
def is_allowed_version(self, version):
|
|
|
|
if not self.allowed_versions:
|
|
|
|
return True
|
2016-08-10 17:19:56 +03:00
|
|
|
return ((version is not None and version == self.default_version) or
|
|
|
|
(version in self.allowed_versions))
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
|
|
|
|
class AcceptHeaderVersioning(BaseVersioning):
|
|
|
|
"""
|
|
|
|
GET /something/ HTTP/1.1
|
|
|
|
Host: example.com
|
|
|
|
Accept: application/json; version=1.0
|
|
|
|
"""
|
2015-01-07 15:46:23 +03:00
|
|
|
invalid_version_message = _('Invalid version in "Accept" header.')
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
|
|
|
media_type = _MediaType(request.accepted_media_type)
|
2014-12-16 19:37:32 +03:00
|
|
|
version = media_type.params.get(self.version_param, self.default_version)
|
2014-12-17 15:41:46 +03:00
|
|
|
version = unicode_http_header(version)
|
|
|
|
if not self.is_allowed_version(version):
|
|
|
|
raise exceptions.NotAcceptable(self.invalid_version_message)
|
|
|
|
return version
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
# We don't need to implement `reverse`, as the versioning is based
|
|
|
|
# on the `Accept` header, not on the request URL.
|
|
|
|
|
|
|
|
|
|
|
|
class URLPathVersioning(BaseVersioning):
|
|
|
|
"""
|
2014-12-16 19:37:32 +03:00
|
|
|
To the client this is the same style as `NamespaceVersioning`.
|
|
|
|
The difference is in the backend - this implementation uses
|
|
|
|
Django's URL keyword arguments to determine the version.
|
|
|
|
|
|
|
|
An example URL conf for two views that accept two different versions.
|
|
|
|
|
|
|
|
urlpatterns = [
|
2020-09-08 17:32:27 +03:00
|
|
|
re_path(r'^(?P<version>[v1|v2]+)/users/$', users_list, name='users-list'),
|
|
|
|
re_path(r'^(?P<version>[v1|v2]+)/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
|
2014-12-16 19:37:32 +03:00
|
|
|
]
|
|
|
|
|
2014-12-16 18:34:19 +03:00
|
|
|
GET /1.0/something/ HTTP/1.1
|
|
|
|
Host: example.com
|
|
|
|
Accept: application/json
|
|
|
|
"""
|
2015-01-07 15:46:23 +03:00
|
|
|
invalid_version_message = _('Invalid version in URL path.')
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
2014-12-17 15:41:46 +03:00
|
|
|
version = kwargs.get(self.version_param, self.default_version)
|
2019-01-08 14:34:54 +03:00
|
|
|
if version is None:
|
|
|
|
version = self.default_version
|
|
|
|
|
2014-12-17 15:41:46 +03:00
|
|
|
if not self.is_allowed_version(version):
|
|
|
|
raise exceptions.NotFound(self.invalid_version_message)
|
|
|
|
return version
|
2014-12-16 18:34:19 +03:00
|
|
|
|
|
|
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
|
|
|
if request.version is not None:
|
2023-05-18 06:46:40 +03:00
|
|
|
kwargs = {
|
|
|
|
self.version_param: request.version,
|
|
|
|
**(kwargs or {})
|
|
|
|
}
|
2014-12-16 18:34:19 +03:00
|
|
|
|
2019-04-30 18:53:44 +03:00
|
|
|
return super().reverse(
|
2014-12-16 19:14:08 +03:00
|
|
|
viewname, args, kwargs, request, format, **extra
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class NamespaceVersioning(BaseVersioning):
|
|
|
|
"""
|
|
|
|
To the client this is the same style as `URLPathVersioning`.
|
|
|
|
The difference is in the backend - this implementation uses
|
|
|
|
Django's URL namespaces to determine the version.
|
|
|
|
|
2016-08-08 11:32:22 +03:00
|
|
|
An example URL conf that is namespaced into two separate versions
|
2014-12-16 19:37:32 +03:00
|
|
|
|
|
|
|
# users/urls.py
|
|
|
|
urlpatterns = [
|
2020-09-08 17:32:27 +03:00
|
|
|
path('/users/', users_list, name='users-list'),
|
|
|
|
path('/users/<int:pk>/', users_detail, name='users-detail')
|
2014-12-16 19:37:32 +03:00
|
|
|
]
|
|
|
|
|
|
|
|
# urls.py
|
|
|
|
urlpatterns = [
|
2020-09-08 17:32:27 +03:00
|
|
|
path('v1/', include('users.urls', namespace='v1')),
|
|
|
|
path('v2/', include('users.urls', namespace='v2'))
|
2014-12-16 19:37:32 +03:00
|
|
|
]
|
|
|
|
|
2014-12-16 19:14:08 +03:00
|
|
|
GET /1.0/something/ HTTP/1.1
|
|
|
|
Host: example.com
|
|
|
|
Accept: application/json
|
|
|
|
"""
|
2016-06-23 18:00:11 +03:00
|
|
|
invalid_version_message = _('Invalid version in URL path. Does not match any version namespace.')
|
2014-12-16 19:14:08 +03:00
|
|
|
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
|
|
|
resolver_match = getattr(request, 'resolver_match', None)
|
2017-01-08 19:09:23 +03:00
|
|
|
if resolver_match is None or not resolver_match.namespace:
|
2014-12-16 19:14:08 +03:00
|
|
|
return self.default_version
|
2016-06-23 18:00:11 +03:00
|
|
|
|
|
|
|
# Allow for possibly nested namespaces.
|
|
|
|
possible_versions = resolver_match.namespace.split(':')
|
|
|
|
for version in possible_versions:
|
|
|
|
if self.is_allowed_version(version):
|
|
|
|
return version
|
|
|
|
raise exceptions.NotFound(self.invalid_version_message)
|
2014-12-16 19:14:08 +03:00
|
|
|
|
|
|
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
|
|
|
if request.version is not None:
|
2015-02-05 03:58:09 +03:00
|
|
|
viewname = self.get_versioned_viewname(viewname, request)
|
2019-04-30 18:53:44 +03:00
|
|
|
return super().reverse(
|
2014-12-16 19:14:08 +03:00
|
|
|
viewname, args, kwargs, request, format, **extra
|
2014-12-16 18:34:19 +03:00
|
|
|
)
|
2014-12-17 15:41:46 +03:00
|
|
|
|
2015-02-05 03:58:09 +03:00
|
|
|
def get_versioned_viewname(self, viewname, request):
|
|
|
|
return request.version + ':' + viewname
|
2015-01-29 04:08:34 +03:00
|
|
|
|
2014-12-17 15:41:46 +03:00
|
|
|
|
|
|
|
class HostNameVersioning(BaseVersioning):
|
|
|
|
"""
|
|
|
|
GET /something/ HTTP/1.1
|
|
|
|
Host: v1.example.com
|
|
|
|
Accept: application/json
|
|
|
|
"""
|
|
|
|
hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
|
2015-01-07 15:46:23 +03:00
|
|
|
invalid_version_message = _('Invalid version in hostname.')
|
2014-12-17 15:41:46 +03:00
|
|
|
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
2016-08-08 11:32:22 +03:00
|
|
|
hostname, separator, port = request.get_host().partition(':')
|
2014-12-17 15:41:46 +03:00
|
|
|
match = self.hostname_regex.match(hostname)
|
|
|
|
if not match:
|
|
|
|
return self.default_version
|
|
|
|
version = match.group(1)
|
|
|
|
if not self.is_allowed_version(version):
|
|
|
|
raise exceptions.NotFound(self.invalid_version_message)
|
|
|
|
return version
|
|
|
|
|
|
|
|
# We don't need to implement `reverse`, as the hostname will already be
|
|
|
|
# preserved as part of the REST framework `reverse` implementation.
|
|
|
|
|
|
|
|
|
|
|
|
class QueryParameterVersioning(BaseVersioning):
|
|
|
|
"""
|
|
|
|
GET /something/?version=0.1 HTTP/1.1
|
|
|
|
Host: example.com
|
|
|
|
Accept: application/json
|
|
|
|
"""
|
2015-01-07 15:46:23 +03:00
|
|
|
invalid_version_message = _('Invalid version in query parameter.')
|
2014-12-17 15:41:46 +03:00
|
|
|
|
|
|
|
def determine_version(self, request, *args, **kwargs):
|
2016-01-14 21:00:30 +03:00
|
|
|
version = request.query_params.get(self.version_param, self.default_version)
|
2014-12-17 15:41:46 +03:00
|
|
|
if not self.is_allowed_version(version):
|
|
|
|
raise exceptions.NotFound(self.invalid_version_message)
|
|
|
|
return version
|
|
|
|
|
|
|
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
2019-04-30 18:53:44 +03:00
|
|
|
url = super().reverse(
|
2014-12-17 15:41:46 +03:00
|
|
|
viewname, args, kwargs, request, format, **extra
|
|
|
|
)
|
|
|
|
if request.version is not None:
|
|
|
|
return replace_query_param(url, self.version_param, request.version)
|
|
|
|
return url
|