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
This commit is contained in:
Jacob Kaplan-Moss 2009-01-14 20:22:25 +00:00
parent 6c4e5f0f0e
commit 1f84630c87
10 changed files with 484 additions and 257 deletions

View File

@ -13,5 +13,5 @@ urlpatterns = patterns('',
# (r'^admin/doc/', include('django.contrib.admindocs.urls')), # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin: # Uncomment the next line to enable the admin:
# (r'^admin/(.*)', admin.site.root), # (r'^admin/', include(admin.site.urls)),
) )

View File

@ -5,11 +5,12 @@ from django.forms.models import BaseInlineFormSet
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets from django.contrib.admin import widgets
from django.contrib.admin import helpers from django.contrib.admin import helpers
from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models, transaction from django.db import models, transaction
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response from django.shortcuts import get_object_or_404, render_to_response
from django.utils.functional import update_wrapper
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list from django.utils.text import capfirst, get_text_list
@ -38,12 +39,12 @@ class BaseModelAdmin(object):
filter_horizontal = () filter_horizontal = ()
radio_fields = {} radio_fields = {}
prepopulated_fields = {} prepopulated_fields = {}
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" """
Hook for specifying the form Field instance for a given database Field Hook for specifying the form Field instance for a given database Field
instance. instance.
If kwargs are given, they're passed to the form Field's constructor. If kwargs are given, they're passed to the form Field's constructor.
""" """
@ -63,18 +64,18 @@ class BaseModelAdmin(object):
else: else:
# Otherwise, use the default select widget. # Otherwise, use the default select widget.
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For DateTimeFields, use a special field and widget. # For DateTimeFields, use a special field and widget.
if isinstance(db_field, models.DateTimeField): if isinstance(db_field, models.DateTimeField):
kwargs['form_class'] = forms.SplitDateTimeField kwargs['form_class'] = forms.SplitDateTimeField
kwargs['widget'] = widgets.AdminSplitDateTime() kwargs['widget'] = widgets.AdminSplitDateTime()
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For DateFields, add a custom CSS class. # For DateFields, add a custom CSS class.
if isinstance(db_field, models.DateField): if isinstance(db_field, models.DateField):
kwargs['widget'] = widgets.AdminDateWidget kwargs['widget'] = widgets.AdminDateWidget
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For TimeFields, add a custom CSS class. # For TimeFields, add a custom CSS class.
if isinstance(db_field, models.TimeField): if isinstance(db_field, models.TimeField):
kwargs['widget'] = widgets.AdminTimeWidget kwargs['widget'] = widgets.AdminTimeWidget
@ -94,22 +95,22 @@ class BaseModelAdmin(object):
if isinstance(db_field, models.IntegerField): if isinstance(db_field, models.IntegerField):
kwargs['widget'] = widgets.AdminIntegerFieldWidget kwargs['widget'] = widgets.AdminIntegerFieldWidget
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For CommaSeparatedIntegerFields, add a custom CSS class. # For CommaSeparatedIntegerFields, add a custom CSS class.
if isinstance(db_field, models.CommaSeparatedIntegerField): if isinstance(db_field, models.CommaSeparatedIntegerField):
kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For TextInputs, add a custom CSS class. # For TextInputs, add a custom CSS class.
if isinstance(db_field, models.CharField): if isinstance(db_field, models.CharField):
kwargs['widget'] = widgets.AdminTextInputWidget kwargs['widget'] = widgets.AdminTextInputWidget
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For FileFields and ImageFields add a link to the current file. # For FileFields and ImageFields add a link to the current file.
if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField): if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
kwargs['widget'] = widgets.AdminFileWidget kwargs['widget'] = widgets.AdminFileWidget
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
# For ForeignKey or ManyToManyFields, use a special widget. # For ForeignKey or ManyToManyFields, use a special widget.
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
@ -139,10 +140,10 @@ class BaseModelAdmin(object):
if formfield is not None: if formfield is not None:
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
return formfield return formfield
# For any other type of field, just call its formfield() method. # For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
def _declared_fieldsets(self): def _declared_fieldsets(self):
if self.fieldsets: if self.fieldsets:
return self.fieldsets return self.fieldsets
@ -154,7 +155,7 @@ class BaseModelAdmin(object):
class ModelAdmin(BaseModelAdmin): class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model." "Encapsulates all admin options and functionality for a given model."
__metaclass__ = forms.MediaDefiningClass __metaclass__ = forms.MediaDefiningClass
list_display = ('__str__',) list_display = ('__str__',)
list_display_links = () list_display_links = ()
list_filter = () list_filter = ()
@ -166,13 +167,13 @@ class ModelAdmin(BaseModelAdmin):
save_on_top = False save_on_top = False
ordering = None ordering = None
inlines = [] inlines = []
# Custom templates (designed to be over-ridden in subclasses) # Custom templates (designed to be over-ridden in subclasses)
change_form_template = None change_form_template = None
change_list_template = None change_list_template = None
delete_confirmation_template = None delete_confirmation_template = None
object_history_template = None object_history_template = None
def __init__(self, model, admin_site): def __init__(self, model, admin_site):
self.model = model self.model = model
self.opts = model._meta self.opts = model._meta
@ -182,59 +183,79 @@ class ModelAdmin(BaseModelAdmin):
inline_instance = inline_class(self.model, self.admin_site) inline_instance = inline_class(self.model, self.admin_site)
self.inline_instances.append(inline_instance) self.inline_instances.append(inline_instance)
super(ModelAdmin, self).__init__() super(ModelAdmin, self).__init__()
def __call__(self, request, url): def get_urls(self):
# Delegate to the appropriate method, based on the URL. from django.conf.urls.defaults import patterns, url
if url is None:
return self.changelist_view(request) def wrap(view):
elif url == "add": def wrapper(*args, **kwargs):
return self.add_view(request) return self.admin_site.admin_view(view)(*args, **kwargs)
elif url.endswith('/history'): return update_wrapper(wrapper, view)
return self.history_view(request, unquote(url[:-8]))
elif url.endswith('/delete'): info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
return self.delete_view(request, unquote(url[:-7]))
else: urlpatterns = patterns('',
return self.change_view(request, unquote(url)) url(r'^$',
wrap(self.changelist_view),
name='%sadmin_%s_%s_changelist' % info),
url(r'^add/$',
wrap(self.add_view),
name='%sadmin_%s_%s_add' % info),
url(r'^(.+)/history/$',
wrap(self.history_view),
name='%sadmin_%s_%s_history' % info),
url(r'^(.+)/delete/$',
wrap(self.delete_view),
name='%sadmin_%s_%s_delete' % info),
url(r'^(.+)/$',
wrap(self.change_view),
name='%sadmin_%s_%s_change' % info),
)
return urlpatterns
def urls(self):
return self.get_urls()
urls = property(urls)
def _media(self): def _media(self):
from django.conf import settings from django.conf import settings
js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
if self.prepopulated_fields: if self.prepopulated_fields:
js.append('js/urlify.js') js.append('js/urlify.js')
if self.opts.get_ordered_objects(): if self.opts.get_ordered_objects():
js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
media = property(_media) media = property(_media)
def has_add_permission(self, request): def has_add_permission(self, request):
"Returns True if the given request has permission to add an object." "Returns True if the given request has permission to add an object."
opts = self.opts opts = self.opts
return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
""" """
Returns True if the given request has permission to change the given Returns True if the given request has permission to change the given
Django model instance. Django model instance.
If `obj` is None, this should return True if the given request has If `obj` is None, this should return True if the given request has
permission to change *any* object of the given type. permission to change *any* object of the given type.
""" """
opts = self.opts opts = self.opts
return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
""" """
Returns True if the given request has permission to change the given Returns True if the given request has permission to change the given
Django model instance. Django model instance.
If `obj` is None, this should return True if the given request has If `obj` is None, this should return True if the given request has
permission to delete *any* object of the given type. permission to delete *any* object of the given type.
""" """
opts = self.opts opts = self.opts
return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
def queryset(self, request): def queryset(self, request):
""" """
Returns a QuerySet of all model instances that can be edited by the Returns a QuerySet of all model instances that can be edited by the
@ -246,14 +267,14 @@ class ModelAdmin(BaseModelAdmin):
if ordering: if ordering:
qs = qs.order_by(*ordering) qs = qs.order_by(*ordering)
return qs return qs
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
"Hook for specifying fieldsets for the add form." "Hook for specifying fieldsets for the add form."
if self.declared_fieldsets: if self.declared_fieldsets:
return self.declared_fieldsets return self.declared_fieldsets
form = self.get_form(request, obj) form = self.get_form(request, obj)
return [(None, {'fields': form.base_fields.keys()})] return [(None, {'fields': form.base_fields.keys()})]
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
""" """
Returns a Form class for use in the admin add view. This is used by Returns a Form class for use in the admin add view. This is used by
@ -275,42 +296,42 @@ class ModelAdmin(BaseModelAdmin):
} }
defaults.update(kwargs) defaults.update(kwargs)
return modelform_factory(self.model, **defaults) return modelform_factory(self.model, **defaults)
def get_formsets(self, request, obj=None): def get_formsets(self, request, obj=None):
for inline in self.inline_instances: for inline in self.inline_instances:
yield inline.get_formset(request, obj) yield inline.get_formset(request, obj)
def log_addition(self, request, object): def log_addition(self, request, object):
""" """
Log that an object has been successfully added. Log that an object has been successfully added.
The default implementation creates an admin LogEntry object. The default implementation creates an admin LogEntry object.
""" """
from django.contrib.admin.models import LogEntry, ADDITION from django.contrib.admin.models import LogEntry, ADDITION
LogEntry.objects.log_action( LogEntry.objects.log_action(
user_id = request.user.pk, user_id = request.user.pk,
content_type_id = ContentType.objects.get_for_model(object).pk, content_type_id = ContentType.objects.get_for_model(object).pk,
object_id = object.pk, object_id = object.pk,
object_repr = force_unicode(object), object_repr = force_unicode(object),
action_flag = ADDITION action_flag = ADDITION
) )
def log_change(self, request, object, message): def log_change(self, request, object, message):
""" """
Log that an object has been successfully changed. Log that an object has been successfully changed.
The default implementation creates an admin LogEntry object. The default implementation creates an admin LogEntry object.
""" """
from django.contrib.admin.models import LogEntry, CHANGE from django.contrib.admin.models import LogEntry, CHANGE
LogEntry.objects.log_action( LogEntry.objects.log_action(
user_id = request.user.pk, user_id = request.user.pk,
content_type_id = ContentType.objects.get_for_model(object).pk, content_type_id = ContentType.objects.get_for_model(object).pk,
object_id = object.pk, object_id = object.pk,
object_repr = force_unicode(object), object_repr = force_unicode(object),
action_flag = CHANGE, action_flag = CHANGE,
change_message = message change_message = message
) )
def log_deletion(self, request, object, object_repr): def log_deletion(self, request, object, object_repr):
""" """
Log that an object has been successfully deleted. Note that since the Log that an object has been successfully deleted. Note that since the
@ -321,13 +342,13 @@ class ModelAdmin(BaseModelAdmin):
""" """
from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.models import LogEntry, DELETION
LogEntry.objects.log_action( LogEntry.objects.log_action(
user_id = request.user.id, user_id = request.user.id,
content_type_id = ContentType.objects.get_for_model(self.model).pk, content_type_id = ContentType.objects.get_for_model(self.model).pk,
object_id = object.pk, object_id = object.pk,
object_repr = object_repr, object_repr = object_repr,
action_flag = DELETION action_flag = DELETION
) )
def construct_change_message(self, request, form, formsets): def construct_change_message(self, request, form, formsets):
""" """
@ -336,7 +357,7 @@ class ModelAdmin(BaseModelAdmin):
change_message = [] change_message = []
if form.changed_data: if form.changed_data:
change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and'))) change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
if formsets: if formsets:
for formset in formsets: for formset in formsets:
for added_object in formset.new_objects: for added_object in formset.new_objects:
@ -357,11 +378,11 @@ class ModelAdmin(BaseModelAdmin):
def message_user(self, request, message): def message_user(self, request, message):
""" """
Send a message to the user. The default implementation Send a message to the user. The default implementation
posts a message using the auth Message object. posts a message using the auth Message object.
""" """
request.user.message_set.create(message=message) request.user.message_set.create(message=message)
def save_form(self, request, form, change): def save_form(self, request, form, change):
""" """
Given a ModelForm return an unsaved instance. ``change`` is True if Given a ModelForm return an unsaved instance. ``change`` is True if
@ -374,13 +395,13 @@ class ModelAdmin(BaseModelAdmin):
Given a model instance save it to the database. Given a model instance save it to the database.
""" """
obj.save() obj.save()
def save_formset(self, request, form, formset, change): def save_formset(self, request, form, formset, change):
""" """
Given an inline formset save it to the database. Given an inline formset save it to the database.
""" """
formset.save() formset.save()
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta opts = self.model._meta
app_label = opts.app_label app_label = opts.app_label
@ -432,7 +453,7 @@ class ModelAdmin(BaseModelAdmin):
return HttpResponseRedirect(request.path) return HttpResponseRedirect(request.path)
else: else:
self.message_user(request, msg) self.message_user(request, msg)
# Figure out where to redirect. If the user has change permission, # Figure out where to redirect. If the user has change permission,
# redirect to the change-list page for this object. Otherwise, # redirect to the change-list page for this object. Otherwise,
# redirect to the admin index. # redirect to the admin index.
@ -466,15 +487,15 @@ class ModelAdmin(BaseModelAdmin):
else: else:
self.message_user(request, msg) self.message_user(request, msg)
return HttpResponseRedirect("../") return HttpResponseRedirect("../")
def add_view(self, request, form_url='', extra_context=None): def add_view(self, request, form_url='', extra_context=None):
"The 'add' admin view for this model." "The 'add' admin view for this model."
model = self.model model = self.model
opts = model._meta opts = model._meta
if not self.has_add_permission(request): if not self.has_add_permission(request):
raise PermissionDenied raise PermissionDenied
ModelForm = self.get_form(request) ModelForm = self.get_form(request)
formsets = [] formsets = []
if request.method == 'POST': if request.method == 'POST':
@ -513,17 +534,17 @@ class ModelAdmin(BaseModelAdmin):
for FormSet in self.get_formsets(request): for FormSet in self.get_formsets(request):
formset = FormSet(instance=self.model()) formset = FormSet(instance=self.model())
formsets.append(formset) formsets.append(formset)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
media = self.media + adminForm.media media = self.media + adminForm.media
inline_admin_formsets = [] inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets): for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request)) fieldsets = list(inline.get_fieldsets(request))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets) inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
inline_admin_formsets.append(inline_admin_formset) inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media media = media + inline_admin_formset.media
context = { context = {
'title': _('Add %s') % force_unicode(opts.verbose_name), 'title': _('Add %s') % force_unicode(opts.verbose_name),
'adminform': adminForm, 'adminform': adminForm,
@ -538,29 +559,29 @@ class ModelAdmin(BaseModelAdmin):
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, add=True) return self.render_change_form(request, context, add=True)
add_view = transaction.commit_on_success(add_view) add_view = transaction.commit_on_success(add_view)
def change_view(self, request, object_id, extra_context=None): def change_view(self, request, object_id, extra_context=None):
"The 'change' admin view for this model." "The 'change' admin view for this model."
model = self.model model = self.model
opts = model._meta opts = model._meta
try: try:
obj = model._default_manager.get(pk=object_id) obj = model._default_manager.get(pk=unquote(object_id))
except model.DoesNotExist: except model.DoesNotExist:
# Don't raise Http404 just yet, because we haven't checked # Don't raise Http404 just yet, because we haven't checked
# permissions yet. We don't want an unauthenticated user to be able # permissions yet. We don't want an unauthenticated user to be able
# to determine whether a given object exists. # to determine whether a given object exists.
obj = None obj = None
if not self.has_change_permission(request, obj): if not self.has_change_permission(request, obj):
raise PermissionDenied raise PermissionDenied
if obj is None: if obj is None:
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
if request.method == 'POST' and request.POST.has_key("_saveasnew"): if request.method == 'POST' and request.POST.has_key("_saveasnew"):
return self.add_view(request, form_url='../../add/') return self.add_view(request, form_url='../../add/')
ModelForm = self.get_form(request, obj) ModelForm = self.get_form(request, obj)
formsets = [] formsets = []
if request.method == 'POST': if request.method == 'POST':
@ -575,7 +596,7 @@ class ModelAdmin(BaseModelAdmin):
formset = FormSet(request.POST, request.FILES, formset = FormSet(request.POST, request.FILES,
instance=new_object) instance=new_object)
formsets.append(formset) formsets.append(formset)
if all_valid(formsets) and form_validated: if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, change=True) self.save_model(request, new_object, form, change=True)
form.save_m2m() form.save_m2m()
@ -585,16 +606,16 @@ class ModelAdmin(BaseModelAdmin):
change_message = self.construct_change_message(request, form, formsets) change_message = self.construct_change_message(request, form, formsets)
self.log_change(request, new_object, change_message) self.log_change(request, new_object, change_message)
return self.response_change(request, new_object) return self.response_change(request, new_object)
else: else:
form = ModelForm(instance=obj) form = ModelForm(instance=obj)
for FormSet in self.get_formsets(request, obj): for FormSet in self.get_formsets(request, obj):
formset = FormSet(instance=obj) formset = FormSet(instance=obj)
formsets.append(formset) formsets.append(formset)
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
media = self.media + adminForm.media media = self.media + adminForm.media
inline_admin_formsets = [] inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets): for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj)) fieldsets = list(inline.get_fieldsets(request, obj))
@ -617,7 +638,7 @@ class ModelAdmin(BaseModelAdmin):
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj) return self.render_change_form(request, context, change=True, obj=obj)
change_view = transaction.commit_on_success(change_view) change_view = transaction.commit_on_success(change_view)
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
"The 'change list' admin view for this model." "The 'change list' admin view for this model."
from django.contrib.admin.views.main import ChangeList, ERROR_FLAG from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
@ -637,7 +658,7 @@ class ModelAdmin(BaseModelAdmin):
if ERROR_FLAG in request.GET.keys(): if ERROR_FLAG in request.GET.keys():
return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
context = { context = {
'title': cl.title, 'title': cl.title,
'is_popup': cl.is_popup, 'is_popup': cl.is_popup,
@ -652,32 +673,32 @@ class ModelAdmin(BaseModelAdmin):
'admin/%s/change_list.html' % app_label, 'admin/%s/change_list.html' % app_label,
'admin/change_list.html' 'admin/change_list.html'
], context, context_instance=template.RequestContext(request)) ], context, context_instance=template.RequestContext(request))
def delete_view(self, request, object_id, extra_context=None): def delete_view(self, request, object_id, extra_context=None):
"The 'delete' admin view for this model." "The 'delete' admin view for this model."
opts = self.model._meta opts = self.model._meta
app_label = opts.app_label app_label = opts.app_label
try: try:
obj = self.model._default_manager.get(pk=object_id) obj = self.model._default_manager.get(pk=unquote(object_id))
except self.model.DoesNotExist: except self.model.DoesNotExist:
# Don't raise Http404 just yet, because we haven't checked # Don't raise Http404 just yet, because we haven't checked
# permissions yet. We don't want an unauthenticated user to be able # permissions yet. We don't want an unauthenticated user to be able
# to determine whether a given object exists. # to determine whether a given object exists.
obj = None obj = None
if not self.has_delete_permission(request, obj): if not self.has_delete_permission(request, obj):
raise PermissionDenied raise PermissionDenied
if obj is None: if obj is None:
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
# Populate deleted_objects, a data structure of all related objects that # Populate deleted_objects, a data structure of all related objects that
# will also be deleted. # will also be deleted.
deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []] deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
perms_needed = set() perms_needed = set()
get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
if request.POST: # The user has already confirmed the deletion. if request.POST: # The user has already confirmed the deletion.
if perms_needed: if perms_needed:
raise PermissionDenied raise PermissionDenied
@ -690,7 +711,7 @@ class ModelAdmin(BaseModelAdmin):
if not self.has_change_permission(request, None): if not self.has_change_permission(request, None):
return HttpResponseRedirect("../../../../") return HttpResponseRedirect("../../../../")
return HttpResponseRedirect("../../") return HttpResponseRedirect("../../")
context = { context = {
"title": _("Are you sure?"), "title": _("Are you sure?"),
"object_name": force_unicode(opts.verbose_name), "object_name": force_unicode(opts.verbose_name),
@ -707,7 +728,7 @@ class ModelAdmin(BaseModelAdmin):
"admin/%s/delete_confirmation.html" % app_label, "admin/%s/delete_confirmation.html" % app_label,
"admin/delete_confirmation.html" "admin/delete_confirmation.html"
], context, context_instance=template.RequestContext(request)) ], context, context_instance=template.RequestContext(request))
def history_view(self, request, object_id, extra_context=None): def history_view(self, request, object_id, extra_context=None):
"The 'history' admin view for this model." "The 'history' admin view for this model."
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
@ -735,10 +756,38 @@ class ModelAdmin(BaseModelAdmin):
"admin/object_history.html" "admin/object_history.html"
], context, context_instance=template.RequestContext(request)) ], context, context_instance=template.RequestContext(request))
#
# DEPRECATED methods.
#
def __call__(self, request, url):
"""
DEPRECATED: this is the old way of URL resolution, replaced by
``get_urls()``. This only called by AdminSite.root(), which is also
deprecated.
Again, remember that the following code only exists for
backwards-compatibility. Any new URLs, changes to existing URLs, or
whatever need to be done up in get_urls(), above!
This function still exists for backwards-compatibility; it will be
removed in Django 1.3.
"""
# Delegate to the appropriate method, based on the URL.
if url is None:
return self.changelist_view(request)
elif url == "add":
return self.add_view(request)
elif url.endswith('/history'):
return self.history_view(request, unquote(url[:-8]))
elif url.endswith('/delete'):
return self.delete_view(request, unquote(url[:-7]))
else:
return self.change_view(request, unquote(url))
class InlineModelAdmin(BaseModelAdmin): class InlineModelAdmin(BaseModelAdmin):
""" """
Options for inline editing of ``model`` instances. Options for inline editing of ``model`` instances.
Provide ``name`` to specify the attribute name of the ``ForeignKey`` from Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
``model`` to its parent. This is required if ``model`` has more than one ``model`` to its parent. This is required if ``model`` has more than one
``ForeignKey`` to its parent. ``ForeignKey`` to its parent.
@ -751,7 +800,7 @@ class InlineModelAdmin(BaseModelAdmin):
template = None template = None
verbose_name = None verbose_name = None
verbose_name_plural = None verbose_name_plural = None
def __init__(self, parent_model, admin_site): def __init__(self, parent_model, admin_site):
self.admin_site = admin_site self.admin_site = admin_site
self.parent_model = parent_model self.parent_model = parent_model
@ -771,7 +820,7 @@ class InlineModelAdmin(BaseModelAdmin):
js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
media = property(_media) media = property(_media)
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
"""Returns a BaseInlineFormSet class for use in admin add/change views.""" """Returns a BaseInlineFormSet class for use in admin add/change views."""
if self.declared_fieldsets: if self.declared_fieldsets:
@ -794,13 +843,13 @@ class InlineModelAdmin(BaseModelAdmin):
} }
defaults.update(kwargs) defaults.update(kwargs)
return inlineformset_factory(self.parent_model, self.model, **defaults) return inlineformset_factory(self.parent_model, self.model, **defaults)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets: if self.declared_fieldsets:
return self.declared_fieldsets return self.declared_fieldsets
form = self.get_formset(request).form form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})] return [(None, {'fields': form.base_fields.keys()})]
class StackedInline(InlineModelAdmin): class StackedInline(InlineModelAdmin):
template = 'admin/edit_inline/stacked.html' template = 'admin/edit_inline/stacked.html'

View File

@ -1,4 +1,3 @@
import base64
import re import re
from django import http, template from django import http, template
from django.contrib.admin import ModelAdmin from django.contrib.admin import ModelAdmin
@ -6,12 +5,12 @@ from django.contrib.auth import authenticate, login
from django.db.models.base import ModelBase from django.db.models.base import ModelBase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.utils.functional import update_wrapper
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy, ugettext as _ from django.utils.translation import ugettext_lazy, ugettext as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.conf import settings from django.conf import settings
from django.utils.hashcompat import md5_constructor
ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
LOGIN_FORM_KEY = 'this_is_the_login_form' LOGIN_FORM_KEY = 'this_is_the_login_form'
@ -29,24 +28,33 @@ class AdminSite(object):
register() method, and the root() method can then be used as a Django view function register() method, and the root() method can then be used as a Django view function
that presents a full admin interface for the collection of registered models. that presents a full admin interface for the collection of registered models.
""" """
index_template = None index_template = None
login_template = None login_template = None
app_index_template = None app_index_template = None
def __init__(self): def __init__(self, name=None):
self._registry = {} # model_class class -> admin_class instance 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/'
if name is None:
name = ''
else:
name += '_'
self.name = name
def register(self, model_or_iterable, admin_class=None, **options): def register(self, model_or_iterable, admin_class=None, **options):
""" """
Registers the given model(s) with the given admin class. Registers the given model(s) with the given admin class.
The model(s) should be Model classes, not instances. The model(s) should be Model classes, not instances.
If an admin class isn't given, it will use ModelAdmin (the default If an admin class isn't given, it will use ModelAdmin (the default
admin options). If keyword arguments are given -- e.g., list_display -- admin options). If keyword arguments are given -- e.g., list_display --
they'll be applied as options to the admin class. they'll be applied as options to the admin class.
If a model is already registered, this will raise AlreadyRegistered. If a model is already registered, this will raise AlreadyRegistered.
""" """
# Don't import the humongous validation code unless required # Don't import the humongous validation code unless required
@ -54,7 +62,7 @@ class AdminSite(object):
from django.contrib.admin.validation import validate from django.contrib.admin.validation import validate
else: else:
validate = lambda model, adminclass: None validate = lambda model, adminclass: None
if not admin_class: if not admin_class:
admin_class = ModelAdmin admin_class = ModelAdmin
if isinstance(model_or_iterable, ModelBase): if isinstance(model_or_iterable, ModelBase):
@ -62,7 +70,7 @@ class AdminSite(object):
for model in model_or_iterable: for model in model_or_iterable:
if model in self._registry: if model in self._registry:
raise AlreadyRegistered('The model %s is already registered' % model.__name__) raise AlreadyRegistered('The model %s is already registered' % model.__name__)
# If we got **options then dynamically construct a subclass of # If we got **options then dynamically construct a subclass of
# admin_class with those **options. # admin_class with those **options.
if options: if options:
@ -71,17 +79,17 @@ class AdminSite(object):
# which causes issues later on. # which causes issues later on.
options['__module__'] = __name__ options['__module__'] = __name__
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
# Validate (which might be a no-op) # Validate (which might be a no-op)
validate(admin_class, model) validate(admin_class, model)
# Instantiate the admin class to save in the registry # Instantiate the admin class to save in the registry
self._registry[model] = admin_class(model, self) self._registry[model] = admin_class(model, self)
def unregister(self, model_or_iterable): def unregister(self, model_or_iterable):
""" """
Unregisters the given model(s). Unregisters the given model(s).
If a model isn't already registered, this will raise NotRegistered. If a model isn't already registered, this will raise NotRegistered.
""" """
if isinstance(model_or_iterable, ModelBase): if isinstance(model_or_iterable, ModelBase):
@ -90,92 +98,100 @@ class AdminSite(object):
if model not in self._registry: if model not in self._registry:
raise NotRegistered('The model %s is not registered' % model.__name__) raise NotRegistered('The model %s is not registered' % model.__name__)
del self._registry[model] del self._registry[model]
def has_permission(self, request): def has_permission(self, request):
""" """
Returns True if the given HttpRequest has permission to view Returns True if the given HttpRequest has permission to view
*at least one* page in the admin site. *at least one* page in the admin site.
""" """
return request.user.is_authenticated() and request.user.is_staff return request.user.is_authenticated() and request.user.is_staff
def check_dependencies(self): def check_dependencies(self):
""" """
Check that all things needed to run the admin have been correctly installed. Check that all things needed to run the admin have been correctly installed.
The default implementation checks that LogEntry, ContentType and the The default implementation checks that LogEntry, ContentType and the
auth context processor are installed. auth context processor are installed.
""" """
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
if not LogEntry._meta.installed: if not LogEntry._meta.installed:
raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.") raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.")
if not ContentType._meta.installed: if not ContentType._meta.installed:
raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.") raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.")
if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: 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.") raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
def root(self, request, url): def admin_view(self, view):
""" """
Handles main URL routing for the admin app. Decorator to create an "admin view attached to this ``AdminSite``. This
wraps the view and provides permission checking by calling
`url` is the remainder of the URL -- e.g. 'comments/comment/'. ``self.has_permission``.
You'll want to use this from within ``AdminSite.get_urls()``:
class MyAdminSite(AdminSite):
def get_urls(self):
from django.conf.urls.defaults import patterns, url
urls = super(MyAdminSite, self).get_urls()
urls += patterns('',
url(r'^my_view/$', self.protected_view(some_view))
)
return urls
""" """
if request.method == 'GET' and not request.path.endswith('/'): def inner(request, *args, **kwargs):
return http.HttpResponseRedirect(request.path + '/') if not self.has_permission(request):
return self.login(request)
if settings.DEBUG: return view(request, *args, **kwargs)
self.check_dependencies() return update_wrapper(inner, view)
# Figure out the admin base URL path and stash it for later use def get_urls(self):
self.root_path = re.sub(re.escape(url) + '$', '', request.path) from django.conf.urls.defaults import patterns, url, include
url = url.rstrip('/') # Trim trailing slash, if it exists. def wrap(view):
def wrapper(*args, **kwargs):
# The 'logout' view doesn't require that the person is logged in. return self.admin_view(view)(*args, **kwargs)
if url == 'logout': return update_wrapper(wrapper, view)
return self.logout(request)
# Admin-site-wide views.
# Check permission to continue or display login form. urlpatterns = patterns('',
if not self.has_permission(request): url(r'^$',
return self.login(request) wrap(self.index),
name='%sadmin_index' % self.name),
if url == '': url(r'^logout/$',
return self.index(request) wrap(self.logout),
elif url == 'password_change': name='%sadmin_logout'),
return self.password_change(request) url(r'^password_change/$',
elif url == 'password_change/done': wrap(self.password_change),
return self.password_change_done(request) name='%sadmin_password_change' % self.name),
elif url == 'jsi18n': url(r'^password_change/done/$',
return self.i18n_javascript(request) wrap(self.password_change_done),
# URLs starting with 'r/' are for the "View on site" links. name='%sadmin_password_change_done' % self.name),
elif url.startswith('r/'): url(r'^jsi18n/$',
from django.contrib.contenttypes.views import shortcut wrap(self.i18n_javascript),
return shortcut(request, *url.split('/')[1:]) name='%sadmin_jsi18n' % self.name),
else: url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
if '/' in url: 'django.views.defaults.shortcut'),
return self.model_page(request, *url.split('/', 2)) url(r'^(?P<app_label>\w+)/$',
else: wrap(self.app_index),
return self.app_index(request, url) name='%sadmin_app_list' % self.name),
)
raise http.Http404('The requested admin page does not exist.')
# Add in each model's views.
def model_page(self, request, app_label, model_name, rest_of_url=None): for model, model_admin in self._registry.iteritems():
""" urlpatterns += patterns('',
Handles the model-specific functionality of the admin site, delegating url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
to the appropriate ModelAdmin class. include(model_admin.urls))
""" )
from django.db import models return urlpatterns
model = models.get_model(app_label, model_name)
if model is None: def urls(self):
raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) return self.get_urls()
try: urls = property(urls)
admin_obj = self._registry[model]
except KeyError:
raise http.Http404("This model exists but has not been registered with the admin site.")
return admin_obj(request, rest_of_url)
model_page = never_cache(model_page)
def password_change(self, request): def password_change(self, request):
""" """
Handles the "change password" task -- both form display and validation. Handles the "change password" task -- both form display and validation.
@ -183,18 +199,18 @@ class AdminSite(object):
from django.contrib.auth.views import password_change from django.contrib.auth.views import password_change
return password_change(request, return password_change(request,
post_change_redirect='%spassword_change/done/' % self.root_path) post_change_redirect='%spassword_change/done/' % self.root_path)
def password_change_done(self, request): def password_change_done(self, request):
""" """
Displays the "success" page after a password change. Displays the "success" page after a password change.
""" """
from django.contrib.auth.views import password_change_done from django.contrib.auth.views import password_change_done
return password_change_done(request) return password_change_done(request)
def i18n_javascript(self, request): def i18n_javascript(self, request):
""" """
Displays the i18n JavaScript that the Django admin requires. Displays the i18n JavaScript that the Django admin requires.
This takes into account the USE_I18N setting. If it's set to False, the This takes into account the USE_I18N setting. If it's set to False, the
generated JavaScript will be leaner and faster. generated JavaScript will be leaner and faster.
""" """
@ -203,23 +219,23 @@ class AdminSite(object):
else: else:
from django.views.i18n import null_javascript_catalog as javascript_catalog from django.views.i18n import null_javascript_catalog as javascript_catalog
return javascript_catalog(request, packages='django.conf') return javascript_catalog(request, packages='django.conf')
def logout(self, request): def logout(self, request):
""" """
Logs out the user for the given HttpRequest. Logs out the user for the given HttpRequest.
This should *not* assume the user is already logged in. This should *not* assume the user is already logged in.
""" """
from django.contrib.auth.views import logout from django.contrib.auth.views import logout
return logout(request) return logout(request)
logout = never_cache(logout) logout = never_cache(logout)
def login(self, request): def login(self, request):
""" """
Displays the login form for the given HttpRequest. Displays the login form for the given HttpRequest.
""" """
from django.contrib.auth.models import User from django.contrib.auth.models import User
# If this isn't already the login page, display it. # If this isn't already the login page, display it.
if not request.POST.has_key(LOGIN_FORM_KEY): if not request.POST.has_key(LOGIN_FORM_KEY):
if request.POST: if request.POST:
@ -227,14 +243,14 @@ class AdminSite(object):
else: else:
message = "" message = ""
return self.display_login_form(request, message) return self.display_login_form(request, message)
# Check that the user accepts cookies. # Check that the user accepts cookies.
if not request.session.test_cookie_worked(): if not request.session.test_cookie_worked():
message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
return self.display_login_form(request, message) return self.display_login_form(request, message)
else: else:
request.session.delete_test_cookie() request.session.delete_test_cookie()
# Check the password. # Check the password.
username = request.POST.get('username', None) username = request.POST.get('username', None)
password = request.POST.get('password', None) password = request.POST.get('password', None)
@ -254,7 +270,7 @@ class AdminSite(object):
else: else:
message = _("Usernames cannot contain the '@' character.") message = _("Usernames cannot contain the '@' character.")
return self.display_login_form(request, message) return self.display_login_form(request, message)
# The user data is correct; log in the user in and continue. # The user data is correct; log in the user in and continue.
else: else:
if user.is_active and user.is_staff: if user.is_active and user.is_staff:
@ -263,7 +279,7 @@ class AdminSite(object):
else: else:
return self.display_login_form(request, ERROR_MESSAGE) return self.display_login_form(request, ERROR_MESSAGE)
login = never_cache(login) login = never_cache(login)
def index(self, request, extra_context=None): def index(self, request, extra_context=None):
""" """
Displays the main admin index page, which lists all of the installed Displays the main admin index page, which lists all of the installed
@ -274,14 +290,14 @@ class AdminSite(object):
for model, model_admin in self._registry.items(): for model, model_admin in self._registry.items():
app_label = model._meta.app_label app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label) has_module_perms = user.has_module_perms(app_label)
if has_module_perms: if has_module_perms:
perms = { perms = {
'add': model_admin.has_add_permission(request), 'add': model_admin.has_add_permission(request),
'change': model_admin.has_change_permission(request), 'change': model_admin.has_change_permission(request),
'delete': model_admin.has_delete_permission(request), 'delete': model_admin.has_delete_permission(request),
} }
# Check whether user has any perm for this module. # Check whether user has any perm for this module.
# If so, add the module to the model_list. # If so, add the module to the model_list.
if True in perms.values(): if True in perms.values():
@ -299,15 +315,15 @@ class AdminSite(object):
'has_module_perms': has_module_perms, 'has_module_perms': has_module_perms,
'models': [model_dict], 'models': [model_dict],
} }
# Sort the apps alphabetically. # Sort the apps alphabetically.
app_list = app_dict.values() app_list = app_dict.values()
app_list.sort(lambda x, y: cmp(x['name'], y['name'])) app_list.sort(lambda x, y: cmp(x['name'], y['name']))
# Sort the models alphabetically within each app. # Sort the models alphabetically within each app.
for app in app_list: for app in app_list:
app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) app['models'].sort(lambda x, y: cmp(x['name'], y['name']))
context = { context = {
'title': _('Site administration'), 'title': _('Site administration'),
'app_list': app_list, 'app_list': app_list,
@ -318,7 +334,7 @@ class AdminSite(object):
context_instance=template.RequestContext(request) context_instance=template.RequestContext(request)
) )
index = never_cache(index) index = never_cache(index)
def display_login_form(self, request, error_message='', extra_context=None): def display_login_form(self, request, error_message='', extra_context=None):
request.session.set_test_cookie() request.session.set_test_cookie()
context = { context = {
@ -331,7 +347,7 @@ class AdminSite(object):
return render_to_response(self.login_template or 'admin/login.html', context, return render_to_response(self.login_template or 'admin/login.html', context,
context_instance=template.RequestContext(request) context_instance=template.RequestContext(request)
) )
def app_index(self, request, app_label, extra_context=None): def app_index(self, request, app_label, extra_context=None):
user = request.user user = request.user
has_module_perms = user.has_module_perms(app_label) has_module_perms = user.has_module_perms(app_label)
@ -377,6 +393,81 @@ class AdminSite(object):
return render_to_response(self.app_index_template or 'admin/app_index.html', context, return render_to_response(self.app_index_template or 'admin/app_index.html', context,
context_instance=template.RequestContext(request) context_instance=template.RequestContext(request)
) )
def root(self, request, url):
"""
DEPRECATED. This function is the old way of handling URL resolution, and
is deprecated in favor of real URL resolution -- see ``get_urls()``.
This function still exists for backwards-compatibility; it will be
removed in Django 1.3.
"""
import warnings
warnings.warn(
"AdminSite.root() is deprecated; use include(admin.site.urls) instead.",
PendingDeprecationWarning
)
#
# Again, remember that the following only exists for
# backwards-compatibility. Any new URLs, changes to existing URLs, or
# whatever need to be done up in get_urls(), above!
#
if request.method == 'GET' and not request.path.endswith('/'):
return http.HttpResponseRedirect(request.path + '/')
if settings.DEBUG:
self.check_dependencies()
# Figure out the admin base URL path and stash it for later use
self.root_path = re.sub(re.escape(url) + '$', '', request.path)
url = url.rstrip('/') # Trim trailing slash, if it exists.
# The 'logout' view doesn't require that the person is logged in.
if url == 'logout':
return self.logout(request)
# Check permission to continue or display login form.
if not self.has_permission(request):
return self.login(request)
if url == '':
return self.index(request)
elif url == 'password_change':
return self.password_change(request)
elif url == 'password_change/done':
return self.password_change_done(request)
elif url == 'jsi18n':
return self.i18n_javascript(request)
# URLs starting with 'r/' are for the "View on site" links.
elif url.startswith('r/'):
from django.contrib.contenttypes.views import shortcut
return shortcut(request, *url.split('/')[1:])
else:
if '/' in url:
return self.model_page(request, *url.split('/', 2))
else:
return self.app_index(request, url)
raise http.Http404('The requested admin page does not exist.')
def model_page(self, request, app_label, model_name, rest_of_url=None):
"""
DEPRECATED. This is the old way of handling a model view on the admin
site; the new views should use get_urls(), above.
"""
from django.db import models
model = models.get_model(app_label, model_name)
if model is None:
raise http.Http404("App %r, model %r, not found." % (app_label, model_name))
try:
admin_obj = self._registry[model]
except KeyError:
raise http.Http404("This model exists but has not been registered with the admin site.")
return admin_obj(request, rest_of_url)
model_page = never_cache(model_page)
# This global object represents the default admin site, for the common case. # This global object represents the default admin site, for the common case.
# You can instantiate AdminSite in your own code to create a custom admin site. # You can instantiate AdminSite in your own code to create a custom admin site.

View File

@ -6,7 +6,6 @@ from django.utils.text import capfirst
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
def quote(s): def quote(s):
""" """
Ensure that primary key values do not confuse the admin URLs by escaping Ensure that primary key values do not confuse the admin URLs by escaping

View File

@ -40,6 +40,12 @@ class UserAdmin(admin.ModelAdmin):
if url.endswith('password'): if url.endswith('password'):
return self.user_change_password(request, url.split('/')[0]) return self.user_change_password(request, url.split('/')[0])
return super(UserAdmin, self).__call__(request, url) return super(UserAdmin, self).__call__(request, url)
def get_urls(self):
from django.conf.urls.defaults import patterns
return patterns('',
(r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password))
) + super(UserAdmin, self).get_urls()
def add_view(self, request): def add_view(self, request):
# It's an error for a user to have add permission but NOT change # It's an error for a user to have add permission but NOT change

View File

@ -57,7 +57,7 @@ activate the admin site for your installation, do these three things:
# (r'^admin/doc/', include('django.contrib.admindocs.urls')), # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
# Uncomment the next line to enable the admin: # Uncomment the next line to enable the admin:
**(r'^admin/(.*)', admin.site.root),** **(r'^admin/', include(admin.site.urls)),**
) )
(The bold lines are the ones that needed to be uncommented.) (The bold lines are the ones that needed to be uncommented.)

View File

@ -632,6 +632,49 @@ model instance::
instance.save() instance.save()
formset.save_m2m() formset.save_m2m()
``get_urls(self)``
~~~~~~~~~~~~~~~~~~~
The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for
that ModelAdmin in the same way as a URLconf. Therefore you can extend them as
documented in :ref:`topics-http-urls`::
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_view/$', self.my_view)
)
return my_urls + urls
.. note::
Notice that the custom patterns are included *before* the regular admin
URLs: the admin URL patterns are very permissive and will match nearly
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::
class MyModelAdmin(admin.ModelAdmin):
def get_urls(self):
urls = super(MyModelAdmin, self).get_urls()
my_urls = patterns('',
(r'^my_view/$', self.admin_site.admin_view(self.my_view))
)
return my_urls + urls
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.
``ModelAdmin`` media definitions ``ModelAdmin`` media definitions
-------------------------------- --------------------------------
@ -1027,7 +1070,7 @@ In this example, we register the default ``AdminSite`` instance
admin.autodiscover() admin.autodiscover()
urlpatterns = patterns('', urlpatterns = patterns('',
('^admin/(.*)', admin.site.root), ('^admin/', include(admin.site.urls)),
) )
Above we used ``admin.autodiscover()`` to automatically load the Above we used ``admin.autodiscover()`` to automatically load the
@ -1041,15 +1084,13 @@ In this example, we register the ``AdminSite`` instance
from myproject.admin import admin_site from myproject.admin import admin_site
urlpatterns = patterns('', urlpatterns = patterns('',
('^myadmin/(.*)', admin_site.root), ('^myadmin/', include(admin_site.urls)),
) )
There is really no need to use autodiscover when using your own ``AdminSite`` There is really no need to use autodiscover when using your own ``AdminSite``
instance since you will likely be importing all the per-app admin.py modules instance since you will likely be importing all the per-app admin.py modules
in your ``myproject.admin`` module. in your ``myproject.admin`` module.
Note that the regular expression in the URLpattern *must* group everything in
the URL that comes after the URL root -- hence the ``(.*)`` in these examples.
Multiple admin sites in the same URLconf Multiple admin sites in the same URLconf
---------------------------------------- ----------------------------------------
@ -1068,6 +1109,29 @@ respectively::
from myproject.admin import basic_site, advanced_site from myproject.admin import basic_site, advanced_site
urlpatterns = patterns('', urlpatterns = patterns('',
('^basic-admin/(.*)', basic_site.root), ('^basic-admin/', include(basic_site.urls)),
('^advanced-admin/(.*)', advanced_site.root), ('^advanced-admin/', include(advanced_site.urls)),
) )
Adding views to admin sites
---------------------------
It possible to add additional views to the admin site in the same way one can
add them to ``ModelAdmins``. This by using the ``get_urls()`` method on an
AdminSite in the same way as `described above`__
__ `get_urls(self)`_
Protecting Custom ``AdminSite`` and ``ModelAdmin``
--------------------------------------------------
By default all the views in the Django admin are protected so that only staff
members can access them. If you add your own views to either a ``ModelAdmin``
or ``AdminSite`` you should ensure that where necessary they are protected in
the same manner. To do this use the ``admin_perm_test`` decorator provided in
``django.contrib.admin.utils.admin_perm_test``. It can be used in the same way
as the ``login_requied`` decorator.
.. note::
The ``admin_perm_test`` decorator can only be used on methods which are on
``ModelAdmins`` or ``AdminSites``, you cannot use it on arbitrary functions.

View File

@ -0,0 +1,30 @@
"""
A second, custom AdminSite -- see tests.CustomAdminSiteTests.
"""
from django.conf.urls.defaults import patterns
from django.contrib import admin
from django.http import HttpResponse
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)
site.register(models.Section, inlines=[models.ArticleInline])
site.register(models.Thing, models.ThingAdmin)

View File

@ -14,6 +14,11 @@ from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
class AdminViewBasicTest(TestCase): class AdminViewBasicTest(TestCase):
fixtures = ['admin-views-users.xml', 'admin-views-colors.xml'] fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
# Store the bit of the URL where the admin is registered as a class
# variable. That way we can test a second AdminSite just by subclassing
# this test case and changing urlbit.
urlbit = 'admin'
def setUp(self): def setUp(self):
self.client.login(username='super', password='secret') self.client.login(username='super', password='secret')
@ -24,20 +29,20 @@ class AdminViewBasicTest(TestCase):
""" """
If you leave off the trailing slash, app should redirect and add it. If you leave off the trailing slash, app should redirect and add it.
""" """
request = self.client.get('/test_admin/admin/admin_views/article/add') request = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit)
self.assertRedirects(request, self.assertRedirects(request,
'/test_admin/admin/admin_views/article/add/' '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301
) )
def testBasicAddGet(self): def testBasicAddGet(self):
""" """
A smoke test to ensure GET on the add_view works. A smoke test to ensure GET on the add_view works.
""" """
response = self.client.get('/test_admin/admin/admin_views/section/add/') response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit)
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
def testAddWithGETArgs(self): def testAddWithGETArgs(self):
response = self.client.get('/test_admin/admin/admin_views/section/add/', {'name': 'My Section'}) response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'})
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
self.failUnless( self.failUnless(
'value="My Section"' in response.content, 'value="My Section"' in response.content,
@ -48,7 +53,7 @@ class AdminViewBasicTest(TestCase):
""" """
A smoke test to ensureGET on the change_view works. A smoke test to ensureGET on the change_view works.
""" """
response = self.client.get('/test_admin/admin/admin_views/section/1/') response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
def testBasicAddPost(self): def testBasicAddPost(self):
@ -61,7 +66,7 @@ class AdminViewBasicTest(TestCase):
"article_set-TOTAL_FORMS": u"3", "article_set-TOTAL_FORMS": u"3",
"article_set-INITIAL_FORMS": u"0", "article_set-INITIAL_FORMS": u"0",
} }
response = self.client.post('/test_admin/admin/admin_views/section/add/', post_data) response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
self.failUnlessEqual(response.status_code, 302) # redirect somewhere self.failUnlessEqual(response.status_code, 302) # redirect somewhere
def testBasicEditPost(self): def testBasicEditPost(self):
@ -106,7 +111,7 @@ class AdminViewBasicTest(TestCase):
"article_set-5-date_0": u"", "article_set-5-date_0": u"",
"article_set-5-date_1": u"", "article_set-5-date_1": u"",
} }
response = self.client.post('/test_admin/admin/admin_views/section/1/', post_data) response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data)
self.failUnlessEqual(response.status_code, 302) # redirect somewhere self.failUnlessEqual(response.status_code, 302) # redirect somewhere
def testChangeListSortingCallable(self): def testChangeListSortingCallable(self):
@ -114,7 +119,7 @@ class AdminViewBasicTest(TestCase):
Ensure we can sort on a list_display field that is a callable Ensure we can sort on a list_display field that is a callable
(column 2 is callable_year in ArticleAdmin) (column 2 is callable_year in ArticleAdmin)
""" """
response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 2}) response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
self.failUnless( self.failUnless(
response.content.index('Oldest content') < response.content.index('Middle content') and response.content.index('Oldest content') < response.content.index('Middle content') and
@ -127,7 +132,7 @@ class AdminViewBasicTest(TestCase):
Ensure we can sort on a list_display field that is a Model method Ensure we can sort on a list_display field that is a Model method
(colunn 3 is 'model_year' in ArticleAdmin) (colunn 3 is 'model_year' in ArticleAdmin)
""" """
response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'dsc', 'o': 3}) response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
self.failUnless( self.failUnless(
response.content.index('Newest content') < response.content.index('Middle content') and response.content.index('Newest content') < response.content.index('Middle content') and
@ -140,7 +145,7 @@ class AdminViewBasicTest(TestCase):
Ensure we can sort on a list_display field that is a ModelAdmin method Ensure we can sort on a list_display field that is a ModelAdmin method
(colunn 4 is 'modeladmin_year' in ArticleAdmin) (colunn 4 is 'modeladmin_year' in ArticleAdmin)
""" """
response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 4}) response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
self.failUnless( self.failUnless(
response.content.index('Oldest content') < response.content.index('Middle content') and response.content.index('Oldest content') < response.content.index('Middle content') and
@ -150,7 +155,7 @@ class AdminViewBasicTest(TestCase):
def testLimitedFilter(self): def testLimitedFilter(self):
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" """Ensure admin changelist filters do not contain objects excluded via limit_choices_to."""
response = self.client.get('/test_admin/admin/admin_views/thing/') response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit)
self.failUnlessEqual(response.status_code, 200) self.failUnlessEqual(response.status_code, 200)
self.failUnless( self.failUnless(
'<div id="changelist-filter">' in response.content, '<div id="changelist-filter">' in response.content,
@ -163,11 +168,30 @@ class AdminViewBasicTest(TestCase):
def testIncorrectLookupParameters(self): def testIncorrectLookupParameters(self):
"""Ensure incorrect lookup parameters are handled gracefully.""" """Ensure incorrect lookup parameters are handled gracefully."""
response = self.client.get('/test_admin/admin/admin_views/thing/', {'notarealfield': '5'}) response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'})
self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1') self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
response = self.client.get('/test_admin/admin/admin_views/thing/', {'color__id__exact': 'StringNotInteger!'}) response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1') self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
class CustomModelAdminTest(AdminViewBasicTest):
urlbit = "admin2"
def testCustomAdminSiteLoginTemplate(self):
self.client.logout()
request = self.client.get('/test_admin/admin2/')
self.assertTemplateUsed(request, 'custom_admin/login.html')
self.assert_('Hello from a custom login template' in request.content)
def testCustomAdminSiteIndexViewAndTemplate(self):
request = self.client.get('/test_admin/admin2/')
self.assertTemplateUsed(request, 'custom_admin/index.html')
self.assert_('Hello from a custom index template *bar*' in request.content)
def testCustomAdminSiteView(self):
self.client.login(username='super', password='secret')
response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
self.assert_(response.content == "Django is a magical pony!", response.content)
def get_perm(Model, perm): def get_perm(Model, perm):
"""Return the permission object, for the Model""" """Return the permission object, for the Model"""
ct = ContentType.objects.get_for_model(Model) ct = ContentType.objects.get_for_model(Model)
@ -432,44 +456,6 @@ class AdminViewPermissionsTest(TestCase):
self.client.get('/test_admin/admin/logout/') self.client.get('/test_admin/admin/logout/')
def testCustomAdminSiteTemplates(self):
from django.contrib import admin
self.assertEqual(admin.site.index_template, None)
self.assertEqual(admin.site.login_template, None)
self.client.get('/test_admin/admin/logout/')
request = self.client.get('/test_admin/admin/')
self.assertTemplateUsed(request, 'admin/login.html')
self.client.post('/test_admin/admin/', self.changeuser_login)
request = self.client.get('/test_admin/admin/')
self.assertTemplateUsed(request, 'admin/index.html')
self.client.get('/test_admin/admin/logout/')
admin.site.login_template = 'custom_admin/login.html'
admin.site.index_template = 'custom_admin/index.html'
request = self.client.get('/test_admin/admin/')
self.assertTemplateUsed(request, 'custom_admin/login.html')
self.assert_('Hello from a custom login template' in request.content)
self.client.post('/test_admin/admin/', self.changeuser_login)
request = self.client.get('/test_admin/admin/')
self.assertTemplateUsed(request, 'custom_admin/index.html')
self.assert_('Hello from a custom index template' in request.content)
# Finally, using monkey patching check we can inject custom_context arguments in to index
original_index = admin.site.index
def index(*args, **kwargs):
kwargs['extra_context'] = {'foo': '*bar*'}
return original_index(*args, **kwargs)
admin.site.index = index
request = self.client.get('/test_admin/admin/')
self.assertTemplateUsed(request, 'custom_admin/index.html')
self.assert_('Hello from a custom index template *bar*' in request.content)
self.client.get('/test_admin/admin/logout/')
del admin.site.index # Resets to using the original
admin.site.login_template = None
admin.site.index_template = None
def testDeleteView(self): def testDeleteView(self):
"""Delete view should restrict access and actually delete items.""" """Delete view should restrict access and actually delete items."""

View File

@ -1,9 +1,11 @@
from django.conf.urls.defaults import * from django.conf.urls.defaults import *
from django.contrib import admin from django.contrib import admin
import views import views
import customadmin
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^admin/doc/', include('django.contrib.admindocs.urls')), (r'^admin/doc/', include('django.contrib.admindocs.urls')),
(r'^admin/secure-view/$', views.secure_view), (r'^admin/secure-view/$', views.secure_view),
(r'^admin/(.*)', admin.site.root), (r'^admin/', include(admin.site.urls)),
(r'^admin2/', include(customadmin.site.urls)),
) )