1
from django import forms, template
2
from django.forms.formsets import all_valid
3
from django.forms.models import modelform_factory, inlineformset_factory
4
from django.forms.models import BaseInlineFormSet
5
from django.contrib.contenttypes.models import ContentType
6
from django.contrib.admin import widgets
7
from django.contrib.admin import helpers
8
from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects
9
from django.core.exceptions import PermissionDenied
10
from django.db import models, transaction
11
from django.http import Http404, HttpResponse, HttpResponseRedirect
12
from django.shortcuts import get_object_or_404, render_to_response
13
from django.utils.html import escape
14
from django.utils.safestring import mark_safe
15
from django.utils.text import capfirst, get_text_list
16
from django.utils.translation import ugettext as _
17
from django.utils.encoding import force_unicode
21
from sets import Set as set # Python 2.3 fallback
23
HORIZONTAL, VERTICAL = 1, 2
24
# returns the <ul> class for a given radio_admin field
25
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
27
class IncorrectLookupParameters(Exception):
30
class BaseModelAdmin(object):
31
"""Functionality common to both ModelAdmin and InlineAdmin."""
36
form = forms.ModelForm
38
filter_horizontal = ()
40
prepopulated_fields = {}
42
def formfield_for_dbfield(self, db_field, **kwargs):
44
Hook for specifying the form Field instance for a given database Field
47
If kwargs are given, they're passed to the form Field's constructor.
50
# If the field specifies choices, we don't need to look for special
51
# admin widgets - we just need to use a select widget of some kind.
53
if db_field.name in self.radio_fields:
54
# If the field is named as a radio_field, use a RadioSelect
55
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
56
'class': get_ul_class(self.radio_fields[db_field.name]),
58
kwargs['choices'] = db_field.get_choices(
59
include_blank = db_field.blank,
60
blank_choice=[('', _('None'))]
62
return db_field.formfield(**kwargs)
64
# Otherwise, use the default select widget.
65
return db_field.formfield(**kwargs)
67
# For DateTimeFields, use a special field and widget.
68
if isinstance(db_field, models.DateTimeField):
69
kwargs['form_class'] = forms.SplitDateTimeField
70
kwargs['widget'] = widgets.AdminSplitDateTime()
71
return db_field.formfield(**kwargs)
73
# For DateFields, add a custom CSS class.
74
if isinstance(db_field, models.DateField):
75
kwargs['widget'] = widgets.AdminDateWidget
76
return db_field.formfield(**kwargs)
78
# For TimeFields, add a custom CSS class.
79
if isinstance(db_field, models.TimeField):
80
kwargs['widget'] = widgets.AdminTimeWidget
81
return db_field.formfield(**kwargs)
83
# For TextFields, add a custom CSS class.
84
if isinstance(db_field, models.TextField):
85
kwargs['widget'] = widgets.AdminTextareaWidget
86
return db_field.formfield(**kwargs)
88
# For URLFields, add a custom CSS class.
89
if isinstance(db_field, models.URLField):
90
kwargs['widget'] = widgets.AdminURLFieldWidget
91
return db_field.formfield(**kwargs)
93
# For IntegerFields, add a custom CSS class.
94
if isinstance(db_field, models.IntegerField):
95
kwargs['widget'] = widgets.AdminIntegerFieldWidget
96
return db_field.formfield(**kwargs)
98
# For CommaSeparatedIntegerFields, add a custom CSS class.
99
if isinstance(db_field, models.CommaSeparatedIntegerField):
100
kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget
101
return db_field.formfield(**kwargs)
103
# For TextInputs, add a custom CSS class.
104
if isinstance(db_field, models.CharField):
105
kwargs['widget'] = widgets.AdminTextInputWidget
106
return db_field.formfield(**kwargs)
108
# For FileFields and ImageFields add a link to the current file.
109
if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
110
kwargs['widget'] = widgets.AdminFileWidget
111
return db_field.formfield(**kwargs)
113
# For ForeignKey or ManyToManyFields, use a special widget.
114
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
115
if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
116
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
117
elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields:
118
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
119
'class': get_ul_class(self.radio_fields[db_field.name]),
121
kwargs['empty_label'] = db_field.blank and _('None') or None
123
if isinstance(db_field, models.ManyToManyField):
124
# If it uses an intermediary model, don't show field in admin.
125
if db_field.rel.through is not None:
127
elif db_field.name in self.raw_id_fields:
128
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
129
kwargs['help_text'] = ''
130
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
131
kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
132
# Wrap the widget's render() method with a method that adds
133
# extra HTML to the end of the rendered output.
134
formfield = db_field.formfield(**kwargs)
135
# Don't wrap raw_id fields. Their add function is in the popup window.
136
if not db_field.name in self.raw_id_fields:
137
# formfield can be None if it came from a OneToOneField with
139
if formfield is not None:
140
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
143
# For any other type of field, just call its formfield() method.
144
return db_field.formfield(**kwargs)
146
def _declared_fieldsets(self):
148
return self.fieldsets
150
return [(None, {'fields': self.fields})]
152
declared_fieldsets = property(_declared_fieldsets)
154
class ModelAdmin(BaseModelAdmin):
155
"Encapsulates all admin options and functionality for a given model."
156
__metaclass__ = forms.MediaDefiningClass
158
list_display = ('__str__',)
159
list_display_links = ()
161
list_select_related = False
164
date_hierarchy = None
170
# Custom templates (designed to be over-ridden in subclasses)
171
change_form_template = None
172
change_list_template = None
173
delete_confirmation_template = None
174
object_history_template = None
176
def __init__(self, model, admin_site):
178
self.opts = model._meta
179
self.admin_site = admin_site
180
self.inline_instances = []
181
for inline_class in self.inlines:
182
inline_instance = inline_class(self.model, self.admin_site)
183
self.inline_instances.append(inline_instance)
184
super(ModelAdmin, self).__init__()
186
def __call__(self, request, url):
187
# Delegate to the appropriate method, based on the URL.
189
return self.changelist_view(request)
191
return self.add_view(request)
192
elif url.endswith('/history'):
193
return self.history_view(request, unquote(url[:-8]))
194
elif url.endswith('/delete'):
195
return self.delete_view(request, unquote(url[:-7]))
197
return self.change_view(request, unquote(url))
200
from django.conf import settings
202
js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
203
if self.prepopulated_fields:
204
js.append('js/urlify.js')
205
if self.opts.get_ordered_objects():
206
js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
208
return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
209
media = property(_media)
211
def has_add_permission(self, request):
212
"Returns True if the given request has permission to add an object."
214
return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
216
def has_change_permission(self, request, obj=None):
218
Returns True if the given request has permission to change the given
219
Django model instance.
221
If `obj` is None, this should return True if the given request has
222
permission to change *any* object of the given type.
225
return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
227
def has_delete_permission(self, request, obj=None):
229
Returns True if the given request has permission to change the given
230
Django model instance.
232
If `obj` is None, this should return True if the given request has
233
permission to delete *any* object of the given type.
236
return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
238
def queryset(self, request):
240
Returns a QuerySet of all model instances that can be edited by the
241
admin site. This is used by changelist_view.
243
qs = self.model._default_manager.get_query_set()
244
# TODO: this should be handled by some parameter to the ChangeList.
245
ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
247
qs = qs.order_by(*ordering)
250
def get_fieldsets(self, request, obj=None):
251
"Hook for specifying fieldsets for the add form."
252
if self.declared_fieldsets:
253
return self.declared_fieldsets
254
form = self.get_form(request, obj)
255
return [(None, {'fields': form.base_fields.keys()})]
257
def get_form(self, request, obj=None, **kwargs):
259
Returns a Form class for use in the admin add view. This is used by
260
add_view and change_view.
262
if self.declared_fieldsets:
263
fields = flatten_fieldsets(self.declared_fieldsets)
266
if self.exclude is None:
269
exclude = self.exclude
273
"exclude": exclude + kwargs.get("exclude", []),
274
"formfield_callback": self.formfield_for_dbfield,
276
defaults.update(kwargs)
277
return modelform_factory(self.model, **defaults)
279
def get_formsets(self, request, obj=None):
280
for inline in self.inline_instances:
281
yield inline.get_formset(request, obj)
283
def log_addition(self, request, object):
285
Log that an object has been successfully added.
287
The default implementation creates an admin LogEntry object.
289
from django.contrib.admin.models import LogEntry, ADDITION
290
LogEntry.objects.log_action(
291
user_id = request.user.pk,
292
content_type_id = ContentType.objects.get_for_model(object).pk,
293
object_id = object.pk,
294
object_repr = force_unicode(object),
295
action_flag = ADDITION
298
def log_change(self, request, object, message):
300
Log that an object has been successfully changed.
302
The default implementation creates an admin LogEntry object.
304
from django.contrib.admin.models import LogEntry, CHANGE
305
LogEntry.objects.log_action(
306
user_id = request.user.pk,
307
content_type_id = ContentType.objects.get_for_model(object).pk,
308
object_id = object.pk,
309
object_repr = force_unicode(object),
310
action_flag = CHANGE,
311
change_message = message
314
def log_deletion(self, request, object, object_repr):
316
Log that an object has been successfully deleted. Note that since the
317
object is deleted, it might no longer be safe to call *any* methods
318
on the object, hence this method getting object_repr.
320
The default implementation creates an admin LogEntry object.
322
from django.contrib.admin.models import LogEntry, DELETION
323
LogEntry.objects.log_action(
324
user_id = request.user.id,
325
content_type_id = ContentType.objects.get_for_model(self.model).pk,
326
object_id = object.pk,
327
object_repr = object_repr,
328
action_flag = DELETION
332
def construct_change_message(self, request, form, formsets):
334
Construct a change message from a changed object.
337
if form.changed_data:
338
change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
341
for formset in formsets:
342
for added_object in formset.new_objects:
343
change_message.append(_('Added %(name)s "%(object)s".')
344
% {'name': added_object._meta.verbose_name,
345
'object': added_object})
346
for changed_object, changed_fields in formset.changed_objects:
347
change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
348
% {'list': get_text_list(changed_fields, _('and')),
349
'name': changed_object._meta.verbose_name,
350
'object': changed_object})
351
for deleted_object in formset.deleted_objects:
352
change_message.append(_('Deleted %(name)s "%(object)s".')
353
% {'name': deleted_object._meta.verbose_name,
354
'object': deleted_object})
355
change_message = ' '.join(change_message)
356
return change_message or _('No fields changed.')
358
def message_user(self, request, message):
360
Send a message to the user. The default implementation
361
posts a message using the auth Message object.
363
request.user.message_set.create(message=message)
365
def save_form(self, request, form, change):
367
Given a ModelForm return an unsaved instance. ``change`` is True if
368
the object is being changed, and False if it's being added.
370
return form.save(commit=False)
372
def save_model(self, request, obj, form, change):
374
Given a model instance save it to the database.
378
def save_formset(self, request, form, formset, change):
380
Given an inline formset save it to the database.
384
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
385
opts = self.model._meta
386
app_label = opts.app_label
387
ordered_objects = opts.get_ordered_objects()
391
'has_add_permission': self.has_add_permission(request),
392
'has_change_permission': self.has_change_permission(request, obj),
393
'has_delete_permission': self.has_delete_permission(request, obj),
394
'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
395
'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
396
'ordered_objects': ordered_objects,
397
'form_url': mark_safe(form_url),
399
'content_type_id': ContentType.objects.get_for_model(self.model).id,
400
'save_as': self.save_as,
401
'save_on_top': self.save_on_top,
402
'root_path': self.admin_site.root_path,
404
return render_to_response(self.change_form_template or [
405
"admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
406
"admin/%s/change_form.html" % app_label,
407
"admin/change_form.html"
408
], context, context_instance=template.RequestContext(request))
410
def response_add(self, request, obj, post_url_continue='../%s/'):
412
Determines the HttpResponse for the add_view stage.
415
pk_value = obj._get_pk_val()
417
msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
418
# Here, we distinguish between different save types by checking for
419
# the presence of keys in request.POST.
420
if request.POST.has_key("_continue"):
421
self.message_user(request, msg + ' ' + _("You may edit it again below."))
422
if request.POST.has_key("_popup"):
423
post_url_continue += "?_popup=1"
424
return HttpResponseRedirect(post_url_continue % pk_value)
426
if request.POST.has_key("_popup"):
427
return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
428
# escape() calls force_unicode.
429
(escape(pk_value), escape(obj)))
430
elif request.POST.has_key("_addanother"):
431
self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
432
return HttpResponseRedirect(request.path)
434
self.message_user(request, msg)
436
# Figure out where to redirect. If the user has change permission,
437
# redirect to the change-list page for this object. Otherwise,
438
# redirect to the admin index.
439
if self.has_change_permission(request, None):
442
post_url = '../../../'
443
return HttpResponseRedirect(post_url)
445
def response_change(self, request, obj):
447
Determines the HttpResponse for the change_view stage.
450
pk_value = obj._get_pk_val()
452
msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
453
if request.POST.has_key("_continue"):
454
self.message_user(request, msg + ' ' + _("You may edit it again below."))
455
if request.REQUEST.has_key('_popup'):
456
return HttpResponseRedirect(request.path + "?_popup=1")
458
return HttpResponseRedirect(request.path)
459
elif request.POST.has_key("_saveasnew"):
460
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj}
461
self.message_user(request, msg)
462
return HttpResponseRedirect("../%s/" % pk_value)
463
elif request.POST.has_key("_addanother"):
464
self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
465
return HttpResponseRedirect("../add/")
467
self.message_user(request, msg)
468
return HttpResponseRedirect("../")
470
def add_view(self, request, form_url='', extra_context=None):
471
"The 'add' admin view for this model."
474
app_label = opts.app_label
476
if not self.has_add_permission(request):
477
raise PermissionDenied
479
if self.has_change_permission(request, None):
480
# redirect to list view
483
# Object list will give 'Permission Denied', so go back to admin home
484
post_url = '../../../'
486
ModelForm = self.get_form(request)
488
if request.method == 'POST':
489
form = ModelForm(request.POST, request.FILES)
491
form_validated = True
492
new_object = self.save_form(request, form, change=False)
494
form_validated = False
495
new_object = self.model()
496
for FormSet in self.get_formsets(request):
497
formset = FormSet(data=request.POST, files=request.FILES,
499
save_as_new=request.POST.has_key("_saveasnew"))
500
formsets.append(formset)
501
if all_valid(formsets) and form_validated:
502
self.save_model(request, new_object, form, change=False)
504
for formset in formsets:
505
self.save_formset(request, form, formset, change=False)
507
self.log_addition(request, new_object)
508
return self.response_add(request, new_object)
510
# Prepare the dict of initial data from the request.
511
# We have to special-case M2Ms as a list of comma-separated PKs.
512
initial = dict(request.GET.items())
515
f = opts.get_field(k)
516
except models.FieldDoesNotExist:
518
if isinstance(f, models.ManyToManyField):
519
initial[k] = initial[k].split(",")
520
form = ModelForm(initial=initial)
521
for FormSet in self.get_formsets(request):
522
formset = FormSet(instance=self.model())
523
formsets.append(formset)
525
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
526
media = self.media + adminForm.media
528
inline_admin_formsets = []
529
for inline, formset in zip(self.inline_instances, formsets):
530
fieldsets = list(inline.get_fieldsets(request))
531
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
532
inline_admin_formsets.append(inline_admin_formset)
533
media = media + inline_admin_formset.media
536
'title': _('Add %s') % force_unicode(opts.verbose_name),
537
'adminform': adminForm,
538
'is_popup': request.REQUEST.has_key('_popup'),
539
'show_delete': False,
540
'media': mark_safe(media),
541
'inline_admin_formsets': inline_admin_formsets,
542
'errors': helpers.AdminErrorList(form, formsets),
543
'root_path': self.admin_site.root_path,
544
'app_label': app_label,
546
context.update(extra_context or {})
547
return self.render_change_form(request, context, add=True)
548
add_view = transaction.commit_on_success(add_view)
550
def change_view(self, request, object_id, extra_context=None):
551
"The 'change' admin view for this model."
554
app_label = opts.app_label
557
obj = model._default_manager.get(pk=object_id)
558
except model.DoesNotExist:
559
# Don't raise Http404 just yet, because we haven't checked
560
# permissions yet. We don't want an unauthenticated user to be able
561
# to determine whether a given object exists.
564
if not self.has_change_permission(request, obj):
565
raise PermissionDenied
568
raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id)))
570
if request.POST and request.POST.has_key("_saveasnew"):
571
return self.add_view(request, form_url='../../add/')
573
ModelForm = self.get_form(request, obj)
575
if request.method == 'POST':
576
form = ModelForm(request.POST, request.FILES, instance=obj)
578
form_validated = True
579
new_object = self.save_form(request, form, change=True)
581
form_validated = False
583
for FormSet in self.get_formsets(request, new_object):
584
formset = FormSet(request.POST, request.FILES,
586
formsets.append(formset)
588
if all_valid(formsets) and form_validated:
589
self.save_model(request, new_object, form, change=True)
591
for formset in formsets:
592
self.save_formset(request, form, formset, change=True)
594
change_message = self.construct_change_message(request, form, formsets)
595
self.log_change(request, new_object, change_message)
596
return self.response_change(request, new_object)
598
form = ModelForm(instance=obj)
599
for FormSet in self.get_formsets(request, obj):
600
formset = FormSet(instance=obj)
601
formsets.append(formset)
603
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
604
media = self.media + adminForm.media
606
inline_admin_formsets = []
607
for inline, formset in zip(self.inline_instances, formsets):
608
fieldsets = list(inline.get_fieldsets(request, obj))
609
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
610
inline_admin_formsets.append(inline_admin_formset)
611
media = media + inline_admin_formset.media
614
'title': _('Change %s') % force_unicode(opts.verbose_name),
615
'adminform': adminForm,
616
'object_id': object_id,
618
'is_popup': request.REQUEST.has_key('_popup'),
619
'media': mark_safe(media),
620
'inline_admin_formsets': inline_admin_formsets,
621
'errors': helpers.AdminErrorList(form, formsets),
622
'root_path': self.admin_site.root_path,
623
'app_label': app_label,
625
context.update(extra_context or {})
626
return self.render_change_form(request, context, change=True, obj=obj)
627
change_view = transaction.commit_on_success(change_view)
629
def changelist_view(self, request, extra_context=None):
630
"The 'change list' admin view for this model."
631
from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
632
opts = self.model._meta
633
app_label = opts.app_label
634
if not self.has_change_permission(request, None):
635
raise PermissionDenied
637
cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter,
638
self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self)
639
except IncorrectLookupParameters:
640
# Wacky lookup parameters were given, so redirect to the main
641
# changelist page, without parameters, and pass an 'invalid=1'
642
# parameter via the query string. If wacky parameters were given and
643
# the 'invalid=1' parameter was already in the query string, something
644
# is screwed up with the database, so display an error page.
645
if ERROR_FLAG in request.GET.keys():
646
return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
647
return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
651
'is_popup': cl.is_popup,
653
'has_add_permission': self.has_add_permission(request),
654
'root_path': self.admin_site.root_path,
655
'app_label': app_label,
657
context.update(extra_context or {})
658
return render_to_response(self.change_list_template or [
659
'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
660
'admin/%s/change_list.html' % app_label,
661
'admin/change_list.html'
662
], context, context_instance=template.RequestContext(request))
664
def delete_view(self, request, object_id, extra_context=None):
665
"The 'delete' admin view for this model."
666
opts = self.model._meta
667
app_label = opts.app_label
670
obj = self.model._default_manager.get(pk=object_id)
671
except self.model.DoesNotExist:
672
# Don't raise Http404 just yet, because we haven't checked
673
# permissions yet. We don't want an unauthenticated user to be able
674
# to determine whether a given object exists.
677
if not self.has_delete_permission(request, obj):
678
raise PermissionDenied
681
raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id)))
683
# Populate deleted_objects, a data structure of all related objects that
684
# will also be deleted.
685
deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
687
get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
689
if request.POST: # The user has already confirmed the deletion.
691
raise PermissionDenied
692
obj_display = str(obj)
695
self.log_deletion(request, obj, obj_display)
696
self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
698
if not self.has_change_permission(request, None):
699
return HttpResponseRedirect("../../../../")
700
return HttpResponseRedirect("../../")
703
"title": _("Are you sure?"),
704
"object_name": force_unicode(opts.verbose_name),
706
"deleted_objects": deleted_objects,
707
"perms_lacking": perms_needed,
709
"root_path": self.admin_site.root_path,
710
"app_label": app_label,
712
context.update(extra_context or {})
713
return render_to_response(self.delete_confirmation_template or [
714
"admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
715
"admin/%s/delete_confirmation.html" % app_label,
716
"admin/delete_confirmation.html"
717
], context, context_instance=template.RequestContext(request))
719
def history_view(self, request, object_id, extra_context=None):
720
"The 'history' admin view for this model."
721
from django.contrib.admin.models import LogEntry
724
app_label = opts.app_label
725
action_list = LogEntry.objects.filter(
726
object_id = object_id,
727
content_type__id__exact = ContentType.objects.get_for_model(model).id
728
).select_related().order_by('action_time')
729
# If no history was found, see whether this object even exists.
730
obj = get_object_or_404(model, pk=object_id)
732
'title': _('Change history: %s') % force_unicode(obj),
733
'action_list': action_list,
734
'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
736
'root_path': self.admin_site.root_path,
737
'app_label': app_label,
739
context.update(extra_context or {})
740
return render_to_response(self.object_history_template or [
741
"admin/%s/%s/object_history.html" % (opts.app_label, opts.object_name.lower()),
742
"admin/%s/object_history.html" % opts.app_label,
743
"admin/object_history.html"
744
], context, context_instance=template.RequestContext(request))
746
class InlineModelAdmin(BaseModelAdmin):
748
Options for inline editing of ``model`` instances.
750
Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
751
``model`` to its parent. This is required if ``model`` has more than one
752
``ForeignKey`` to its parent.
756
formset = BaseInlineFormSet
761
verbose_name_plural = None
763
def __init__(self, parent_model, admin_site):
764
self.admin_site = admin_site
765
self.parent_model = parent_model
766
self.opts = self.model._meta
767
super(InlineModelAdmin, self).__init__()
768
if self.verbose_name is None:
769
self.verbose_name = self.model._meta.verbose_name
770
if self.verbose_name_plural is None:
771
self.verbose_name_plural = self.model._meta.verbose_name_plural
774
from django.conf import settings
776
if self.prepopulated_fields:
777
js.append('js/urlify.js')
778
if self.filter_vertical or self.filter_horizontal:
779
js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
780
return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
781
media = property(_media)
783
def get_formset(self, request, obj=None, **kwargs):
784
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
785
if self.declared_fieldsets:
786
fields = flatten_fieldsets(self.declared_fieldsets)
789
if self.exclude is None:
792
exclude = self.exclude
795
"formset": self.formset,
796
"fk_name": self.fk_name,
798
"exclude": exclude + kwargs.get("exclude", []),
799
"formfield_callback": self.formfield_for_dbfield,
801
"max_num": self.max_num,
803
defaults.update(kwargs)
804
return inlineformset_factory(self.parent_model, self.model, **defaults)
806
def get_fieldsets(self, request, obj=None):
807
if self.declared_fieldsets:
808
return self.declared_fieldsets
809
form = self.get_formset(request).form
810
return [(None, {'fields': form.base_fields.keys()})]
812
class StackedInline(InlineModelAdmin):
813
template = 'admin/edit_inline/stacked.html'
815
class TabularInline(InlineModelAdmin):
816
template = 'admin/edit_inline/tabular.html'