From e992e57d3e66708015899efd21bb0174377baeed Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 13 Jul 2009 13:46:31 +0000 Subject: [PATCH 01/15] Fixed #11416 -- Restored use of the never_cache decorator on admin views. Thanks to Ramiro Morales and Michael Newmann for their work on the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11229 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/sites.py | 26 +++++--- docs/ref/contrib/admin/index.txt | 28 ++++++-- tests/regressiontests/admin_views/tests.py | 74 ++++++++++++++++++++++ 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 6e9ef1161f..16b254ed20 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -114,20 +114,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 +159,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,19 +177,25 @@ 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. @@ -201,13 +207,13 @@ class AdminSite(object): wrap(self.logout), name='%sadmin_logout'), url(r'^password_change/$', - wrap(self.password_change), + wrap(self.password_change, cacheable=True), name='%sadmin_password_change' % self.name), url(r'^password_change/done/$', - wrap(self.password_change_done), + wrap(self.password_change_done, cacheable=True), name='%sadmin_password_change_done' % self.name), url(r'^jsi18n/$', - wrap(self.i18n_javascript), + wrap(self.i18n_javascript, cacheable=True), name='%sadmin_jsi18n' % self.name), url(r'^r/(?P\d+)/(?P.+)/$', 'django.views.defaults.shortcut'), diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 394ebd1f24..f0f5621fe6 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) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 99168fdeee..38fe5ccc9d 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 @@ -1527,3 +1528,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) + From 9c5075d1259bba80c846e3ddfd1f2e81c884c801 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Wed, 15 Jul 2009 02:20:51 +0000 Subject: [PATCH 02/15] Forgot to add `collect` to `GeoManager`. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11232 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/models/manager.py | 3 +++ 1 file changed, 3 insertions(+) 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) From b1e645b6cdec97091ddfbe3e47f41879ebd5af7a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:52:39 +0000 Subject: [PATCH 03/15] Fixed #10287 -- Added better examples in the docs of formset validation. Thanks to Andrew Badr for the text. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11234 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/topics/forms/formsets.txt | 49 +++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 13 deletions(-) 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()`` From e114cbf3de39f07888c15d9b6e40309ad3eb267a Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:53:29 +0000 Subject: [PATCH 04/15] Fixed #11469 -- Removed suspicion that Django developers have trouble counting to four. Thanks to msgre for the report. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11235 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/intro/tutorial03.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 75514ab7bc3c20fb0b07d7caa0b5cf7b74840778 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:54:11 +0000 Subject: [PATCH 05/15] Fixed #11374 -- Modified the documentation for forms.BooleanField to allow for the fact that it can be rendered using widgets other than a checkbox. Thanks to lygaret for the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11236 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/ref/forms/fields.txt | 41 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) 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) From 3c6036a5b4a5c8ef921b71c1752abcda9728afb5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:54:45 +0000 Subject: [PATCH 06/15] Fixed #11364 -- Modified the jsi18n example to use the {% url %} tag rather than a placeholder. Thanks to jcassee for the suggestion. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11237 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/topics/i18n.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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')); From 5727374d9583ded4c87626a6c48bd669d432182f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:55:50 +0000 Subject: [PATCH 07/15] Fixed #11348 -- Trimmed the width of a screenshot image in the admin docs. Thanks to smcoll for the new image. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11238 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../contrib/admin/_images/article_actions.png | Bin 35643 -> 13367 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/ref/contrib/admin/_images/article_actions.png b/docs/ref/contrib/admin/_images/article_actions.png index 254a8ad5576f9f5d1a4d5a00375a06b4b9a5c1ff..df4ab8f1ecd5dbd2046cbdef46a129ad98505fcd 100644 GIT binary patch literal 13367 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 Date: Wed, 15 Jul 2009 13:56:50 +0000 Subject: [PATCH 08/15] Fixed #11235 -- Added a missing clause from some sample SQL in the queryset docs. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11239 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/ref/models/querysets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 93d86479e377eb5a3f3fb8436a1ddcbfb9ad9b7e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 15 Jul 2009 13:57:46 +0000 Subject: [PATCH 09/15] Fixed #11480 -- Corrected markup error in admin docs. Thank to msgre for the report. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11240 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/ref/contrib/admin/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index f0f5621fe6..d74cb0c55a 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -774,7 +774,7 @@ problems: 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: +``ModelAdmin`` instance); use it like so:: class MyModelAdmin(admin.ModelAdmin): def get_urls(self): From 49f0a4bb5bb3ef5c22873de799570372dbc1d4e4 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 16 Jul 2009 16:06:53 +0000 Subject: [PATCH 10/15] Fixed #10908 -- Clarified the procedure for creating test users in the testing docs. Thanks to gruszczy and timo. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11248 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/topics/testing.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index d235e0deba..cec6002b7b 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -686,7 +686,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() From 9fd19c01611e5ed1ed64bbeb462ab96499d72a6c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 16 Jul 2009 16:08:18 +0000 Subject: [PATCH 11/15] Fixed #11229 -- Updated the mod_wsgi deployment documentation to avoid references to mod_python and techniques that are not recommended or required for mod_wsgi. Thanks to Graham Dumpleton for the suggestion and guidance. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11249 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/howto/deployment/modwsgi.txt | 187 +++++++++++++++++++----------- 1 file changed, 118 insertions(+), 69 deletions(-) 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 From 8d48eaa064c88533be5082e3f45638fbd48491d8 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 16 Jul 2009 16:16:13 +0000 Subject: [PATCH 12/15] Fixed #10061 -- Added namespacing for named URLs - most importantly, for the admin site, where the absence of this facility was causing problems. Thanks to the many people who contributed to and helped review this patch. This change is backwards incompatible for anyone that is using the named URLs introduced in [9739]. Any usage of the old admin_XXX names need to be modified to use the new namespaced format; in many cases this will be as simple as a search & replace for "admin_" -> "admin:". See the docs for more details on the new URL names, and the namespace resolution strategy. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11250 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/urls/defaults.py | 16 +- django/contrib/admin/options.py | 24 +-- django/contrib/admin/sites.py | 44 ++--- .../contrib/admin/templates/admin/base.html | 25 ++- django/contrib/admin/widgets.py | 15 +- .../admindocs/templates/admin_doc/index.html | 2 +- django/contrib/admindocs/views.py | 11 +- django/core/urlresolvers.py | 107 ++++++++++--- django/template/context.py | 7 +- django/template/defaulttags.py | 6 +- docs/ref/contrib/admin/index.txt | 80 ++++++---- docs/ref/templates/api.txt | 17 +- docs/ref/templates/builtins.txt | 10 ++ docs/topics/http/urls.txt | 151 +++++++++++++++++- .../admin_views/customadmin.py | 8 +- tests/regressiontests/admin_views/tests.py | 5 + .../admin_widgets/widgetadmin.py | 2 +- .../included_namespace_urls.py | 13 ++ .../urlpatterns_reverse/namespace_urls.py | 38 +++++ .../urlpatterns_reverse/tests.py | 82 +++++++++- 20 files changed, 544 insertions(+), 119 deletions(-) create mode 100644 tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py create mode 100644 tests/regressiontests/urlpatterns_reverse/namespace_urls.py 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 16b254ed20..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() @@ -202,24 +200,24 @@ class AdminSite(object): 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, cacheable=True), - name='%sadmin_password_change' % self.name), + name='password_change'), url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), - name='%sadmin_password_change_done' % self.name), + name='password_change_done'), url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), - name='%sadmin_jsi18n' % self.name), + 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. @@ -231,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): @@ -239,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): """ @@ -368,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) @@ -382,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): @@ -425,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/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/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index d74cb0c55a..584672e4f0 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1242,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 @@ -1256,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 ------------------------ @@ -1353,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: @@ -1370,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 @@ -1408,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/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/http/urls.txt b/docs/topics/http/urls.txt index 17978d4328..926fddf2c1 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -400,7 +400,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 +417,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 ``included()`` into your projects ``urlpatterns`` when you +deploy the admin instance. + .. _`Django Web site`: http://www.djangoproject.com/ Captured parameters @@ -439,6 +446,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 deploying 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, and 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 applicaiton and instance namespace as arguments +to the ``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 namespace 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 +672,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. Django then 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 :method:``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 it's 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 =============== @@ -624,7 +763,7 @@ 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 +785,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 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 38fe5ccc9d..aafa303cec 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -205,6 +205,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 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')) + From 53b8809277c40f5011a16d6e27b221e2f48f7f97 Mon Sep 17 00:00:00 2001 From: Justin Bronn Date: Thu, 16 Jul 2009 22:38:22 +0000 Subject: [PATCH 13/15] Fixed #11489 -- `GeoQuery.resolve_columns` now recognizes annotations; disabled problematic test cases on Oracle and added notes on why. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11251 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/gis/db/models/sql/query.py | 10 +++++++++- django/contrib/gis/tests/relatedapp/tests.py | 20 ++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) 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') From 3469f4b819913476c197717221887f71c4a88471 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 16 Jul 2009 23:45:36 +0000 Subject: [PATCH 14/15] Fixed #11491 -- Corrected minor typo in new namespace URL docs. Thanks to Carl Meyer for the report. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11253 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/topics/http/urls.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 926fddf2c1..2ec4e9d954 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -451,8 +451,8 @@ included URLconf, as expected. Defining URL Namespaces ----------------------- -When you need to deploying multiple instances of a single application, it can -be helpful to be able to differentiate between instances. This is especially +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. From 0c9d0bf7d653336c5ee7d15f23adeb098a9f5dba Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 17 Jul 2009 00:55:21 +0000 Subject: [PATCH 15/15] Fixed #11492 -- Corrected some typos, and added some extra markup for the URLs documentation. Thanks to Ramiro Morales for the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11258 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- docs/topics/http/urls.txt | 44 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 2ec4e9d954..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. @@ -421,7 +433,7 @@ 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 ``included()`` into your projects ``urlpatterns`` when you +attribute that you ``include()`` into your projects ``urlpatterns`` when you deploy the admin instance. .. _`Django Web site`: http://www.djangoproject.com/ @@ -466,15 +478,15 @@ A URL namespace comes in two parts, both of which are strings: * An **instance namespace**. This identifies a specific instance of an application. Instance namespaces should be unique across your entire - project. However, and instance namespace can be the same as the + 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 applicaiton and instance namespace as arguments -to the ``include()`` when you construct your URL patterns. For example,:: +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')), @@ -494,7 +506,7 @@ instance namespace. For example, the ``urls`` attribute of Django's an admin site, plus the name of the admin instance, and the application namespace ``admin``. -Once you have defined namespace URLs, you can reverse them. For details on +Once you have defined namespaced URLs, you can reverse them. For details on reversing namespaced urls, see the documentation on :ref:`reversing namespaced URLs `. @@ -679,18 +691,18 @@ 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``. +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 +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. Django then looks for a matching application namespace (in this + 1. First, Django looks for a matching application namespace (in this example, ``myapp``). This will yield a list of instances of that application. @@ -702,15 +714,15 @@ the fully qualified name into parts, and then tries the following lookup: template. The current application can also be specified manually as an argument - to the :method:``reverse()`` function. + 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``) + 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 it's instance name may be. + 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 @@ -762,7 +774,6 @@ 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, current_app=None) ``viewname`` is either the function name (either a function reference, or the @@ -812,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