2008-07-22 11:24:09 +08:00
from django import forms , template
2008-07-19 09:22:26 +08:00
from django . forms . formsets import all_valid
2009-03-18 04:51:47 +08:00
from django . forms . models import modelform_factory , modelformset_factory , inlineformset_factory
2008-08-09 04:27:48 +08:00
from django . forms . models import BaseInlineFormSet
2008-07-19 07:54:34 +08:00
from django . contrib . contenttypes . models import ContentType
from django . contrib . admin import widgets
2008-08-18 23:49:58 +08:00
from django . contrib . admin import helpers
2009-03-24 04:22:56 +08:00
from django . contrib . admin . util import unquote , flatten_fieldsets , get_deleted_objects , model_ngettext , model_format_dict
2009-12-10 00:57:23 +08:00
from django . contrib import messages
2009-10-27 08:36:34 +08:00
from django . views . decorators . csrf import csrf_protect
2009-12-28 22:08:11 +08:00
from django . core . exceptions import PermissionDenied , ValidationError
2008-07-19 07:54:34 +08:00
from django . db import models , transaction
2009-03-24 04:22:56 +08:00
from django . db . models . fields import BLANK_CHOICE_DASH
2008-07-19 07:54:34 +08:00
from django . http import Http404 , HttpResponse , HttpResponseRedirect
from django . shortcuts import get_object_or_404 , render_to_response
2009-04-07 04:23:33 +08:00
from django . utils . datastructures import SortedDict
2009-01-15 04:22:25 +08:00
from django . utils . functional import update_wrapper
2008-07-19 07:54:34 +08:00
from django . utils . html import escape
from django . utils . safestring import mark_safe
2009-01-16 23:32:31 +08:00
from django . utils . functional import curry
2008-07-19 07:54:34 +08:00
from django . utils . text import capfirst , get_text_list
from django . utils . translation import ugettext as _
2009-05-06 06:25:35 +08:00
from django . utils . translation import ungettext , ugettext_lazy
2008-07-19 07:54:34 +08:00
from django . utils . encoding import force_unicode
2008-08-02 13:29:17 +08:00
try :
set
except NameError :
from sets import Set as set # Python 2.3 fallback
2008-07-19 07:54:34 +08:00
HORIZONTAL , VERTICAL = 1 , 2
# returns the <ul> class for a given radio_admin field
get_ul_class = lambda x : ' radiolist %s ' % ( ( x == HORIZONTAL ) and ' inline ' or ' ' )
class IncorrectLookupParameters ( Exception ) :
pass
2009-01-16 23:32:31 +08:00
# Defaults for formfield_overrides. ModelAdmin subclasses can change this
# by adding to ModelAdmin.formfield_overrides.
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
FORMFIELD_FOR_DBFIELD_DEFAULTS = {
2009-03-18 17:19:24 +08:00
models . DateTimeField : {
2009-01-16 23:32:31 +08:00
' form_class ' : forms . SplitDateTimeField ,
2009-03-18 17:19:24 +08:00
' widget ' : widgets . AdminSplitDateTime
2009-01-16 23:32:31 +08:00
} ,
2009-12-17 23:10:38 +08:00
models . DateField : { ' widget ' : widgets . AdminDateWidget } ,
models . TimeField : { ' widget ' : widgets . AdminTimeWidget } ,
models . TextField : { ' widget ' : widgets . AdminTextareaWidget } ,
models . URLField : { ' widget ' : widgets . AdminURLFieldWidget } ,
models . IntegerField : { ' widget ' : widgets . AdminIntegerFieldWidget } ,
models . BigIntegerField : { ' widget ' : widgets . AdminIntegerFieldWidget } ,
models . CharField : { ' widget ' : widgets . AdminTextInputWidget } ,
models . ImageField : { ' widget ' : widgets . AdminFileWidget } ,
models . FileField : { ' widget ' : widgets . AdminFileWidget } ,
2009-01-16 23:32:31 +08:00
}
2008-07-19 07:54:34 +08:00
class BaseModelAdmin ( object ) :
""" Functionality common to both ModelAdmin and InlineAdmin. """
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
raw_id_fields = ( )
fields = None
2008-09-03 01:26:24 +08:00
exclude = None
2008-07-19 07:54:34 +08:00
fieldsets = None
form = forms . ModelForm
filter_vertical = ( )
filter_horizontal = ( )
radio_fields = { }
prepopulated_fields = { }
2009-01-16 23:32:31 +08:00
formfield_overrides = { }
2009-12-23 02:29:00 +08:00
readonly_fields = ( )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
def __init__ ( self ) :
self . formfield_overrides = dict ( FORMFIELD_FOR_DBFIELD_DEFAULTS , * * self . formfield_overrides )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def formfield_for_dbfield ( self , db_field , * * kwargs ) :
"""
Hook for specifying the form Field instance for a given database Field
instance .
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
If kwargs are given , they ' re passed to the form Field ' s constructor .
"""
2009-01-16 23:32:31 +08:00
request = kwargs . pop ( " request " , None )
2009-03-18 17:19:24 +08:00
2008-07-30 19:41:04 +08:00
# If the field specifies choices, we don't need to look for special
# admin widgets - we just need to use a select widget of some kind.
if db_field . choices :
2009-01-16 23:32:31 +08:00
return self . formfield_for_choice_field ( db_field , request , * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
# ForeignKey or ManyToManyFields
if isinstance ( db_field , ( models . ForeignKey , models . ManyToManyField ) ) :
# Combine the field kwargs with any options for formfield_overrides.
2009-03-18 17:19:24 +08:00
# Make sure the passed in **kwargs override anything in
2009-01-16 23:32:31 +08:00
# formfield_overrides because **kwargs is more specific, and should
# always win.
if db_field . __class__ in self . formfield_overrides :
kwargs = dict ( self . formfield_overrides [ db_field . __class__ ] , * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
# Get the correct formfield.
if isinstance ( db_field , models . ForeignKey ) :
formfield = self . formfield_for_foreignkey ( db_field , request , * * kwargs )
elif isinstance ( db_field , models . ManyToManyField ) :
formfield = self . formfield_for_manytomany ( db_field , request , * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
2009-03-18 17:19:24 +08:00
# rendered output. formfield can be None if it came from a
2009-01-16 23:32:31 +08:00
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field . name not in self . raw_id_fields :
formfield . widget = widgets . RelatedFieldWidgetWrapper ( formfield . widget , db_field . rel , self . admin_site )
return formfield
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
2009-05-06 06:25:35 +08:00
for klass in db_field . __class__ . mro ( ) :
if klass in self . formfield_overrides :
kwargs = dict ( self . formfield_overrides [ klass ] , * * kwargs )
return db_field . formfield ( * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
# For any other type of field, just call its formfield() method.
return db_field . formfield ( * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
def formfield_for_choice_field ( self , db_field , request = None , * * kwargs ) :
"""
Get a form Field for a database Field that has declared choices .
"""
# If the field is named as a radio_field, use a RadioSelect
if db_field . name in self . radio_fields :
# Avoid stomping on custom widget/choices arguments.
if ' widget ' not in kwargs :
2008-09-01 04:11:28 +08:00
kwargs [ ' widget ' ] = widgets . AdminRadioSelect ( attrs = {
' class ' : get_ul_class ( self . radio_fields [ db_field . name ] ) ,
} )
2009-01-16 23:32:31 +08:00
if ' choices ' not in kwargs :
2008-09-01 04:11:28 +08:00
kwargs [ ' choices ' ] = db_field . get_choices (
include_blank = db_field . blank ,
blank_choice = [ ( ' ' , _ ( ' None ' ) ) ]
2008-07-30 19:41:04 +08:00
)
2009-01-16 23:32:31 +08:00
return db_field . formfield ( * * kwargs )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
def formfield_for_foreignkey ( self , db_field , request = None , * * kwargs ) :
"""
Get a form Field for a ForeignKey .
"""
2009-12-22 23:18:51 +08:00
db = kwargs . get ( ' using ' )
2009-01-16 23:32:31 +08:00
if db_field . name in self . raw_id_fields :
2009-12-22 23:18:51 +08:00
kwargs [ ' widget ' ] = widgets . ForeignKeyRawIdWidget ( db_field . rel , using = db )
2009-01-16 23:32:31 +08:00
elif db_field . name in self . radio_fields :
kwargs [ ' widget ' ] = widgets . AdminRadioSelect ( attrs = {
' class ' : get_ul_class ( self . radio_fields [ db_field . name ] ) ,
} )
kwargs [ ' empty_label ' ] = db_field . blank and _ ( ' None ' ) or None
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
return db_field . formfield ( * * kwargs )
def formfield_for_manytomany ( self , db_field , request = None , * * kwargs ) :
"""
Get a form Field for a ManyToManyField .
"""
2009-11-03 22:02:49 +08:00
# If it uses an intermediary model that isn't auto created, don't show
# a field in admin.
if not db_field . rel . through . _meta . auto_created :
2009-01-16 23:32:31 +08:00
return None
2009-12-22 23:18:51 +08:00
db = kwargs . get ( ' using ' )
2009-03-18 17:19:24 +08:00
2009-01-16 23:32:31 +08:00
if db_field . name in self . raw_id_fields :
2009-12-22 23:18:51 +08:00
kwargs [ ' widget ' ] = widgets . ManyToManyRawIdWidget ( db_field . rel , using = db )
2009-01-16 23:32:31 +08:00
kwargs [ ' help_text ' ] = ' '
elif db_field . name in ( list ( self . filter_vertical ) + list ( self . filter_horizontal ) ) :
kwargs [ ' widget ' ] = widgets . FilteredSelectMultiple ( db_field . verbose_name , ( db_field . name in self . filter_vertical ) )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
return db_field . formfield ( * * kwargs )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def _declared_fieldsets ( self ) :
if self . fieldsets :
return self . fieldsets
elif self . fields :
return [ ( None , { ' fields ' : self . fields } ) ]
return None
declared_fieldsets = property ( _declared_fieldsets )
2009-12-23 02:29:00 +08:00
def get_readonly_fields ( self , request , obj = None ) :
return self . readonly_fields
2008-07-19 07:54:34 +08:00
class ModelAdmin ( BaseModelAdmin ) :
" Encapsulates all admin options and functionality for a given model. "
__metaclass__ = forms . MediaDefiningClass
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
list_display = ( ' __str__ ' , )
list_display_links = ( )
list_filter = ( )
list_select_related = False
list_per_page = 100
2009-03-18 04:51:47 +08:00
list_editable = ( )
2008-07-19 07:54:34 +08:00
search_fields = ( )
date_hierarchy = None
save_as = False
save_on_top = False
ordering = None
inlines = [ ]
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
# Custom templates (designed to be over-ridden in subclasses)
change_form_template = None
change_list_template = None
delete_confirmation_template = None
object_history_template = None
2009-03-18 17:19:24 +08:00
2009-03-24 04:22:56 +08:00
# Actions
2009-04-07 04:23:33 +08:00
actions = [ ]
2009-03-24 04:22:56 +08:00
action_form = helpers . ActionForm
actions_on_top = True
actions_on_bottom = False
2008-07-19 07:54:34 +08:00
def __init__ ( self , model , admin_site ) :
self . model = model
self . opts = model . _meta
self . admin_site = admin_site
self . inline_instances = [ ]
for inline_class in self . inlines :
inline_instance = inline_class ( self . model , self . admin_site )
self . inline_instances . append ( inline_instance )
2009-04-07 04:23:33 +08:00
if ' action_checkbox ' not in self . list_display and self . actions is not None :
2009-03-24 04:22:56 +08:00
self . list_display = [ ' action_checkbox ' ] + list ( self . list_display )
if not self . list_display_links :
for name in self . list_display :
if name != ' action_checkbox ' :
self . list_display_links = [ name ]
break
2008-07-19 07:54:34 +08:00
super ( ModelAdmin , self ) . __init__ ( )
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
def get_urls ( self ) :
from django . conf . urls . defaults import patterns , url
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
def wrap ( view ) :
def wrapper ( * args , * * kwargs ) :
return self . admin_site . admin_view ( view ) ( * args , * * kwargs )
return update_wrapper ( wrapper , view )
2009-03-18 17:19:24 +08:00
2009-07-17 00:16:13 +08:00
info = self . model . _meta . app_label , self . model . _meta . module_name
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
urlpatterns = patterns ( ' ' ,
url ( r ' ^$ ' ,
wrap ( self . changelist_view ) ,
2009-07-17 00:16:13 +08:00
name = ' %s _ %s _changelist ' % info ) ,
2009-01-15 04:22:25 +08:00
url ( r ' ^add/$ ' ,
wrap ( self . add_view ) ,
2009-07-17 00:16:13 +08:00
name = ' %s _ %s _add ' % info ) ,
2009-01-15 04:22:25 +08:00
url ( r ' ^(.+)/history/$ ' ,
wrap ( self . history_view ) ,
2009-07-17 00:16:13 +08:00
name = ' %s _ %s _history ' % info ) ,
2009-01-15 04:22:25 +08:00
url ( r ' ^(.+)/delete/$ ' ,
wrap ( self . delete_view ) ,
2009-07-17 00:16:13 +08:00
name = ' %s _ %s _delete ' % info ) ,
2009-01-15 04:22:25 +08:00
url ( r ' ^(.+)/$ ' ,
wrap ( self . change_view ) ,
2009-07-17 00:16:13 +08:00
name = ' %s _ %s _change ' % info ) ,
2009-01-15 04:22:25 +08:00
)
return urlpatterns
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
def urls ( self ) :
return self . get_urls ( )
urls = property ( urls )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def _media ( self ) :
from django . conf import settings
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
js = [ ' js/core.js ' , ' js/admin/RelatedObjectLookups.js ' ]
2009-04-07 04:23:33 +08:00
if self . actions is not None :
2009-03-24 04:22:56 +08:00
js . extend ( [ ' js/getElementsBySelector.js ' , ' js/actions.js ' ] )
2008-07-19 07:54:34 +08:00
if self . prepopulated_fields :
js . append ( ' js/urlify.js ' )
if self . opts . get_ordered_objects ( ) :
js . extend ( [ ' js/getElementsBySelector.js ' , ' js/dom-drag.js ' , ' js/admin/ordering.js ' ] )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
return forms . Media ( js = [ ' %s %s ' % ( settings . ADMIN_MEDIA_PREFIX , url ) for url in js ] )
media = property ( _media )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def has_add_permission ( self , request ) :
" Returns True if the given request has permission to add an object. "
opts = self . opts
return request . user . has_perm ( opts . app_label + ' . ' + opts . get_add_permission ( ) )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def has_change_permission ( self , request , obj = None ) :
"""
Returns True if the given request has permission to change the given
Django model instance .
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
If ` obj ` is None , this should return True if the given request has
permission to change * any * object of the given type .
"""
opts = self . opts
return request . user . has_perm ( opts . app_label + ' . ' + opts . get_change_permission ( ) )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def has_delete_permission ( self , request , obj = None ) :
"""
Returns True if the given request has permission to change the given
Django model instance .
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
If ` obj ` is None , this should return True if the given request has
permission to delete * any * object of the given type .
"""
opts = self . opts
return request . user . has_perm ( opts . app_label + ' . ' + opts . get_delete_permission ( ) )
2009-03-18 17:19:24 +08:00
2009-04-09 03:47:46 +08:00
def get_model_perms ( self , request ) :
"""
Returns a dict of all perms for this model . This dict has the keys
` ` add ` ` , ` ` change ` ` , and ` ` delete ` ` mapping to the True / False for each
of those actions .
"""
return {
' add ' : self . has_add_permission ( request ) ,
' change ' : self . has_change_permission ( request ) ,
' delete ' : self . has_delete_permission ( request ) ,
}
2008-07-19 07:54:34 +08:00
def queryset ( self , request ) :
"""
Returns a QuerySet of all model instances that can be edited by the
admin site . This is used by changelist_view .
"""
qs = self . model . _default_manager . get_query_set ( )
# TODO: this should be handled by some parameter to the ChangeList.
ordering = self . ordering or ( ) # otherwise we might try to *None, which is bad ;)
if ordering :
qs = qs . order_by ( * ordering )
return qs
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def get_fieldsets ( self , request , obj = None ) :
" Hook for specifying fieldsets for the add form. "
if self . declared_fieldsets :
return self . declared_fieldsets
2008-08-23 13:05:21 +08:00
form = self . get_form ( request , obj )
2009-12-23 02:29:00 +08:00
fields = form . base_fields . keys ( ) + list ( self . get_readonly_fields ( request , obj ) )
return [ ( None , { ' fields ' : fields } ) ]
2009-03-18 17:19:24 +08:00
2008-08-16 05:57:36 +08:00
def get_form ( self , request , obj = None , * * kwargs ) :
2008-07-19 07:54:34 +08:00
"""
Returns a Form class for use in the admin add view . This is used by
add_view and change_view .
"""
if self . declared_fieldsets :
fields = flatten_fieldsets ( self . declared_fieldsets )
else :
fields = None
2008-09-03 01:26:24 +08:00
if self . exclude is None :
exclude = [ ]
else :
2008-09-27 13:57:10 +08:00
exclude = list ( self . exclude )
2009-12-23 02:29:00 +08:00
exclude . extend ( kwargs . get ( " exclude " , [ ] ) )
exclude . extend ( self . get_readonly_fields ( request , obj ) )
2009-04-22 23:48:51 +08:00
# if exclude is an empty list we pass None to be consistant with the
# default on modelform_factory
2009-12-23 02:29:00 +08:00
exclude = exclude or None
2008-08-16 05:57:36 +08:00
defaults = {
" form " : self . form ,
" fields " : fields ,
2009-12-23 02:29:00 +08:00
" exclude " : exclude ,
2009-01-16 23:32:31 +08:00
" formfield_callback " : curry ( self . formfield_for_dbfield , request = request ) ,
2008-08-16 05:57:36 +08:00
}
defaults . update ( kwargs )
return modelform_factory ( self . model , * * defaults )
2009-03-18 17:19:24 +08:00
2009-12-18 18:08:05 +08:00
def get_changelist ( self , request , * * kwargs ) :
"""
Returns the ChangeList class for use on the changelist page .
"""
from django . contrib . admin . views . main import ChangeList
return ChangeList
2009-12-28 22:08:11 +08:00
def get_object ( self , request , object_id ) :
"""
Returns an instance matching the primary key provided . ` ` None ` ` is
returned if no match is found ( or the object_id failed validation
against the primary key field ) .
"""
queryset = self . queryset ( request )
model = queryset . model
try :
object_id = model . _meta . pk . to_python ( object_id )
return queryset . get ( pk = object_id )
except ( model . DoesNotExist , ValidationError ) :
return None
2009-03-18 04:51:47 +08:00
def get_changelist_form ( self , request , * * kwargs ) :
"""
Returns a Form class for use in the Formset on the changelist page .
"""
defaults = {
" formfield_callback " : curry ( self . formfield_for_dbfield , request = request ) ,
}
defaults . update ( kwargs )
return modelform_factory ( self . model , * * defaults )
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
def get_changelist_formset ( self , request , * * kwargs ) :
"""
2009-03-18 17:19:24 +08:00
Returns a FormSet class for use on the changelist page if list_editable
2009-03-18 04:51:47 +08:00
is used .
"""
defaults = {
" formfield_callback " : curry ( self . formfield_for_dbfield , request = request ) ,
}
defaults . update ( kwargs )
2009-03-18 17:19:24 +08:00
return modelformset_factory ( self . model ,
self . get_changelist_form ( request ) , extra = 0 ,
2009-03-18 04:51:47 +08:00
fields = self . list_editable , * * defaults )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def get_formsets ( self , request , obj = None ) :
for inline in self . inline_instances :
yield inline . get_formset ( request , obj )
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
def log_addition ( self , request , object ) :
"""
2009-01-15 04:22:25 +08:00
Log that an object has been successfully added .
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
The default implementation creates an admin LogEntry object .
"""
from django . contrib . admin . models import LogEntry , ADDITION
LogEntry . objects . log_action (
2009-01-15 04:22:25 +08:00
user_id = request . user . pk ,
2008-08-10 00:45:15 +08:00
content_type_id = ContentType . objects . get_for_model ( object ) . pk ,
object_id = object . pk ,
2009-01-15 04:22:25 +08:00
object_repr = force_unicode ( object ) ,
2008-08-10 00:45:15 +08:00
action_flag = ADDITION
)
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
def log_change ( self , request , object , message ) :
"""
2009-01-15 04:22:25 +08:00
Log that an object has been successfully changed .
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
The default implementation creates an admin LogEntry object .
"""
from django . contrib . admin . models import LogEntry , CHANGE
LogEntry . objects . log_action (
2009-01-15 04:22:25 +08:00
user_id = request . user . pk ,
content_type_id = ContentType . objects . get_for_model ( object ) . pk ,
object_id = object . pk ,
object_repr = force_unicode ( object ) ,
action_flag = CHANGE ,
2008-08-10 00:45:15 +08:00
change_message = message
)
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
def log_deletion ( self , request , object , object_repr ) :
"""
Log that an object has been successfully deleted . Note that since the
object is deleted , it might no longer be safe to call * any * methods
on the object , hence this method getting object_repr .
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
The default implementation creates an admin LogEntry object .
"""
from django . contrib . admin . models import LogEntry , DELETION
LogEntry . objects . log_action (
2009-01-15 04:22:25 +08:00
user_id = request . user . id ,
content_type_id = ContentType . objects . get_for_model ( self . model ) . pk ,
object_id = object . pk ,
2008-08-10 00:45:15 +08:00
object_repr = object_repr ,
action_flag = DELETION
)
2009-03-18 17:19:24 +08:00
2009-03-24 04:22:56 +08:00
def action_checkbox ( self , obj ) :
"""
A list_display column containing a checkbox widget .
"""
return helpers . checkbox . render ( helpers . ACTION_CHECKBOX_NAME , force_unicode ( obj . pk ) )
action_checkbox . short_description = mark_safe ( ' <input type= " checkbox " id= " action-toggle " /> ' )
action_checkbox . allow_tags = True
2009-04-07 04:23:33 +08:00
def get_actions ( self , request ) :
2009-03-24 04:22:56 +08:00
"""
Return a dictionary mapping the names of all actions for this
ModelAdmin to a tuple of ( callable , name , description ) for each action .
"""
2009-04-07 04:23:33 +08:00
# If self.actions is explicitally set to None that means that we don't
# want *any* actions enabled on this page.
if self . actions is None :
return [ ]
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
actions = [ ]
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Gather actions from the admin site first
for ( name , func ) in self . admin_site . actions :
description = getattr ( func , ' short_description ' , name . replace ( ' _ ' , ' ' ) )
actions . append ( ( func , name , description ) )
2009-05-06 06:25:35 +08:00
# Then gather them from the model admin and all parent classes,
2009-04-07 04:23:33 +08:00
# starting with self and working back up.
for klass in self . __class__ . mro ( ) [ : : - 1 ] :
class_actions = getattr ( klass , ' actions ' , [ ] )
# Avoid trying to iterate over None
if not class_actions :
2009-05-06 06:25:35 +08:00
continue
2009-04-07 04:23:33 +08:00
actions . extend ( [ self . get_action ( action ) for action in class_actions ] )
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# get_action might have returned None, so filter any of those out.
actions = filter ( None , actions )
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Convert the actions into a SortedDict keyed by name
# and sorted by description.
actions . sort ( lambda a , b : cmp ( a [ 2 ] . lower ( ) , b [ 2 ] . lower ( ) ) )
actions = SortedDict ( [
( name , ( func , name , desc ) )
for func , name , desc in actions
] )
2009-05-06 06:25:35 +08:00
2009-03-24 04:22:56 +08:00
return actions
2009-04-07 04:23:33 +08:00
def get_action_choices ( self , request , default_choices = BLANK_CHOICE_DASH ) :
2009-03-24 04:22:56 +08:00
"""
Return a list of choices for use in a form object . Each choice is a
tuple ( name , description ) .
"""
choices = [ ] + default_choices
for func , name , description in self . get_actions ( request ) . itervalues ( ) :
choice = ( name , description % model_format_dict ( self . opts ) )
choices . append ( choice )
return choices
def get_action ( self , action ) :
"""
2009-08-14 01:04:42 +08:00
Return a given action from a parameter , which can either be a callable ,
2009-03-24 04:22:56 +08:00
or the name of a method on the ModelAdmin . Return is a tuple of
( callable , name , description ) .
"""
2009-04-07 04:23:33 +08:00
# If the action is a callable, just use it.
2009-03-24 04:22:56 +08:00
if callable ( action ) :
func = action
action = action . __name__
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Next, look for a method. Grab it off self.__class__ to get an unbound
# method instead of a bound one; this ensures that the calling
# conventions are the same for functions and methods.
elif hasattr ( self . __class__ , action ) :
func = getattr ( self . __class__ , action )
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Finally, look for a named method on the admin site
else :
try :
func = self . admin_site . get_action ( action )
except KeyError :
return None
2009-05-06 06:25:35 +08:00
2009-03-24 04:22:56 +08:00
if hasattr ( func , ' short_description ' ) :
description = func . short_description
else :
description = capfirst ( action . replace ( ' _ ' , ' ' ) )
return func , action , description
2008-08-10 00:45:15 +08:00
def construct_change_message ( self , request , form , formsets ) :
"""
Construct a change message from a changed object .
"""
change_message = [ ]
if form . changed_data :
change_message . append ( _ ( ' Changed %s . ' ) % get_text_list ( form . changed_data , _ ( ' and ' ) ) )
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
if formsets :
for formset in formsets :
for added_object in formset . new_objects :
change_message . append ( _ ( ' Added %(name)s " %(object)s " . ' )
% { ' name ' : added_object . _meta . verbose_name ,
2008-11-11 01:13:41 +08:00
' object ' : force_unicode ( added_object ) } )
2008-08-10 00:45:15 +08:00
for changed_object , changed_fields in formset . changed_objects :
change_message . append ( _ ( ' Changed %(list)s for %(name)s " %(object)s " . ' )
% { ' list ' : get_text_list ( changed_fields , _ ( ' and ' ) ) ,
' name ' : changed_object . _meta . verbose_name ,
2008-11-11 01:13:41 +08:00
' object ' : force_unicode ( changed_object ) } )
2008-08-10 00:45:15 +08:00
for deleted_object in formset . deleted_objects :
change_message . append ( _ ( ' Deleted %(name)s " %(object)s " . ' )
% { ' name ' : deleted_object . _meta . verbose_name ,
2008-11-11 01:13:41 +08:00
' object ' : force_unicode ( deleted_object ) } )
2008-08-10 00:45:15 +08:00
change_message = ' ' . join ( change_message )
return change_message or _ ( ' No fields changed. ' )
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
def message_user ( self , request , message ) :
"""
2009-01-15 04:22:25 +08:00
Send a message to the user . The default implementation
2009-12-10 00:57:23 +08:00
posts a message using the django . contrib . messages backend .
2008-08-10 00:45:15 +08:00
"""
2009-12-10 00:57:23 +08:00
messages . info ( request , message )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
def save_form ( self , request , form , change ) :
2008-08-10 01:12:48 +08:00
"""
2008-08-10 04:52:40 +08:00
Given a ModelForm return an unsaved instance . ` ` change ` ` is True if
the object is being changed , and False if it ' s being added.
2008-08-10 01:12:48 +08:00
"""
2008-08-10 04:52:40 +08:00
return form . save ( commit = False )
2009-03-18 17:19:24 +08:00
2008-08-12 01:20:47 +08:00
def save_model ( self , request , obj , form , change ) :
"""
Given a model instance save it to the database .
"""
obj . save ( )
2009-03-18 17:19:24 +08:00
2008-08-10 01:12:48 +08:00
def save_formset ( self , request , form , formset , change ) :
"""
2008-08-12 01:20:47 +08:00
Given an inline formset save it to the database .
2008-08-10 01:12:48 +08:00
"""
2008-08-12 01:20:47 +08:00
formset . save ( )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
def render_change_form ( self , request , context , add = False , change = False , form_url = ' ' , obj = None ) :
opts = self . model . _meta
app_label = opts . app_label
ordered_objects = opts . get_ordered_objects ( )
context . update ( {
' add ' : add ,
' change ' : change ,
' has_add_permission ' : self . has_add_permission ( request ) ,
' has_change_permission ' : self . has_change_permission ( request , obj ) ,
' has_delete_permission ' : self . has_delete_permission ( request , obj ) ,
' has_file_field ' : True , # FIXME - this should check if form or formsets have a FileField,
' has_absolute_url ' : hasattr ( self . model , ' get_absolute_url ' ) ,
' ordered_objects ' : ordered_objects ,
' form_url ' : mark_safe ( form_url ) ,
' opts ' : opts ,
' content_type_id ' : ContentType . objects . get_for_model ( self . model ) . id ,
' save_as ' : self . save_as ,
' save_on_top ' : self . save_on_top ,
' root_path ' : self . admin_site . root_path ,
} )
2009-07-17 00:16:13 +08:00
context_instance = template . RequestContext ( request , current_app = self . admin_site . name )
2008-08-10 04:52:40 +08:00
return render_to_response ( self . change_form_template or [
" admin/ %s / %s /change_form.html " % ( app_label , opts . object_name . lower ( ) ) ,
" admin/ %s /change_form.html " % app_label ,
" admin/change_form.html "
2009-07-17 00:16:13 +08:00
] , context , context_instance = context_instance )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
def response_add ( self , request , obj , post_url_continue = ' ../ %s / ' ) :
2008-07-19 07:54:34 +08:00
"""
2008-08-10 04:52:40 +08:00
Determines the HttpResponse for the add_view stage .
2008-07-19 07:54:34 +08:00
"""
2008-08-10 04:52:40 +08:00
opts = obj . _meta
pk_value = obj . _get_pk_val ( )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
msg = _ ( ' The %(name)s " %(obj)s " was added successfully. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' obj ' : force_unicode ( obj ) }
2008-07-19 07:54:34 +08:00
# Here, we distinguish between different save types by checking for
# the presence of keys in request.POST.
if request . POST . has_key ( " _continue " ) :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg + ' ' + _ ( " You may edit it again below. " ) )
2008-07-19 07:54:34 +08:00
if request . POST . has_key ( " _popup " ) :
post_url_continue + = " ?_popup=1 "
return HttpResponseRedirect ( post_url_continue % pk_value )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if request . POST . has_key ( " _popup " ) :
return HttpResponse ( ' <script type= " text/javascript " >opener.dismissAddAnotherPopup(window, " %s " , " %s " );</script> ' % \
# escape() calls force_unicode.
2008-08-10 04:52:40 +08:00
( escape ( pk_value ) , escape ( obj ) ) )
2008-07-19 07:54:34 +08:00
elif request . POST . has_key ( " _addanother " ) :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg + ' ' + ( _ ( " You may add another %s below. " ) % force_unicode ( opts . verbose_name ) ) )
2008-07-19 07:54:34 +08:00
return HttpResponseRedirect ( request . path )
else :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
# Figure out where to redirect. If the user has change permission,
# redirect to the change-list page for this object. Otherwise,
# redirect to the admin index.
if self . has_change_permission ( request , None ) :
post_url = ' ../ '
else :
post_url = ' ../../../ '
return HttpResponseRedirect ( post_url )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
def response_change ( self , request , obj ) :
2008-07-19 07:54:34 +08:00
"""
2008-08-10 04:52:40 +08:00
Determines the HttpResponse for the change_view stage .
2008-07-19 07:54:34 +08:00
"""
2008-08-10 04:52:40 +08:00
opts = obj . _meta
pk_value = obj . _get_pk_val ( )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
msg = _ ( ' The %(name)s " %(obj)s " was changed successfully. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' obj ' : force_unicode ( obj ) }
2008-07-19 07:54:34 +08:00
if request . POST . has_key ( " _continue " ) :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg + ' ' + _ ( " You may edit it again below. " ) )
2008-07-19 07:54:34 +08:00
if request . REQUEST . has_key ( ' _popup ' ) :
return HttpResponseRedirect ( request . path + " ?_popup=1 " )
else :
return HttpResponseRedirect ( request . path )
elif request . POST . has_key ( " _saveasnew " ) :
2008-08-10 04:52:40 +08:00
msg = _ ( ' The %(name)s " %(obj)s " was added successfully. You may edit it again below. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' obj ' : obj }
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg )
2008-07-19 07:54:34 +08:00
return HttpResponseRedirect ( " ../ %s / " % pk_value )
elif request . POST . has_key ( " _addanother " ) :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg + ' ' + ( _ ( " You may add another %s below. " ) % force_unicode ( opts . verbose_name ) ) )
2008-07-19 07:54:34 +08:00
return HttpResponseRedirect ( " ../add/ " )
else :
2008-08-10 00:45:15 +08:00
self . message_user ( request , msg )
2008-07-19 07:54:34 +08:00
return HttpResponseRedirect ( " ../ " )
2009-03-18 17:19:24 +08:00
2009-03-24 04:22:56 +08:00
def response_action ( self , request , queryset ) :
"""
Handle an admin action . This is called if a request is POSTed to the
changelist ; it returns an HttpResponse if the action was handled , and
None otherwise .
"""
# There can be multiple action forms on the page (at the top
# and bottom of the change list, for example). Get the action
# whose button was pushed.
try :
action_index = int ( request . POST . get ( ' index ' , 0 ) )
except ValueError :
action_index = 0
# Construct the action form.
data = request . POST . copy ( )
data . pop ( helpers . ACTION_CHECKBOX_NAME , None )
data . pop ( " index " , None )
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Use the action whose button was pushed
try :
data . update ( { ' action ' : data . getlist ( ' action ' ) [ action_index ] } )
except IndexError :
# If we didn't get an action from the chosen form that's invalid
# POST data, so by deleting action it'll fail the validation check
# below. So no need to do anything here
pass
2009-05-06 06:25:35 +08:00
2009-03-24 04:22:56 +08:00
action_form = self . action_form ( data , auto_id = None )
action_form . fields [ ' action ' ] . choices = self . get_action_choices ( request )
# If the form's valid we can handle the action.
if action_form . is_valid ( ) :
action = action_form . cleaned_data [ ' action ' ]
func , name , description = self . get_actions ( request ) [ action ]
2009-03-24 16:22:23 +08:00
# Get the list of selected PKs. If nothing's selected, we can't
2009-03-24 04:22:56 +08:00
# perform an action on it, so bail.
selected = request . POST . getlist ( helpers . ACTION_CHECKBOX_NAME )
if not selected :
2009-12-13 05:51:47 +08:00
# Reminder that something needs to be selected or nothing will happen
2009-12-15 00:01:43 +08:00
msg = _ ( " Items must be selected in order to perform actions on them. No items have been changed. " )
self . message_user ( request , msg )
2009-03-24 04:22:56 +08:00
return None
2009-04-07 04:23:33 +08:00
response = func ( self , request , queryset . filter ( pk__in = selected ) )
2009-03-24 16:22:23 +08:00
2009-03-24 04:22:56 +08:00
# Actions may return an HttpResponse, which will be used as the
# response from the POST. If not, we'll be a good little HTTP
# citizen and redirect back to the changelist page.
if isinstance ( response , HttpResponse ) :
return response
else :
return HttpResponseRedirect ( " . " )
2009-12-13 05:51:47 +08:00
else :
2009-12-15 00:01:43 +08:00
msg = _ ( " No action selected. " )
self . message_user ( request , msg )
2009-03-24 04:22:56 +08:00
Fixed #9977 - CsrfMiddleware gets template tag added, session dependency removed, and turned on by default.
This is a large change to CSRF protection for Django. It includes:
* removing the dependency on the session framework.
* deprecating CsrfResponseMiddleware, and replacing with a core template tag.
* turning on CSRF protection by default by adding CsrfViewMiddleware to
the default value of MIDDLEWARE_CLASSES.
* protecting all contrib apps (whatever is in settings.py)
using a decorator.
For existing users of the CSRF functionality, it should be a seamless update,
but please note that it includes DEPRECATION of features in Django 1.1,
and there are upgrade steps which are detailed in the docs.
Many thanks to 'Glenn' and 'bthomas', who did a lot of the thinking and work
on the patch, and to lots of other people including Simon Willison and
Russell Keith-Magee who refined the ideas.
Details of the rationale for these changes is found here:
http://code.djangoproject.com/wiki/CsrfProtection
As of this commit, the CSRF code is mainly in 'contrib'. The code will be
moved to core in a separate commit, to make the changeset as readable as
possible.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11660 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-10-27 07:23:07 +08:00
@csrf_protect
@transaction.commit_on_success
2008-07-19 07:54:34 +08:00
def add_view ( self , request , form_url = ' ' , extra_context = None ) :
" The ' add ' admin view for this model. "
model = self . model
opts = model . _meta
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if not self . has_add_permission ( request ) :
raise PermissionDenied
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
ModelForm = self . get_form ( request )
2008-08-10 04:52:40 +08:00
formsets = [ ]
2008-07-19 07:54:34 +08:00
if request . method == ' POST ' :
form = ModelForm ( request . POST , request . FILES )
2008-08-10 04:52:40 +08:00
if form . is_valid ( ) :
form_validated = True
new_object = self . save_form ( request , form , change = False )
else :
form_validated = False
new_object = self . model ( )
2009-03-10 19:19:26 +08:00
prefixes = { }
2009-12-22 23:18:51 +08:00
for FormSet , inline in zip ( self . get_formsets ( request ) , self . inline_instances ) :
2009-03-10 19:19:26 +08:00
prefix = FormSet . get_default_prefix ( )
prefixes [ prefix ] = prefixes . get ( prefix , 0 ) + 1
if prefixes [ prefix ] != 1 :
prefix = " %s - %s " % ( prefix , prefixes [ prefix ] )
2008-08-10 04:52:40 +08:00
formset = FormSet ( data = request . POST , files = request . FILES ,
instance = new_object ,
2009-03-10 19:19:26 +08:00
save_as_new = request . POST . has_key ( " _saveasnew " ) ,
2009-12-22 23:18:51 +08:00
prefix = prefix , queryset = inline . queryset ( request ) )
2008-08-10 04:52:40 +08:00
formsets . append ( formset )
if all_valid ( formsets ) and form_validated :
2008-08-12 01:20:47 +08:00
self . save_model ( request , new_object , form , change = False )
2008-08-10 04:52:40 +08:00
form . save_m2m ( )
for formset in formsets :
2008-08-12 01:20:47 +08:00
self . save_formset ( request , form , formset , change = False )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
self . log_addition ( request , new_object )
return self . response_add ( request , new_object )
2008-07-19 07:54:34 +08:00
else :
2008-08-30 00:09:29 +08:00
# Prepare the dict of initial data from the request.
# We have to special-case M2Ms as a list of comma-separated PKs.
initial = dict ( request . GET . items ( ) )
for k in initial :
try :
f = opts . get_field ( k )
2008-08-30 03:50:22 +08:00
except models . FieldDoesNotExist :
2008-08-30 02:12:31 +08:00
continue
2008-08-30 00:09:29 +08:00
if isinstance ( f , models . ManyToManyField ) :
initial [ k ] = initial [ k ] . split ( " , " )
form = ModelForm ( initial = initial )
2009-03-10 19:19:26 +08:00
prefixes = { }
2009-12-22 23:18:51 +08:00
for FormSet , inline in zip ( self . get_formsets ( request ) ,
self . inline_instances ) :
2009-03-10 19:19:26 +08:00
prefix = FormSet . get_default_prefix ( )
prefixes [ prefix ] = prefixes . get ( prefix , 0 ) + 1
if prefixes [ prefix ] != 1 :
prefix = " %s - %s " % ( prefix , prefixes [ prefix ] )
2009-12-22 23:18:51 +08:00
formset = FormSet ( instance = self . model ( ) , prefix = prefix ,
queryset = inline . queryset ( request ) )
2008-08-10 04:52:40 +08:00
formsets . append ( formset )
2009-03-18 17:19:24 +08:00
2009-12-23 02:29:00 +08:00
adminForm = helpers . AdminForm ( form , list ( self . get_fieldsets ( request ) ) ,
self . prepopulated_fields , self . get_readonly_fields ( request ) ,
model_admin = self )
2008-07-19 07:54:34 +08:00
media = self . media + adminForm . media
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
inline_admin_formsets = [ ]
2008-08-10 04:52:40 +08:00
for inline , formset in zip ( self . inline_instances , formsets ) :
2008-07-19 07:54:34 +08:00
fieldsets = list ( inline . get_fieldsets ( request ) )
2009-12-23 02:29:00 +08:00
readonly = list ( inline . get_readonly_fields ( request ) )
inline_admin_formset = helpers . InlineAdminFormSet ( inline , formset ,
fieldsets , readonly , model_admin = self )
2008-07-19 07:54:34 +08:00
inline_admin_formsets . append ( inline_admin_formset )
2008-08-16 01:36:33 +08:00
media = media + inline_admin_formset . media
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
context = {
2008-07-30 08:52:01 +08:00
' title ' : _ ( ' Add %s ' ) % force_unicode ( opts . verbose_name ) ,
2008-07-19 07:54:34 +08:00
' adminform ' : adminForm ,
' is_popup ' : request . REQUEST . has_key ( ' _popup ' ) ,
' show_delete ' : False ,
' media ' : mark_safe ( media ) ,
' inline_admin_formsets ' : inline_admin_formsets ,
2008-08-18 23:49:58 +08:00
' errors ' : helpers . AdminErrorList ( form , formsets ) ,
2008-07-19 07:54:34 +08:00
' root_path ' : self . admin_site . root_path ,
2008-09-08 13:45:17 +08:00
' app_label ' : opts . app_label ,
2008-07-19 07:54:34 +08:00
}
context . update ( extra_context or { } )
2009-05-08 20:53:14 +08:00
return self . render_change_form ( request , context , form_url = form_url , add = True )
2009-03-18 17:19:24 +08:00
Fixed #9977 - CsrfMiddleware gets template tag added, session dependency removed, and turned on by default.
This is a large change to CSRF protection for Django. It includes:
* removing the dependency on the session framework.
* deprecating CsrfResponseMiddleware, and replacing with a core template tag.
* turning on CSRF protection by default by adding CsrfViewMiddleware to
the default value of MIDDLEWARE_CLASSES.
* protecting all contrib apps (whatever is in settings.py)
using a decorator.
For existing users of the CSRF functionality, it should be a seamless update,
but please note that it includes DEPRECATION of features in Django 1.1,
and there are upgrade steps which are detailed in the docs.
Many thanks to 'Glenn' and 'bthomas', who did a lot of the thinking and work
on the patch, and to lots of other people including Simon Willison and
Russell Keith-Magee who refined the ideas.
Details of the rationale for these changes is found here:
http://code.djangoproject.com/wiki/CsrfProtection
As of this commit, the CSRF code is mainly in 'contrib'. The code will be
moved to core in a separate commit, to make the changeset as readable as
possible.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11660 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-10-27 07:23:07 +08:00
@csrf_protect
@transaction.commit_on_success
2008-07-19 07:54:34 +08:00
def change_view ( self , request , object_id , extra_context = None ) :
" The ' change ' admin view for this model. "
model = self . model
opts = model . _meta
2009-03-18 17:19:24 +08:00
2009-12-28 22:08:11 +08:00
obj = self . get_object ( request , unquote ( object_id ) )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if not self . has_change_permission ( request , obj ) :
raise PermissionDenied
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if obj is None :
2008-11-07 00:44:42 +08:00
raise Http404 ( _ ( ' %(name)s object with primary key %(key)r does not exist. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' key ' : escape ( object_id ) } )
2009-03-18 17:19:24 +08:00
2008-09-17 16:59:55 +08:00
if request . method == ' POST ' and request . POST . has_key ( " _saveasnew " ) :
2009-05-08 20:53:14 +08:00
return self . add_view ( request , form_url = ' ../add/ ' )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
ModelForm = self . get_form ( request , obj )
2008-08-10 04:52:40 +08:00
formsets = [ ]
2008-07-19 07:54:34 +08:00
if request . method == ' POST ' :
form = ModelForm ( request . POST , request . FILES , instance = obj )
2008-08-10 04:52:40 +08:00
if form . is_valid ( ) :
form_validated = True
new_object = self . save_form ( request , form , change = True )
else :
form_validated = False
new_object = obj
2009-03-10 19:19:26 +08:00
prefixes = { }
2009-12-22 23:18:51 +08:00
for FormSet , inline in zip ( self . get_formsets ( request , new_object ) ,
self . inline_instances ) :
2009-03-10 19:19:26 +08:00
prefix = FormSet . get_default_prefix ( )
prefixes [ prefix ] = prefixes . get ( prefix , 0 ) + 1
if prefixes [ prefix ] != 1 :
prefix = " %s - %s " % ( prefix , prefixes [ prefix ] )
2008-08-10 04:52:40 +08:00
formset = FormSet ( request . POST , request . FILES ,
2009-12-22 23:18:51 +08:00
instance = new_object , prefix = prefix ,
queryset = inline . queryset ( request ) )
2008-08-10 04:52:40 +08:00
formsets . append ( formset )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
if all_valid ( formsets ) and form_validated :
2008-08-12 01:20:47 +08:00
self . save_model ( request , new_object , form , change = True )
2008-08-10 04:52:40 +08:00
form . save_m2m ( )
for formset in formsets :
2008-08-12 01:20:47 +08:00
self . save_formset ( request , form , formset , change = True )
2009-03-18 17:19:24 +08:00
2008-08-10 04:52:40 +08:00
change_message = self . construct_change_message ( request , form , formsets )
self . log_change ( request , new_object , change_message )
return self . response_change ( request , new_object )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
else :
form = ModelForm ( instance = obj )
2009-03-10 19:19:26 +08:00
prefixes = { }
2009-12-22 23:18:51 +08:00
for FormSet , inline in zip ( self . get_formsets ( request , obj ) , self . inline_instances ) :
2009-03-10 19:19:26 +08:00
prefix = FormSet . get_default_prefix ( )
prefixes [ prefix ] = prefixes . get ( prefix , 0 ) + 1
if prefixes [ prefix ] != 1 :
prefix = " %s - %s " % ( prefix , prefixes [ prefix ] )
2009-12-22 23:18:51 +08:00
formset = FormSet ( instance = obj , prefix = prefix ,
queryset = inline . queryset ( request ) )
2008-08-10 04:52:40 +08:00
formsets . append ( formset )
2009-03-18 17:19:24 +08:00
2009-12-23 02:29:00 +08:00
adminForm = helpers . AdminForm ( form , self . get_fieldsets ( request , obj ) ,
self . prepopulated_fields , self . get_readonly_fields ( request , obj ) ,
model_admin = self )
2008-07-19 07:54:34 +08:00
media = self . media + adminForm . media
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
inline_admin_formsets = [ ]
2008-08-10 04:52:40 +08:00
for inline , formset in zip ( self . inline_instances , formsets ) :
2008-07-19 07:54:34 +08:00
fieldsets = list ( inline . get_fieldsets ( request , obj ) )
2009-12-23 02:29:00 +08:00
readonly = list ( inline . get_readonly_fields ( request , obj ) )
inline_admin_formset = helpers . InlineAdminFormSet ( inline , formset ,
fieldsets , readonly , model_admin = self )
2008-07-19 07:54:34 +08:00
inline_admin_formsets . append ( inline_admin_formset )
2008-07-23 11:25:41 +08:00
media = media + inline_admin_formset . media
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
context = {
2008-07-30 08:52:01 +08:00
' title ' : _ ( ' Change %s ' ) % force_unicode ( opts . verbose_name ) ,
2008-07-19 07:54:34 +08:00
' adminform ' : adminForm ,
' object_id ' : object_id ,
' original ' : obj ,
' is_popup ' : request . REQUEST . has_key ( ' _popup ' ) ,
' media ' : mark_safe ( media ) ,
' inline_admin_formsets ' : inline_admin_formsets ,
2008-08-18 23:49:58 +08:00
' errors ' : helpers . AdminErrorList ( form , formsets ) ,
2008-07-19 07:54:34 +08:00
' root_path ' : self . admin_site . root_path ,
2008-09-08 13:45:17 +08:00
' app_label ' : opts . app_label ,
2008-07-19 07:54:34 +08:00
}
context . update ( extra_context or { } )
return self . render_change_form ( request , context , change = True , obj = obj )
2009-03-18 17:19:24 +08:00
Fixed #9977 - CsrfMiddleware gets template tag added, session dependency removed, and turned on by default.
This is a large change to CSRF protection for Django. It includes:
* removing the dependency on the session framework.
* deprecating CsrfResponseMiddleware, and replacing with a core template tag.
* turning on CSRF protection by default by adding CsrfViewMiddleware to
the default value of MIDDLEWARE_CLASSES.
* protecting all contrib apps (whatever is in settings.py)
using a decorator.
For existing users of the CSRF functionality, it should be a seamless update,
but please note that it includes DEPRECATION of features in Django 1.1,
and there are upgrade steps which are detailed in the docs.
Many thanks to 'Glenn' and 'bthomas', who did a lot of the thinking and work
on the patch, and to lots of other people including Simon Willison and
Russell Keith-Magee who refined the ideas.
Details of the rationale for these changes is found here:
http://code.djangoproject.com/wiki/CsrfProtection
As of this commit, the CSRF code is mainly in 'contrib'. The code will be
moved to core in a separate commit, to make the changeset as readable as
possible.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11660 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-10-27 07:23:07 +08:00
@csrf_protect
2008-07-19 07:54:34 +08:00
def changelist_view ( self , request , extra_context = None ) :
" The ' change list ' admin view for this model. "
2009-12-18 18:08:05 +08:00
from django . contrib . admin . views . main import ERROR_FLAG
2008-07-19 07:54:34 +08:00
opts = self . model . _meta
app_label = opts . app_label
if not self . has_change_permission ( request , None ) :
raise PermissionDenied
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Check actions to see if any are available on this changelist
actions = self . get_actions ( request )
2009-05-06 06:25:35 +08:00
2009-04-07 04:23:33 +08:00
# Remove action checkboxes if there aren't any actions available.
list_display = list ( self . list_display )
if not actions :
try :
list_display . remove ( ' action_checkbox ' )
except ValueError :
pass
2009-05-06 06:25:35 +08:00
2009-12-18 18:08:05 +08:00
ChangeList = self . get_changelist ( request )
2008-07-19 07:54:34 +08:00
try :
2009-04-07 04:23:33 +08:00
cl = ChangeList ( request , self . model , list_display , self . list_display_links , self . list_filter ,
2009-03-18 04:51:47 +08:00
self . date_hierarchy , self . search_fields , self . list_select_related , self . list_per_page , self . list_editable , self )
2008-07-19 07:54:34 +08:00
except IncorrectLookupParameters :
# Wacky lookup parameters were given, so redirect to the main
# changelist page, without parameters, and pass an 'invalid=1'
# parameter via the query string. If wacky parameters were given and
# the 'invalid=1' parameter was already in the query string, something
# is screwed up with the database, so display an error page.
if ERROR_FLAG in request . GET . keys ( ) :
return render_to_response ( ' admin/invalid_setup.html ' , { ' title ' : _ ( ' Database error ' ) } )
return HttpResponseRedirect ( request . path + ' ? ' + ERROR_FLAG + ' =1 ' )
2009-05-06 06:25:35 +08:00
2009-03-24 04:22:56 +08:00
# If the request was POSTed, this might be a bulk action or a bulk edit.
# Try to look up an action first, but if this isn't an action the POST
# will fall through to the bulk edit check, below.
2009-04-07 04:23:33 +08:00
if actions and request . method == ' POST ' :
2009-03-24 04:22:56 +08:00
response = self . response_action ( request , queryset = cl . get_query_set ( ) )
if response :
return response
2009-03-24 16:22:23 +08:00
2009-03-18 04:51:47 +08:00
# If we're allowing changelist editing, we need to construct a formset
# for the changelist given all the fields to be edited. Then we'll
# use the formset to validate/process POSTed data.
formset = cl . formset = None
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
# Handle POSTed bulk-edit data.
if request . method == " POST " and self . list_editable :
FormSet = self . get_changelist_formset ( request )
formset = cl . formset = FormSet ( request . POST , request . FILES , queryset = cl . result_list )
if formset . is_valid ( ) :
changecount = 0
for form in formset . forms :
if form . has_changed ( ) :
obj = self . save_form ( request , form , change = True )
self . save_model ( request , obj , form , change = True )
form . save_m2m ( )
change_msg = self . construct_change_message ( request , form , None )
self . log_change ( request , obj , change_msg )
changecount + = 1
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
if changecount :
2009-03-18 17:19:24 +08:00
if changecount == 1 :
name = force_unicode ( opts . verbose_name )
else :
name = force_unicode ( opts . verbose_name_plural )
2009-05-06 06:25:35 +08:00
msg = ungettext ( " %(count)s %(name)s was changed successfully. " ,
" %(count)s %(name)s were changed successfully. " ,
changecount ) % { ' count ' : changecount ,
' name ' : name ,
' obj ' : force_unicode ( obj ) }
2009-03-18 04:51:47 +08:00
self . message_user ( request , msg )
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
return HttpResponseRedirect ( request . get_full_path ( ) )
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
# Handle GET -- construct a formset for display.
elif self . list_editable :
FormSet = self . get_changelist_formset ( request )
formset = cl . formset = FormSet ( queryset = cl . result_list )
2009-03-18 17:19:24 +08:00
2009-03-18 04:51:47 +08:00
# Build the list of media to be used by the formset.
if formset :
media = self . media + formset . media
else :
2009-03-24 04:22:56 +08:00
media = self . media
2009-03-24 16:22:23 +08:00
2009-03-24 04:22:56 +08:00
# Build the action form and populate it with available actions.
2009-04-07 04:23:33 +08:00
if actions :
action_form = self . action_form ( auto_id = None )
action_form . fields [ ' action ' ] . choices = self . get_action_choices ( request )
else :
action_form = None
2009-03-18 04:51:47 +08:00
2008-07-19 07:54:34 +08:00
context = {
' title ' : cl . title ,
' is_popup ' : cl . is_popup ,
' cl ' : cl ,
2009-03-18 04:51:47 +08:00
' media ' : media ,
2008-07-19 07:54:34 +08:00
' has_add_permission ' : self . has_add_permission ( request ) ,
' root_path ' : self . admin_site . root_path ,
2008-08-23 12:00:15 +08:00
' app_label ' : app_label ,
2009-03-24 04:22:56 +08:00
' action_form ' : action_form ,
' actions_on_top ' : self . actions_on_top ,
' actions_on_bottom ' : self . actions_on_bottom ,
2008-07-19 07:54:34 +08:00
}
context . update ( extra_context or { } )
2009-07-17 00:16:13 +08:00
context_instance = template . RequestContext ( request , current_app = self . admin_site . name )
2008-07-19 07:54:34 +08:00
return render_to_response ( self . change_list_template or [
' admin/ %s / %s /change_list.html ' % ( app_label , opts . object_name . lower ( ) ) ,
' admin/ %s /change_list.html ' % app_label ,
' admin/change_list.html '
2009-07-17 00:16:13 +08:00
] , context , context_instance = context_instance )
2009-03-18 17:19:24 +08:00
Fixed #9977 - CsrfMiddleware gets template tag added, session dependency removed, and turned on by default.
This is a large change to CSRF protection for Django. It includes:
* removing the dependency on the session framework.
* deprecating CsrfResponseMiddleware, and replacing with a core template tag.
* turning on CSRF protection by default by adding CsrfViewMiddleware to
the default value of MIDDLEWARE_CLASSES.
* protecting all contrib apps (whatever is in settings.py)
using a decorator.
For existing users of the CSRF functionality, it should be a seamless update,
but please note that it includes DEPRECATION of features in Django 1.1,
and there are upgrade steps which are detailed in the docs.
Many thanks to 'Glenn' and 'bthomas', who did a lot of the thinking and work
on the patch, and to lots of other people including Simon Willison and
Russell Keith-Magee who refined the ideas.
Details of the rationale for these changes is found here:
http://code.djangoproject.com/wiki/CsrfProtection
As of this commit, the CSRF code is mainly in 'contrib'. The code will be
moved to core in a separate commit, to make the changeset as readable as
possible.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11660 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2009-10-27 07:23:07 +08:00
@csrf_protect
2008-07-19 07:54:34 +08:00
def delete_view ( self , request , object_id , extra_context = None ) :
" The ' delete ' admin view for this model. "
opts = self . model . _meta
app_label = opts . app_label
2009-03-18 17:19:24 +08:00
2009-12-28 22:08:11 +08:00
obj = self . get_object ( request , unquote ( object_id ) )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if not self . has_delete_permission ( request , obj ) :
raise PermissionDenied
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if obj is None :
2008-11-07 00:44:42 +08:00
raise Http404 ( _ ( ' %(name)s object with primary key %(key)r does not exist. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' key ' : escape ( object_id ) } )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
# Populate deleted_objects, a data structure of all related objects that
# will also be deleted.
2009-01-15 04:22:25 +08:00
deleted_objects = [ mark_safe ( u ' %s : <a href= " ../../ %s / " > %s </a> ' % ( escape ( force_unicode ( capfirst ( opts . verbose_name ) ) ) , object_id , escape ( obj ) ) ) , [ ] ]
2008-08-02 13:29:17 +08:00
perms_needed = set ( )
2008-07-19 07:54:34 +08:00
get_deleted_objects ( deleted_objects , perms_needed , request . user , obj , opts , 1 , self . admin_site )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if request . POST : # The user has already confirmed the deletion.
if perms_needed :
raise PermissionDenied
2008-09-19 01:10:16 +08:00
obj_display = force_unicode ( obj )
2009-05-07 21:10:59 +08:00
self . log_deletion ( request , obj , obj_display )
2008-07-19 07:54:34 +08:00
obj . delete ( )
2009-03-18 17:19:24 +08:00
2008-08-10 00:45:15 +08:00
self . message_user ( request , _ ( ' The %(name)s " %(obj)s " was deleted successfully. ' ) % { ' name ' : force_unicode ( opts . verbose_name ) , ' obj ' : force_unicode ( obj_display ) } )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
if not self . has_change_permission ( request , None ) :
return HttpResponseRedirect ( " ../../../../ " )
return HttpResponseRedirect ( " ../../ " )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
context = {
" title " : _ ( " Are you sure? " ) ,
2008-07-30 08:52:01 +08:00
" object_name " : force_unicode ( opts . verbose_name ) ,
2008-07-19 07:54:34 +08:00
" object " : obj ,
" deleted_objects " : deleted_objects ,
" perms_lacking " : perms_needed ,
" opts " : opts ,
" root_path " : self . admin_site . root_path ,
2008-08-23 12:00:15 +08:00
" app_label " : app_label ,
2008-07-19 07:54:34 +08:00
}
context . update ( extra_context or { } )
2009-07-17 00:16:13 +08:00
context_instance = template . RequestContext ( request , current_app = self . admin_site . name )
2008-07-19 07:54:34 +08:00
return render_to_response ( self . delete_confirmation_template or [
" admin/ %s / %s /delete_confirmation.html " % ( app_label , opts . object_name . lower ( ) ) ,
" admin/ %s /delete_confirmation.html " % app_label ,
" admin/delete_confirmation.html "
2009-07-17 00:16:13 +08:00
] , context , context_instance = context_instance )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def history_view ( self , request , object_id , extra_context = None ) :
" The ' history ' admin view for this model. "
from django . contrib . admin . models import LogEntry
model = self . model
opts = model . _meta
2008-08-26 03:56:14 +08:00
app_label = opts . app_label
2008-07-19 07:54:34 +08:00
action_list = LogEntry . objects . filter (
object_id = object_id ,
content_type__id__exact = ContentType . objects . get_for_model ( model ) . id
) . select_related ( ) . order_by ( ' action_time ' )
# If no history was found, see whether this object even exists.
2009-12-11 03:58:20 +08:00
obj = get_object_or_404 ( model , pk = unquote ( object_id ) )
2008-07-19 07:54:34 +08:00
context = {
' title ' : _ ( ' Change history: %s ' ) % force_unicode ( obj ) ,
' action_list ' : action_list ,
2008-07-30 08:52:01 +08:00
' module_name ' : capfirst ( force_unicode ( opts . verbose_name_plural ) ) ,
2008-07-19 07:54:34 +08:00
' object ' : obj ,
' root_path ' : self . admin_site . root_path ,
2008-08-26 03:56:14 +08:00
' app_label ' : app_label ,
2008-07-19 07:54:34 +08:00
}
context . update ( extra_context or { } )
2009-07-17 00:16:13 +08:00
context_instance = template . RequestContext ( request , current_app = self . admin_site . name )
2008-07-19 07:54:34 +08:00
return render_to_response ( self . object_history_template or [
2008-09-08 13:45:17 +08:00
" admin/ %s / %s /object_history.html " % ( app_label , opts . object_name . lower ( ) ) ,
" admin/ %s /object_history.html " % app_label ,
2008-07-19 07:54:34 +08:00
" admin/object_history.html "
2009-07-17 00:16:13 +08:00
] , context , context_instance = context_instance )
2008-07-19 07:54:34 +08:00
2009-01-15 04:22:25 +08:00
#
# 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 .
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
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 !
2009-03-18 17:19:24 +08:00
2009-01-15 04:22:25 +08:00
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 ) )
2008-07-19 07:54:34 +08:00
class InlineModelAdmin ( BaseModelAdmin ) :
"""
Options for inline editing of ` ` model ` ` instances .
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
Provide ` ` name ` ` to specify the attribute name of the ` ` ForeignKey ` ` from
` ` model ` ` to its parent . This is required if ` ` model ` ` has more than one
` ` ForeignKey ` ` to its parent .
"""
model = None
fk_name = None
2008-08-09 04:27:48 +08:00
formset = BaseInlineFormSet
2008-07-19 07:54:34 +08:00
extra = 3
max_num = 0
template = None
verbose_name = None
verbose_name_plural = None
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def __init__ ( self , parent_model , admin_site ) :
self . admin_site = admin_site
self . parent_model = parent_model
self . opts = self . model . _meta
super ( InlineModelAdmin , self ) . __init__ ( )
if self . verbose_name is None :
self . verbose_name = self . model . _meta . verbose_name
if self . verbose_name_plural is None :
self . verbose_name_plural = self . model . _meta . verbose_name_plural
2009-03-18 17:19:24 +08:00
2008-08-16 01:38:39 +08:00
def _media ( self ) :
from django . conf import settings
js = [ ]
if self . prepopulated_fields :
js . append ( ' js/urlify.js ' )
2008-08-16 04:33:45 +08:00
if self . filter_vertical or self . filter_horizontal :
js . extend ( [ ' js/SelectBox.js ' , ' js/SelectFilter2.js ' ] )
2008-08-16 01:38:39 +08:00
return forms . Media ( js = [ ' %s %s ' % ( settings . ADMIN_MEDIA_PREFIX , url ) for url in js ] )
media = property ( _media )
2009-03-18 17:19:24 +08:00
2008-08-16 05:57:36 +08:00
def get_formset ( self , request , obj = None , * * kwargs ) :
2008-07-19 07:54:34 +08:00
""" Returns a BaseInlineFormSet class for use in admin add/change views. """
if self . declared_fieldsets :
fields = flatten_fieldsets ( self . declared_fieldsets )
else :
fields = None
2008-09-03 01:26:24 +08:00
if self . exclude is None :
exclude = [ ]
else :
2008-09-27 13:57:10 +08:00
exclude = list ( self . exclude )
2009-12-23 02:29:00 +08:00
exclude . extend ( kwargs . get ( " exclude " , [ ] ) )
exclude . extend ( self . get_readonly_fields ( request , obj ) )
2009-04-22 23:48:51 +08:00
# if exclude is an empty list we use None, since that's the actual
# default
2009-12-23 02:29:00 +08:00
exclude = exclude or None
2008-08-16 05:57:36 +08:00
defaults = {
" form " : self . form ,
" formset " : self . formset ,
" fk_name " : self . fk_name ,
" fields " : fields ,
2009-12-23 02:29:00 +08:00
" exclude " : exclude ,
2009-01-16 23:32:31 +08:00
" formfield_callback " : curry ( self . formfield_for_dbfield , request = request ) ,
2008-08-16 05:57:36 +08:00
" extra " : self . extra ,
" max_num " : self . max_num ,
}
defaults . update ( kwargs )
return inlineformset_factory ( self . parent_model , self . model , * * defaults )
2009-03-18 17:19:24 +08:00
2008-07-19 07:54:34 +08:00
def get_fieldsets ( self , request , obj = None ) :
if self . declared_fieldsets :
return self . declared_fieldsets
form = self . get_formset ( request ) . form
2009-12-23 02:29:00 +08:00
fields = form . base_fields . keys ( ) + list ( self . get_readonly_fields ( request , obj ) )
return [ ( None , { ' fields ' : fields } ) ]
2009-03-18 17:19:24 +08:00
2009-12-22 23:18:51 +08:00
def queryset ( self , request ) :
return self . model . _default_manager . all ( )
2008-07-19 07:54:34 +08:00
class StackedInline ( InlineModelAdmin ) :
template = ' admin/edit_inline/stacked.html '
class TabularInline ( InlineModelAdmin ) :
template = ' admin/edit_inline/tabular.html '