diff options
| author | Jannis Leidel <jannis@leidel.info> | 2011-06-15 17:29:10 +0000 |
|---|---|---|
| committer | Jannis Leidel <jannis@leidel.info> | 2011-06-15 17:29:10 +0000 |
| commit | 896e3c69c7eec311085da349a329ee80c8fca132 (patch) | |
| tree | dcc608e4b47fb9621e731f3af476443a56ca56aa /django | |
| parent | 62bb4b8c37ada04c10d2a37990326636ea7791d6 (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.py | 20 | ||||
| -rw-r--r-- | django/conf/urls/i18n.py | 17 | ||||
| -rw-r--r-- | django/contrib/admindocs/views.py | 6 | ||||
| -rw-r--r-- | django/core/urlresolvers.py | 119 | ||||
| -rw-r--r-- | django/middleware/locale.py | 26 | ||||
| -rw-r--r-- | django/utils/translation/__init__.py | 3 | ||||
| -rw-r--r-- | django/utils/translation/trans_null.py | 4 | ||||
| -rw-r--r-- | django/utils/translation/trans_real.py | 24 |
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): |
