summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorJannis Leidel <jannis@leidel.info>2011-06-15 17:29:10 +0000
committerJannis Leidel <jannis@leidel.info>2011-06-15 17:29:10 +0000
commit896e3c69c7eec311085da349a329ee80c8fca132 (patch)
treedcc608e4b47fb9621e731f3af476443a56ca56aa /django
parent62bb4b8c37ada04c10d2a37990326636ea7791d6 (diff)
Fixed #11585 -- Added ability to translate and prefix URL patterns with a language code as an alternative method for language discovery. Many thanks to Orne Brocaar for his initial work and Carl Meyer for feedback.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16405 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
-rw-r--r--django/conf/urls/defaults.py20
-rw-r--r--django/conf/urls/i18n.py17
-rw-r--r--django/contrib/admindocs/views.py6
-rw-r--r--django/core/urlresolvers.py119
-rw-r--r--django/middleware/locale.py26
-rw-r--r--django/utils/translation/__init__.py3
-rw-r--r--django/utils/translation/trans_null.py4
-rw-r--r--django/utils/translation/trans_real.py24
8 files changed, 176 insertions, 43 deletions
diff --git a/django/conf/urls/defaults.py b/django/conf/urls/defaults.py
index 3ab8bab3ec..84b1f25bf7 100644
--- a/django/conf/urls/defaults.py
+++ b/django/conf/urls/defaults.py
@@ -1,5 +1,8 @@
-from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
+from django.core.urlresolvers import (RegexURLPattern,
+ RegexURLResolver, LocaleRegexURLResolver)
from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
__all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
@@ -15,6 +18,21 @@ def include(arg, namespace=None, app_name=None):
else:
# No namespace hint - use manually provided namespace
urlconf_module = arg
+
+ if isinstance(urlconf_module, basestring):
+ urlconf_module = import_module(urlconf_module)
+ patterns = getattr(urlconf_module, 'urlpatterns', urlconf_module)
+
+ # Make sure we can iterate through the patterns (without this, some
+ # testcases will break).
+ if isinstance(patterns, (list, tuple)):
+ for url_pattern in patterns:
+ # Test if the LocaleRegexURLResolver is used within the include;
+ # this should throw an error since this is not allowed!
+ if isinstance(url_pattern, LocaleRegexURLResolver):
+ raise ImproperlyConfigured(
+ 'Using i18n_patterns in an included URLconf is not allowed.')
+
return (urlconf_module, app_name, namespace)
def patterns(prefix, *args):
diff --git a/django/conf/urls/i18n.py b/django/conf/urls/i18n.py
index 00e2d6017b..67931aaf1c 100644
--- a/django/conf/urls/i18n.py
+++ b/django/conf/urls/i18n.py
@@ -1,4 +1,19 @@
-from django.conf.urls.defaults import *
+from django.conf import settings
+from django.conf.urls.defaults import patterns
+from django.core.urlresolvers import LocaleRegexURLResolver
+
+def i18n_patterns(prefix, *args):
+ """
+ Adds the language code prefix to every URL pattern within this
+ function. This may only be used in the root URLconf, not in an included
+ URLconf.
+
+ """
+ pattern_list = patterns(prefix, *args)
+ if not settings.USE_I18N:
+ return pattern_list
+ return [LocaleRegexURLResolver(pattern_list)]
+
urlpatterns = patterns('',
(r'^setlang/$', 'django.views.i18n.set_language'),
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 319b489e46..079f7ab2da 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -346,12 +346,12 @@ def extract_views_from_urlpatterns(urlpatterns, base=''):
"""
views = []
for p in urlpatterns:
- if hasattr(p, '_get_callback'):
+ if hasattr(p, 'callback'):
try:
- views.append((p._get_callback(), base + p.regex.pattern))
+ views.append((p.callback, base + p.regex.pattern))
except ViewDoesNotExist:
continue
- elif hasattr(p, '_get_url_patterns'):
+ elif hasattr(p, 'url_patterns'):
try:
patterns = p.url_patterns
except ImportError:
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
index 424886e68b..5a78c9bfc3 100644
--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -11,13 +11,14 @@ import re
from threading import local
from django.http import Http404
-from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import iri_to_uri, force_unicode, smart_str
from django.utils.functional import memoize, lazy
from django.utils.importlib import import_module
from django.utils.regex_helper import normalize
+from django.utils.translation import get_language
+
_resolver_cache = {} # Maps URLconf modules to RegexURLResolver instances.
_callable_cache = {} # Maps view and url pattern names to their view functions.
@@ -50,13 +51,13 @@ class ResolverMatch(object):
url_name = '.'.join([func.__module__, func.__name__])
self.url_name = url_name
+ @property
def namespace(self):
return ':'.join(self.namespaces)
- namespace = property(namespace)
+ @property
def view_name(self):
return ':'.join([ x for x in [ self.namespace, self.url_name ] if x ])
- view_name = property(view_name)
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
@@ -115,13 +116,43 @@ def get_mod_func(callback):
return callback, ''
return callback[:dot], callback[dot+1:]
-class RegexURLPattern(object):
+class LocaleRegexProvider(object):
+ """
+ A mixin to provide a default regex property which can vary by active
+ language.
+
+ """
+ def __init__(self, regex):
+ # regex is either a string representing a regular expression, or a
+ # translatable string (using ugettext_lazy) representing a regular
+ # expression.
+ self._regex = regex
+ self._regex_dict = {}
+
+
+ @property
+ def regex(self):
+ """
+ Returns a compiled regular expression, depending upon the activated
+ language-code.
+ """
+ language_code = get_language()
+ if language_code not in self._regex_dict:
+ if isinstance(self._regex, basestring):
+ compiled_regex = re.compile(self._regex, re.UNICODE)
+ else:
+ regex = force_unicode(self._regex)
+ compiled_regex = re.compile(regex, re.UNICODE)
+ self._regex_dict[language_code] = compiled_regex
+ return self._regex_dict[language_code]
+
+
+class RegexURLPattern(LocaleRegexProvider):
def __init__(self, regex, callback, default_args=None, name=None):
- # regex is a string representing a regular expression.
+ LocaleRegexProvider.__init__(self, regex)
# callback is either a string like 'foo.views.news.stories.story_detail'
# which represents the path to a module and a view function name, or a
# callable object (view).
- self.regex = re.compile(regex, re.UNICODE)
if callable(callback):
self._callback = callback
else:
@@ -157,7 +188,8 @@ class RegexURLPattern(object):
return ResolverMatch(self.callback, args, kwargs, self.name)
- def _get_callback(self):
+ @property
+ def callback(self):
if self._callback is not None:
return self._callback
try:
@@ -169,13 +201,11 @@ class RegexURLPattern(object):
mod_name, func_name = get_mod_func(self._callback_str)
raise ViewDoesNotExist("Tried %s in module %s. Error was: %s" % (func_name, mod_name, str(e)))
return self._callback
- callback = property(_get_callback)
-class RegexURLResolver(object):
+class RegexURLResolver(LocaleRegexProvider):
def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
- # regex is a string representing a regular expression.
+ LocaleRegexProvider.__init__(self, regex)
# urlconf_name is a string representing the module containing URLconfs.
- self.regex = re.compile(regex, re.UNICODE)
self.urlconf_name = urlconf_name
if not isinstance(urlconf_name, basestring):
self._urlconf_module = self.urlconf_name
@@ -183,9 +213,9 @@ class RegexURLResolver(object):
self.default_kwargs = default_kwargs or {}
self.namespace = namespace
self.app_name = app_name
- self._reverse_dict = None
- self._namespace_dict = None
- self._app_dict = None
+ self._reverse_dict = {}
+ self._namespace_dict = {}
+ self._app_dict = {}
def __repr__(self):
return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern))
@@ -194,6 +224,7 @@ class RegexURLResolver(object):
lookups = MultiValueDict()
namespaces = {}
apps = {}
+ language_code = get_language()
for pattern in reversed(self.url_patterns):
p_pattern = pattern.regex.pattern
if p_pattern.startswith('^'):
@@ -220,27 +251,30 @@ class RegexURLResolver(object):
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
if pattern.name is not None:
lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
- self._reverse_dict = lookups
- self._namespace_dict = namespaces
- self._app_dict = apps
+ self._reverse_dict[language_code] = lookups
+ self._namespace_dict[language_code] = namespaces
+ self._app_dict[language_code] = apps
- def _get_reverse_dict(self):
- if self._reverse_dict is None:
+ @property
+ def reverse_dict(self):
+ language_code = get_language()
+ if language_code not in self._reverse_dict:
self._populate()
- return self._reverse_dict
- reverse_dict = property(_get_reverse_dict)
+ return self._reverse_dict[language_code]
- def _get_namespace_dict(self):
- if self._namespace_dict is None:
+ @property
+ def namespace_dict(self):
+ language_code = get_language()
+ if language_code not in self._namespace_dict:
self._populate()
- return self._namespace_dict
- namespace_dict = property(_get_namespace_dict)
+ return self._namespace_dict[language_code]
- def _get_app_dict(self):
- if self._app_dict is None:
+ @property
+ def app_dict(self):
+ language_code = get_language()
+ if language_code not in self._app_dict:
self._populate()
- return self._app_dict
- app_dict = property(_get_app_dict)
+ return self._app_dict[language_code]
def resolve(self, path):
tried = []
@@ -267,22 +301,22 @@ class RegexURLResolver(object):
raise Resolver404({'tried': tried, 'path': new_path})
raise Resolver404({'path' : path})
- def _get_urlconf_module(self):
+ @property
+ def urlconf_module(self):
try:
return self._urlconf_module
except AttributeError:
self._urlconf_module = import_module(self.urlconf_name)
return self._urlconf_module
- urlconf_module = property(_get_urlconf_module)
- def _get_url_patterns(self):
+ @property
+ def url_patterns(self):
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
try:
iter(patterns)
except TypeError:
raise ImproperlyConfigured("The included urlconf %s doesn't have any patterns in it" % self.urlconf_name)
return patterns
- url_patterns = property(_get_url_patterns)
def _resolve_special(self, view_type):
callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
@@ -343,6 +377,25 @@ class RegexURLResolver(object):
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
"arguments '%s' not found." % (lookup_view_s, args, kwargs))
+class LocaleRegexURLResolver(RegexURLResolver):
+ """
+ A URL resolver that always matches the active language code as URL prefix.
+
+ Rather than taking a regex argument, we just override the ``regex``
+ function to always return the active language-code as regex.
+ """
+ def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
+ super(LocaleRegexURLResolver, self).__init__(
+ None, urlconf_name, default_kwargs, app_name, namespace)
+
+ @property
+ def regex(self):
+ language_code = get_language()
+ if language_code not in self._regex_dict:
+ regex_compiled = re.compile('^%s/' % language_code, re.UNICODE)
+ self._regex_dict[language_code] = regex_compiled
+ return self._regex_dict[language_code]
+
def resolve(path, urlconf=None):
if urlconf is None:
urlconf = get_urlconf()
diff --git a/django/middleware/locale.py b/django/middleware/locale.py
index b5e4949378..d42a615b04 100644
--- a/django/middleware/locale.py
+++ b/django/middleware/locale.py
@@ -1,5 +1,7 @@
-"this is the locale selecting middleware that will look at accept headers"
+"This is the locale selecting middleware that will look at accept headers"
+from django.core.urlresolvers import get_resolver, LocaleRegexURLResolver
+from django.http import HttpResponseRedirect
from django.utils.cache import patch_vary_headers
from django.utils import translation
@@ -18,8 +20,26 @@ class LocaleMiddleware(object):
request.LANGUAGE_CODE = translation.get_language()
def process_response(self, request, response):
+ language = translation.get_language()
+ translation.deactivate()
+
+ if (response.status_code == 404 and
+ not translation.get_language_from_path(request.path_info)
+ and self.is_language_prefix_patterns_used()):
+ return HttpResponseRedirect(
+ '/%s%s' % (language, request.get_full_path()))
+
patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response:
- response['Content-Language'] = translation.get_language()
- translation.deactivate()
+ response['Content-Language'] = language
return response
+
+ def is_language_prefix_patterns_used(self):
+ """
+ Returns `True` if the `LocaleRegexURLResolver` is used
+ at root level of the urlpatterns, else it returns `False`.
+ """
+ for url_pattern in get_resolver(None).url_patterns:
+ if isinstance(url_pattern, LocaleRegexURLResolver):
+ return True
+ return False
diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py
index adb5566da3..acf4d8dead 100644
--- a/django/utils/translation/__init__.py
+++ b/django/utils/translation/__init__.py
@@ -144,6 +144,9 @@ def to_locale(language):
def get_language_from_request(request):
return _trans.get_language_from_request(request)
+def get_language_from_path(path):
+ return _trans.get_language_from_path(path)
+
def templatize(src, origin=None):
return _trans.templatize(src, origin)
diff --git a/django/utils/translation/trans_null.py b/django/utils/translation/trans_null.py
index 5622c03f55..daee96eb75 100644
--- a/django/utils/translation/trans_null.py
+++ b/django/utils/translation/trans_null.py
@@ -58,3 +58,7 @@ def to_locale(language):
def get_language_from_request(request):
return settings.LANGUAGE_CODE
+
+def get_language_from_path(request):
+ return None
+
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 9e8285bcf7..9f930b54ef 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -35,6 +35,8 @@ accept_language_re = re.compile(r'''
(?:\s*,\s*|$) # Multiple accepts per header.
''', re.VERBOSE)
+language_code_prefix_re = re.compile(r'^/([\w-]+)/')
+
def to_locale(language, to_lower=False):
"""
Turns a language name (en-us) into a locale name (en_US). If 'to_lower' is
@@ -336,14 +338,28 @@ def check_for_language(lang_code):
"""
Checks whether there is a global language file for the given language
code. This is used to decide whether a user-provided language is
- available. This is only used for language codes from either the cookies or
- session and during format localization.
+ available. This is only used for language codes from either the cookies
+ or session and during format localization.
"""
for path in all_locale_paths():
if gettext_module.find('django', path, [to_locale(lang_code)]) is not None:
return True
return False
+def get_language_from_path(path, supported=None):
+ """
+ Returns the language-code if there is a valid language-code
+ found in the `path`.
+ """
+ if supported is None:
+ from django.conf import settings
+ supported = dict(settings.LANGUAGES)
+ regex_match = language_code_prefix_re.match(path)
+ if regex_match:
+ lang_code = regex_match.group(1)
+ if lang_code in supported and check_for_language(lang_code):
+ return lang_code
+
def get_language_from_request(request):
"""
Analyzes the request to find what language the user wants the system to
@@ -355,6 +371,10 @@ def get_language_from_request(request):
from django.conf import settings
supported = dict(settings.LANGUAGES)
+ lang_code = get_language_from_path(request.path_info, supported)
+ if lang_code is not None:
+ return lang_code
+
if hasattr(request, 'session'):
lang_code = request.session.get('django_language', None)
if lang_code in supported and lang_code is not None and check_for_language(lang_code):