Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #6470: made the admin use a URL resolver.

This *is* backwards compatible, but `admin.site.root()` has been deprecated. The new style is `('^admin/', include(admin.site.urls))`; users will need to update their code to take advantage of the new customizable admin URLs.

Thanks to Alex Gaynor.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9739 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 1f84630c87f8032b0167e6db41acaf50ab710879 1 parent 6c4e5f0
Jacob Kaplan-Moss authored January 14, 2009
2  django/conf/project_template/urls.py
@@ -13,5 +13,5 @@
13 13
     # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
14 14
 
15 15
     # Uncomment the next line to enable the admin:
16  
-    # (r'^admin/(.*)', admin.site.root),
  16
+    # (r'^admin/', include(admin.site.urls)),
17 17
 )
241  django/contrib/admin/options.py
@@ -5,11 +5,12 @@
5 5
 from django.contrib.contenttypes.models import ContentType
6 6
 from django.contrib.admin import widgets
7 7
 from django.contrib.admin import helpers
8  
-from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects
  8
+from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects
9 9
 from django.core.exceptions import PermissionDenied
10 10
 from django.db import models, transaction
11 11
 from django.http import Http404, HttpResponse, HttpResponseRedirect
12 12
 from django.shortcuts import get_object_or_404, render_to_response
  13
+from django.utils.functional import update_wrapper
13 14
 from django.utils.html import escape
14 15
 from django.utils.safestring import mark_safe
15 16
 from django.utils.text import capfirst, get_text_list
@@ -38,12 +39,12 @@ class BaseModelAdmin(object):
38 39
     filter_horizontal = ()
39 40
     radio_fields = {}
40 41
     prepopulated_fields = {}
41  
-
  42
+    
42 43
     def formfield_for_dbfield(self, db_field, **kwargs):
43 44
         """
44 45
         Hook for specifying the form Field instance for a given database Field
45 46
         instance.
46  
-
  47
+        
47 48
         If kwargs are given, they're passed to the form Field's constructor.
48 49
         """
49 50
         
@@ -63,18 +64,18 @@ def formfield_for_dbfield(self, db_field, **kwargs):
63 64
             else:
64 65
                 # Otherwise, use the default select widget.
65 66
                 return db_field.formfield(**kwargs)
66  
-
  67
+        
67 68
         # For DateTimeFields, use a special field and widget.
68 69
         if isinstance(db_field, models.DateTimeField):
69 70
             kwargs['form_class'] = forms.SplitDateTimeField
70 71
             kwargs['widget'] = widgets.AdminSplitDateTime()
71 72
             return db_field.formfield(**kwargs)
72  
-
  73
+        
73 74
         # For DateFields, add a custom CSS class.
74 75
         if isinstance(db_field, models.DateField):
75 76
             kwargs['widget'] = widgets.AdminDateWidget
76 77
             return db_field.formfield(**kwargs)
77  
-
  78
+        
78 79
         # For TimeFields, add a custom CSS class.
79 80
         if isinstance(db_field, models.TimeField):
80 81
             kwargs['widget'] = widgets.AdminTimeWidget
@@ -94,22 +95,22 @@ def formfield_for_dbfield(self, db_field, **kwargs):
94 95
         if isinstance(db_field, models.IntegerField):
95 96
             kwargs['widget'] = widgets.AdminIntegerFieldWidget
96 97
             return db_field.formfield(**kwargs)
97  
-
  98
+        
98 99
         # For CommaSeparatedIntegerFields, add a custom CSS class.
99 100
         if isinstance(db_field, models.CommaSeparatedIntegerField):
100 101
             kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget
101 102
             return db_field.formfield(**kwargs)
102  
-
  103
+        
103 104
         # For TextInputs, add a custom CSS class.
104 105
         if isinstance(db_field, models.CharField):
105 106
             kwargs['widget'] = widgets.AdminTextInputWidget
106 107
             return db_field.formfield(**kwargs)
107  
-    
  108
+        
108 109
         # For FileFields and ImageFields add a link to the current file.
109 110
         if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
110 111
             kwargs['widget'] = widgets.AdminFileWidget
111 112
             return db_field.formfield(**kwargs)
112  
-
  113
+        
113 114
         # For ForeignKey or ManyToManyFields, use a special widget.
114 115
         if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
115 116
             if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
@@ -139,10 +140,10 @@ def formfield_for_dbfield(self, db_field, **kwargs):
139 140
                 if formfield is not None:
140 141
                     formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
141 142
             return formfield
142  
-
  143
+        
143 144
         # For any other type of field, just call its formfield() method.
144 145
         return db_field.formfield(**kwargs)
145  
-
  146
+    
146 147
     def _declared_fieldsets(self):
147 148
         if self.fieldsets:
148 149
             return self.fieldsets
@@ -154,7 +155,7 @@ def _declared_fieldsets(self):
154 155
 class ModelAdmin(BaseModelAdmin):
155 156
     "Encapsulates all admin options and functionality for a given model."
156 157
     __metaclass__ = forms.MediaDefiningClass
157  
-
  158
+    
158 159
     list_display = ('__str__',)
159 160
     list_display_links = ()
160 161
     list_filter = ()
@@ -166,13 +167,13 @@ class ModelAdmin(BaseModelAdmin):
166 167
     save_on_top = False
167 168
     ordering = None
168 169
     inlines = []
169  
-
  170
+    
170 171
     # Custom templates (designed to be over-ridden in subclasses)
171 172
     change_form_template = None
172 173
     change_list_template = None
173 174
     delete_confirmation_template = None
174 175
     object_history_template = None
175  
-
  176
+    
176 177
     def __init__(self, model, admin_site):
177 178
         self.model = model
178 179
         self.opts = model._meta
@@ -182,59 +183,79 @@ def __init__(self, model, admin_site):
182 183
             inline_instance = inline_class(self.model, self.admin_site)
183 184
             self.inline_instances.append(inline_instance)
184 185
         super(ModelAdmin, self).__init__()
185  
-
186  
-    def __call__(self, request, url):
187  
-        # Delegate to the appropriate method, based on the URL.
188  
-        if url is None:
189  
-            return self.changelist_view(request)
190  
-        elif url == "add":
191  
-            return self.add_view(request)
192  
-        elif url.endswith('/history'):
193  
-            return self.history_view(request, unquote(url[:-8]))
194  
-        elif url.endswith('/delete'):
195  
-            return self.delete_view(request, unquote(url[:-7]))
196  
-        else:
197  
-            return self.change_view(request, unquote(url))
198  
-
  186
+        
  187
+    def get_urls(self):
  188
+        from django.conf.urls.defaults import patterns, url
  189
+        
  190
+        def wrap(view):
  191
+            def wrapper(*args, **kwargs):
  192
+                return self.admin_site.admin_view(view)(*args, **kwargs)
  193
+            return update_wrapper(wrapper, view)
  194
+        
  195
+        info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
  196
+        
  197
+        urlpatterns = patterns('',
  198
+            url(r'^$',
  199
+                wrap(self.changelist_view),
  200
+                name='%sadmin_%s_%s_changelist' % info),
  201
+            url(r'^add/$',
  202
+                wrap(self.add_view),
  203
+                name='%sadmin_%s_%s_add' % info),
  204
+            url(r'^(.+)/history/$',
  205
+                wrap(self.history_view),
  206
+                name='%sadmin_%s_%s_history' % info),
  207
+            url(r'^(.+)/delete/$',
  208
+                wrap(self.delete_view),
  209
+                name='%sadmin_%s_%s_delete' % info),
  210
+            url(r'^(.+)/$',
  211
+                wrap(self.change_view),
  212
+                name='%sadmin_%s_%s_change' % info),
  213
+        )
  214
+        return urlpatterns
  215
+    
  216
+    def urls(self):
  217
+        return self.get_urls()
  218
+    urls = property(urls)
  219
+    
199 220
     def _media(self):
200 221
         from django.conf import settings
201  
-
  222
+        
202 223
         js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
203 224
         if self.prepopulated_fields:
204 225
             js.append('js/urlify.js')
205 226
         if self.opts.get_ordered_objects():
206 227
             js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
207  
-
  228
+        
208 229
         return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
209 230
     media = property(_media)
210  
-
  231
+    
211 232
     def has_add_permission(self, request):
212 233
         "Returns True if the given request has permission to add an object."
213 234
         opts = self.opts
214 235
         return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
215  
-
  236
+    
216 237
     def has_change_permission(self, request, obj=None):
217 238
         """
218 239
         Returns True if the given request has permission to change the given
219 240
         Django model instance.
220  
-
  241
+        
221 242
         If `obj` is None, this should return True if the given request has
222 243
         permission to change *any* object of the given type.
223 244
         """
224 245
         opts = self.opts
225 246
         return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
226  
-
  247
+    
227 248
     def has_delete_permission(self, request, obj=None):
228 249
         """
229 250
         Returns True if the given request has permission to change the given
230 251
         Django model instance.
231  
-
  252
+        
232 253
         If `obj` is None, this should return True if the given request has
233 254
         permission to delete *any* object of the given type.
234 255
         """
235 256
         opts = self.opts
236 257
         return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
237  
-
  258
+    
238 259
     def queryset(self, request):
239 260
         """
240 261
         Returns a QuerySet of all model instances that can be edited by the
@@ -246,14 +267,14 @@ def queryset(self, request):
246 267
         if ordering:
247 268
             qs = qs.order_by(*ordering)
248 269
         return qs
249  
-
  270
+    
250 271
     def get_fieldsets(self, request, obj=None):
251 272
         "Hook for specifying fieldsets for the add form."
252 273
         if self.declared_fieldsets:
253 274
             return self.declared_fieldsets
254 275
         form = self.get_form(request, obj)
255 276
         return [(None, {'fields': form.base_fields.keys()})]
256  
-
  277
+    
257 278
     def get_form(self, request, obj=None, **kwargs):
258 279
         """
259 280
         Returns a Form class for use in the admin add view. This is used by
@@ -275,42 +296,42 @@ def get_form(self, request, obj=None, **kwargs):
275 296
         }
276 297
         defaults.update(kwargs)
277 298
         return modelform_factory(self.model, **defaults)
278  
-
  299
+    
279 300
     def get_formsets(self, request, obj=None):
280 301
         for inline in self.inline_instances:
281 302
             yield inline.get_formset(request, obj)
282  
-            
  303
+    
283 304
     def log_addition(self, request, object):
284 305
         """
285  
-        Log that an object has been successfully added. 
  306
+        Log that an object has been successfully added.
286 307
         
287 308
         The default implementation creates an admin LogEntry object.
288 309
         """
289 310
         from django.contrib.admin.models import LogEntry, ADDITION
290 311
         LogEntry.objects.log_action(
291  
-            user_id         = request.user.pk, 
  312
+            user_id         = request.user.pk,
292 313
             content_type_id = ContentType.objects.get_for_model(object).pk,
293 314
             object_id       = object.pk,
294  
-            object_repr     = force_unicode(object), 
  315
+            object_repr     = force_unicode(object),
295 316
             action_flag     = ADDITION
296 317
         )
297  
-        
  318
+    
298 319
     def log_change(self, request, object, message):
299 320
         """
300  
-        Log that an object has been successfully changed. 
  321
+        Log that an object has been successfully changed.
301 322
         
302 323
         The default implementation creates an admin LogEntry object.
303 324
         """
304 325
         from django.contrib.admin.models import LogEntry, CHANGE
305 326
         LogEntry.objects.log_action(
306  
-            user_id         = request.user.pk, 
307  
-            content_type_id = ContentType.objects.get_for_model(object).pk, 
308  
-            object_id       = object.pk, 
309  
-            object_repr     = force_unicode(object), 
310  
-            action_flag     = CHANGE, 
  327
+            user_id         = request.user.pk,
  328
+            content_type_id = ContentType.objects.get_for_model(object).pk,
  329
+            object_id       = object.pk,
  330
+            object_repr     = force_unicode(object),
  331
+            action_flag     = CHANGE,
311 332
             change_message  = message
312 333
         )
313  
-        
  334
+    
314 335
     def log_deletion(self, request, object, object_repr):
315 336
         """
316 337
         Log that an object has been successfully deleted. Note that since the
@@ -321,13 +342,13 @@ def log_deletion(self, request, object, object_repr):
321 342
         """
322 343
         from django.contrib.admin.models import LogEntry, DELETION
323 344
         LogEntry.objects.log_action(
324  
-            user_id         = request.user.id, 
325  
-            content_type_id = ContentType.objects.get_for_model(self.model).pk, 
326  
-            object_id       = object.pk, 
  345
+            user_id         = request.user.id,
  346
+            content_type_id = ContentType.objects.get_for_model(self.model).pk,
  347
+            object_id       = object.pk,
327 348
             object_repr     = object_repr,
328 349
             action_flag     = DELETION
329 350
         )
330  
-        
  351
+    
331 352
     
332 353
     def construct_change_message(self, request, form, formsets):
333 354
         """
@@ -336,7 +357,7 @@ def construct_change_message(self, request, form, formsets):
336 357
         change_message = []
337 358
         if form.changed_data:
338 359
             change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
339  
-
  360
+        
340 361
         if formsets:
341 362
             for formset in formsets:
342 363
                 for added_object in formset.new_objects:
@@ -357,11 +378,11 @@ def construct_change_message(self, request, form, formsets):
357 378
     
358 379
     def message_user(self, request, message):
359 380
         """
360  
-        Send a message to the user. The default implementation 
  381
+        Send a message to the user. The default implementation
361 382
         posts a message using the auth Message object.
362 383
         """
363 384
         request.user.message_set.create(message=message)
364  
-
  385
+    
365 386
     def save_form(self, request, form, change):
366 387
         """
367 388
         Given a ModelForm return an unsaved instance. ``change`` is True if
@@ -374,13 +395,13 @@ def save_model(self, request, obj, form, change):
374 395
         Given a model instance save it to the database.
375 396
         """
376 397
         obj.save()
377  
-
  398
+    
378 399
     def save_formset(self, request, form, formset, change):
379 400
         """
380 401
         Given an inline formset save it to the database.
381 402
         """
382 403
         formset.save()
383  
-
  404
+    
384 405
     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
385 406
         opts = self.model._meta
386 407
         app_label = opts.app_label
@@ -432,7 +453,7 @@ def response_add(self, request, obj, post_url_continue='../%s/'):
432 453
             return HttpResponseRedirect(request.path)
433 454
         else:
434 455
             self.message_user(request, msg)
435  
-
  456
+            
436 457
             # Figure out where to redirect. If the user has change permission,
437 458
             # redirect to the change-list page for this object. Otherwise,
438 459
             # redirect to the admin index.
@@ -466,15 +487,15 @@ def response_change(self, request, obj):
466 487
         else:
467 488
             self.message_user(request, msg)
468 489
             return HttpResponseRedirect("../")
469  
-
  490
+    
470 491
     def add_view(self, request, form_url='', extra_context=None):
471 492
         "The 'add' admin view for this model."
472 493
         model = self.model
473 494
         opts = model._meta
474  
-
  495
+        
475 496
         if not self.has_add_permission(request):
476 497
             raise PermissionDenied
477  
-
  498
+        
478 499
         ModelForm = self.get_form(request)
479 500
         formsets = []
480 501
         if request.method == 'POST':
@@ -513,17 +534,17 @@ def add_view(self, request, form_url='', extra_context=None):
513 534
             for FormSet in self.get_formsets(request):
514 535
                 formset = FormSet(instance=self.model())
515 536
                 formsets.append(formset)
516  
-
  537
+        
517 538
         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
518 539
         media = self.media + adminForm.media
519  
-
  540
+        
520 541
         inline_admin_formsets = []
521 542
         for inline, formset in zip(self.inline_instances, formsets):
522 543
             fieldsets = list(inline.get_fieldsets(request))
523 544
             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
524 545
             inline_admin_formsets.append(inline_admin_formset)
525 546
             media = media + inline_admin_formset.media
526  
-
  547
+        
527 548
         context = {
528 549
             'title': _('Add %s') % force_unicode(opts.verbose_name),
529 550
             'adminform': adminForm,
@@ -538,29 +559,29 @@ def add_view(self, request, form_url='', extra_context=None):
538 559
         context.update(extra_context or {})
539 560
         return self.render_change_form(request, context, add=True)
540 561
     add_view = transaction.commit_on_success(add_view)
541  
-
  562
+    
542 563
     def change_view(self, request, object_id, extra_context=None):
543 564
         "The 'change' admin view for this model."
544 565
         model = self.model
545 566
         opts = model._meta
546  
-
  567
+        
547 568
         try:
548  
-            obj = model._default_manager.get(pk=object_id)
  569
+            obj = model._default_manager.get(pk=unquote(object_id))
549 570
         except model.DoesNotExist:
550 571
             # Don't raise Http404 just yet, because we haven't checked
551 572
             # permissions yet. We don't want an unauthenticated user to be able
552 573
             # to determine whether a given object exists.
553 574
             obj = None
554  
-
  575
+        
555 576
         if not self.has_change_permission(request, obj):
556 577
             raise PermissionDenied
557  
-
  578
+        
558 579
         if obj is None:
559 580
             raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
560  
-
  581
+        
561 582
         if request.method == 'POST' and request.POST.has_key("_saveasnew"):
562 583
             return self.add_view(request, form_url='../../add/')
563  
-
  584
+        
564 585
         ModelForm = self.get_form(request, obj)
565 586
         formsets = []
566 587
         if request.method == 'POST':
@@ -575,7 +596,7 @@ def change_view(self, request, object_id, extra_context=None):
575 596
                 formset = FormSet(request.POST, request.FILES,
576 597
                                   instance=new_object)
577 598
                 formsets.append(formset)
578  
-
  599
+            
579 600
             if all_valid(formsets) and form_validated:
580 601
                 self.save_model(request, new_object, form, change=True)
581 602
                 form.save_m2m()
@@ -585,16 +606,16 @@ def change_view(self, request, object_id, extra_context=None):
585 606
                 change_message = self.construct_change_message(request, form, formsets)
586 607
                 self.log_change(request, new_object, change_message)
587 608
                 return self.response_change(request, new_object)
588  
-                
  609
+        
589 610
         else:
590 611
             form = ModelForm(instance=obj)
591 612
             for FormSet in self.get_formsets(request, obj):
592 613
                 formset = FormSet(instance=obj)
593 614
                 formsets.append(formset)
594  
-
  615
+        
595 616
         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
596 617
         media = self.media + adminForm.media
597  
-
  618
+        
598 619
         inline_admin_formsets = []
599 620
         for inline, formset in zip(self.inline_instances, formsets):
600 621
             fieldsets = list(inline.get_fieldsets(request, obj))
@@ -617,7 +638,7 @@ def change_view(self, request, object_id, extra_context=None):
617 638
         context.update(extra_context or {})
618 639
         return self.render_change_form(request, context, change=True, obj=obj)
619 640
     change_view = transaction.commit_on_success(change_view)
620  
-
  641
+    
621 642
     def changelist_view(self, request, extra_context=None):
622 643
         "The 'change list' admin view for this model."
623 644
         from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
@@ -637,7 +658,7 @@ def changelist_view(self, request, extra_context=None):
637 658
             if ERROR_FLAG in request.GET.keys():
638 659
                 return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
639 660
             return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
640  
-
  661
+        
641 662
         context = {
642 663
             'title': cl.title,
643 664
             'is_popup': cl.is_popup,
@@ -652,32 +673,32 @@ def changelist_view(self, request, extra_context=None):
652 673
             'admin/%s/change_list.html' % app_label,
653 674
             'admin/change_list.html'
654 675
         ], context, context_instance=template.RequestContext(request))
655  
-
  676
+    
656 677
     def delete_view(self, request, object_id, extra_context=None):
657 678
         "The 'delete' admin view for this model."
658 679
         opts = self.model._meta
659 680
         app_label = opts.app_label
660  
-
  681
+        
661 682
         try:
662  
-            obj = self.model._default_manager.get(pk=object_id)
  683
+            obj = self.model._default_manager.get(pk=unquote(object_id))
663 684
         except self.model.DoesNotExist:
664 685
             # Don't raise Http404 just yet, because we haven't checked
665 686
             # permissions yet. We don't want an unauthenticated user to be able
666 687
             # to determine whether a given object exists.
667 688
             obj = None
668  
-
  689
+        
669 690
         if not self.has_delete_permission(request, obj):
670 691
             raise PermissionDenied
671  
-
  692
+        
672 693
         if obj is None:
673 694
             raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
674  
-
  695
+        
675 696
         # Populate deleted_objects, a data structure of all related objects that
676 697
         # will also be deleted.
677  
-        deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
  698
+        deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
678 699
         perms_needed = set()
679 700
         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
680  
-
  701
+        
681 702
         if request.POST: # The user has already confirmed the deletion.
682 703
             if perms_needed:
683 704
                 raise PermissionDenied
@@ -690,7 +711,7 @@ def delete_view(self, request, object_id, extra_context=None):
690 711
             if not self.has_change_permission(request, None):
691 712
                 return HttpResponseRedirect("../../../../")
692 713
             return HttpResponseRedirect("../../")
693  
-
  714
+        
694 715
         context = {
695 716
             "title": _("Are you sure?"),
696 717
             "object_name": force_unicode(opts.verbose_name),
@@ -707,7 +728,7 @@ def delete_view(self, request, object_id, extra_context=None):
707 728
             "admin/%s/delete_confirmation.html" % app_label,
708 729
             "admin/delete_confirmation.html"
709 730
         ], context, context_instance=template.RequestContext(request))
710  
-
  731
+    
711 732
     def history_view(self, request, object_id, extra_context=None):
712 733
         "The 'history' admin view for this model."
713 734
         from django.contrib.admin.models import LogEntry
@@ -735,10 +756,38 @@ def history_view(self, request, object_id, extra_context=None):
735 756
             "admin/object_history.html"
736 757
         ], context, context_instance=template.RequestContext(request))
737 758
 
  759
+    #
  760
+    # DEPRECATED methods.
  761
+    #
  762
+    def __call__(self, request, url):
  763
+        """
  764
+        DEPRECATED: this is the old way of URL resolution, replaced by
  765
+        ``get_urls()``. This only called by AdminSite.root(), which is also
  766
+        deprecated.
  767
+        
  768
+        Again, remember that the following code only exists for
  769
+        backwards-compatibility. Any new URLs, changes to existing URLs, or
  770
+        whatever need to be done up in get_urls(), above!
  771
+        
  772
+        This function still exists for backwards-compatibility; it will be
  773
+        removed in Django 1.3.
  774
+        """
  775
+        # Delegate to the appropriate method, based on the URL.
  776
+        if url is None:
  777
+            return self.changelist_view(request)
  778
+        elif url == "add":
  779
+            return self.add_view(request)
  780
+        elif url.endswith('/history'):
  781
+            return self.history_view(request, unquote(url[:-8]))
  782
+        elif url.endswith('/delete'):
  783
+            return self.delete_view(request, unquote(url[:-7]))
  784
+        else:
  785
+            return self.change_view(request, unquote(url))
  786
+
738 787
 class InlineModelAdmin(BaseModelAdmin):
739 788
     """
740 789
     Options for inline editing of ``model`` instances.
741  
-
  790
+    
742 791
     Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
743 792
     ``model`` to its parent. This is required if ``model`` has more than one
744 793
     ``ForeignKey`` to its parent.
@@ -751,7 +800,7 @@ class InlineModelAdmin(BaseModelAdmin):
751 800
     template = None
752 801
     verbose_name = None
753 802
     verbose_name_plural = None
754  
-
  803
+    
755 804
     def __init__(self, parent_model, admin_site):
756 805
         self.admin_site = admin_site
757 806
         self.parent_model = parent_model
@@ -771,7 +820,7 @@ def _media(self):
771 820
             js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
772 821
         return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
773 822
     media = property(_media)
774  
-
  823
+    
775 824
     def get_formset(self, request, obj=None, **kwargs):
776 825
         """Returns a BaseInlineFormSet class for use in admin add/change views."""
777 826
         if self.declared_fieldsets:
@@ -794,13 +843,13 @@ def get_formset(self, request, obj=None, **kwargs):
794 843
         }
795 844
         defaults.update(kwargs)
796 845
         return inlineformset_factory(self.parent_model, self.model, **defaults)
797  
-
  846
+    
798 847
     def get_fieldsets(self, request, obj=None):
799 848
         if self.declared_fieldsets:
800 849
             return self.declared_fieldsets
801 850
         form = self.get_formset(request).form
802 851
         return [(None, {'fields': form.base_fields.keys()})]
803  
-
  852
+    
804 853
 class StackedInline(InlineModelAdmin):
805 854
     template = 'admin/edit_inline/stacked.html'
806 855
 
285  django/contrib/admin/sites.py
... ...
@@ -1,4 +1,3 @@
1  
-import base64
2 1
 import re
3 2
 from django import http, template
4 3
 from django.contrib.admin import ModelAdmin
@@ -6,12 +5,12 @@
6 5
 from django.db.models.base import ModelBase
7 6
 from django.core.exceptions import ImproperlyConfigured
8 7
 from django.shortcuts import render_to_response
  8
+from django.utils.functional import update_wrapper
9 9
 from django.utils.safestring import mark_safe
10 10
 from django.utils.text import capfirst
11 11
 from django.utils.translation import ugettext_lazy, ugettext as _
12 12
 from django.views.decorators.cache import never_cache
13 13
 from django.conf import settings
14  
-from django.utils.hashcompat import md5_constructor
15 14
 
16 15
 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
17 16
 LOGIN_FORM_KEY = 'this_is_the_login_form'
@@ -29,24 +28,33 @@ class AdminSite(object):
29 28
     register() method, and the root() method can then be used as a Django view function
30 29
     that presents a full admin interface for the collection of registered models.
31 30
     """
32  
-
  31
+    
33 32
     index_template = None
34 33
     login_template = None
35 34
     app_index_template = None
36  
-
37  
-    def __init__(self):
  35
+    
  36
+    def __init__(self, name=None):
38 37
         self._registry = {} # model_class class -> admin_class instance
39  
-
  38
+        # TODO Root path is used to calculate urls under the old root() method
  39
+        # in order to maintain backwards compatibility we are leaving that in
  40
+        # so root_path isn't needed, not sure what to do about this.
  41
+        self.root_path = 'admin/'
  42
+        if name is None:
  43
+            name = ''
  44
+        else:
  45
+            name += '_'
  46
+        self.name = name
  47
+    
40 48
     def register(self, model_or_iterable, admin_class=None, **options):
41 49
         """
42 50
         Registers the given model(s) with the given admin class.
43  
-
  51
+        
44 52
         The model(s) should be Model classes, not instances.
45  
-
  53
+        
46 54
         If an admin class isn't given, it will use ModelAdmin (the default
47 55
         admin options). If keyword arguments are given -- e.g., list_display --
48 56
         they'll be applied as options to the admin class.
49  
-
  57
+        
50 58
         If a model is already registered, this will raise AlreadyRegistered.
51 59
         """
52 60
         # Don't import the humongous validation code unless required
@@ -54,7 +62,7 @@ def register(self, model_or_iterable, admin_class=None, **options):
54 62
             from django.contrib.admin.validation import validate
55 63
         else:
56 64
             validate = lambda model, adminclass: None
57  
-
  65
+        
58 66
         if not admin_class:
59 67
             admin_class = ModelAdmin
60 68
         if isinstance(model_or_iterable, ModelBase):
@@ -62,7 +70,7 @@ def register(self, model_or_iterable, admin_class=None, **options):
62 70
         for model in model_or_iterable:
63 71
             if model in self._registry:
64 72
                 raise AlreadyRegistered('The model %s is already registered' % model.__name__)
65  
-
  73
+            
66 74
             # If we got **options then dynamically construct a subclass of
67 75
             # admin_class with those **options.
68 76
             if options:
@@ -71,17 +79,17 @@ def register(self, model_or_iterable, admin_class=None, **options):
71 79
                 # which causes issues later on.
72 80
                 options['__module__'] = __name__
73 81
                 admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
74  
-
  82
+            
75 83
             # Validate (which might be a no-op)
76 84
             validate(admin_class, model)
77  
-
  85
+            
78 86
             # Instantiate the admin class to save in the registry
79 87
             self._registry[model] = admin_class(model, self)
80  
-
  88
+    
81 89
     def unregister(self, model_or_iterable):
82 90
         """
83 91
         Unregisters the given model(s).
84  
-
  92
+        
85 93
         If a model isn't already registered, this will raise NotRegistered.
86 94
         """
87 95
         if isinstance(model_or_iterable, ModelBase):
@@ -90,92 +98,100 @@ def unregister(self, model_or_iterable):
90 98
             if model not in self._registry:
91 99
                 raise NotRegistered('The model %s is not registered' % model.__name__)
92 100
             del self._registry[model]
93  
-
  101
+    
94 102
     def has_permission(self, request):
95 103
         """
96 104
         Returns True if the given HttpRequest has permission to view
97 105
         *at least one* page in the admin site.
98 106
         """
99 107
         return request.user.is_authenticated() and request.user.is_staff
100  
-
  108
+    
101 109
     def check_dependencies(self):
102 110
         """
103 111
         Check that all things needed to run the admin have been correctly installed.
104  
-
  112
+        
105 113
         The default implementation checks that LogEntry, ContentType and the
106 114
         auth context processor are installed.
107 115
         """
108 116
         from django.contrib.admin.models import LogEntry
109 117
         from django.contrib.contenttypes.models import ContentType
110  
-
  118
+        
111 119
         if not LogEntry._meta.installed:
112 120
             raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.")
113 121
         if not ContentType._meta.installed:
114 122
             raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.")
115 123
         if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
116 124
             raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
117  
-
118  
-    def root(self, request, url):
  125
+        
  126
+    def admin_view(self, view):
119 127
         """
120  
-        Handles main URL routing for the admin app.
121  
-
122  
-        `url` is the remainder of the URL -- e.g. 'comments/comment/'.
  128
+        Decorator to create an "admin view attached to this ``AdminSite``. This
  129
+        wraps the view and provides permission checking by calling
  130
+        ``self.has_permission``.
  131
+        
  132
+        You'll want to use this from within ``AdminSite.get_urls()``:
  133
+            
  134
+            class MyAdminSite(AdminSite):
  135
+                
  136
+                def get_urls(self):
  137
+                    from django.conf.urls.defaults import patterns, url
  138
+                    
  139
+                    urls = super(MyAdminSite, self).get_urls()
  140
+                    urls += patterns('',
  141
+                        url(r'^my_view/$', self.protected_view(some_view))
  142
+                    )
  143
+                    return urls
123 144
         """
124  
-        if request.method == 'GET' and not request.path.endswith('/'):
125  
-            return http.HttpResponseRedirect(request.path + '/')
126  
-
127  
-        if settings.DEBUG:
128  
-            self.check_dependencies()
129  
-
130  
-        # Figure out the admin base URL path and stash it for later use
131  
-        self.root_path = re.sub(re.escape(url) + '$', '', request.path)
132  
-
133  
-        url = url.rstrip('/') # Trim trailing slash, if it exists.
134  
-
135  
-        # The 'logout' view doesn't require that the person is logged in.
136  
-        if url == 'logout':
137  
-            return self.logout(request)
138  
-
139  
-        # Check permission to continue or display login form.
140  
-        if not self.has_permission(request):
141  
-            return self.login(request)
142  
-
143  
-        if url == '':
144  
-            return self.index(request)
145  
-        elif url == 'password_change':
146  
-            return self.password_change(request)
147  
-        elif url == 'password_change/done':
148  
-            return self.password_change_done(request)
149  
-        elif url == 'jsi18n':
150  
-            return self.i18n_javascript(request)
151  
-        # URLs starting with 'r/' are for the "View on site" links.
152  
-        elif url.startswith('r/'):
153  
-            from django.contrib.contenttypes.views import shortcut
154  
-            return shortcut(request, *url.split('/')[1:])
155  
-        else:
156  
-            if '/' in url:
157  
-                return self.model_page(request, *url.split('/', 2))
158  
-            else:
159  
-                return self.app_index(request, url)
160  
-
161  
-        raise http.Http404('The requested admin page does not exist.')
162  
-
163  
-    def model_page(self, request, app_label, model_name, rest_of_url=None):
164  
-        """
165  
-        Handles the model-specific functionality of the admin site, delegating
166  
-        to the appropriate ModelAdmin class.
167  
-        """
168  
-        from django.db import models
169  
-        model = models.get_model(app_label, model_name)
170  
-        if model is None:
171  
-            raise http.Http404("App %r, model %r, not found." % (app_label, model_name))
172  
-        try:
173  
-            admin_obj = self._registry[model]
174  
-        except KeyError:
175  
-            raise http.Http404("This model exists but has not been registered with the admin site.")
176  
-        return admin_obj(request, rest_of_url)
177  
-    model_page = never_cache(model_page)
178  
-
  145
+        def inner(request, *args, **kwargs):
  146
+            if not self.has_permission(request):
  147
+                return self.login(request)
  148
+            return view(request, *args, **kwargs)
  149
+        return update_wrapper(inner, view)
  150
+    
  151
+    def get_urls(self):
  152
+        from django.conf.urls.defaults import patterns, url, include
  153
+        
  154
+        def wrap(view):
  155
+            def wrapper(*args, **kwargs):
  156
+                return self.admin_view(view)(*args, **kwargs)
  157
+            return update_wrapper(wrapper, view)
  158
+        
  159
+        # Admin-site-wide views.
  160
+        urlpatterns = patterns('',
  161
+            url(r'^$',
  162
+                wrap(self.index),
  163
+                name='%sadmin_index' % self.name),
  164
+            url(r'^logout/$',
  165
+                wrap(self.logout),
  166
+                name='%sadmin_logout'),
  167
+            url(r'^password_change/$',
  168
+                wrap(self.password_change),
  169
+                name='%sadmin_password_change' % self.name),
  170
+            url(r'^password_change/done/$',
  171
+                wrap(self.password_change_done),
  172
+                name='%sadmin_password_change_done' % self.name),
  173
+            url(r'^jsi18n/$',
  174
+                wrap(self.i18n_javascript),
  175
+                name='%sadmin_jsi18n' % self.name),
  176
+            url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
  177
+                'django.views.defaults.shortcut'),
  178
+            url(r'^(?P<app_label>\w+)/$',
  179
+                wrap(self.app_index),
  180
+                name='%sadmin_app_list' % self.name),
  181
+        )
  182
+        
  183
+        # Add in each model's views.
  184
+        for model, model_admin in self._registry.iteritems():
  185
+            urlpatterns += patterns('',
  186
+                url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
  187
+                    include(model_admin.urls))
  188
+            )
  189
+        return urlpatterns
  190
+    
  191
+    def urls(self):
  192
+        return self.get_urls()
  193
+    urls = property(urls)
  194
+        
179 195
     def password_change(self, request):
180 196
         """
181 197
         Handles the "change password" task -- both form display and validation.
@@ -183,18 +199,18 @@ def password_change(self, request):
183 199
         from django.contrib.auth.views import password_change
184 200
         return password_change(request,
185 201
             post_change_redirect='%spassword_change/done/' % self.root_path)
186  
-
  202
+    
187 203
     def password_change_done(self, request):
188 204
         """
189 205
         Displays the "success" page after a password change.
190 206
         """
191 207
         from django.contrib.auth.views import password_change_done
192 208
         return password_change_done(request)
193  
-
  209
+    
194 210
     def i18n_javascript(self, request):
195 211
         """
196 212
         Displays the i18n JavaScript that the Django admin requires.
197  
-
  213
+        
198 214
         This takes into account the USE_I18N setting. If it's set to False, the
199 215
         generated JavaScript will be leaner and faster.
200 216
         """
@@ -203,23 +219,23 @@ def i18n_javascript(self, request):
203 219
         else:
204 220
             from django.views.i18n import null_javascript_catalog as javascript_catalog
205 221
         return javascript_catalog(request, packages='django.conf')
206  
-
  222
+    
207 223
     def logout(self, request):
208 224
         """
209 225
         Logs out the user for the given HttpRequest.
210  
-
  226
+        
211 227
         This should *not* assume the user is already logged in.
212 228
         """
213 229
         from django.contrib.auth.views import logout
214 230
         return logout(request)
215 231
     logout = never_cache(logout)
216  
-
  232
+    
217 233
     def login(self, request):
218 234
         """
219 235
         Displays the login form for the given HttpRequest.
220 236
         """
221 237
         from django.contrib.auth.models import User
222  
-
  238
+        
223 239
         # If this isn't already the login page, display it.
224 240
         if not request.POST.has_key(LOGIN_FORM_KEY):
225 241
             if request.POST:
@@ -227,14 +243,14 @@ def login(self, request):
227 243
             else:
228 244
                 message = ""
229 245
             return self.display_login_form(request, message)
230  
-
  246
+        
231 247
         # Check that the user accepts cookies.
232 248
         if not request.session.test_cookie_worked():
233 249
             message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
234 250
             return self.display_login_form(request, message)
235 251
         else:
236 252
             request.session.delete_test_cookie()
237  
-
  253
+        
238 254
         # Check the password.
239 255
         username = request.POST.get('username', None)
240 256
         password = request.POST.get('password', None)
@@ -254,7 +270,7 @@ def login(self, request):
254 270
                     else:
255 271
                         message = _("Usernames cannot contain the '@' character.")
256 272
             return self.display_login_form(request, message)
257  
-
  273
+        
258 274
         # The user data is correct; log in the user in and continue.
259 275
         else:
260 276
             if user.is_active and user.is_staff:
@@ -263,7 +279,7 @@ def login(self, request):
263 279
             else:
264 280
                 return self.display_login_form(request, ERROR_MESSAGE)
265 281
     login = never_cache(login)
266  
-
  282
+    
267 283
     def index(self, request, extra_context=None):
268 284
         """
269 285
         Displays the main admin index page, which lists all of the installed
@@ -274,14 +290,14 @@ def index(self, request, extra_context=None):
274 290
         for model, model_admin in self._registry.items():
275 291
             app_label = model._meta.app_label
276 292
             has_module_perms = user.has_module_perms(app_label)
277  
-
  293
+            
278 294
             if has_module_perms:
279 295
                 perms = {
280 296
                     'add': model_admin.has_add_permission(request),
281 297
                     'change': model_admin.has_change_permission(request),
282 298
                     'delete': model_admin.has_delete_permission(request),
283 299
                 }
284  
-
  300
+                
285 301
                 # Check whether user has any perm for this module.
286 302
                 # If so, add the module to the model_list.
287 303
                 if True in perms.values():
@@ -299,15 +315,15 @@ def index(self, request, extra_context=None):
299 315
                             'has_module_perms': has_module_perms,
300 316
                             'models': [model_dict],
301 317
                         }
302  
-
  318
+        
303 319
         # Sort the apps alphabetically.
304 320
         app_list = app_dict.values()
305 321
         app_list.sort(lambda x, y: cmp(x['name'], y['name']))
306  
-
  322
+        
307 323
         # Sort the models alphabetically within each app.
308 324
         for app in app_list:
309 325
             app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
310  
-
  326
+        
311 327
         context = {
312 328
             'title': _('Site administration'),
313 329
             'app_list': app_list,
@@ -318,7 +334,7 @@ def index(self, request, extra_context=None):
318 334
             context_instance=template.RequestContext(request)
319 335
         )
320 336
     index = never_cache(index)
321  
-
  337
+    
322 338
     def display_login_form(self, request, error_message='', extra_context=None):
323 339
         request.session.set_test_cookie()
324 340
         context = {
@@ -331,7 +347,7 @@ def display_login_form(self, request, error_message='', extra_context=None):
331 347
         return render_to_response(self.login_template or 'admin/login.html', context,
332 348
             context_instance=template.RequestContext(request)
333 349
         )
334  
-
  350
+    
335 351
     def app_index(self, request, app_label, extra_context=None):
336 352
         user = request.user
337 353
         has_module_perms = user.has_module_perms(app_label)
@@ -377,6 +393,81 @@ def app_index(self, request, app_label, extra_context=None):
377 393
         return render_to_response(self.app_index_template or 'admin/app_index.html', context,
378 394
             context_instance=template.RequestContext(request)
379 395
         )
  396
+        
  397
+    def root(self, request, url):
  398
+        """
  399
+        DEPRECATED. This function is the old way of handling URL resolution, and
  400
+        is deprecated in favor of real URL resolution -- see ``get_urls()``.
  401
+        
  402
+        This function still exists for backwards-compatibility; it will be
  403
+        removed in Django 1.3.
  404
+        """
  405
+        import warnings
  406
+        warnings.warn(
  407
+            "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", 
  408
+            PendingDeprecationWarning
  409
+        )
  410
+        
  411
+        #
  412
+        # Again, remember that the following only exists for
  413
+        # backwards-compatibility. Any new URLs, changes to existing URLs, or
  414
+        # whatever need to be done up in get_urls(), above!
  415
+        #
  416
+        
  417
+        if request.method == 'GET' and not request.path.endswith('/'):
  418
+            return http.HttpResponseRedirect(request.path + '/')
  419
+        
  420
+        if settings.DEBUG:
  421
+            self.check_dependencies()
  422
+        
  423
+        # Figure out the admin base URL path and stash it for later use
  424
+        self.root_path = re.sub(re.escape(url) + '$', '', request.path)
  425
+        
  426
+        url = url.rstrip('/') # Trim trailing slash, if it exists.
  427
+        
  428
+        # The 'logout' view doesn't require that the person is logged in.
  429
+        if url == 'logout':
  430
+            return self.logout(request)
  431
+        
  432
+        # Check permission to continue or display login form.
  433
+        if not self.has_permission(request):
  434
+            return self.login(request)
  435
+        
  436
+        if url == '':
  437
+            return self.index(request)
  438
+        elif url == 'password_change':
  439
+            return self.password_change(request)
  440
+        elif url == 'password_change/done':
  441
+            return self.password_change_done(request)
  442
+        elif url == 'jsi18n':
  443
+            return self.i18n_javascript(request)
  444
+        # URLs starting with 'r/' are for the "View on site" links.
  445
+        elif url.startswith('r/'):
  446
+            from django.contrib.contenttypes.views import shortcut
  447
+            return shortcut(request, *url.split('/')[1:])
  448
+        else:
  449
+            if '/' in url:
  450
+                return self.model_page(request, *url.split('/', 2))
  451
+            else:
  452
+                return self.app_index(request, url)
  453
+        
  454
+        raise http.Http404('The requested admin page does not exist.')
  455
+        
  456
+    def model_page(self, request, app_label, model_name, rest_of_url=None):
  457
+        """
  458
+        DEPRECATED. This is the old way of handling a model view on the admin
  459
+        site; the new views should use get_urls(), above.
  460
+        """
  461
+        from django.db import models
  462
+        model = models.get_model(app_label, model_name)
  463
+        if model is None:
  464
+            raise http.Http404("App %r, model %r, not found." % (app_label, model_name))
  465
+        try:
  466
+            admin_obj = self._registry[model]
  467
+        except KeyError:
  468
+            raise http.Http404("This model exists but has not been registered with the admin site.")
  469
+        return admin_obj(request, rest_of_url)
  470
+    model_page = never_cache(model_page)    
380 471
 
381 472
 # This global object represents the default admin site, for the common case.
382 473
 # You can instantiate AdminSite in your own code to create a custom admin site.
1  django/contrib/admin/util.py
@@ -6,7 +6,6 @@
6 6
 from django.utils.encoding import force_unicode
7 7
 from django.utils.translation import ugettext as _
8