From 896e3c69c7eec311085da349a329ee80c8fca132 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 15 Jun 2011 17:29:10 +0000 Subject: [PATCH] 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 --- AUTHORS | 1 + django/conf/urls/defaults.py | 20 +- django/conf/urls/i18n.py | 17 +- django/contrib/admindocs/views.py | 6 +- django/core/urlresolvers.py | 119 ++++++--- django/middleware/locale.py | 26 +- django/utils/translation/__init__.py | 3 + django/utils/translation/trans_null.py | 4 + django/utils/translation/trans_real.py | 24 +- docs/releases/1.4.txt | 10 + docs/topics/i18n/deployment.txt | 15 +- docs/topics/i18n/internationalization.txt | 132 ++++++++++ .../regressiontests/i18n/patterns/__init__.py | 0 .../patterns/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 651 bytes .../patterns/locale/en/LC_MESSAGES/django.po | 37 +++ .../patterns/locale/nl/LC_MESSAGES/django.mo | Bin 0 -> 696 bytes .../patterns/locale/nl/LC_MESSAGES/django.po | 38 +++ .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 696 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 38 +++ .../i18n/patterns/templates/404.html | 0 .../i18n/patterns/templates/dummy.html | 0 tests/regressiontests/i18n/patterns/tests.py | 241 ++++++++++++++++++ .../i18n/patterns/urls/__init__.py | 0 .../i18n/patterns/urls/default.py | 19 ++ .../i18n/patterns/urls/disabled.py | 9 + .../i18n/patterns/urls/namespace.py | 10 + .../i18n/patterns/urls/wrong.py | 8 + .../i18n/patterns/urls/wrong_namespace.py | 11 + tests/regressiontests/i18n/tests.py | 19 +- 29 files changed, 750 insertions(+), 57 deletions(-) create mode 100644 tests/regressiontests/i18n/patterns/__init__.py create mode 100644 tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo create mode 100644 tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po create mode 100644 tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo create mode 100644 tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.po create mode 100644 tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 tests/regressiontests/i18n/patterns/templates/404.html create mode 100644 tests/regressiontests/i18n/patterns/templates/dummy.html create mode 100644 tests/regressiontests/i18n/patterns/tests.py create mode 100644 tests/regressiontests/i18n/patterns/urls/__init__.py create mode 100644 tests/regressiontests/i18n/patterns/urls/default.py create mode 100644 tests/regressiontests/i18n/patterns/urls/disabled.py create mode 100644 tests/regressiontests/i18n/patterns/urls/namespace.py create mode 100644 tests/regressiontests/i18n/patterns/urls/wrong.py create mode 100644 tests/regressiontests/i18n/patterns/urls/wrong_namespace.py diff --git a/AUTHORS b/AUTHORS index 8cb71c1280..0e774138df 100644 --- a/AUTHORS +++ b/AUTHORS @@ -94,6 +94,7 @@ answer newbie questions, and generally made Django that much better: Sean Brant Andrew Brehaut David Brenneman + Orne Brocaar brut.alll@gmail.com bthomas btoll@bestweb.net 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): diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 129a98cc2a..e7e8f45795 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -167,6 +167,16 @@ a :class:`~django.forms.fields.GenericIPAddressField` form field and the validators :data:`~django.core.validators.validate_ipv46_address` and :data:`~django.core.validators.validate_ipv6_address` +Translating URL patterns +~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.4 gained the ability to look for a language prefix in the URL pattern +when using the new :func:`django.conf.urls.i18n.i18n_patterns` helper function. +Additionally, it's now possible to define translatable URL patterns using +:func:`~django.utils.translation.ugettext_lazy`. See +:ref:`url-internationalization` for more information about the language prefix +and how to internationalize URL patterns. + Minor features ~~~~~~~~~~~~~~ diff --git a/docs/topics/i18n/deployment.txt b/docs/topics/i18n/deployment.txt index f06fa5e191..641deae939 100644 --- a/docs/topics/i18n/deployment.txt +++ b/docs/topics/i18n/deployment.txt @@ -59,7 +59,9 @@ matters, you should follow these guidelines: * Make sure it's one of the first middlewares installed. * It should come after ``SessionMiddleware``, because ``LocaleMiddleware`` - makes use of session data. + makes use of session data. And it should come before ``CommonMiddleware`` + because ``CommonMiddleware`` needs an activated language in order + to resolve the requested URL. * If you use ``CacheMiddleware``, put ``LocaleMiddleware`` after it. For example, your :setting:`MIDDLEWARE_CLASSES` might look like this:: @@ -76,8 +78,15 @@ For example, your :setting:`MIDDLEWARE_CLASSES` might look like this:: ``LocaleMiddleware`` tries to determine the user's language preference by following this algorithm: - * First, it looks for a ``django_language`` key in the current user's - session. +.. versionchanged:: 1.4 + + * First, it looks for the language prefix in the requested URL. This is + only performed when you are using the ``i18n_patterns`` function in your + root URLconf. See :ref:`url-internationalization` for more information + about the language prefix and how to internationalize URL patterns. + + * Failing that, it looks for a ``django_language`` key in the current + user's session. * Failing that, it looks for a cookie. diff --git a/docs/topics/i18n/internationalization.txt b/docs/topics/i18n/internationalization.txt index ccc47cb260..559f60fcc5 100644 --- a/docs/topics/i18n/internationalization.txt +++ b/docs/topics/i18n/internationalization.txt @@ -753,6 +753,138 @@ This isn't as fast as string interpolation in Python, so keep it to those cases where you really need it (for example, in conjunction with ``ngettext`` to produce proper pluralizations). +.. _url-internationalization: + +Specifying translation strings: In URL patterns +=============================================== + +.. versionadded:: 1.4 + +.. module:: django.conf.urls.i18n + +Django provides two mechanisms to internationalize URL patterns: + +* Adding the language prefix to the root of the URL patterns to make it + possible for :class:`~django.middleware.locale.LocaleMiddleware` to detect + the language to activate from the requested URL. + +* Making URL patterns themselves translatable via the + :func:`django.utils.translation.ugettext_lazy()` function. + +.. warning:: + + Using either one of these features requires that an active language be set + for each request; in other words, you need to have + :class:`django.middleware.locale.LocaleMiddleware` in your + :setting:`MIDDLEWARE_CLASSES` setting. + +Language prefix in URL patterns +------------------------------- + +.. function:: i18n_patterns(prefix, pattern_description, ...) + +This function can be used in your root URLconf as a replacement for the normal +:func:`django.conf.urls.defaults.patterns` function. Django will automatically +prepend the current active language code to all url patterns defined within +:func:`~django.conf.urls.i18n.i18n_patterns`. Example URL patterns:: + + from django.conf.urls.defaults import patterns, include, url + from django.conf.urls.i18n import i18n_patterns + + urlpatterns = patterns('' + url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'), + ) + + news_patterns = patterns('' + url(r'^$', 'news.views.index', name='index'), + url(r'^category/(?P[\w-]+)/$', 'news.views.category', name='category'), + url(r'^(?P[\w-]+)/$', 'news.views.details', name='detail'), + ) + + urlpatterns += i18n_patterns('', + url(r'^about/$', 'about.view', name='about'), + url(r'^news/$', include(news_patterns, namespace='news')), + ) + + +After defining these URL patterns, Django will automatically add the +language prefix to the URL patterns that were added by the ``i18n_patterns`` +function. Example:: + + from django.core.urlresolvers import reverse + from django.utils.translation import activate + + >>> activate('en') + >>> reverse('sitemap_xml') + '/sitemap.xml' + >>> reverse('news:index') + '/en/news/' + + >>> activate('nl') + >>> reverse('news:detail', kwargs={'slug': 'news-slug'}) + '/nl/news/news-slug/' + +.. warning:: + + :func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in your root + URLconf. Using it within an included URLconf will throw an + :exc:`ImproperlyConfigured` exception. + +.. warning:: + + Ensure that you don't have non-prefixed URL patterns that might collide + with an automatically-added language prefix. + + +Translating URL patterns +------------------------ + +URL patterns can also be marked translatable using the +:func:`~django.utils.translation.ugettext_lazy` function. Example:: + + from django.conf.urls.defaults import patterns, include, url + from django.conf.urls.i18n import i18n_patterns + from django.utils.translation import ugettext_lazy as _ + + urlpatterns = patterns('' + url(r'^sitemap\.xml$', 'sitemap.view', name='sitemap_xml'), + ) + + news_patterns = patterns('' + url(r'^$', 'news.views.index', name='index'), + url(_(r'^category/(?P[\w-]+)/$'), 'news.views.category', name='category'), + url(r'^(?P[\w-]+)/$', 'news.views.details', name='detail'), + ) + + urlpatterns += i18n_patterns('', + url(_(r'^about/$'), 'about.view', name='about'), + url(_(r'^news/$'), include(news_patterns, namespace='news')), + ) + + +After you've created the translations (see :doc:`localization` for more +information), the :func:`~django.core.urlresolvers.reverse` function will +return the URL in the active language. Example:: + + from django.core.urlresolvers import reverse + from django.utils.translation import activate + + >>> activate('en') + >>> reverse('news:category', kwargs={'slug': 'recent'}) + '/en/news/category/recent/' + + >>> activate('nl') + >>> reverse('news:category', kwargs={'slug': 'recent'}) + '/nl/nieuws/categorie/recent/' + +.. warning:: + + In most cases, it's best to use translated URLs only within a + language-code-prefixed block of patterns (using + :func:`~django.conf.urls.i18n.i18n_patterns`), to avoid the possibility + that a carelessly translated URL causes a collision with a non-translated + URL pattern. + .. _set_language-redirect-view: The ``set_language`` redirect view diff --git a/tests/regressiontests/i18n/patterns/__init__.py b/tests/regressiontests/i18n/patterns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..ec7644b504c3a368f4fd1c48bf1fc666890d797e GIT binary patch literal 651 zcmb`EPfi;#6vn3%P@9SkYlOV$qE+N%3^YPR8c5R=O*?@oOxP46nHe(}P3_3`^pDh4 z@6bE6>mHyd=oNZ`ZrSrWK}BNClYajEJfG)1+kaOVzXynAum%$F1bhWPet^f|C-CdP zzzX;c*1;d}2>b)_gCMwqdjIai+#f@|e+>2h4C?*gps)v!SuA3F2$tsQte>0dTqZor zbfsJ~m>5w=>x5}O4JOWTWlQcvekXgjJ8avsDmw2^KGD+bi-aes6aA0E_3Vyca!16#Wf`5oe(mmK8``+70vRBH6Hbtj4hzI@K)3HWa>uKeLLa{GX ybnV1v*PNDIsvVMDaASpgIZofu?OUI3>RcGwSDDVGDk8FdCf)z)xQ*aGU*#HT$)y+o literal 0 HcmV?d00001 diff --git a/tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po b/tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000000..9a14a80ceb --- /dev/null +++ b/tests/regressiontests/i18n/patterns/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,37 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-06-15 11:33+0200\n" +"PO-Revision-Date: 2011-06-14 16:16+0100\n" +"Last-Translator: Jannis Leidel \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" + +#: urls/default.py:11 +msgid "^translated/$" +msgstr "^translated/$" + +#: urls/default.py:12 +msgid "^translated/(?P[\\w-]+)/$" +msgstr "^translated/(?P[\\w-]+)/$" + +#: urls/default.py:17 +msgid "^users/$" +msgstr "^users/$" + +#: urls/default.py:18 urls/wrong.py:7 +msgid "^account/" +msgstr "^account/" + +#: urls/namespace.py:9 urls/wrong_namespace.py:10 +msgid "^register/$" +msgstr "^register/$" diff --git a/tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..86c847ecf2be01cee036efd0e67f58a33282baa7 GIT binary patch literal 696 zcmZvZ!EO^V5QY~h2o`bSjF6FfXcalxO(CkbN!k$7DzIszuG{=KXVAL5k7(WfpO*XI(4yQ!Q>_*JUPG?eVb#yl zt<9XJsVb#yd2OlgowbV~)&AmIF, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-06-15 11:33+0200\n" +"PO-Revision-Date: 2011-06-14 16:16+0100\n" +"Last-Translator: Jannis Leidel \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: urls/default.py:11 +msgid "^translated/$" +msgstr "^vertaald/$" + +#: urls/default.py:12 +msgid "^translated/(?P[\\w-]+)/$" +msgstr "^vertaald/(?P[\\w-]+)/$" + +#: urls/default.py:17 +msgid "^users/$" +msgstr "^gebruikers/$" + +#: urls/default.py:18 urls/wrong.py:7 +msgid "^account/" +msgstr "^profiel/" + +#: urls/namespace.py:9 urls/wrong_namespace.py:10 +msgid "^register/$" +msgstr "^registeren/$" diff --git a/tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..1d7b346c278c58207efa2f1ba17b1ae72c0a35a4 GIT binary patch literal 696 zcmZvZ&2AGh5XToNUyIkht*xJOQu36L96ucoSNw!qRX59{X?4c<0ycrOyG5E65!rMs6S<5I?>k*O9M? zU;mEWM1CN5k)OylHFtH{yCAh?BEqI&;FRPXhJl6iuM4d9!vPWgj}A7 zbv=veTxKFob){S@m>I~WbzoZef|)Zy*+MwT&PB_QCLLQ;dGE!`H}s&f;?CjG&){`QdU52w#ZyF1|&N^Kk++gxV!ag|$|Ximb(ZbAnJgu_Gh z3FhUb9Wh3uHOd|k#`yi!M%0cXG}0-&mj3edzE#53ID6QL82Vyi9ZeRm(1w$zLMdrU z3^Id)bdKwAv#7(SREN5UXH;H^9B2aKgp;_xv%TkQ@5J$DA)DIdy>L7l56@o5no&5= z%7MaU?@FxN!CTiV3nA48NsolF;JSOs7F|E{`JxWN(4k6oCRNVK`YY*9v-`hMR7MnZ zOPdqRi7M;RcH0|dKJ6|m(j#mo3}z{27pKiLKrJjfmR0X%rhQ+0H~&Td?_VX1tgU|m DD!H{c literal 0 HcmV?d00001 diff --git a/tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po b/tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000000..fd3388e4b0 --- /dev/null +++ b/tests/regressiontests/i18n/patterns/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,38 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2011-06-15 11:34+0200\n" +"PO-Revision-Date: 2011-06-14 16:17+0100\n" +"Last-Translator: Jannis Leidel \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +#: urls/default.py:11 +msgid "^translated/$" +msgstr "^traduzidos/$" + +#: urls/default.py:12 +msgid "^translated/(?P[\\w-]+)/$" +msgstr "^traduzidos/(?P[\\w-]+)/$" + +#: urls/default.py:17 +msgid "^users/$" +msgstr "^usuarios/$" + +#: urls/default.py:18 urls/wrong.py:7 +msgid "^account/" +msgstr "^conta/" + +#: urls/namespace.py:9 urls/wrong_namespace.py:10 +msgid "^register/$" +msgstr "^registre-se/$" diff --git a/tests/regressiontests/i18n/patterns/templates/404.html b/tests/regressiontests/i18n/patterns/templates/404.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/patterns/templates/dummy.html b/tests/regressiontests/i18n/patterns/templates/dummy.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/patterns/tests.py b/tests/regressiontests/i18n/patterns/tests.py new file mode 100644 index 0000000000..9451bc6966 --- /dev/null +++ b/tests/regressiontests/i18n/patterns/tests.py @@ -0,0 +1,241 @@ +import os + +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse, clear_url_caches +from django.test import TestCase +from django.test.utils import override_settings +from django.utils import translation + + +class URLTestCaseBase(TestCase): + """ + TestCase base-class for the URL tests. + """ + urls = 'regressiontests.i18n.patterns.urls.default' + + def setUp(self): + # Make sure the cache is empty before we are doing our tests. + clear_url_caches() + + def tearDown(self): + # Make sure we will leave an empty cache for other testcases. + clear_url_caches() + +URLTestCaseBase = override_settings( + USE_I18N=True, + LOCALE_PATHS=( + os.path.join(os.path.dirname(__file__), 'locale'), + ), + TEMPLATE_DIRS=( + os.path.join(os.path.dirname(__file__), 'templates'), + ), + LANGUAGE_CODE='en', + LANGUAGES=( + ('nl', 'Dutch'), + ('en', 'English'), + ('pt-br', 'Brazilian Portuguese'), + ), + MIDDLEWARE_CLASSES=( + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), +)(URLTestCaseBase) + + +class URLPrefixTests(URLTestCaseBase): + """ + Tests if the `i18n_patterns` is adding the prefix correctly. + """ + def test_not_prefixed(self): + with translation.override('en'): + self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + with translation.override('nl'): + self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + + def test_prefixed(self): + with translation.override('en'): + self.assertEqual(reverse('prefixed'), '/en/prefixed/') + with translation.override('nl'): + self.assertEqual(reverse('prefixed'), '/nl/prefixed/') + + @override_settings(ROOT_URLCONF='regressiontests.i18n.patterns.urls.wrong') + def test_invalid_prefix_use(self): + self.assertRaises(ImproperlyConfigured, lambda: reverse('account:register')) + + +class URLDisabledTests(URLTestCaseBase): + urls = 'regressiontests.i18n.patterns.urls.disabled' + + @override_settings(USE_I18N=False) + def test_prefixed_i18n_disabled(self): + with translation.override('en'): + self.assertEqual(reverse('prefixed'), '/prefixed/') + with translation.override('nl'): + self.assertEqual(reverse('prefixed'), '/prefixed/') + + +class URLTranslationTests(URLTestCaseBase): + """ + Tests if the pattern-strings are translated correctly (within the + `i18n_patterns` and the normal `patterns` function). + """ + def test_no_prefix_translated(self): + with translation.override('en'): + self.assertEqual(reverse('no-prefix-translated'), '/translated/') + self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/translated/yeah/') + + with translation.override('nl'): + self.assertEqual(reverse('no-prefix-translated'), '/vertaald/') + self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/vertaald/yeah/') + + with translation.override('pt-br'): + self.assertEqual(reverse('no-prefix-translated'), '/traduzidos/') + self.assertEqual(reverse('no-prefix-translated-slug', kwargs={'slug': 'yeah'}), '/traduzidos/yeah/') + + def test_users_url(self): + with translation.override('en'): + self.assertEqual(reverse('users'), '/en/users/') + + with translation.override('nl'): + self.assertEqual(reverse('users'), '/nl/gebruikers/') + + with translation.override('pt-br'): + self.assertEqual(reverse('users'), '/pt-br/usuarios/') + + +class URLNamespaceTests(URLTestCaseBase): + """ + Tests if the translations are still working within namespaces. + """ + def test_account_register(self): + with translation.override('en'): + self.assertEqual(reverse('account:register'), '/en/account/register/') + + with translation.override('nl'): + self.assertEqual(reverse('account:register'), '/nl/profiel/registeren/') + + +class URLRedirectTests(URLTestCaseBase): + """ + Tests if the user gets redirected to the right URL when there is no + language-prefix in the request URL. + """ + def test_no_prefix_response(self): + response = self.client.get('/not-prefixed/') + self.assertEqual(response.status_code, 200) + + def test_en_redirect(self): + response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='en') + self.assertRedirects(response, 'http://testserver/en/account/register/') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 200) + + def test_en_redirect_wrong_url(self): + response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='en') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], 'http://testserver/en/profiel/registeren/') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 404) + + def test_nl_redirect(self): + response = self.client.get('/profiel/registeren/', HTTP_ACCEPT_LANGUAGE='nl') + self.assertRedirects(response, 'http://testserver/nl/profiel/registeren/') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 200) + + def test_nl_redirect_wrong_url(self): + response = self.client.get('/account/register/', HTTP_ACCEPT_LANGUAGE='nl') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], 'http://testserver/nl/account/register/') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 404) + + def test_pt_br_redirect(self): + response = self.client.get('/conta/registre-se/', HTTP_ACCEPT_LANGUAGE='pt-br') + self.assertRedirects(response, 'http://testserver/pt-br/conta/registre-se/') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 200) + + +class URLRedirectWithoutTrailingSlashTests(URLTestCaseBase): + """ + Tests the redirect when the requested URL doesn't end with a slash + (`settings.APPEND_SLASH=True`). + """ + def test_not_prefixed_redirect(self): + response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en') + self.assertEqual(response.status_code, 301) + self.assertEqual(response['location'], 'http://testserver/not-prefixed/') + + def test_en_redirect(self): + response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], 'http://testserver/en/account/register') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 301) + self.assertEqual(response['location'], 'http://testserver/en/account/register/') + + +class URLRedirectWithoutTrailingSlashSettingTests(URLTestCaseBase): + """ + Tests the redirect when the requested URL doesn't end with a slash + (`settings.APPEND_SLASH=False`). + """ + @override_settings(APPEND_SLASH=False) + def test_not_prefixed_redirect(self): + response = self.client.get('/not-prefixed', HTTP_ACCEPT_LANGUAGE='en') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], 'http://testserver/en/not-prefixed') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 404) + + @override_settings(APPEND_SLASH=False) + def test_en_redirect(self): + response = self.client.get('/account/register', HTTP_ACCEPT_LANGUAGE='en') + self.assertEqual(response.status_code, 302) + self.assertEqual(response['location'], 'http://testserver/en/account/register') + + response = self.client.get(response['location']) + self.assertEqual(response.status_code, 404) + + +class URLResponseTests(URLTestCaseBase): + """ + Tests if the response has the right language-code. + """ + def test_not_prefixed_with_prefix(self): + response = self.client.get('/en/not-prefixed/') + self.assertEqual(response.status_code, 404) + + def test_en_url(self): + response = self.client.get('/en/account/register/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['content-language'], 'en') + self.assertEqual(response.context['LANGUAGE_CODE'], 'en') + + def test_nl_url(self): + response = self.client.get('/nl/profiel/registeren/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['content-language'], 'nl') + self.assertEqual(response.context['LANGUAGE_CODE'], 'nl') + + def test_wrong_en_prefix(self): + response = self.client.get('/en/profiel/registeren/') + self.assertEqual(response.status_code, 404) + + def test_wrong_nl_prefix(self): + response = self.client.get('/nl/account/register/') + self.assertEqual(response.status_code, 404) + + def test_pt_br_url(self): + response = self.client.get('/pt-br/conta/registre-se/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response['content-language'], 'pt-br') + self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br') diff --git a/tests/regressiontests/i18n/patterns/urls/__init__.py b/tests/regressiontests/i18n/patterns/urls/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/i18n/patterns/urls/default.py b/tests/regressiontests/i18n/patterns/urls/default.py new file mode 100644 index 0000000000..8d178e67d4 --- /dev/null +++ b/tests/regressiontests/i18n/patterns/urls/default.py @@ -0,0 +1,19 @@ +from django.conf.urls.defaults import patterns, include, url +from django.conf.urls.i18n import i18n_patterns +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import TemplateView + + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = patterns('', + url(r'^not-prefixed/$', view, name='not-prefixed'), + url(_(r'^translated/$'), view, name='no-prefix-translated'), + url(_(r'^translated/(?P[\w-]+)/$'), view, name='no-prefix-translated-slug'), +) + +urlpatterns += i18n_patterns('', + url(r'^prefixed/$', view, name='prefixed'), + url(_(r'^users/$'), view, name='users'), + url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.namespace', namespace='account')), +) diff --git a/tests/regressiontests/i18n/patterns/urls/disabled.py b/tests/regressiontests/i18n/patterns/urls/disabled.py new file mode 100644 index 0000000000..e4094695f9 --- /dev/null +++ b/tests/regressiontests/i18n/patterns/urls/disabled.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import url +from django.conf.urls.i18n import i18n_patterns +from django.views.generic import TemplateView + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = i18n_patterns('', + url(r'^prefixed/$', view, name='prefixed'), +) diff --git a/tests/regressiontests/i18n/patterns/urls/namespace.py b/tests/regressiontests/i18n/patterns/urls/namespace.py new file mode 100644 index 0000000000..878e08feae --- /dev/null +++ b/tests/regressiontests/i18n/patterns/urls/namespace.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import patterns, include, url +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import TemplateView + + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = patterns('', + url(_(r'^register/$'), view, name='register'), +) diff --git a/tests/regressiontests/i18n/patterns/urls/wrong.py b/tests/regressiontests/i18n/patterns/urls/wrong.py new file mode 100644 index 0000000000..3e45743cdf --- /dev/null +++ b/tests/regressiontests/i18n/patterns/urls/wrong.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, include, url +from django.conf.urls.i18n import i18n_patterns +from django.utils.translation import ugettext_lazy as _ + + +urlpatterns = i18n_patterns('', + url(_(r'^account/'), include('regressiontests.i18n.patterns.urls.wrong_namespace', namespace='account')), +) diff --git a/tests/regressiontests/i18n/patterns/urls/wrong_namespace.py b/tests/regressiontests/i18n/patterns/urls/wrong_namespace.py new file mode 100644 index 0000000000..90ee2a0538 --- /dev/null +++ b/tests/regressiontests/i18n/patterns/urls/wrong_namespace.py @@ -0,0 +1,11 @@ +from django.conf.urls.defaults import include, url +from django.conf.urls.i18n import i18n_patterns +from django.utils.translation import ugettext_lazy as _ +from django.views.generic import TemplateView + + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = i18n_patterns('', + url(_(r'^register/$'), view, name='register'), +) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index e3add3e3d8..9c24c3fb40 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -3,13 +3,12 @@ from __future__ import with_statement import datetime import decimal import os -import sys import pickle from threading import local from django.conf import settings from django.template import Template, Context -from django.test import TestCase +from django.test import TestCase, RequestFactory from django.utils.formats import (get_format, date_format, time_format, localize, localize_input, iter_format_modules, get_format_modules) from django.utils.importlib import import_module @@ -18,14 +17,14 @@ from django.utils.safestring import mark_safe, SafeString, SafeUnicode from django.utils import translation from django.utils.translation import (ugettext, ugettext_lazy, activate, deactivate, gettext_lazy, pgettext, npgettext, to_locale, - get_language_info, get_language) + get_language_info, get_language, get_language_from_request) from forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from models import Company, TestModel from commands.tests import * - +from patterns.tests import * from test_warnings import DeprecationWarningTests class TranslationTests(TestCase): @@ -494,6 +493,9 @@ class FormattingTests(TestCase): class MiscTests(TestCase): + def setUp(self): + self.rf = RequestFactory() + def test_parse_spec_http_header(self): """ Testing HTTP header parsing. First, we test that we can parse the @@ -534,10 +536,8 @@ class MiscTests(TestCase): """ Now test that we parse a literal HTTP header correctly. """ - from django.utils.translation.trans_real import get_language_from_request g = get_language_from_request - from django.http import HttpRequest - r = HttpRequest + r = self.rf.get('/') r.COOKIES = {} r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-br'} self.assertEqual('pt-br', g(r)) @@ -569,10 +569,8 @@ class MiscTests(TestCase): """ Now test that we parse language preferences stored in a cookie correctly. """ - from django.utils.translation.trans_real import get_language_from_request g = get_language_from_request - from django.http import HttpRequest - r = HttpRequest + r = self.rf.get('/') r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: 'pt-br'} r.META = {} self.assertEqual('pt-br', g(r)) @@ -827,4 +825,3 @@ class MultipleLocaleActivationTests(TestCase): t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}") with translation.override('nl'): self.assertEqual(t.render(Context({})), 'Nee') -