summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Kubasik <kevin@kubasik.net>2009-07-22 12:43:04 +0000
committerKevin Kubasik <kevin@kubasik.net>2009-07-22 12:43:04 +0000
commit7e26c1047b4e4f471d4f7020a6571e6e8bced0de (patch)
tree491f7d3f9e14979d4ebbf4d04a23b15042895744
parentb162138e4c2cfe6a4362c0fbf46953ef94ced103 (diff)
parent0c9d0bf7d653336c5ee7d15f23adeb098a9f5dba (diff)
[gsoc2009-testing] Upstream merge
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/test-improvements@11292 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/conf/urls/defaults.py16
-rw-r--r--django/contrib/admin/options.py24
-rw-r--r--django/contrib/admin/sites.py70
-rw-r--r--django/contrib/admin/templates/admin/base.html25
-rw-r--r--django/contrib/admin/widgets.py15
-rw-r--r--django/contrib/admindocs/templates/admin_doc/index.html2
-rw-r--r--django/contrib/admindocs/views.py11
-rw-r--r--django/contrib/gis/db/models/manager.py3
-rw-r--r--django/contrib/gis/db/models/sql/query.py10
-rw-r--r--django/contrib/gis/tests/relatedapp/tests.py20
-rw-r--r--django/core/urlresolvers.py107
-rw-r--r--django/template/context.py7
-rw-r--r--django/template/defaulttags.py6
-rw-r--r--docs/howto/deployment/modwsgi.txt187
-rw-r--r--docs/intro/tutorial03.txt2
-rw-r--r--docs/ref/contrib/admin/_images/article_actions.pngbin35643 -> 13367 bytes
-rw-r--r--docs/ref/contrib/admin/index.txt108
-rw-r--r--docs/ref/forms/fields.txt41
-rw-r--r--docs/ref/models/querysets.txt2
-rw-r--r--docs/ref/templates/api.txt17
-rw-r--r--docs/ref/templates/builtins.txt10
-rw-r--r--docs/topics/forms/formsets.txt49
-rw-r--r--docs/topics/http/urls.txt169
-rw-r--r--docs/topics/i18n.txt8
-rw-r--r--docs/topics/testing.txt8
-rw-r--r--tests/regressiontests/admin_views/customadmin.py8
-rw-r--r--tests/regressiontests/admin_views/tests.py79
-rw-r--r--tests/regressiontests/admin_widgets/widgetadmin.py2
-rw-r--r--tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py13
-rw-r--r--tests/regressiontests/urlpatterns_reverse/namespace_urls.py38
-rw-r--r--tests/regressiontests/urlpatterns_reverse/tests.py82
31 files changed, 885 insertions, 254 deletions
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<content_type_id>\d+)/(?P<object_id>.+)/$',
'django.views.defaults.shortcut'),
url(r'^(?P<app_label>\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 %}
</div>
{% if user.is_authenticated and user.is_staff %}
- <div id="user-tools">{% trans 'Welcome,' %} <strong>{% firstof user.first_name user.username %}</strong>. {% block userlinks %}{% url django-admindocs-docroot as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> / {% endif %}<a href="{{ root_path }}password_change/">{% trans 'Change password' %}</a> / <a href="{{ root_path }}logout/">{% trans 'Log out' %}</a>{% endblock %}</div>
+ <div id="user-tools">
+ {% trans 'Welcome,' %}
+ <strong>{% firstof user.first_name user.username %}</strong>.
+ {% block userlinks %}
+ {% url django-admindocs-docroot as docsroot %}
+ {% if docsroot %}
+ <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
+ {% endif %}
+ {% url admin:password_change as password_change_url %}
+ {% if password_change_url %}
+ <a href="{{ password_change_url }}">
+ {% else %}
+ <a href="{{ root_path }}password_change/">
+ {% endif %}
+ {% trans 'Change password' %}</a> /
+ {% url admin:logout as logout_url %}
+ {% if logout_url %}
+ <a href="{{ logout_url }}">
+ {% else %}
+ <a href="{{ root_path }}logout/">
+ {% endif %}
+ {% trans 'Log out' %}</a>
+ {% endblock %}
+ </div>
{% endif %}
{% block nav-global %}{% endblock %}
</div>
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 %}<div class="breadcrumbs"><a href="../">Home</a> &rsaquo; Documentation</div>{% endblock %}
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="{{ root_path }}">Home</a> &rsaquo; Documentation</div>{% 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<howto-deployment-modpython>` 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/
+
+ <Directory /usr/local/wsgi/static>
+ Order deny,allow
+ Allow from all
+ </Directory>
+
+ WSGIScriptAlias / /usr/local/wsgi/scripts/django.wsgi
+
+ <Directory /usr/local/wsgi/scripts>
+ Order allow,deny
+ Allow from all
+ </Directory>
+
+.. _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
--- a/docs/ref/contrib/admin/_images/article_actions.png
+++ b/docs/ref/contrib/admin/_images/article_actions.png
Binary files 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 <admin-reverse-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 <naming-url-patterns>`.
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,))
+
+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.
+
+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::
-However, if the admin instance was named ``custom``, you would need to call::
+ >>> change_url = urlresolvers.reverse('custom:polls_choice_change', args=(c.id,))
- >>> change_url = urlresolvers.reverse('custom_admin_polls_choice_change', args=(c.id,))
+For more details, see the documentation on :ref:`reversing namespaced URLs
+<topics-http-reversing-url-namespaces>`.
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``
- * 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``.
+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<topics-http-reversing-url-namespaces>`.
+ 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::
<a href="{{ the_url }}">Link to optional stuff</a>
{% 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
+<topics-http-reversing-url-namespaces>`, 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(<module or pattern_list>)
+
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 <naming-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::
+
+ (<patterns object>, <application namespace>, <instance namespace>)
+
+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 <topics-http-reversing-url-namespaces>`.
+
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 <topics-http-reversing-url-namespaces>`.
+
.. 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::
- <script type="text/javascript" src="/path/to/jsi18n/"></script>
+ <script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
-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('<a href="/test_admin/%s/logout/">' % self.urlbit not in response.content)
+ self.failIf('<a href="/test_admin/%s/password_change/">' % 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<arg1>\d+)/(?P<arg2>\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<arg1>\d+)/(?P<arg2>\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<arg1>\d+)/(?P<arg2>\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'))
+