diff --git a/django/conf/urls/defaults.py b/django/conf/urls/defaults.py index 26cdd3e1ff..3ab8bab3ec 100644 --- a/django/conf/urls/defaults.py +++ b/django/conf/urls/defaults.py @@ -6,7 +6,16 @@ __all__ = ['handler404', 'handler500', 'include', 'patterns', 'url'] handler404 = 'django.views.defaults.page_not_found' handler500 = 'django.views.defaults.server_error' -include = lambda urlconf_module: [urlconf_module] +def include(arg, namespace=None, app_name=None): + if isinstance(arg, tuple): + # callable returning a namespace hint + if namespace: + raise ImproperlyConfigured('Cannot override the namespace for a dynamic module that provides a namespace') + urlconf_module, app_name, namespace = arg + else: + # No namespace hint - use manually provided namespace + urlconf_module = arg + return (urlconf_module, app_name, namespace) def patterns(prefix, *args): pattern_list = [] @@ -19,9 +28,10 @@ def patterns(prefix, *args): return pattern_list def url(regex, view, kwargs=None, name=None, prefix=''): - if type(view) == list: + if isinstance(view, (list,tuple)): # For include(...) processing. - return RegexURLResolver(regex, view[0], kwargs) + urlconf_module, app_name, namespace = view + return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace) else: if isinstance(view, basestring): if not view: diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8297eca74e..31a28ccf0b 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -226,24 +226,24 @@ class ModelAdmin(BaseModelAdmin): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name + info = self.model._meta.app_label, self.model._meta.module_name urlpatterns = patterns('', url(r'^$', wrap(self.changelist_view), - name='%sadmin_%s_%s_changelist' % info), + name='%s_%s_changelist' % info), url(r'^add/$', wrap(self.add_view), - name='%sadmin_%s_%s_add' % info), + name='%s_%s_add' % info), url(r'^(.+)/history/$', wrap(self.history_view), - name='%sadmin_%s_%s_history' % info), + name='%s_%s_history' % info), url(r'^(.+)/delete/$', wrap(self.delete_view), - name='%sadmin_%s_%s_delete' % info), + name='%s_%s_delete' % info), url(r'^(.+)/$', wrap(self.change_view), - name='%sadmin_%s_%s_change' % info), + name='%s_%s_change' % info), ) return urlpatterns @@ -582,11 +582,12 @@ class ModelAdmin(BaseModelAdmin): 'save_on_top': self.save_on_top, 'root_path': self.admin_site.root_path, }) + context_instance = template.RequestContext(request, current_app=self.admin_site.name) return render_to_response(self.change_form_template or [ "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), "admin/%s/change_form.html" % app_label, "admin/change_form.html" - ], context, context_instance=template.RequestContext(request)) + ], context, context_instance=context_instance) def response_add(self, request, obj, post_url_continue='../%s/'): """ @@ -977,11 +978,12 @@ class ModelAdmin(BaseModelAdmin): 'actions_on_bottom': self.actions_on_bottom, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.admin_site.name) return render_to_response(self.change_list_template or [ 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()), 'admin/%s/change_list.html' % app_label, 'admin/change_list.html' - ], context, context_instance=template.RequestContext(request)) + ], context, context_instance=context_instance) def delete_view(self, request, object_id, extra_context=None): "The 'delete' admin view for this model." @@ -1032,11 +1034,12 @@ class ModelAdmin(BaseModelAdmin): "app_label": app_label, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.admin_site.name) return render_to_response(self.delete_confirmation_template or [ "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), "admin/%s/delete_confirmation.html" % app_label, "admin/delete_confirmation.html" - ], context, context_instance=template.RequestContext(request)) + ], context, context_instance=context_instance) def history_view(self, request, object_id, extra_context=None): "The 'history' admin view for this model." @@ -1059,11 +1062,12 @@ class ModelAdmin(BaseModelAdmin): 'app_label': app_label, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.admin_site.name) return render_to_response(self.object_history_template or [ "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()), "admin/%s/object_history.html" % app_label, "admin/object_history.html" - ], context, context_instance=template.RequestContext(request)) + ], context, context_instance=context_instance) # # DEPRECATED methods. diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 6e9ef1161f..abcff14cd1 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -5,6 +5,7 @@ from django.contrib.admin import actions from django.contrib.auth import authenticate, login from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import reverse from django.shortcuts import render_to_response from django.utils.functional import update_wrapper from django.utils.safestring import mark_safe @@ -38,17 +39,14 @@ class AdminSite(object): login_template = None app_index_template = None - def __init__(self, name=None): + def __init__(self, name=None, app_name='admin'): self._registry = {} # model_class class -> admin_class instance - # TODO Root path is used to calculate urls under the old root() method - # in order to maintain backwards compatibility we are leaving that in - # so root_path isn't needed, not sure what to do about this. - self.root_path = 'admin/' + self.root_path = None if name is None: - name = '' + self.name = 'admin' else: - name += '_' - self.name = name + self.name = name + self.app_name = app_name self._actions = {'delete_selected': actions.delete_selected} self._global_actions = self._actions.copy() @@ -114,20 +112,20 @@ class AdminSite(object): name = name or action.__name__ self._actions[name] = action self._global_actions[name] = action - + def disable_action(self, name): """ Disable a globally-registered action. Raises KeyError for invalid names. """ del self._actions[name] - + def get_action(self, name): """ Explicitally get a registered global action wheather it's enabled or not. Raises KeyError for invalid names. """ return self._global_actions[name] - + def actions(self): """ Get all the enabled actions as an iterable of (name, func). @@ -159,9 +157,9 @@ class AdminSite(object): if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") - def admin_view(self, view): + def admin_view(self, view, cacheable=False): """ - Decorator to create an "admin view attached to this ``AdminSite``. This + Decorator to create an admin view attached to this ``AdminSite``. This wraps the view and provides permission checking by calling ``self.has_permission``. @@ -177,43 +175,49 @@ class AdminSite(object): url(r'^my_view/$', self.admin_view(some_view)) ) return urls + + By default, admin_views are marked non-cacheable using the + ``never_cache`` decorator. If the view can be safely cached, set + cacheable=True. """ def inner(request, *args, **kwargs): if not self.has_permission(request): return self.login(request) return view(request, *args, **kwargs) + if not cacheable: + inner = never_cache(inner) return update_wrapper(inner, view) def get_urls(self): from django.conf.urls.defaults import patterns, url, include - def wrap(view): + def wrap(view, cacheable=False): def wrapper(*args, **kwargs): - return self.admin_view(view)(*args, **kwargs) + return self.admin_view(view, cacheable)(*args, **kwargs) return update_wrapper(wrapper, view) # Admin-site-wide views. urlpatterns = patterns('', url(r'^$', wrap(self.index), - name='%sadmin_index' % self.name), + name='index'), url(r'^logout/$', wrap(self.logout), - name='%sadmin_logout'), + name='logout'), url(r'^password_change/$', - wrap(self.password_change), - name='%sadmin_password_change' % self.name), + wrap(self.password_change, cacheable=True), + name='password_change'), url(r'^password_change/done/$', - wrap(self.password_change_done), - name='%sadmin_password_change_done' % self.name), + wrap(self.password_change_done, cacheable=True), + name='password_change_done'), url(r'^jsi18n/$', - wrap(self.i18n_javascript), - name='%sadmin_jsi18n' % self.name), + wrap(self.i18n_javascript, cacheable=True), + name='jsi18n'), url(r'^r/(?P\d+)/(?P.+)/$', 'django.views.defaults.shortcut'), url(r'^(?P\w+)/$', wrap(self.app_index), - name='%sadmin_app_list' % self.name), + name='app_list') ) # Add in each model's views. @@ -225,7 +229,7 @@ class AdminSite(object): return urlpatterns def urls(self): - return self.get_urls() + return self.get_urls(), self.app_name, self.name urls = property(urls) def password_change(self, request): @@ -233,8 +237,11 @@ class AdminSite(object): Handles the "change password" task -- both form display and validation. """ from django.contrib.auth.views import password_change - return password_change(request, - post_change_redirect='%spassword_change/done/' % self.root_path) + if self.root_path is not None: + url = '%spassword_change/done/' % self.root_path + else: + url = reverse('admin:password_change_done', current_app=self.name) + return password_change(request, post_change_redirect=url) def password_change_done(self, request): """ @@ -362,8 +369,9 @@ class AdminSite(object): 'root_path': self.root_path, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.name) return render_to_response(self.index_template or 'admin/index.html', context, - context_instance=template.RequestContext(request) + context_instance=context_instance ) index = never_cache(index) @@ -376,8 +384,9 @@ class AdminSite(object): 'root_path': self.root_path, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.name) return render_to_response(self.login_template or 'admin/login.html', context, - context_instance=template.RequestContext(request) + context_instance=context_instance ) def app_index(self, request, app_label, extra_context=None): @@ -419,9 +428,10 @@ class AdminSite(object): 'root_path': self.root_path, } context.update(extra_context or {}) + context_instance = template.RequestContext(request, current_app=self.name) return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label, 'admin/app_index.html'), context, - context_instance=template.RequestContext(request) + context_instance=context_instance ) def root(self, request, url): diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 8cab43963a..95257285eb 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -23,7 +23,30 @@ {% block branding %}{% endblock %} {% if user.is_authenticated and user.is_staff %} -
{% trans 'Welcome,' %} {% firstof user.first_name user.username %}. {% block userlinks %}{% url django-admindocs-docroot as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %}
+
+ {% trans 'Welcome,' %} + {% firstof user.first_name user.username %}. + {% block userlinks %} + {% url django-admindocs-docroot as docsroot %} + {% if docsroot %} + {% trans 'Documentation' %} / + {% endif %} + {% url admin:password_change as password_change_url %} + {% if password_change_url %} + + {% else %} + + {% endif %} + {% trans 'Change password' %} / + {% url admin:logout as logout_url %} + {% if logout_url %} + + {% else %} + + {% endif %} + {% trans 'Log out' %} + {% endblock %} +
{% endif %} {% block nav-global %}{% endblock %} diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 7ae5e647db..1a081bc293 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -125,7 +125,7 @@ class ForeignKeyRawIdWidget(forms.TextInput): if value: output.append(self.label_for_value(value)) return mark_safe(u''.join(output)) - + def base_url_parameters(self): params = {} if self.rel.limit_choices_to: @@ -137,14 +137,14 @@ class ForeignKeyRawIdWidget(forms.TextInput): v = str(v) items.append((k, v)) params.update(dict(items)) - return params - + return params + def url_parameters(self): from django.contrib.admin.views.main import TO_FIELD_VAR params = self.base_url_parameters() params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) return params - + def label_for_value(self, value): key = self.rel.get_related_field().name obj = self.rel.to._default_manager.get(**{key: value}) @@ -165,10 +165,10 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): else: value = '' return super(ManyToManyRawIdWidget, self).render(name, value, attrs) - + def url_parameters(self): return self.base_url_parameters() - + def label_for_value(self, value): return '' @@ -222,8 +222,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): rel_to = self.rel.to info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) try: - related_info = (self.admin_site.name,) + info - related_url = reverse('%sadmin_%s_%s_add' % related_info) + related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name) except NoReverseMatch: related_url = '../../../%s/%s/add/' % info self.widget.choices = self.choices diff --git a/django/contrib/admindocs/templates/admin_doc/index.html b/django/contrib/admindocs/templates/admin_doc/index.html index 242fc7339a..a8b21c330d 100644 --- a/django/contrib/admindocs/templates/admin_doc/index.html +++ b/django/contrib/admindocs/templates/admin_doc/index.html @@ -1,6 +1,6 @@ {% extends "admin/base_site.html" %} {% load i18n %} -{% block breadcrumbs %}{% endblock %} +{% block breadcrumbs %}{% endblock %} {% block title %}Documentation{% endblock %} {% block content %} diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 4f22fe0a0a..571f393ff8 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -22,11 +22,14 @@ class GenericSite(object): name = 'my site' def get_root_path(): - from django.contrib import admin try: - return urlresolvers.reverse(admin.site.root, args=['']) + return urlresolvers.reverse('admin:index') except urlresolvers.NoReverseMatch: - return getattr(settings, "ADMIN_SITE_ROOT_URL", "/admin/") + from django.contrib import admin + try: + return urlresolvers.reverse(admin.site.root, args=['']) + except urlresolvers.NoReverseMatch: + return getattr(settings, "ADMIN_SITE_ROOT_URL", "/admin/") def doc_index(request): if not utils.docutils_is_available: @@ -179,7 +182,7 @@ model_index = staff_member_required(model_index) def model_detail(request, app_label, model_name): if not utils.docutils_is_available: return missing_docutils_page(request) - + # Get the model class. try: app_mod = models.get_app(app_label) diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 2bfda990ff..eac66f4a83 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -19,6 +19,9 @@ class GeoManager(Manager): def centroid(self, *args, **kwargs): return self.get_query_set().centroid(*args, **kwargs) + def collect(self, *args, **kwargs): + return self.get_query_set().collect(*args, **kwargs) + def difference(self, *args, **kwargs): return self.get_query_set().difference(*args, **kwargs) diff --git a/django/contrib/gis/db/models/sql/query.py b/django/contrib/gis/db/models/sql/query.py index 5df15a88b1..094fc5815f 100644 --- a/django/contrib/gis/db/models/sql/query.py +++ b/django/contrib/gis/db/models/sql/query.py @@ -13,7 +13,9 @@ from django.contrib.gis.measure import Area, Distance ALL_TERMS = sql.constants.QUERY_TERMS.copy() ALL_TERMS.update(SpatialBackend.gis_terms) +# Pulling out other needed constants/routines to avoid attribute lookups. TABLE_NAME = sql.constants.TABLE_NAME +get_proxied_model = sql.query.get_proxied_model class GeoQuery(sql.Query): """ @@ -153,7 +155,9 @@ class GeoQuery(sql.Query): opts = self.model._meta aliases = set() only_load = self.deferred_to_columns() - proxied_model = opts.proxy and opts.proxy_for_model or 0 + # Skip all proxy to the root proxied model + proxied_model = get_proxied_model(opts) + if start_alias: seen = {None: start_alias} for field, model in opts.get_fields_with_model(): @@ -205,6 +209,10 @@ class GeoQuery(sql.Query): """ values = [] aliases = self.extra_select.keys() + if self.aggregates: + # If we have an aggregate annotation, must extend the aliases + # so their corresponding row values are included. + aliases.extend([None for i in xrange(len(self.aggregates))]) # Have to set a starting row number offset that is used for # determining the correct starting row index -- needed for diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index dcc5af1ac7..684d33e5e2 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -231,8 +231,12 @@ class RelatedGeoModelTest(unittest.TestCase): q = pickle.loads(q_str) self.assertEqual(GeoQuery, q.__class__) - def test12_count(self): - "Testing `Count` aggregate use with the `GeoManager`. See #11087." + # TODO: fix on Oracle -- get the following error because the SQL is ordered + # by a geometry object, which Oracle apparently doesn't like: + # ORA-22901: cannot compare nested table or VARRAY or LOB attributes of an object type + @no_oracle + def test12a_count(self): + "Testing `Count` aggregate use with the `GeoManager` on geo-fields." # Creating a new City, 'Fort Worth', that uses the same location # as Dallas. dallas = City.objects.get(name='Dallas') @@ -242,6 +246,8 @@ class RelatedGeoModelTest(unittest.TestCase): loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id) self.assertEqual(2, loc.num_cities) + def test12b_count(self): + "Testing `Count` aggregate use with the `GeoManager` on non geo-fields. See #11087." # Creating some data for the Book/Author non-geo models that # use GeoManager. See #11087. tp = Author.objects.create(name='Trevor Paglen') @@ -250,13 +256,19 @@ class RelatedGeoModelTest(unittest.TestCase): Book.objects.create(title='Blank Spots on the Map', author=tp) wp = Author.objects.create(name='William Patry') Book.objects.create(title='Patry on Copyright', author=wp) - + # Should only be one author (Trevor Paglen) returned by this query, and - # the annotation should have 3 for the number of books. + # the annotation should have 3 for the number of books. Also testing + # with a `GeoValuesQuerySet` (see #11489). qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1) + vqs = Author.objects.values('name').annotate(num_books=Count('books')).filter(num_books__gt=1) self.assertEqual(1, len(qs)) self.assertEqual(3, qs[0].num_books) + self.assertEqual(1, len(vqs)) + self.assertEqual(3, vqs[0]['num_books']) + # TODO: The phantom model does appear on Oracle. + @no_oracle def test13_select_related_null_fk(self): "Testing `select_related` on a nullable ForeignKey via `GeoManager`. See #11381." no_author = Book.objects.create(title='Without Author') diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 10e97bbcd5..4f9eb982e2 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -139,7 +139,7 @@ class RegexURLPattern(object): callback = property(_get_callback) class RegexURLResolver(object): - def __init__(self, regex, urlconf_name, default_kwargs=None): + def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None): # regex is a string representing a regular expression. # urlconf_name is a string representing the module containing URLconfs. self.regex = re.compile(regex, re.UNICODE) @@ -148,19 +148,29 @@ class RegexURLResolver(object): self._urlconf_module = self.urlconf_name self.callback = None self.default_kwargs = default_kwargs or {} - self._reverse_dict = MultiValueDict() + self.namespace = namespace + self.app_name = app_name + self._reverse_dict = None + self._namespace_dict = None + self._app_dict = None def __repr__(self): - return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern) + return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern) - def _get_reverse_dict(self): - if not self._reverse_dict: - lookups = MultiValueDict() - for pattern in reversed(self.url_patterns): - p_pattern = pattern.regex.pattern - if p_pattern.startswith('^'): - p_pattern = p_pattern[1:] - if isinstance(pattern, RegexURLResolver): + def _populate(self): + lookups = MultiValueDict() + namespaces = {} + apps = {} + for pattern in reversed(self.url_patterns): + p_pattern = pattern.regex.pattern + if p_pattern.startswith('^'): + p_pattern = p_pattern[1:] + if isinstance(pattern, RegexURLResolver): + if pattern.namespace: + namespaces[pattern.namespace] = (p_pattern, pattern) + if pattern.app_name: + apps.setdefault(pattern.app_name, []).append(pattern.namespace) + else: parent = normalize(pattern.regex.pattern) for name in pattern.reverse_dict: for matches, pat in pattern.reverse_dict.getlist(name): @@ -168,14 +178,36 @@ class RegexURLResolver(object): for piece, p_args in parent: new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches]) lookups.appendlist(name, (new_matches, p_pattern + pat)) - else: - bits = normalize(p_pattern) - lookups.appendlist(pattern.callback, (bits, p_pattern)) - lookups.appendlist(pattern.name, (bits, p_pattern)) - self._reverse_dict = lookups + for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items(): + namespaces[namespace] = (p_pattern + prefix, sub_pattern) + for app_name, namespace_list in pattern.app_dict.items(): + apps.setdefault(app_name, []).extend(namespace_list) + else: + bits = normalize(p_pattern) + lookups.appendlist(pattern.callback, (bits, p_pattern)) + lookups.appendlist(pattern.name, (bits, p_pattern)) + self._reverse_dict = lookups + self._namespace_dict = namespaces + self._app_dict = apps + + def _get_reverse_dict(self): + if self._reverse_dict is None: + self._populate() return self._reverse_dict reverse_dict = property(_get_reverse_dict) + def _get_namespace_dict(self): + if self._namespace_dict is None: + self._populate() + return self._namespace_dict + namespace_dict = property(_get_namespace_dict) + + def _get_app_dict(self): + if self._app_dict is None: + self._populate() + return self._app_dict + app_dict = property(_get_app_dict) + def resolve(self, path): tried = [] match = self.regex.search(path) @@ -261,12 +293,51 @@ class RegexURLResolver(object): def resolve(path, urlconf=None): return get_resolver(urlconf).resolve(path) -def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None): +def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None): + resolver = get_resolver(urlconf) args = args or [] kwargs = kwargs or {} + if prefix is None: prefix = get_script_prefix() - return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname, + + if not isinstance(viewname, basestring): + view = viewname + else: + parts = viewname.split(':') + parts.reverse() + view = parts[0] + path = parts[1:] + + resolved_path = [] + while path: + ns = path.pop() + + # Lookup the name to see if it could be an app identifier + try: + app_list = resolver.app_dict[ns] + # Yes! Path part matches an app in the current Resolver + if current_app and current_app in app_list: + # If we are reversing for a particular app, use that namespace + ns = current_app + elif ns not in app_list: + # The name isn't shared by one of the instances (i.e., the default) + # so just pick the first instance as the default. + ns = app_list[0] + except KeyError: + pass + + try: + extra, resolver = resolver.namespace_dict[ns] + resolved_path.append(ns) + prefix = prefix + extra + except KeyError, key: + if resolved_path: + raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path))) + else: + raise NoReverseMatch("%s is not a registered namespace" % key) + + return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view, *args, **kwargs))) def clear_url_caches(): diff --git a/django/template/context.py b/django/template/context.py index 0ccb5faecf..1c43387468 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -9,10 +9,11 @@ class ContextPopException(Exception): class Context(object): "A stack container for variable context" - def __init__(self, dict_=None, autoescape=True): + def __init__(self, dict_=None, autoescape=True, current_app=None): dict_ = dict_ or {} self.dicts = [dict_] self.autoescape = autoescape + self.current_app = current_app def __repr__(self): return repr(self.dicts) @@ -96,8 +97,8 @@ class RequestContext(Context): Additional processors can be specified as a list of callables using the "processors" keyword argument. """ - def __init__(self, request, dict=None, processors=None): - Context.__init__(self, dict) + def __init__(self, request, dict=None, processors=None, current_app=None): + Context.__init__(self, dict, current_app=current_app) if processors is None: processors = () else: diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 7d91cd6415..de746997ab 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -367,17 +367,17 @@ class URLNode(Node): # {% url ... as var %} construct in which cause return nothing. url = '' try: - url = reverse(self.view_name, args=args, kwargs=kwargs) + url = reverse(self.view_name, args=args, kwargs=kwargs, current_app=context.current_app) except NoReverseMatch, e: if settings.SETTINGS_MODULE: project_name = settings.SETTINGS_MODULE.split('.')[0] try: url = reverse(project_name + '.' + self.view_name, - args=args, kwargs=kwargs) + args=args, kwargs=kwargs, current_app=context.current_app) except NoReverseMatch: if self.asvar is None: # Re-raise the original exception, not the one with - # the path relative to the project. This makes a + # the path relative to the project. This makes a # better error message. raise e else: diff --git a/docs/howto/deployment/modwsgi.txt b/docs/howto/deployment/modwsgi.txt index 902e312551..8bfbfa74f4 100644 --- a/docs/howto/deployment/modwsgi.txt +++ b/docs/howto/deployment/modwsgi.txt @@ -1,69 +1,118 @@ -.. _howto-deployment-modwsgi: - -========================================== -How to use Django with Apache and mod_wsgi -========================================== - -Deploying Django with Apache_ and `mod_wsgi`_ is the recommended way to get -Django into production. - -.. _Apache: http://httpd.apache.org/ -.. _mod_wsgi: http://code.google.com/p/modwsgi/ - -mod_wsgi is an Apache module which can be used to host any Python application -which supports the `Python WSGI interface`_, including Django. Django will work -with any version of Apache which supports mod_wsgi. - -.. _python wsgi interface: http://www.python.org/dev/peps/pep-0333/ - -The `official mod_wsgi documentation`_ is fantastic; it's your source for all -the details about how to use mod_wsgi. You'll probably want to start with the -`installation and configuration documentation`_. - -.. _official mod_wsgi documentation: http://code.google.com/p/modwsgi/ -.. _installation and configuration documentation: http://code.google.com/p/modwsgi/wiki/InstallationInstructions - -Basic Configuration -=================== - -Once you've got mod_wsgi installed and activated, edit your ``httpd.conf`` file -and add:: - - WSGIScriptAlias / /path/to/mysite/apache/django.wsgi - -The first bit above is the url you want to be serving your application at (``/`` -indicates the root url), and the second is the location of a "WSGI file" -- see -below -- on your system, usually inside of your project. This tells Apache -to serve any request below the given URL using the WSGI application defined by that file. - -Next we'll need to actually create this WSGI application, so create the file -mentioned in the second part of ``WSGIScriptAlias`` and add:: - - import os - import sys - - os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' - - import django.core.handlers.wsgi - application = django.core.handlers.wsgi.WSGIHandler() - -If your project is not on your ``PYTHONPATH`` by default you can add:: - - sys.path.append('/usr/local/django') - -just above the final ``import`` line to place your project on the path. Remember to -replace 'mysite.settings' with your correct settings file, and '/usr/local/django' -with your own project's location. - -See the :ref:`Apache/mod_python documentation` for -directions on serving static media, and the `mod_wsgi documentation`_ for an -explanation of other directives and configuration options you can use. - -Details -======= - -For more details, see the `mod_wsgi documentation`_, which explains the above in -more detail, and walks through all the various options you've got when deploying -under mod_wsgi. - -.. _mod_wsgi documentation: http://code.google.com/p/modwsgi/wiki/IntegrationWithDjango +.. _howto-deployment-modwsgi: + +========================================== +How to use Django with Apache and mod_wsgi +========================================== + +Deploying Django with Apache_ and `mod_wsgi`_ is the recommended way to get +Django into production. + +.. _Apache: http://httpd.apache.org/ +.. _mod_wsgi: http://code.google.com/p/modwsgi/ + +mod_wsgi is an Apache module which can be used to host any Python application +which supports the `Python WSGI interface`_, including Django. Django will work +with any version of Apache which supports mod_wsgi. + +.. _python wsgi interface: http://www.python.org/dev/peps/pep-0333/ + +The `official mod_wsgi documentation`_ is fantastic; it's your source for all +the details about how to use mod_wsgi. You'll probably want to start with the +`installation and configuration documentation`_. + +.. _official mod_wsgi documentation: http://code.google.com/p/modwsgi/ +.. _installation and configuration documentation: http://code.google.com/p/modwsgi/wiki/InstallationInstructions + +Basic Configuration +=================== + +Once you've got mod_wsgi installed and activated, edit your ``httpd.conf`` file +and add:: + + WSGIScriptAlias / /path/to/mysite/apache/django.wsgi + +The first bit above is the url you want to be serving your application at (``/`` +indicates the root url), and the second is the location of a "WSGI file" -- see +below -- on your system, usually inside of your project. This tells Apache +to serve any request below the given URL using the WSGI application defined by that file. + +Next we'll need to actually create this WSGI application, so create the file +mentioned in the second part of ``WSGIScriptAlias`` and add:: + + import os + import sys + + os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings' + + import django.core.handlers.wsgi + application = django.core.handlers.wsgi.WSGIHandler() + +If your project is not on your ``PYTHONPATH`` by default you can add:: + + sys.path.append('/usr/local/django') + +just above the final ``import`` line to place your project on the path. Remember to +replace 'mysite.settings' with your correct settings file, and '/usr/local/django' +with your own project's location. + +Serving media files +=================== + +Django doesn't serve media files itself; it leaves that job to whichever Web +server you choose. + +We recommend using a separate Web server -- i.e., one that's not also running +Django -- for serving media. Here are some good choices: + + * lighttpd_ + * Nginx_ + * TUX_ + * A stripped-down version of Apache_ + * Cherokee_ + +If, however, you have no option but to serve media files on the same Apache +``VirtualHost`` as Django, you can set up Apache to serve some URLs as +static media, and others using the mod_wsgi interface to Django. + +This example sets up Django at the site root, but explicitly serves ``robots.txt``, +``favicon.ico``, any CSS file, and anything in the ``/media/`` URL space as a static +file. All other URLs will be served using mod_wsgi:: + + Alias /robots.txt /usr/local/wsgi/static/robots.txt + Alias /favicon.ico /usr/local/wsgi/static/favicon.ico + + AliasMatch /([^/]*\.css) /usr/local/wsgi/static/styles/$1 + + Alias /media/ /usr/local/wsgi/static/media/ + + + Order deny,allow + Allow from all + + + WSGIScriptAlias / /usr/local/wsgi/scripts/django.wsgi + + + Order allow,deny + Allow from all + + +.. _lighttpd: http://www.lighttpd.net/ +.. _Nginx: http://wiki.codemongers.com/Main +.. _TUX: http://en.wikipedia.org/wiki/TUX_web_server +.. _Apache: http://httpd.apache.org/ +.. _Cherokee: http://www.cherokee-project.com/ + +More details on configuring a mod_wsgi site to serve static files can be found +in the mod_wsgi documentation on `hosting static files`_. + +.. _hosting static files: http://code.google.com/p/modwsgi/wiki/ConfigurationGuidelines#Hosting_Of_Static_Files + +Details +======= + +For more details, see the `mod_wsgi documentation on Django integration`_, +which explains the above in more detail, and walks through all the various +options you've got when deploying under mod_wsgi. + +.. _mod_wsgi documentation on Django integration: http://code.google.com/p/modwsgi/wiki/IntegrationWithDjango diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index f4ef5f76fe..687407a284 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -365,7 +365,7 @@ That takes care of setting ``handler404`` in the current module. As you can see in ``django/conf/urls/defaults.py``, ``handler404`` is set to :func:`django.views.defaults.page_not_found` by default. -Three more things to note about 404 views: +Four more things to note about 404 views: * If :setting:`DEBUG` is set to ``True`` (in your settings module) then your 404 view will never be used (and thus the ``404.html`` template will never diff --git a/docs/ref/contrib/admin/_images/article_actions.png b/docs/ref/contrib/admin/_images/article_actions.png index 254a8ad557..df4ab8f1ec 100644 Binary files a/docs/ref/contrib/admin/_images/article_actions.png and b/docs/ref/contrib/admin/_images/article_actions.png differ diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 394ebd1f24..584672e4f0 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -762,12 +762,19 @@ documented in :ref:`topics-http-urls`:: anything, so you'll usually want to prepend your custom URLs to the built-in ones. -Note, however, that the ``self.my_view`` function registered above will *not* -have any permission check done; it'll be accessible to the general public. Since -this is usually not what you want, Django provides a convience wrapper to check -permissions. This wrapper is :meth:`AdminSite.admin_view` (i.e. -``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it like -so:: +However, the ``self.my_view`` function registered above suffers from two +problems: + + * It will *not* perform and permission checks, so it will be accessible to + the general public. + * It will *not* provide any header details to prevent caching. This means if + the page retrieves data from the database, and caching middleware is + active, the page could show outdated information. + +Since this is usually not what you want, Django provides a convenience wrapper +to check permissions and mark the view as non-cacheable. This wrapper is +:meth:`AdminSite.admin_view` (i.e. ``self.admin_site.admin_view`` inside a +``ModelAdmin`` instance); use it like so:: class MyModelAdmin(admin.ModelAdmin): def get_urls(self): @@ -781,7 +788,14 @@ Notice the wrapped view in the fifth line above:: (r'^my_view/$', self.admin_site.admin_view(self.my_view)) -This wrapping will protect ``self.my_view`` from unauthorized access. +This wrapping will protect ``self.my_view`` from unauthorized access and will +apply the ``django.views.decorators.cache.never_cache`` decorator to make sure +it is not cached if the cache middleware is active. + +If the page is cacheable, but you still want the permission check to be performed, +you can pass a ``cacheable=True`` argument to :meth:`AdminSite.admin_view`:: + + (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs) @@ -1228,7 +1242,7 @@ or :attr:`AdminSite.login_template` properties. ``AdminSite`` objects ===================== -.. class:: AdminSite +.. class:: AdminSite(name=None) A Django administrative site is represented by an instance of ``django.contrib.admin.sites.AdminSite``; by default, an instance of @@ -1242,6 +1256,14 @@ or add anything you like. Then, simply create an instance of your Python class), and register your models and ``ModelAdmin`` subclasses with it instead of using the default. +.. versionadded:: 1.1 + +When constructing an instance of an ``AdminSite``, you are able to provide +a unique instance name using the ``name`` argument to the constructor. This +instance name is used to identify the instance, especially when +:ref:`reversing admin URLs `. If no instance name is +provided, a default instance name of ``admin`` will be used. + ``AdminSite`` attributes ------------------------ @@ -1339,10 +1361,10 @@ a pattern for your new view. .. note:: Any view you render that uses the admin templates, or extends the base - admin template, should include in it's context a variable named - ``admin_site`` that contains the name of the :class:`AdminSite` instance. For - :class:`AdminSite` instances, this means ``self.name``; for :class:`ModelAdmin` - instances, this means ``self.admin_site.name``. + admin template, should provide the ``current_app`` argument to + ``RequestContext`` or ``Context`` when rendering the template. It should + be set to either ``self.name`` if your view is on an ``AdminSite`` or + ``self.admin_site.name`` if your view is on a ``ModelAdmin``. .. _admin-reverse-urls: @@ -1356,37 +1378,31 @@ accessible using Django's :ref:`URL reversing system `. The :class:`AdminSite` provides the following named URL patterns: - ====================== =============================== ============= - Page URL name Parameters - ====================== =============================== ============= - Index ``admin_index`` - Logout ``admin_logout`` - Password change ``admin_password_change`` - Password change done ``admin_password_change_done`` - i18n javascript ``admin_jsi18n`` - Application index page ``admin_app_list`` ``app_label`` - ====================== =============================== ============= - -These names will be prefixed with the name of the :class:`AdminSite` instance, -plus an underscore. For example, if your :class:`AdminSite` was named -``custom``, then the Logout view would be served using a URL with the name -``custom_admin_logout``. The default :class:`AdminSite` doesn't use a prefix -in it's URL names. + ====================== ======================== ============= + Page URL name Parameters + ====================== ======================== ============= + Index ``index`` + Logout ``logout`` + Password change ``password_change`` + Password change done ``password_change_done`` + i18n javascript ``jsi18n`` + Application index page ``app_list`` ``app_label`` + ====================== ======================== ============= Each :class:`ModelAdmin` instance provides an additional set of named URLs: - ====================== ===================================================== ============= - Page URL name Parameters - ====================== ===================================================== ============= - Changelist ``admin_{{ app_label }}_{{ model_name }}_changelist`` - Add ``admin_{{ app_label }}_{{ model_name }}_add`` - History ``admin_{{ app_label }}_{{ model_name }}_history`` ``object_id`` - Delete ``admin_{{ app_label }}_{{ model_name }}_delete`` ``object_id`` - Change ``admin_{{ app_label }}_{{ model_name }}_change`` ``object_id`` - ====================== ===================================================== ============= + ====================== =============================================== ============= + Page URL name Parameters + ====================== =============================================== ============= + Changelist ``{{ app_label }}_{{ model_name }}_changelist`` + Add ``{{ app_label }}_{{ model_name }}_add`` + History ``{{ app_label }}_{{ model_name }}_history`` ``object_id`` + Delete ``{{ app_label }}_{{ model_name }}_delete`` ``object_id`` + Change ``{{ app_label }}_{{ model_name }}_change`` ``object_id`` + ====================== =============================================== ============= -Again, these names will be prefixed by the name of the :class:`AdminSite` in -which they are deployed. +These named URLs are registered with the application namespace ``admin``, and +with an instance namespace corresponding to the name of the Site instance. So - if you wanted to get a reference to the Change view for a particular ``Choice`` object (from the polls application) in the default admin, you would @@ -1394,8 +1410,16 @@ call:: >>> from django.core import urlresolvers >>> c = Choice.objects.get(...) - >>> change_url = urlresolvers.reverse('admin_polls_choice_change', args=(c.id,)) + >>> change_url = urlresolvers.reverse('admin:polls_choice_change', args=(c.id,)) -However, if the admin instance was named ``custom``, you would need to call:: +This will find the first registered instance of the admin application (whatever the instance +name), and resolve to the view for changing ``poll.Choice`` instances in that instance. - >>> change_url = urlresolvers.reverse('custom_admin_polls_choice_change', args=(c.id,)) +If you want to find a URL in a specific admin instance, provide the name of that instance +as a ``current_app`` hint to the reverse call. For example, if you specifically wanted +the admin view from the admin instance named ``custom``, you would need to call:: + + >>> change_url = urlresolvers.reverse('custom:polls_choice_change', args=(c.id,)) + +For more details, see the documentation on :ref:`reversing namespaced URLs +`. diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index e532971179..4bb6a7c444 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -275,7 +275,7 @@ For each field, we describe the default widget used if you don't specify * Default widget: ``CheckboxInput`` * Empty value: ``False`` * Normalizes to: A Python ``True`` or ``False`` value. - * Validates that the check box is checked (i.e. the value is ``True``) if + * Validates that the value is ``True`` (e.g. the check box is checked) if the field has ``required=True``. * Error message keys: ``required`` @@ -287,9 +287,10 @@ For each field, we describe the default widget used if you don't specify .. note:: Since all ``Field`` subclasses have ``required=True`` by default, the - validation condition here is important. If you want to include a checkbox - in your form that can be either checked or unchecked, you must remember to - pass in ``required=False`` when creating the ``BooleanField``. + validation condition here is important. If you want to include a boolean + in your form that can be either ``True`` or ``False`` (e.g. a checked or + unchecked checkbox), you must remember to pass in ``required=False`` when + creating the ``BooleanField``. ``CharField`` ~~~~~~~~~~~~~ @@ -328,7 +329,7 @@ Takes one extra required argument: An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. - + ``TypedChoiceField`` ~~~~~~~~~~~~~~~~~~~~ @@ -437,7 +438,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: ``min_value``, ``max_digits``, ``max_decimal_places``, ``max_whole_digits`` -Takes four optional arguments: +Takes four optional arguments: .. attribute:: DecimalField.max_value .. attribute:: DecimalField.min_value @@ -449,7 +450,7 @@ Takes four optional arguments: The maximum number of digits (those before the decimal point plus those after the decimal point, with leading zeros stripped) permitted in the value. - + .. attribute:: DecimalField.decimal_places The maximum number of decimal places permitted. @@ -522,18 +523,18 @@ extra arguments; only ``path`` is required: A regular expression pattern; only files with names matching this expression will be allowed as choices. -``FloatField`` -~~~~~~~~~~~~~~ +``FloatField`` +~~~~~~~~~~~~~~ - * Default widget: ``TextInput`` - * Empty value: ``None`` - * Normalizes to: A Python float. - * Validates that the given value is an float. Leading and trailing - whitespace is allowed, as in Python's ``float()`` function. - * Error message keys: ``required``, ``invalid``, ``max_value``, - ``min_value`` - -Takes two optional arguments for validation, ``max_value`` and ``min_value``. + * Default widget: ``TextInput`` + * Empty value: ``None`` + * Normalizes to: A Python float. + * Validates that the given value is an float. Leading and trailing + whitespace is allowed, as in Python's ``float()`` function. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value`` + +Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. ``ImageField`` @@ -779,10 +780,10 @@ example:: (which is ``"---------"`` by default) with the ``empty_label`` attribute, or you can disable the empty label entirely by setting ``empty_label`` to ``None``:: - + # A custom empty label field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)") - + # No empty label field2 = forms.ModelChoiceField(queryset=..., empty_label=None) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 348486b341..f78ebc506a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -668,7 +668,7 @@ of the arguments is required, but you should use at least one of them. The resulting SQL of the above example would be:: - SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) + SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) AS entry_count FROM blog_blog; Note that the parenthesis required by most database engines around diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 05097b7e59..e3260a96f8 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -86,9 +86,16 @@ Rendering a context Once you have a compiled ``Template`` object, you can render a context -- or multiple contexts -- with it. The ``Context`` class lives at -``django.template.Context``, and the constructor takes one (optional) -argument: a dictionary mapping variable names to variable values. Call the -``Template`` object's ``render()`` method with the context to "fill" the +``django.template.Context``, and the constructor takes two (optional) +arguments: + + * A dictionary mapping variable names to variable values. + + * The name of the current application. This application name is used + to help :ref:`resolve namespaced URLs`. + If you're not using namespaced URLs, you can ignore this argument. + +Call the ``Template`` object's ``render()`` method with the context to "fill" the template:: >>> from django.template import Context, Template @@ -549,13 +556,13 @@ Here are the template loaders that come with Django: Note that the loader performs an optimization when it is first imported: It caches a list of which :setting:`INSTALLED_APPS` packages have a ``templates`` subdirectory. - + This loader is enabled by default. ``django.template.loaders.eggs.load_template_source`` Just like ``app_directories`` above, but it loads templates from Python eggs rather than from the filesystem. - + This loader is disabled by default. Django uses the template loaders in order according to the diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index aedad6562f..a2f8b9f8b3 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -795,6 +795,16 @@ missing. In practice you'll use this to link to views that are optional:: Link to optional stuff {% endif %} +.. versionadded:: 1.1 + +If you'd like to retrieve a namespaced URL, specify the fully qualified name:: + + {% url myapp:view-name %} + +This will follow the normal :ref:`namespaced URL resolution strategy +`, including using any hints provided +by the context as to the current application. + .. templatetag:: widthratio widthratio diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 8e90b54ced..e6146aeaba 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -86,9 +86,9 @@ displayed. Formset validation ------------------ -Validation with a formset is about identical to a regular ``Form``. There is +Validation with a formset is almost identical to a regular ``Form``. There is an ``is_valid`` method on the formset to provide a convenient way to validate -each form in the formset:: +all forms in the formset:: >>> ArticleFormSet = formset_factory(ArticleForm) >>> formset = ArticleFormSet({}) @@ -97,22 +97,25 @@ each form in the formset:: We passed in no data to the formset which is resulting in a valid form. The formset is smart enough to ignore extra forms that were not changed. If we -attempt to provide an article, but fail to do so:: +provide an invalid article:: >>> data = { - ... 'form-TOTAL_FORMS': u'1', - ... 'form-INITIAL_FORMS': u'1', + ... 'form-TOTAL_FORMS': u'2', + ... 'form-INITIAL_FORMS': u'0', ... 'form-0-title': u'Test', - ... 'form-0-pub_date': u'', + ... 'form-0-pub_date': u'16 June 1904', + ... 'form-1-title': u'Test', + ... 'form-1-pub_date': u'', # <-- this date is missing but required ... } >>> formset = ArticleFormSet(data) >>> formset.is_valid() False >>> formset.errors - [{'pub_date': [u'This field is required.']}] + [{}, {'pub_date': [u'This field is required.']}] -As we can see the formset properly performed validation and gave us the -expected errors. +As we can see, ``formset.errors`` is a list whose entries correspond to the +forms in the formset. Validation was performed for each of the two forms, and +the expected error message appears for the second item. .. _understanding-the-managementform: @@ -155,20 +158,40 @@ Custom formset validation ~~~~~~~~~~~~~~~~~~~~~~~~~ A formset has a ``clean`` method similar to the one on a ``Form`` class. This -is where you define your own validation that deals at the formset level:: +is where you define your own validation that works at the formset level:: >>> from django.forms.formsets import BaseFormSet >>> class BaseArticleFormSet(BaseFormSet): ... def clean(self): - ... raise forms.ValidationError, u'An error occured.' + ... """Checks that no two articles have the same title.""" + ... if any(self.errors): + ... # Don't bother validating the formset unless each form is valid on its own + ... return + ... titles = [] + ... for i in range(0, self.total_form_count()): + ... form = self.forms[i] + ... title = form.cleaned_data['title'] + ... if title in titles: + ... raise forms.ValidationError, "Articles in a set must have distinct titles." + ... titles.append(title) >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) - >>> formset = ArticleFormSet({}) + >>> data = { + ... 'form-TOTAL_FORMS': u'2', + ... 'form-INITIAL_FORMS': u'0', + ... 'form-0-title': u'Test', + ... 'form-0-pub_date': u'16 June 1904', + ... 'form-1-title': u'Test', + ... 'form-1-pub_date': u'23 June 1912', + ... } + >>> formset = ArticleFormSet(data) >>> formset.is_valid() False + >>> formset.errors + [{}, {}] >>> formset.non_form_errors() - [u'An error occured.'] + [u'Articles in a set must have distinct titles.'] The formset ``clean`` method is called after all the ``Form.clean`` methods have been called. The errors will be found using the ``non_form_errors()`` diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 17978d4328..b2e99dce7f 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -4,6 +4,8 @@ URL dispatcher ============== +.. module:: django.core.urlresolvers + A clean, elegant URL scheme is an important detail in a high-quality Web application. Django lets you design URLs however you want, with no framework limitations. @@ -182,11 +184,13 @@ your URLconf. This gives your module access to these objects: patterns -------- +.. function:: patterns(prefix, pattern_description, ...) + A function that takes a prefix, and an arbitrary number of URL patterns, and returns a list of URL patterns in the format Django needs. The first argument to ``patterns()`` is a string ``prefix``. See -"The view prefix" below. +`The view prefix`_ below. The remaining arguments should be tuples in this format:: @@ -222,6 +226,8 @@ url .. versionadded:: 1.0 +.. function:: url(regex, view, kwargs=None, name=None, prefix='') + You can use the ``url()`` function, instead of a tuple, as an argument to ``patterns()``. This is convenient if you want to specify a name without the optional extra arguments dictionary. For example:: @@ -244,6 +250,8 @@ The ``prefix`` parameter has the same meaning as the first argument to handler404 ---------- +.. data:: handler404 + A string representing the full Python import path to the view that should be called if none of the URL patterns match. @@ -253,6 +261,8 @@ value should suffice. handler500 ---------- +.. data:: handler500 + A string representing the full Python import path to the view that should be called in case of server errors. Server errors happen when you have runtime errors in view code. @@ -263,12 +273,14 @@ value should suffice. include ------- +.. function:: include() + A function that takes a full Python import path to another URLconf module that should be "included" in this place. .. versionadded:: 1.1 -:meth:``include`` also accepts as an argument an iterable that returns URL +:func:`include` also accepts as an argument an iterable that returns URL patterns. See `Including other URLconfs`_ below. @@ -400,7 +412,7 @@ further processing. .. versionadded:: 1.1 -Another posibility is to include additional URL patterns not by specifying the +Another possibility is to include additional URL patterns not by specifying the URLconf Python module defining them as the `include`_ argument but by using directly the pattern list as returned by `patterns`_ instead. For example:: @@ -417,6 +429,13 @@ directly the pattern list as returned by `patterns`_ instead. For example:: (r'^credit/', include(extra_patterns)), ) +This approach can be seen in use when you deploy an instance of the Django +Admin application. The Django Admin is deployed as instances of a +:class:`AdminSite`; each :class:`AdminSite` instance has an attribute +``urls`` that returns the url patterns available to that instance. It is this +attribute that you ``include()`` into your projects ``urlpatterns`` when you +deploy the admin instance. + .. _`Django Web site`: http://www.djangoproject.com/ Captured parameters @@ -439,6 +458,58 @@ the following example is valid:: In the above example, the captured ``"username"`` variable is passed to the included URLconf, as expected. +.. _topics-http-defining-url-namespaces: + +Defining URL Namespaces +----------------------- + +When you need to deploy multiple instances of a single application, it can be +helpful to be able to differentiate between instances. This is especially +important when using _`named URL patterns `, since +multiple instances of a single application will share named URLs. Namespaces +provide a way to tell these named URLs apart. + +A URL namespace comes in two parts, both of which are strings: + + * An **application namespace**. This describes the name of the application + that is being deployed. Every instance of a single application will have + the same application namespace. For example, Django's admin application + has the somewhat predictable application namespace of ``admin``. + + * An **instance namespace**. This identifies a specific instance of an + application. Instance namespaces should be unique across your entire + project. However, an instance namespace can be the same as the + application namespace. This is used to specify a default instance of an + application. For example, the default Django Admin instance has an + instance namespace of ``admin``. + +URL Namespaces can be specified in two ways. + +Firstly, you can provide the application and instance namespace as arguments +to ``include()`` when you construct your URL patterns. For example,:: + + (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')), + +This will include the URLs defined in ``apps.help.urls`` into the application +namespace ``bar``, with the instance namespace ``foo``. + +Secondly, you can include an object that contains embedded namespace data. If +you ``include()`` a ``patterns`` object, that object will be added to the +global namespace. However, you can also ``include()`` an object that contains +a 3-tuple containing:: + + (, , ) + +This will include the nominated URL patterns into the given application and +instance namespace. For example, the ``urls`` attribute of Django's +:class:`AdminSite` object returns a 3-tuple that contains all the patterns in +an admin site, plus the name of the admin instance, and the application +namespace ``admin``. + +Once you have defined namespaced URLs, you can reverse them. For details on +reversing namespaced urls, see the documentation on :ref:`reversing namespaced +URLs `. + Passing extra options to view functions ======================================= @@ -613,6 +684,86 @@ not restricted to valid Python names. name, will decrease the chances of collision. We recommend something like ``myapp-comment`` instead of ``comment``. +.. _topics-http-reversing-url-namespaces: + +URL namespaces +-------------- + +.. versionadded:: 1.1 + +Namespaced URLs are specified using the ``:`` operator. For example, the main +index page of the admin application is referenced using ``admin:index``. This +indicates a namespace of ``admin``, and a named URL of ``index``. + +Namespaces can also be nested. The named URL ``foo:bar:whiz`` would look for +a pattern named ``whiz`` in the namespace ``bar`` that is itself defined within +the top-level namespace ``foo``. + +When given a namespaced URL (e.g. ``myapp:index``) to resolve, Django splits +the fully qualified name into parts, and then tries the following lookup: + + 1. First, Django looks for a matching application namespace (in this + example, ``myapp``). This will yield a list of instances of that + application. + + 2. If there is a ``current`` application defined, Django finds and returns + the URL resolver for that instance. The ``current`` can be specified + as an attribute on the template context - applications that expect to + have multiple deployments should set the ``current_app`` attribute on + any ``Context`` or ``RequestContext`` that is used to render a + template. + + The current application can also be specified manually as an argument + to the :func:`reverse()` function. + + 3. If there is no current application. Django looks for a default + application instance. The default application instance is the instance + that has an instance namespace matching the application namespace (in + this example, an instance of the ``myapp`` called ``myapp``). + + 4. If there is no default application instance, Django will pick the first + deployed instance of the application, whatever its instance name may be. + + 5. If the provided namespace doesn't match an application namespace in + step 2, Django will attempt a direct lookup of the namespace as an + instance namespace. + +If there are nested namespaces, these steps are repeated for each part of the +namespace until only the view name is unresolved. The view name will then be +resolved into a URL in the namespace that has been found. + +To show this resolution strategy in action, consider an example of two instances +of ``myapp``: one called ``foo``, and one called ``bar``. ``myapp`` has a main +index page with a URL named `index`. Using this setup, the following lookups are +possible: + + * If one of the instances is current - say, if we were rendering a utility page + in the instance ``bar`` - ``myapp:index`` will resolve to the index page of + the instance ``bar``. + + * If there is no current instance - say, if we were rendering a page + somewhere else on the site - ``myapp:index`` will resolve to the first + registered instance of ``myapp``. Since there is no default instance, + the first instance of ``myapp`` that is registered will be used. This could + be ``foo`` or ``bar``, depending on the order they are introduced into the + urlpatterns of the project. + + * ``foo:index`` will always resolve to the index page of the instance ``foo``. + +If there was also a default instance - i.e., an instance named `myapp` - the +following would happen: + + * If one of the instances is current - say, if we were rendering a utility page + in the instance ``bar`` - ``myapp:index`` will resolve to the index page of + the instance ``bar``. + + * If there is no current instance - say, if we were rendering a page somewhere + else on the site - ``myapp:index`` will resolve to the index page of the + default instance. + + * ``foo:index`` will again resolve to the index page of the instance ``foo``. + + Utility methods =============== @@ -623,8 +774,7 @@ If you need to use something similar to the :ttag:`url` template tag in your code, Django provides the following method (in the ``django.core.urlresolvers`` module): -.. currentmodule:: django.core.urlresolvers -.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None) +.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None) ``viewname`` is either the function name (either a function reference, or the string version of the name, if you used that form in ``urlpatterns``) or the @@ -646,6 +796,14 @@ vertical bar (``"|"``) character. You can quite happily use such patterns for matching against incoming URLs and sending them off to views, but you cannot reverse such patterns. +.. versionadded:: 1.1 + +The ``current_app`` argument allows you to provide a hint to the resolver +indicating the application to which the currently executing view belongs. +This ``current_app`` argument is used as a hint to resolve application +namespaces into URLs on specific application instances, according to the +:ref:`namespaced URL resolution strategy `. + .. admonition:: Make sure your views are all correct As part of working out which URL names map to which patterns, the @@ -665,7 +823,6 @@ resolve() The :func:`django.core.urlresolvers.resolve` function can be used for resolving URL paths to the corresponding view functions. It has the following signature: -.. currentmodule:: django.core.urlresolvers .. function:: resolve(path, urlconf=None) ``path`` is the URL path you want to resolve. As with ``reverse()`` above, you diff --git a/docs/topics/i18n.txt b/docs/topics/i18n.txt index 7bf51c11c5..c5f4ab6481 100644 --- a/docs/topics/i18n.txt +++ b/docs/topics/i18n.txt @@ -959,11 +959,11 @@ Using the JavaScript translation catalog To use the catalog, just pull in the dynamically generated script like this:: - + -This is how the admin fetches the translation catalog from the server. When the -catalog is loaded, your JavaScript code can use the standard ``gettext`` -interface to access it:: +This uses reverse URL lookup to find the URL of the JavaScript catalog view. +When the catalog is loaded, your JavaScript code can use the standard +``gettext`` interface to access it:: document.write(gettext('this is to be translated')); diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 78a0796308..d066ca086a 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -691,7 +691,13 @@ arguments at time of construction: user accounts that are valid on your production site will not work under test conditions. You'll need to create users as part of the test suite -- either manually (using the Django model API) or with a test - fixture. + fixture. Remember that if you want your test user to have a password, + you can't set the user's password by setting the password attribute + directly -- you must use the + :meth:`~django.contrib.auth.models.User.set_password()` function to + store a correctly hashed password. Alternatively, you can use the + :meth:`~django.contrib.auth.models.UserManager.create_user` helper + method to create a new user with a correctly hashed password. .. method:: Client.logout() diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py index 70e87ebcfe..80570ea51d 100644 --- a/tests/regressiontests/admin_views/customadmin.py +++ b/tests/regressiontests/admin_views/customadmin.py @@ -10,19 +10,19 @@ import models class Admin2(admin.AdminSite): login_template = 'custom_admin/login.html' index_template = 'custom_admin/index.html' - + # A custom index view. def index(self, request, extra_context=None): return super(Admin2, self).index(request, {'foo': '*bar*'}) - + def get_urls(self): return patterns('', (r'^my_view/$', self.admin_view(self.my_view)), ) + super(Admin2, self).get_urls() - + def my_view(self, request): return HttpResponse("Django is a magical pony!") - + site = Admin2(name="admin2") site.register(models.Article, models.ArticleAdmin) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 216d4d2904..9d24c017b6 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -10,6 +10,7 @@ from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.util import quote from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.utils.cache import get_max_age from django.utils.html import escape # local test models @@ -235,6 +236,11 @@ class AdminViewBasicTest(TestCase): response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'}) self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + def testLogoutAndPasswordChangeURLs(self): + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) + self.failIf('' % self.urlbit not in response.content) + self.failIf('' % self.urlbit not in response.content) + def testNamedGroupFieldChoicesChangeList(self): """ Ensures the admin changelist shows correct values in the relevant column @@ -1590,3 +1596,76 @@ class AdminInlineTests(TestCase): self.failUnlessEqual(Category.objects.get(id=2).order, 13) self.failUnlessEqual(Category.objects.get(id=3).order, 1) self.failUnlessEqual(Category.objects.get(id=4).order, 0) + + +class NeverCacheTests(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testAdminIndex(self): + "Check the never-cache status of the main index" + response = self.client.get('/test_admin/admin/') + self.failUnlessEqual(get_max_age(response), 0) + + def testAppIndex(self): + "Check the never-cache status of an application index" + response = self.client.get('/test_admin/admin/admin_views/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelIndex(self): + "Check the never-cache status of a model index" + response = self.client.get('/test_admin/admin/admin_views/fabric/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelAdd(self): + "Check the never-cache status of a model add page" + response = self.client.get('/test_admin/admin/admin_views/fabric/add/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelView(self): + "Check the never-cache status of a model edit page" + response = self.client.get('/test_admin/admin/admin_views/section/1/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelHistory(self): + "Check the never-cache status of a model history page" + response = self.client.get('/test_admin/admin/admin_views/section/1/history/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelDelete(self): + "Check the never-cache status of a model delete page" + response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') + self.failUnlessEqual(get_max_age(response), 0) + + def testLogin(self): + "Check the never-cache status of login views" + self.client.logout() + response = self.client.get('/test_admin/admin/') + self.failUnlessEqual(get_max_age(response), 0) + + def testLogout(self): + "Check the never-cache status of logout view" + response = self.client.get('/test_admin/admin/logout/') + self.failUnlessEqual(get_max_age(response), 0) + + def testPasswordChange(self): + "Check the never-cache status of the password change view" + self.client.logout() + response = self.client.get('/test_admin/password_change/') + self.failUnlessEqual(get_max_age(response), None) + + def testPasswordChangeDone(self): + "Check the never-cache status of the password change done view" + response = self.client.get('/test_admin/admin/password_change/done/') + self.failUnlessEqual(get_max_age(response), None) + + def testJsi18n(self): + "Check the never-cache status of the Javascript i18n view" + response = self.client.get('/test_admin/jsi18n/') + self.failUnlessEqual(get_max_age(response), None) + diff --git a/tests/regressiontests/admin_widgets/widgetadmin.py b/tests/regressiontests/admin_widgets/widgetadmin.py index bd68954a70..9257c306c9 100644 --- a/tests/regressiontests/admin_widgets/widgetadmin.py +++ b/tests/regressiontests/admin_widgets/widgetadmin.py @@ -19,7 +19,7 @@ class CarTireAdmin(admin.ModelAdmin): return db_field.formfield(**kwargs) return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) -site = WidgetAdmin() +site = WidgetAdmin(name='widget-admin') site.register(models.User) site.register(models.Car, CarAdmin) diff --git a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py new file mode 100644 index 0000000000..073190657c --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py @@ -0,0 +1,13 @@ +from django.conf.urls.defaults import * +from namespace_urls import URLObject + +testobj3 = URLObject('testapp', 'test-ns3') + +urlpatterns = patterns('regressiontests.urlpatterns_reverse.views', + url(r'^normal/$', 'empty_view', name='inc-normal-view'), + url(r'^normal/(?P\d+)/(?P\d+)/$', 'empty_view', name='inc-normal-view'), + + (r'^test3/', include(testobj3.urls)), + (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')), +) + diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py new file mode 100644 index 0000000000..27cc7f7a22 --- /dev/null +++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py @@ -0,0 +1,38 @@ +from django.conf.urls.defaults import * + +class URLObject(object): + def __init__(self, app_name, namespace): + self.app_name = app_name + self.namespace = namespace + + def urls(self): + return patterns('', + url(r'^inner/$', 'empty_view', name='urlobject-view'), + url(r'^inner/(?P\d+)/(?P\d+)/$', 'empty_view', name='urlobject-view'), + ), self.app_name, self.namespace + urls = property(urls) + +testobj1 = URLObject('testapp', 'test-ns1') +testobj2 = URLObject('testapp', 'test-ns2') +default_testobj = URLObject('testapp', 'testapp') + +otherobj1 = URLObject('nodefault', 'other-ns1') +otherobj2 = URLObject('nodefault', 'other-ns2') + +urlpatterns = patterns('regressiontests.urlpatterns_reverse.views', + url(r'^normal/$', 'empty_view', name='normal-view'), + url(r'^normal/(?P\d+)/(?P\d+)/$', 'empty_view', name='normal-view'), + + (r'^test1/', include(testobj1.urls)), + (r'^test2/', include(testobj2.urls)), + (r'^default/', include(default_testobj.urls)), + + (r'^other1/', include(otherobj1.urls)), + (r'^other2/', include(otherobj2.urls)), + + (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')), + (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')), + + (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')), + +) diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 9def6b2eb2..d4f281ba81 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -158,4 +158,84 @@ class ReverseShortcutTests(TestCase): res = redirect('/foo/') self.assertEqual(res['Location'], '/foo/') res = redirect('http://example.com/') - self.assertEqual(res['Location'], 'http://example.com/') \ No newline at end of file + self.assertEqual(res['Location'], 'http://example.com/') + + +class NamespaceTests(TestCase): + urls = 'regressiontests.urlpatterns_reverse.namespace_urls' + + def test_ambiguous_object(self): + "Names deployed via dynamic URL objects that require namespaces can't be resolved" + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view') + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42]) + self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37}) + + def test_ambiguous_urlpattern(self): + "Names deployed via dynamic URL objects that require namespaces can't be resolved" + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing') + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', args=[37,42]) + self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', kwargs={'arg1':42, 'arg2':37}) + + def test_non_existent_namespace(self): + "Non-existent namespaces raise errors" + self.assertRaises(NoReverseMatch, reverse, 'blahblah:urlobject-view') + self.assertRaises(NoReverseMatch, reverse, 'test-ns1:blahblah:urlobject-view') + + def test_normal_name(self): + "Normal lookups work as expected" + self.assertEquals('/normal/', reverse('normal-view')) + self.assertEquals('/normal/37/42/', reverse('normal-view', args=[37,42])) + self.assertEquals('/normal/42/37/', reverse('normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_simple_included_name(self): + "Normal lookups work on names included from other patterns" + self.assertEquals('/included/normal/', reverse('inc-normal-view')) + self.assertEquals('/included/normal/37/42/', reverse('inc-normal-view', args=[37,42])) + self.assertEquals('/included/normal/42/37/', reverse('inc-normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_namespace_object(self): + "Dynamic URL objects can be found using a namespace" + self.assertEquals('/test1/inner/', reverse('test-ns1:urlobject-view')) + self.assertEquals('/test1/inner/37/42/', reverse('test-ns1:urlobject-view', args=[37,42])) + self.assertEquals('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_embedded_namespace_object(self): + "Namespaces can be installed anywhere in the URL pattern tree" + self.assertEquals('/included/test3/inner/', reverse('test-ns3:urlobject-view')) + self.assertEquals('/included/test3/inner/37/42/', reverse('test-ns3:urlobject-view', args=[37,42])) + self.assertEquals('/included/test3/inner/42/37/', reverse('test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_namespace_pattern(self): + "Namespaces can be applied to include()'d urlpatterns" + self.assertEquals('/ns-included1/normal/', reverse('inc-ns1:inc-normal-view')) + self.assertEquals('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42])) + self.assertEquals('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37})) + + def test_multiple_namespace_pattern(self): + "Namespaces can be embedded" + self.assertEquals('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view')) + self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42])) + self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_app_lookup_object(self): + "A default application namespace can be used for lookup" + self.assertEquals('/default/inner/', reverse('testapp:urlobject-view')) + self.assertEquals('/default/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42])) + self.assertEquals('/default/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + def test_app_lookup_object_with_default(self): + "A default application namespace is sensitive to the 'current' app can be used for lookup" + self.assertEquals('/included/test3/inner/', reverse('testapp:urlobject-view', current_app='test-ns3')) + self.assertEquals('/included/test3/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42], current_app='test-ns3')) + self.assertEquals('/included/test3/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='test-ns3')) + + def test_app_lookup_object_without_default(self): + "An application namespace without a default is sensitive to the 'current' app can be used for lookup" + self.assertEquals('/other2/inner/', reverse('nodefault:urlobject-view')) + self.assertEquals('/other2/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42])) + self.assertEquals('/other2/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37})) + + self.assertEquals('/other1/inner/', reverse('nodefault:urlobject-view', current_app='other-ns1')) + self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1')) + self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1')) +