Code

Ticket #6470: admin-urlpatterns.4.diff

File admin-urlpatterns.4.diff, 17.3 KB (added by Alex, 5 years ago)
Line 
1diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py
2index af1d1db..dfb49d3 100644
3--- a/django/conf/project_template/urls.py
4+++ b/django/conf/project_template/urls.py
5@@ -13,5 +13,5 @@ urlpatterns = patterns('',
6     # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
7 
8     # Uncomment the next line to enable the admin:
9-    # (r'^admin/(.*)', admin.site.root),
10+    # (r'^admin/', include(admin.site.urls)),
11 )
12diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
13index 3d60b9d..708af25 100644
14--- a/django/contrib/admin/options.py
15+++ b/django/contrib/admin/options.py
16@@ -5,7 +5,7 @@ from django.forms.models import BaseInlineFormSet
17 from django.contrib.contenttypes.models import ContentType
18 from django.contrib.admin import widgets
19 from django.contrib.admin import helpers
20-from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects
21+from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, admin_perm_test
22 from django.core.exceptions import PermissionDenied
23 from django.db import models, transaction
24 from django.http import Http404, HttpResponse, HttpResponseRedirect
25@@ -196,6 +196,23 @@ class ModelAdmin(BaseModelAdmin):
26         else:
27             return self.change_view(request, unquote(url))
28 
29+    def get_urls(self):
30+        from django.conf.urls.defaults import patterns, url
31+        info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
32+        urlpatterns = patterns('',
33+            url(r'^$', lambda *args, **kwargs: self.changelist_view(*args, **kwargs), name='%sadmin_%s_%s_changelist' % info),
34+            url(r'^add/$', lambda *args, **kwargs: self.add_view(*args, **kwargs), name='%sadmin_%s_%s_add' % info),
35+            url(r'^(.+)/history/$', lambda *args, **kwargs: self.history_view(*args, **kwargs), name='%sadmin_%s_%s_history' % info),
36+            url(r'^(.+)/delete/$', lambda *args, **kwargs: self.delete_view(*args, **kwargs), name='%sadmin_%s_%s_delete' % info),
37+            url(r'^(.+)/$', lambda *args, **kwargs: self.change_view(*args, **kwargs), name='%sadmin_%s_%s_change' % info),
38+        )
39+        return urlpatterns
40+
41+    def urls(self):
42+        return self.get_urls()
43+    urls = property(urls)
44+
45+
46     def _media(self):
47         from django.conf import settings
48 
49@@ -537,7 +554,7 @@ class ModelAdmin(BaseModelAdmin):
50         }
51         context.update(extra_context or {})
52         return self.render_change_form(request, context, add=True)
53-    add_view = transaction.commit_on_success(add_view)
54+    add_view = transaction.commit_on_success(admin_perm_test(add_view))
55 
56     def change_view(self, request, object_id, extra_context=None):
57         "The 'change' admin view for this model."
58@@ -545,7 +562,7 @@ class ModelAdmin(BaseModelAdmin):
59         opts = model._meta
60 
61         try:
62-            obj = model._default_manager.get(pk=object_id)
63+            obj = model._default_manager.get(pk=unquote(object_id))
64         except model.DoesNotExist:
65             # Don't raise Http404 just yet, because we haven't checked
66             # permissions yet. We don't want an unauthenticated user to be able
67@@ -616,7 +633,7 @@ class ModelAdmin(BaseModelAdmin):
68         }
69         context.update(extra_context or {})
70         return self.render_change_form(request, context, change=True, obj=obj)
71-    change_view = transaction.commit_on_success(change_view)
72+    change_view = transaction.commit_on_success(admin_perm_test(change_view))
73 
74     def changelist_view(self, request, extra_context=None):
75         "The 'change list' admin view for this model."
76@@ -652,6 +669,7 @@ class ModelAdmin(BaseModelAdmin):
77             'admin/%s/change_list.html' % app_label,
78             'admin/change_list.html'
79         ], context, context_instance=template.RequestContext(request))
80+    changelist_view = admin_perm_test(changelist_view)
81 
82     def delete_view(self, request, object_id, extra_context=None):
83         "The 'delete' admin view for this model."
84@@ -659,7 +677,7 @@ class ModelAdmin(BaseModelAdmin):
85         app_label = opts.app_label
86 
87         try:
88-            obj = self.model._default_manager.get(pk=object_id)
89+            obj = self.model._default_manager.get(pk=unquote(object_id))
90         except self.model.DoesNotExist:
91             # Don't raise Http404 just yet, because we haven't checked
92             # permissions yet. We don't want an unauthenticated user to be able
93@@ -674,7 +692,7 @@ class ModelAdmin(BaseModelAdmin):
94 
95         # Populate deleted_objects, a data structure of all related objects that
96         # will also be deleted.
97-        deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
98+        deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
99         perms_needed = set()
100         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
101 
102@@ -707,6 +725,7 @@ class ModelAdmin(BaseModelAdmin):
103             "admin/%s/delete_confirmation.html" % app_label,
104             "admin/delete_confirmation.html"
105         ], context, context_instance=template.RequestContext(request))
106+    delete_view = admin_perm_test(delete_view)
107 
108     def history_view(self, request, object_id, extra_context=None):
109         "The 'history' admin view for this model."
110@@ -734,6 +753,7 @@ class ModelAdmin(BaseModelAdmin):
111             "admin/%s/object_history.html" % app_label,
112             "admin/object_history.html"
113         ], context, context_instance=template.RequestContext(request))
114+    history_view = admin_perm_test(history_view)
115 
116 class InlineModelAdmin(BaseModelAdmin):
117     """
118diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
119index c16ab6a..1b24211 100644
120--- a/django/contrib/admin/sites.py
121+++ b/django/contrib/admin/sites.py
122@@ -1,7 +1,9 @@
123 import base64
124 import re
125+
126 from django import http, template
127 from django.contrib.admin import ModelAdmin
128+from django.contrib.admin.util import admin_perm_test
129 from django.contrib.auth import authenticate, login
130 from django.db.models.base import ModelBase
131 from django.core.exceptions import ImproperlyConfigured
132@@ -34,8 +36,17 @@ class AdminSite(object):
133     login_template = None
134     app_index_template = None
135 
136-    def __init__(self):
137+    def __init__(self, name=None):
138         self._registry = {} # model_class class -> admin_class instance
139+        # TODO Root path is used to calculate urls under the old root() method
140+        # in order to maintain backwards compatibility we are leaving that in
141+        # so root_path isn't needed, not sure what to do about this.
142+        self.root_path = 'admin/'
143+        if name is None:
144+            name = ''
145+        else:
146+            name += '_'
147+        self.name = name
148 
149     def register(self, model_or_iterable, admin_class=None, **options):
150         """
151@@ -121,6 +132,9 @@ class AdminSite(object):
152 
153         `url` is the remainder of the URL -- e.g. 'comments/comment/'.
154         """
155+        import warnings
156+        warnings.warn("Using AdminSite.root() is deprecated, you should \
157+            include(AdminSite.urls) instead", PendingDeprecationWarning)
158         if request.method == 'GET' and not request.path.endswith('/'):
159             return http.HttpResponseRedirect(request.path + '/')
160 
161@@ -159,7 +173,28 @@ class AdminSite(object):
162                 return self.app_index(request, url)
163 
164         raise http.Http404('The requested admin page does not exist.')
165-
166+   
167+    def get_urls(self):
168+        from django.conf.urls.defaults import patterns, url, include
169+        urlpatterns = patterns('',
170+            url(r'^$', lambda *args, **kwargs: self.index(*args, **kwargs), name='%sadmin_index' % self.name),
171+            url(r'^logout/$', lambda *args, **kwargs: self.logout(*args, **kwargs), name='%sadmin_logout'),
172+            url(r'^password_change/$', lambda *args, **kwargs: self.password_change(*args, **kwargs), name='%sadmin_password_change' % self.name),
173+            url(r'^password_change/done/$', lambda *args, **kwargs: self.password_change_done(*args, **kwargs), name='%sadmin_password_change_done' % self.name),
174+            url(r'^jsi18n/$', lambda *args, **kwargs: self.i18n_javascript(*args, **kwargs), name='%sadmin_jsi18n' % self.name),
175+            url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', 'django.views.defaults.shortcut'),
176+            url(r'^(?P<app_label>\w+)/$', lambda *args, **kwargs: self.app_index(*args, **kwargs), name='%sadmin_app_list' % self.name),
177+        )
178+        for model, model_admin in self._registry.iteritems():
179+            urlpatterns += patterns('',
180+                url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name), include(model_admin.urls))
181+            )
182+        return urlpatterns
183+   
184+    def urls(self):
185+        return self.get_urls()
186+    urls = property(urls)
187+   
188     def model_page(self, request, app_label, model_name, rest_of_url=None):
189         """
190         Handles the model-specific functionality of the admin site, delegating
191@@ -183,6 +218,7 @@ class AdminSite(object):
192         from django.contrib.auth.views import password_change
193         return password_change(request,
194             post_change_redirect='%spassword_change/done/' % self.root_path)
195+    passoword_change = admin_perm_test(password_change)
196 
197     def password_change_done(self, request):
198         """
199@@ -190,6 +226,7 @@ class AdminSite(object):
200         """
201         from django.contrib.auth.views import password_change_done
202         return password_change_done(request)
203+    password_change_done = admin_perm_test(password_change_done)
204 
205     def i18n_javascript(self, request):
206         """
207@@ -203,6 +240,7 @@ class AdminSite(object):
208         else:
209             from django.views.i18n import null_javascript_catalog as javascript_catalog
210         return javascript_catalog(request, packages='django.conf')
211+    i18n_javascript = admin_perm_test(i18n_javascript)
212 
213     def logout(self, request):
214         """
215@@ -317,7 +355,7 @@ class AdminSite(object):
216         return render_to_response(self.index_template or 'admin/index.html', context,
217             context_instance=template.RequestContext(request)
218         )
219-    index = never_cache(index)
220+    index = never_cache(admin_perm_test(index))
221 
222     def display_login_form(self, request, error_message='', extra_context=None):
223         request.session.set_test_cookie()
224@@ -377,6 +415,7 @@ class AdminSite(object):
225         return render_to_response(self.app_index_template or 'admin/app_index.html', context,
226             context_instance=template.RequestContext(request)
227         )
228+    app_index = admin_perm_test(app_index)
229 
230 # This global object represents the default admin site, for the common case.
231 # You can instantiate AdminSite in your own code to create a custom admin site.
232diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
233index 0900b4e..6b34c78 100644
234--- a/django/contrib/admin/util.py
235+++ b/django/contrib/admin/util.py
236@@ -6,6 +6,17 @@ from django.utils.text import capfirst
237 from django.utils.encoding import force_unicode
238 from django.utils.translation import ugettext as _
239 
240+def admin_perm_test(func):
241+    def inner(admin_site_or_modeladmin, request, *args, **kwargs):
242+        if hasattr(admin_site_or_modeladmin, 'has_permission'):
243+            admin_site = admin_site_or_modeladmin
244+        else:
245+            admin_site = admin_site_or_modeladmin.admin_site
246+        if not admin_site.has_permission(request):
247+            return admin_site.login(request)
248+        # User has right permisssions show the view
249+        return func(admin_site_or_modeladmin, request, *args, **kwargs)
250+    return inner
251 
252 def quote(s):
253     """
254diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
255index 805ca32..f43f3ea 100644
256--- a/django/contrib/auth/admin.py
257+++ b/django/contrib/auth/admin.py
258@@ -40,6 +40,14 @@ class UserAdmin(admin.ModelAdmin):
259         if url.endswith('password'):
260             return self.user_change_password(request, url.split('/')[0])
261         return super(UserAdmin, self).__call__(request, url)
262+   
263+    def get_urls(self):
264+        from django.conf.urls.defaults import patterns
265+        urlpatterns = super(UserAdmin, self).get_urls()
266+        urlpatterns += patterns('',
267+            (r'^(\d+)/password/$', self.user_change_password)
268+        )
269+        return urlpatterns
270 
271     def add_view(self, request):
272         # It's an error for a user to have add permission but NOT change
273diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt
274index 1144167..fb6794b 100644
275--- a/docs/intro/tutorial02.txt
276+++ b/docs/intro/tutorial02.txt
277@@ -57,7 +57,7 @@ activate the admin site for your installation, do these three things:
278               # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
279 
280               # Uncomment the next line to enable the admin:
281-              **(r'^admin/(.*)', admin.site.root),**
282+              **(r'^admin/', include(admin.site.urls)),**
283           )
284 
285       (The bold lines are the ones that needed to be uncommented.)
286diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt
287index f24dc46..13850ce 100644
288--- a/docs/ref/contrib/admin.txt
289+++ b/docs/ref/contrib/admin.txt
290@@ -632,6 +632,22 @@ model instance::
291                 instance.save()
292             formset.save_m2m()
293 
294+``get_urls(self)``
295+~~~~~~~~~~~~~~~~~~~
296+
297+The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for
298+that ModelAdmin in the same way as a URLconf.  Therefore you can extend them as
299+documented in :ref:`topics-http-urls`::
300+   
301+    class MyModelAdmin(admin.ModelAdmin):
302+        def get_urls(self):
303+            urlpatterns = super(MyModelAdmin, self).get_urls()
304+            urlpatterns += patterns('',
305+                (r'^my_view/$', self.my_view)
306+            )
307+            return urlpatterns
308+
309+
310 ``ModelAdmin`` media definitions
311 --------------------------------
312 
313@@ -1027,7 +1043,7 @@ In this example, we register the default ``AdminSite`` instance
314     admin.autodiscover()
315 
316     urlpatterns = patterns('',
317-        ('^admin/(.*)', admin.site.root),
318+        ('^admin/', include(admin.site.urls)),
319     )
320 
321 Above we used ``admin.autodiscover()`` to automatically load the
322@@ -1041,15 +1057,13 @@ In this example, we register the ``AdminSite`` instance
323     from myproject.admin import admin_site
324 
325     urlpatterns = patterns('',
326-        ('^myadmin/(.*)', admin_site.root),
327+        ('^myadmin/', include(admin_site.urls)),
328     )
329 
330 There is really no need to use autodiscover when using your own ``AdminSite``
331 instance since you will likely be importing all the per-app admin.py modules
332 in your ``myproject.admin`` module.
333 
334-Note that the regular expression in the URLpattern *must* group everything in
335-the URL that comes after the URL root -- hence the ``(.*)`` in these examples.
336 
337 Multiple admin sites in the same URLconf
338 ----------------------------------------
339@@ -1068,6 +1082,27 @@ respectively::
340     from myproject.admin import basic_site, advanced_site
341 
342     urlpatterns = patterns('',
343-        ('^basic-admin/(.*)', basic_site.root),
344-        ('^advanced-admin/(.*)', advanced_site.root),
345+        ('^basic-admin/', include(basic_site.urls)),
346+        ('^advanced-admin/', include(advanced_site.urls)),
347     )
348+
349+Adding views to admin sites
350+---------------------------
351+
352+It possible to add additional views to the admin site in the same way one can
353+add them to ``ModelAdmins``.  This by using the ``get_urls()`` method on an
354+AdminSite in the same way as documented on ``ModelAdmins``.
355+
356+Protecting Custom ``AdminSite`` and ``ModelAdmin``
357+--------------------------------------------------
358+
359+By default all the views in the Django admin are protected so that only staff
360+members can access them.  If you add your own views to either a ``ModelAdmin``
361+or ``AdminSite`` you should ensure that where necessary they are protected in
362+the same manner.  To do this use the ``admin_perm_test`` decorator provided in
363+``django.contrib.admin.utils.admin_perm_test``.  It can be used in the same way
364+as the ``login_requied`` decorator.
365+
366+.. note::
367+    The ``admin_perm_test`` decorator can only be used on methods which are on
368+    ``ModelAdmins`` or ``AdminSites``, you cannot use it on arbitrary functions.
369diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
370index 391d1ff..8fb8ad8 100644
371--- a/tests/regressiontests/admin_views/tests.py
372+++ b/tests/regressiontests/admin_views/tests.py
373@@ -26,7 +26,7 @@ class AdminViewBasicTest(TestCase):
374         """
375         request = self.client.get('/test_admin/admin/admin_views/article/add')
376         self.assertRedirects(request,
377-            '/test_admin/admin/admin_views/article/add/'
378+            '/test_admin/admin/admin_views/article/add/', status_code=301
379         )
380     
381     def testBasicAddGet(self):
382diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py
383index 4e5da48..02e0286 100644
384--- a/tests/regressiontests/admin_views/urls.py
385+++ b/tests/regressiontests/admin_views/urls.py
386@@ -5,5 +5,5 @@ import views
387 urlpatterns = patterns('',
388     (r'^admin/doc/', include('django.contrib.admindocs.urls')),
389     (r'^admin/secure-view/$', views.secure_view),
390-    (r'^admin/(.*)', admin.site.root),
391+    (r'^admin/', include(admin.site.urls)),
392 )