Ticket #16724: options.py

File options.py, 57.8 KB (added by Kidwind, 13 years ago)
Line 
1from functools import update_wrapper, partial
2from django import forms
3from django.forms.formsets import all_valid
4from django.forms.models import (modelform_factory, modelformset_factory,
5 inlineformset_factory, BaseInlineFormSet)
6from django.contrib.contenttypes.models import ContentType
7from django.contrib.admin import widgets, helpers
8from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
9from django.contrib.admin.templatetags.admin_static import static
10from django.contrib import messages
11from django.views.decorators.csrf import csrf_protect
12from django.core.exceptions import PermissionDenied, ValidationError
13from django.core.paginator import Paginator
14from django.db import models, transaction, router
15from django.db.models.related import RelatedObject
16from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
17from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
18from django.http import Http404, HttpResponse, HttpResponseRedirect
19from django.shortcuts import get_object_or_404
20from django.template.response import SimpleTemplateResponse, TemplateResponse
21from django.utils.decorators import method_decorator
22from django.utils.datastructures import SortedDict
23from django.utils.html import escape, escapejs
24from django.utils.safestring import mark_safe
25from django.utils.text import capfirst, get_text_list
26from django.utils.translation import ugettext as _
27from django.utils.translation import ungettext
28from django.utils.encoding import force_unicode
29
30HORIZONTAL, VERTICAL = 1, 2
31# returns the <ul> class for a given radio_admin field
32get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
33
34class IncorrectLookupParameters(Exception):
35 pass
36
37# Defaults for formfield_overrides. ModelAdmin subclasses can change this
38# by adding to ModelAdmin.formfield_overrides.
39
40FORMFIELD_FOR_DBFIELD_DEFAULTS = {
41 models.DateTimeField: {
42 'form_class': forms.SplitDateTimeField,
43 'widget': widgets.AdminSplitDateTime
44 },
45 models.DateField: {'widget': widgets.AdminDateWidget},
46 models.TimeField: {'widget': widgets.AdminTimeWidget},
47 models.TextField: {'widget': widgets.AdminTextareaWidget},
48 models.URLField: {'widget': widgets.AdminURLFieldWidget},
49 models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
50 models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
51 models.CharField: {'widget': widgets.AdminTextInputWidget},
52 models.ImageField: {'widget': widgets.AdminFileWidget},
53 models.FileField: {'widget': widgets.AdminFileWidget},
54}
55
56csrf_protect_m = method_decorator(csrf_protect)
57
58class BaseModelAdmin(object):
59 """Functionality common to both ModelAdmin and InlineAdmin."""
60 __metaclass__ = forms.MediaDefiningClass
61
62 raw_id_fields = ()
63 fields = None
64 exclude = None
65 fieldsets = None
66 form = forms.ModelForm
67 filter_vertical = ()
68 filter_horizontal = ()
69 radio_fields = {}
70 prepopulated_fields = {}
71 formfield_overrides = {}
72 readonly_fields = ()
73 ordering = None
74
75 def __init__(self):
76 overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
77 overrides.update(self.formfield_overrides)
78 self.formfield_overrides = overrides
79
80 def formfield_for_dbfield(self, db_field, **kwargs):
81 """
82 Hook for specifying the form Field instance for a given database Field
83 instance.
84
85 If kwargs are given, they're passed to the form Field's constructor.
86 """
87 request = kwargs.pop("request", None)
88
89 # If the field specifies choices, we don't need to look for special
90 # admin widgets - we just need to use a select widget of some kind.
91 if db_field.choices:
92 return self.formfield_for_choice_field(db_field, request, **kwargs)
93
94 # ForeignKey or ManyToManyFields
95 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
96 # Combine the field kwargs with any options for formfield_overrides.
97 # Make sure the passed in **kwargs override anything in
98 # formfield_overrides because **kwargs is more specific, and should
99 # always win.
100 if db_field.__class__ in self.formfield_overrides:
101 kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
102
103 # Get the correct formfield.
104 if isinstance(db_field, models.ForeignKey):
105 formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
106 elif isinstance(db_field, models.ManyToManyField):
107 formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
108
109 # For non-raw_id fields, wrap the widget with a wrapper that adds
110 # extra HTML -- the "add other" interface -- to the end of the
111 # rendered output. formfield can be None if it came from a
112 # OneToOneField with parent_link=True or a M2M intermediary.
113 if formfield and db_field.name not in self.raw_id_fields:
114 related_modeladmin = self.admin_site._registry.get(
115 db_field.rel.to)
116 can_add_related = bool(related_modeladmin and
117 related_modeladmin.has_add_permission(request))
118 formfield.widget = widgets.RelatedFieldWidgetWrapper(
119 formfield.widget, db_field.rel, self.admin_site,
120 can_add_related=can_add_related)
121
122 return formfield
123
124 # If we've got overrides for the formfield defined, use 'em. **kwargs
125 # passed to formfield_for_dbfield override the defaults.
126 for klass in db_field.__class__.mro():
127 if klass in self.formfield_overrides:
128 kwargs = dict(self.formfield_overrides[klass], **kwargs)
129 return db_field.formfield(**kwargs)
130
131 # For any other type of field, just call its formfield() method.
132 return db_field.formfield(**kwargs)
133
134 def formfield_for_choice_field(self, db_field, request=None, **kwargs):
135 """
136 Get a form Field for a database Field that has declared choices.
137 """
138 # If the field is named as a radio_field, use a RadioSelect
139 if db_field.name in self.radio_fields:
140 # Avoid stomping on custom widget/choices arguments.
141 if 'widget' not in kwargs:
142 kwargs['widget'] = widgets.AdminRadioSelect(attrs={
143 'class': get_ul_class(self.radio_fields[db_field.name]),
144 })
145 if 'choices' not in kwargs:
146 kwargs['choices'] = db_field.get_choices(
147 include_blank = db_field.blank,
148 blank_choice=[('', _('None'))]
149 )
150 return db_field.formfield(**kwargs)
151
152 def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
153 """
154 Get a form Field for a ForeignKey.
155 """
156 db = kwargs.get('using')
157 if db_field.name in self.raw_id_fields:
158 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel,
159 self.admin_site, using=db)
160 elif db_field.name in self.radio_fields:
161 kwargs['widget'] = widgets.AdminRadioSelect(attrs={
162 'class': get_ul_class(self.radio_fields[db_field.name]),
163 })
164 kwargs['empty_label'] = db_field.blank and _('None') or None
165
166 return db_field.formfield(**kwargs)
167
168 def formfield_for_manytomany(self, db_field, request=None, **kwargs):
169 """
170 Get a form Field for a ManyToManyField.
171 """
172 # If it uses an intermediary model that isn't auto created, don't show
173 # a field in admin.
174 if not db_field.rel.through._meta.auto_created:
175 return None
176 db = kwargs.get('using')
177
178 if db_field.name in self.raw_id_fields:
179 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel,
180 self.admin_site, using=db)
181 kwargs['help_text'] = ''
182 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
183 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
184
185 return db_field.formfield(**kwargs)
186
187 def _declared_fieldsets(self):
188 if self.fieldsets:
189 return self.fieldsets
190 elif self.fields:
191 return [(None, {'fields': self.fields})]
192 return None
193 declared_fieldsets = property(_declared_fieldsets)
194
195 def get_ordering(self, request):
196 """
197 Hook for specifying field ordering.
198 """
199 return self.ordering or () # otherwise we might try to *None, which is bad ;)
200
201 def get_readonly_fields(self, request, obj=None):
202 """
203 Hook for specifying custom readonly fields.
204 """
205 return self.readonly_fields
206
207 def get_prepopulated_fields(self, request, obj=None):
208 """
209 Hook for specifying custom prepopulated fields.
210 """
211 return self.prepopulated_fields
212
213 def queryset(self, request):
214 """
215 Returns a QuerySet of all model instances that can be edited by the
216 admin site. This is used by changelist_view.
217 """
218 qs = self.model._default_manager.get_query_set()
219 # TODO: this should be handled by some parameter to the ChangeList.
220 ordering = self.get_ordering(request)
221 if ordering:
222 qs = qs.order_by(*ordering)
223 return qs
224
225 def lookup_allowed(self, lookup, value):
226 model = self.model
227 # Check FKey lookups that are allowed, so that popups produced by
228 # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
229 # are allowed to work.
230 for l in model._meta.related_fkey_lookups:
231 for k, v in widgets.url_params_from_lookup_dict(l).items():
232 if k == lookup and v == value:
233 return True
234
235 parts = lookup.split(LOOKUP_SEP)
236
237 # Last term in lookup is a query term (__exact, __startswith etc)
238 # This term can be ignored.
239 if len(parts) > 1 and parts[-1] in QUERY_TERMS:
240 parts.pop()
241
242 # Special case -- foo__id__exact and foo__id queries are implied
243 # if foo has been specificially included in the lookup list; so
244 # drop __id if it is the last part. However, first we need to find
245 # the pk attribute name.
246 pk_attr_name = None
247 for part in parts[:-1]:
248 field, _, _, _ = model._meta.get_field_by_name(part)
249 if hasattr(field, 'rel'):
250 model = field.rel.to
251 pk_attr_name = model._meta.pk.name
252 elif isinstance(field, RelatedObject):
253 model = field.model
254 pk_attr_name = model._meta.pk.name
255 else:
256 pk_attr_name = None
257 if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name:
258 parts.pop()
259
260 try:
261 self.model._meta.get_field_by_name(parts[0])
262 except FieldDoesNotExist:
263 # Lookups on non-existants fields are ok, since they're ignored
264 # later.
265 return True
266 else:
267 if len(parts) == 1:
268 return True
269 clean_lookup = LOOKUP_SEP.join(parts)
270 return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
271
272
273class ModelAdmin(BaseModelAdmin):
274 "Encapsulates all admin options and functionality for a given model."
275
276 list_display = ('__str__',)
277 list_display_links = ()
278 list_filter = ()
279 list_select_related = False
280 list_per_page = 100
281 list_editable = ()
282 search_fields = ()
283 date_hierarchy = None
284 save_as = False
285 save_on_top = False
286 paginator = Paginator
287 inlines = []
288
289 # Custom templates (designed to be over-ridden in subclasses)
290 add_form_template = None
291 change_form_template = None
292 change_list_template = None
293 delete_confirmation_template = None
294 delete_selected_confirmation_template = None
295 object_history_template = None
296
297 # Actions
298 actions = []
299 action_form = helpers.ActionForm
300 actions_on_top = True
301 actions_on_bottom = False
302 actions_selection_counter = True
303
304 def __init__(self, model, admin_site):
305 self.model = model
306 self.opts = model._meta
307 self.admin_site = admin_site
308 self.inline_instances = []
309 for inline_class in self.inlines:
310 inline_instance = inline_class(self.model, self.admin_site)
311 self.inline_instances.append(inline_instance)
312 if 'action_checkbox' not in self.list_display and self.actions is not None:
313 self.list_display = ['action_checkbox'] + list(self.list_display)
314 if not self.list_display_links:
315 for name in self.list_display:
316 if name != 'action_checkbox':
317 self.list_display_links = [name]
318 break
319 super(ModelAdmin, self).__init__()
320
321 def get_urls(self):
322 from django.conf.urls.defaults import patterns, url
323
324 def wrap(view):
325 def wrapper(*args, **kwargs):
326 return self.admin_site.admin_view(view)(*args, **kwargs)
327 return update_wrapper(wrapper, view)
328
329 info = self.model._meta.app_label, self.model._meta.module_name
330
331 urlpatterns = patterns('',
332 url(r'^$',
333 wrap(self.changelist_view),
334 name='%s_%s_changelist' % info),
335 url(r'^add/$',
336 wrap(self.add_view),
337 name='%s_%s_add' % info),
338 url(r'^(.+)/history/$',
339 wrap(self.history_view),
340 name='%s_%s_history' % info),
341 url(r'^(.+)/delete/$',
342 wrap(self.delete_view),
343 name='%s_%s_delete' % info),
344 url(r'^(.+)/$',
345 wrap(self.change_view),
346 name='%s_%s_change' % info),
347 )
348 return urlpatterns
349
350 def urls(self):
351 return self.get_urls()
352 urls = property(urls)
353
354 @property
355 def media(self):
356 js = [
357 'core.js',
358 'admin/RelatedObjectLookups.js',
359 'jquery.min.js',
360 'jquery.init.js'
361 ]
362 if self.actions is not None:
363 js.append('actions.min.js')
364 if self.prepopulated_fields:
365 js.extend(['urlify.js', 'prepopulate.min.js'])
366 if self.opts.get_ordered_objects():
367 js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
368 return forms.Media(js=[static('admin/js/%s' % url) for url in js])
369
370 def has_add_permission(self, request):
371 """
372 Returns True if the given request has permission to add an object.
373 Can be overriden by the user in subclasses.
374 """
375 opts = self.opts
376 return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
377
378 def has_change_permission(self, request, obj=None):
379 """
380 Returns True if the given request has permission to change the given
381 Django model instance, the default implementation doesn't examine the
382 `obj` parameter.
383
384 Can be overriden by the user in subclasses. In such case it should
385 return True if the given request has permission to change the `obj`
386 model instance. If `obj` is None, this should return True if the given
387 request has permission to change *any* object of the given type.
388 """
389 opts = self.opts
390 return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission(), obj)
391
392 def has_delete_permission(self, request, obj=None):
393 """
394 Returns True if the given request has permission to change the given
395 Django model instance, the default implementation doesn't examine the
396 `obj` parameter.
397
398 Can be overriden by the user in subclasses. In such case it should
399 return True if the given request has permission to delete the `obj`
400 model instance. If `obj` is None, this should return True if the given
401 request has permission to delete *any* object of the given type.
402 """
403 opts = self.opts
404 return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission(), obj)
405
406 def get_model_perms(self, request):
407 """
408 Returns a dict of all perms for this model. This dict has the keys
409 ``add``, ``change``, and ``delete`` mapping to the True/False for each
410 of those actions.
411 """
412 return {
413 'add': self.has_add_permission(request),
414 'change': self.has_change_permission(request),
415 'delete': self.has_delete_permission(request),
416 }
417
418 def get_fieldsets(self, request, obj=None):
419 "Hook for specifying fieldsets for the add form."
420 if self.declared_fieldsets:
421 return self.declared_fieldsets
422 form = self.get_form(request, obj)
423 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
424 return [(None, {'fields': fields})]
425
426 def get_form(self, request, obj=None, **kwargs):
427 """
428 Returns a Form class for use in the admin add view. This is used by
429 add_view and change_view.
430 """
431 if self.declared_fieldsets:
432 fields = flatten_fieldsets(self.declared_fieldsets)
433 else:
434 fields = None
435 if self.exclude is None:
436 exclude = []
437 else:
438 exclude = list(self.exclude)
439 exclude.extend(self.get_readonly_fields(request, obj))
440 if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
441 # Take the custom ModelForm's Meta.exclude into account only if the
442 # ModelAdmin doesn't define its own.
443 exclude.extend(self.form._meta.exclude)
444 # if exclude is an empty list we pass None to be consistant with the
445 # default on modelform_factory
446 exclude = exclude or None
447 defaults = {
448 "form": self.form,
449 "fields": fields,
450 "exclude": exclude,
451 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
452 }
453 defaults.update(kwargs)
454 return modelform_factory(self.model, **defaults)
455
456 def get_changelist(self, request, **kwargs):
457 """
458 Returns the ChangeList class for use on the changelist page.
459 """
460 from django.contrib.admin.views.main import ChangeList
461 return ChangeList
462
463 def get_object(self, request, object_id):
464 """
465 Returns an instance matching the primary key provided. ``None`` is
466 returned if no match is found (or the object_id failed validation
467 against the primary key field).
468 """
469 queryset = self.queryset(request)
470 model = queryset.model
471 try:
472 object_id = model._meta.pk.to_python(object_id)
473 return queryset.get(pk=object_id)
474 except (model.DoesNotExist, ValidationError):
475 return None
476
477 def get_changelist_form(self, request, **kwargs):
478 """
479 Returns a Form class for use in the Formset on the changelist page.
480 """
481 defaults = {
482 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
483 }
484 defaults.update(kwargs)
485 return modelform_factory(self.model, **defaults)
486
487 def get_changelist_formset(self, request, **kwargs):
488 """
489 Returns a FormSet class for use on the changelist page if list_editable
490 is used.
491 """
492 defaults = {
493 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
494 }
495 defaults.update(kwargs)
496 return modelformset_factory(self.model,
497 self.get_changelist_form(request), extra=0,
498 fields=self.list_editable, **defaults)
499
500 def get_formsets(self, request, obj=None):
501 for inline in self.inline_instances:
502 yield inline.get_formset(request, obj)
503
504 def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
505 return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
506
507 def log_addition(self, request, object):
508 """
509 Log that an object has been successfully added.
510
511 The default implementation creates an admin LogEntry object.
512 """
513 from django.contrib.admin.models import LogEntry, ADDITION
514 LogEntry.objects.log_action(
515 user_id = request.user.pk,
516 content_type_id = ContentType.objects.get_for_model(object).pk,
517 object_id = object.pk,
518 object_repr = force_unicode(object),
519 action_flag = ADDITION
520 )
521
522 def log_change(self, request, object, message):
523 """
524 Log that an object has been successfully changed.
525
526 The default implementation creates an admin LogEntry object.
527 """
528 from django.contrib.admin.models import LogEntry, CHANGE
529 LogEntry.objects.log_action(
530 user_id = request.user.pk,
531 content_type_id = ContentType.objects.get_for_model(object).pk,
532 object_id = object.pk,
533 object_repr = force_unicode(object),
534 action_flag = CHANGE,
535 change_message = message
536 )
537
538 def log_deletion(self, request, object, object_repr):
539 """
540 Log that an object will be deleted. Note that this method is called
541 before the deletion.
542
543 The default implementation creates an admin LogEntry object.
544 """
545 from django.contrib.admin.models import LogEntry, DELETION
546 LogEntry.objects.log_action(
547 user_id = request.user.id,
548 content_type_id = ContentType.objects.get_for_model(self.model).pk,
549 object_id = object.pk,
550 object_repr = object_repr,
551 action_flag = DELETION
552 )
553
554 def action_checkbox(self, obj):
555 """
556 A list_display column containing a checkbox widget.
557 """
558 return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
559 action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
560 action_checkbox.allow_tags = True
561
562 def get_actions(self, request):
563 """
564 Return a dictionary mapping the names of all actions for this
565 ModelAdmin to a tuple of (callable, name, description) for each action.
566 """
567 # If self.actions is explicitally set to None that means that we don't
568 # want *any* actions enabled on this page.
569 from django.contrib.admin.views.main import IS_POPUP_VAR
570 if self.actions is None or IS_POPUP_VAR in request.GET:
571 return SortedDict()
572
573 actions = []
574
575 # Gather actions from the admin site first
576 for (name, func) in self.admin_site.actions:
577 description = getattr(func, 'short_description', name.replace('_', ' '))
578 actions.append((func, name, description))
579
580 # Then gather them from the model admin and all parent classes,
581 # starting with self and working back up.
582 for klass in self.__class__.mro()[::-1]:
583 class_actions = getattr(klass, 'actions', [])
584 # Avoid trying to iterate over None
585 if not class_actions:
586 continue
587 actions.extend([self.get_action(action) for action in class_actions])
588
589 # get_action might have returned None, so filter any of those out.
590 actions = filter(None, actions)
591
592 # Convert the actions into a SortedDict keyed by name.
593 actions = SortedDict([
594 (name, (func, name, desc))
595 for func, name, desc in actions
596 ])
597
598 return actions
599
600 def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
601 """
602 Return a list of choices for use in a form object. Each choice is a
603 tuple (name, description).
604 """
605 choices = [] + default_choices
606 for func, name, description in self.get_actions(request).itervalues():
607 choice = (name, description % model_format_dict(self.opts))
608 choices.append(choice)
609 return choices
610
611 def get_action(self, action):
612 """
613 Return a given action from a parameter, which can either be a callable,
614 or the name of a method on the ModelAdmin. Return is a tuple of
615 (callable, name, description).
616 """
617 # If the action is a callable, just use it.
618 if callable(action):
619 func = action
620 action = action.__name__
621
622 # Next, look for a method. Grab it off self.__class__ to get an unbound
623 # method instead of a bound one; this ensures that the calling
624 # conventions are the same for functions and methods.
625 elif hasattr(self.__class__, action):
626 func = getattr(self.__class__, action)
627
628 # Finally, look for a named method on the admin site
629 else:
630 try:
631 func = self.admin_site.get_action(action)
632 except KeyError:
633 return None
634
635 if hasattr(func, 'short_description'):
636 description = func.short_description
637 else:
638 description = capfirst(action.replace('_', ' '))
639 return func, action, description
640
641 def get_list_display(self, request):
642 """
643 Return a sequence containing the fields to be displayed on the
644 changelist.
645 """
646 return self.list_display
647
648 def construct_change_message(self, request, form, formsets):
649 """
650 Construct a change message from a changed object.
651 """
652 change_message = []
653 if form.changed_data:
654 change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
655
656 if formsets:
657 for formset in formsets:
658 for added_object in formset.new_objects:
659 change_message.append(_('Added %(name)s "%(object)s".')
660 % {'name': force_unicode(added_object._meta.verbose_name),
661 'object': force_unicode(added_object)})
662 for changed_object, changed_fields in formset.changed_objects:
663 change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
664 % {'list': get_text_list(changed_fields, _('and')),
665 'name': force_unicode(changed_object._meta.verbose_name),
666 'object': force_unicode(changed_object)})
667 for deleted_object in formset.deleted_objects:
668 change_message.append(_('Deleted %(name)s "%(object)s".')
669 % {'name': force_unicode(deleted_object._meta.verbose_name),
670 'object': force_unicode(deleted_object)})
671 change_message = ' '.join(change_message)
672 return change_message or _('No fields changed.')
673
674 def message_user(self, request, message):
675 """
676 Send a message to the user. The default implementation
677 posts a message using the django.contrib.messages backend.
678 """
679 messages.info(request, message)
680
681 def save_form(self, request, form, change):
682 """
683 Given a ModelForm return an unsaved instance. ``change`` is True if
684 the object is being changed, and False if it's being added.
685 """
686 return form.save(commit=False)
687
688 def save_model(self, request, obj, form, change):
689 """
690 Given a model instance save it to the database.
691 """
692 obj.save()
693
694 def delete_model(self, request, obj):
695 """
696 Given a model instance delete it from the database.
697 """
698 obj.delete()
699
700 def save_formset(self, request, form, formset, change):
701 """
702 Given an inline formset save it to the database.
703 """
704 formset.save()
705
706 def save_related(self, request, form, formsets, change):
707 """
708 Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
709 list of inline formsets and a boolean value based on whether the
710 parent is being added or changed, save the related objects to the
711 database. Note that at this point save_form() and save_model() have
712 already been called.
713 """
714 form.save_m2m()
715 for formset in formsets:
716 self.save_formset(request, form, formset, change=change)
717
718 def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
719 opts = self.model._meta
720 app_label = opts.app_label
721 ordered_objects = opts.get_ordered_objects()
722 context.update({
723 'add': add,
724 'change': change,
725 'has_add_permission': self.has_add_permission(request),
726 'has_change_permission': self.has_change_permission(request, obj),
727 'has_delete_permission': self.has_delete_permission(request, obj),
728 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
729 'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
730 'ordered_objects': ordered_objects,
731 'form_url': mark_safe(form_url),
732 'opts': opts,
733 'content_type_id': ContentType.objects.get_for_model(self.model).id,
734 'save_as': self.save_as,
735 'save_on_top': self.save_on_top,
736 })
737 if add and self.add_form_template is not None:
738 form_template = self.add_form_template
739 else:
740 form_template = self.change_form_template
741
742 return TemplateResponse(request, form_template or [
743 "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
744 "admin/%s/change_form.html" % app_label,
745 "admin/change_form.html"
746 ], context, current_app=self.admin_site.name)
747
748 def response_add(self, request, obj, post_url_continue='../%s/'):
749 """
750 Determines the HttpResponse for the add_view stage.
751 """
752 opts = obj._meta
753 pk_value = obj._get_pk_val()
754
755 msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
756 # Here, we distinguish between different save types by checking for
757 # the presence of keys in request.POST.
758 if "_continue" in request.POST:
759 self.message_user(request, msg + ' ' + _("You may edit it again below."))
760 if "_popup" in request.POST:
761 post_url_continue += "?_popup=1"
762 return HttpResponseRedirect(post_url_continue % pk_value)
763
764 if "_popup" in request.POST:
765 return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
766 # escape() calls force_unicode.
767 (escape(pk_value), escapejs(obj)))
768 elif "_addanother" in request.POST:
769 self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
770 return HttpResponseRedirect(request.path)
771 else:
772 self.message_user(request, msg)
773
774 # Figure out where to redirect. If the user has change permission,
775 # redirect to the change-list page for this object. Otherwise,
776 # redirect to the admin index.
777 if self.has_change_permission(request, None):
778 post_url = '../'
779 else:
780 post_url = '../../../'
781 return HttpResponseRedirect(post_url)
782
783 def response_change(self, request, obj):
784 """
785 Determines the HttpResponse for the change_view stage.
786 """
787 opts = obj._meta
788
789 # Handle proxy models automatically created by .only() or .defer()
790 verbose_name = opts.verbose_name
791 if obj._deferred:
792 opts_ = opts.proxy_for_model._meta
793 verbose_name = opts_.verbose_name
794
795 pk_value = obj._get_pk_val()
796
797 msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(verbose_name), 'obj': force_unicode(obj)}
798 if "_continue" in request.POST:
799 self.message_user(request, msg + ' ' + _("You may edit it again below."))
800 if "_popup" in request.REQUEST:
801 return HttpResponseRedirect(request.path + "?_popup=1")
802 else:
803 return HttpResponseRedirect(request.path)
804 elif "_saveasnew" in request.POST:
805 msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj}
806 self.message_user(request, msg)
807 return HttpResponseRedirect("../%s/" % pk_value)
808 elif "_addanother" in request.POST:
809 self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name)))
810 return HttpResponseRedirect("../add/")
811 else:
812 self.message_user(request, msg)
813 # Figure out where to redirect. If the user has change permission,
814 # redirect to the change-list page for this object. Otherwise,
815 # redirect to the admin index.
816 if self.has_change_permission(request, None):
817 return HttpResponseRedirect('../')
818 else:
819 return HttpResponseRedirect('../../../')
820
821 def response_action(self, request, queryset):
822 """
823 Handle an admin action. This is called if a request is POSTed to the
824 changelist; it returns an HttpResponse if the action was handled, and
825 None otherwise.
826 """
827
828 # There can be multiple action forms on the page (at the top
829 # and bottom of the change list, for example). Get the action
830 # whose button was pushed.
831 try:
832 action_index = int(request.POST.get('index', 0))
833 except ValueError:
834 action_index = 0
835
836 # Construct the action form.
837 data = request.POST.copy()
838 data.pop(helpers.ACTION_CHECKBOX_NAME, None)
839 data.pop("index", None)
840
841 # Use the action whose button was pushed
842 try:
843 data.update({'action': data.getlist('action')[action_index]})
844 except IndexError:
845 # If we didn't get an action from the chosen form that's invalid
846 # POST data, so by deleting action it'll fail the validation check
847 # below. So no need to do anything here
848 pass
849
850 action_form = self.action_form(data, auto_id=None)
851 action_form.fields['action'].choices = self.get_action_choices(request)
852
853 # If the form's valid we can handle the action.
854 if action_form.is_valid():
855 action = action_form.cleaned_data['action']
856 select_across = action_form.cleaned_data['select_across']
857 func, name, description = self.get_actions(request)[action]
858
859 # Get the list of selected PKs. If nothing's selected, we can't
860 # perform an action on it, so bail. Except we want to perform
861 # the action explicitly on all objects.
862 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
863 if not selected and not select_across:
864 # Reminder that something needs to be selected or nothing will happen
865 msg = _("Items must be selected in order to perform "
866 "actions on them. No items have been changed.")
867 self.message_user(request, msg)
868 return None
869
870 if not select_across:
871 # Perform the action only on the selected objects
872 queryset = queryset.filter(pk__in=selected)
873
874 response = func(self, request, queryset)
875
876 # Actions may return an HttpResponse, which will be used as the
877 # response from the POST. If not, we'll be a good little HTTP
878 # citizen and redirect back to the changelist page.
879 if isinstance(response, HttpResponse):
880 return response
881 else:
882 return HttpResponseRedirect(request.get_full_path())
883 else:
884 msg = _("No action selected.")
885 self.message_user(request, msg)
886 return None
887
888 @csrf_protect_m
889 @transaction.commit_on_success
890 def add_view(self, request, form_url='', extra_context=None):
891 "The 'add' admin view for this model."
892 model = self.model
893 opts = model._meta
894
895 if not self.has_add_permission(request):
896 raise PermissionDenied
897
898 ModelForm = self.get_form(request)
899 formsets = []
900 if request.method == 'POST':
901 form = ModelForm(request.POST, request.FILES)
902 if form.is_valid():
903 new_object = self.save_form(request, form, change=False)
904 form_validated = True
905 else:
906 form_validated = False
907 new_object = self.model()
908 prefixes = {}
909 for FormSet, inline in zip(self.get_formsets(request), self.inline_instances):
910 prefix = FormSet.get_default_prefix()
911 prefixes[prefix] = prefixes.get(prefix, 0) + 1
912 if prefixes[prefix] != 1:
913 prefix = "%s-%s" % (prefix, prefixes[prefix])
914 formset = FormSet(data=request.POST, files=request.FILES,
915 instance=new_object,
916 save_as_new="_saveasnew" in request.POST,
917 prefix=prefix, queryset=inline.queryset(request))
918 formsets.append(formset)
919 if all_valid(formsets) and form_validated:
920 self.save_model(request, new_object, form, False)
921 self.save_related(request, form, formsets, False)
922 self.log_addition(request, new_object)
923 return self.response_add(request, new_object)
924 else:
925 # Prepare the dict of initial data from the request.
926 # We have to special-case M2Ms as a list of comma-separated PKs.
927 initial = dict(request.GET.items())
928 for k in initial:
929 try:
930 f = opts.get_field(k)
931 except models.FieldDoesNotExist:
932 continue
933 if isinstance(f, models.ManyToManyField):
934 initial[k] = initial[k].split(",")
935 form = ModelForm(initial=initial)
936 prefixes = {}
937 for FormSet, inline in zip(self.get_formsets(request),
938 self.inline_instances):
939 prefix = FormSet.get_default_prefix()
940 prefixes[prefix] = prefixes.get(prefix, 0) + 1
941 if prefixes[prefix] != 1:
942 prefix = "%s-%s" % (prefix, prefixes[prefix])
943 formset = FormSet(instance=self.model(), prefix=prefix,
944 queryset=inline.queryset(request))
945 formsets.append(formset)
946
947 adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
948 self.get_prepopulated_fields(request),
949 self.get_readonly_fields(request),
950 model_admin=self)
951 media = self.media + adminForm.media
952
953 inline_admin_formsets = []
954 for inline, formset in zip(self.inline_instances, formsets):
955 fieldsets = list(inline.get_fieldsets(request))
956 readonly = list(inline.get_readonly_fields(request))
957 prepopulated = dict(inline.get_prepopulated_fields(request))
958 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
959 fieldsets, prepopulated, readonly, model_admin=self)
960 inline_admin_formsets.append(inline_admin_formset)
961 media = media + inline_admin_formset.media
962
963 context = {
964 'title': _('Add %s') % force_unicode(opts.verbose_name),
965 'adminform': adminForm,
966 'is_popup': "_popup" in request.REQUEST,
967 'show_delete': False,
968 'media': mark_safe(media),
969 'inline_admin_formsets': inline_admin_formsets,
970 'errors': helpers.AdminErrorList(form, formsets),
971 'app_label': opts.app_label,
972 }
973 context.update(extra_context or {})
974 return self.render_change_form(request, context, form_url=form_url, add=True)
975
976 @csrf_protect_m
977 @transaction.commit_on_success
978 def change_view(self, request, object_id, extra_context=None):
979 "The 'change' admin view for this model."
980 model = self.model
981 opts = model._meta
982
983 obj = self.get_object(request, unquote(object_id))
984
985 if not self.has_change_permission(request, obj):
986 raise PermissionDenied
987
988 if obj is None:
989 raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
990
991 if request.method == 'POST' and "_saveasnew" in request.POST:
992 return self.add_view(request, form_url='../add/')
993
994 ModelForm = self.get_form(request, obj)
995 formsets = []
996 if request.method == 'POST':
997 form = ModelForm(request.POST, request.FILES, instance=obj)
998 if form.is_valid():
999 form_validated = True
1000 new_object = self.save_form(request, form, change=True)
1001 else:
1002 form_validated = False
1003 new_object = obj
1004 prefixes = {}
1005 for FormSet, inline in zip(self.get_formsets(request, new_object),
1006 self.inline_instances):
1007 prefix = FormSet.get_default_prefix()
1008 prefixes[prefix] = prefixes.get(prefix, 0) + 1
1009 if prefixes[prefix] != 1:
1010 prefix = "%s-%s" % (prefix, prefixes[prefix])
1011 formset = FormSet(request.POST, request.FILES,
1012 instance=new_object, prefix=prefix,
1013 queryset=inline.queryset(request))
1014
1015 formsets.append(formset)
1016
1017 if all_valid(formsets) and form_validated:
1018 self.save_model(request, new_object, form, True)
1019 self.save_related(request, form, formsets, True)
1020 change_message = self.construct_change_message(request, form, formsets)
1021 self.log_change(request, new_object, change_message)
1022 return self.response_change(request, new_object)
1023
1024 else:
1025 form = ModelForm(instance=obj)
1026 prefixes = {}
1027 for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
1028 prefix = FormSet.get_default_prefix()
1029 prefixes[prefix] = prefixes.get(prefix, 0) + 1
1030 if prefixes[prefix] != 1:
1031 prefix = "%s-%s" % (prefix, prefixes[prefix])
1032 formset = FormSet(instance=obj, prefix=prefix,
1033 queryset=inline.queryset(request))
1034 formsets.append(formset)
1035
1036 adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
1037 self.get_prepopulated_fields(request, obj),
1038 self.get_readonly_fields(request, obj),
1039 model_admin=self)
1040 media = self.media + adminForm.media
1041
1042 inline_admin_formsets = []
1043 for inline, formset in zip(self.inline_instances, formsets):
1044 fieldsets = list(inline.get_fieldsets(request, obj))
1045 readonly = list(inline.get_readonly_fields(request, obj))
1046 prepopulated = dict(inline.get_prepopulated_fields(request, obj))
1047 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
1048 fieldsets, prepopulated, readonly, model_admin=self)
1049 inline_admin_formsets.append(inline_admin_formset)
1050 media = media + inline_admin_formset.media
1051
1052 context = {
1053 'title': _('Change %s') % force_unicode(opts.verbose_name),
1054 'adminform': adminForm,
1055 'object_id': object_id,
1056 'original': obj,
1057 'is_popup': "_popup" in request.REQUEST,
1058 'media': mark_safe(media),
1059 'inline_admin_formsets': inline_admin_formsets,
1060 'errors': helpers.AdminErrorList(form, formsets),
1061 'app_label': opts.app_label,
1062 }
1063 context.update(extra_context or {})
1064 return self.render_change_form(request, context, change=True, obj=obj)
1065
1066 @csrf_protect_m
1067 def changelist_view(self, request, extra_context=None):
1068 "The 'change list' admin view for this model."
1069 from django.contrib.admin.views.main import ERROR_FLAG
1070 opts = self.model._meta
1071 app_label = opts.app_label
1072 if not self.has_change_permission(request, None):
1073 raise PermissionDenied
1074
1075 # Check actions to see if any are available on this changelist
1076 actions = self.get_actions(request)
1077
1078 # Remove action checkboxes if there aren't any actions available.
1079 list_display = list(self.get_list_display(request))
1080 if not actions:
1081 try:
1082 list_display.remove('action_checkbox')
1083 except ValueError:
1084 pass
1085
1086 ChangeList = self.get_changelist(request)
1087 try:
1088 cl = ChangeList(request, self.model, list_display, self.list_display_links,
1089 self.list_filter, self.date_hierarchy, self.search_fields,
1090 self.list_select_related, self.list_per_page, self.list_editable, self)
1091 except IncorrectLookupParameters:
1092 # Wacky lookup parameters were given, so redirect to the main
1093 # changelist page, without parameters, and pass an 'invalid=1'
1094 # parameter via the query string. If wacky parameters were given
1095 # and the 'invalid=1' parameter was already in the query string,
1096 # something is screwed up with the database, so display an error
1097 # page.
1098 if ERROR_FLAG in request.GET.keys():
1099 return SimpleTemplateResponse('admin/invalid_setup.html', {
1100 'title': _('Database error'),
1101 })
1102 return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
1103
1104 # If the request was POSTed, this might be a bulk action or a bulk
1105 # edit. Try to look up an action or confirmation first, but if this
1106 # isn't an action the POST will fall through to the bulk edit check,
1107 # below.
1108 action_failed = False
1109 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1110
1111 # Actions with no confirmation
1112 if (actions and request.method == 'POST' and
1113 'index' in request.POST and '_save' not in request.POST):
1114 if selected:
1115 response = self.response_action(request, queryset=cl.get_query_set(request))
1116 if response:
1117 return response
1118 else:
1119 action_failed = True
1120 else:
1121 msg = _("Items must be selected in order to perform "
1122 "actions on them. No items have been changed.")
1123 self.message_user(request, msg)
1124 action_failed = True
1125
1126 # Actions with confirmation
1127 if (actions and request.method == 'POST' and
1128 helpers.ACTION_CHECKBOX_NAME in request.POST and
1129 'index' not in request.POST and '_save' not in request.POST):
1130 if selected:
1131 response = self.response_action(request, queryset=cl.get_query_set(request))
1132 if response:
1133 return response
1134 else:
1135 action_failed = True
1136
1137 # If we're allowing changelist editing, we need to construct a formset
1138 # for the changelist given all the fields to be edited. Then we'll
1139 # use the formset to validate/process POSTed data.
1140 formset = cl.formset = None
1141
1142 # Handle POSTed bulk-edit data.
1143 if (request.method == "POST" and cl.list_editable and
1144 '_save' in request.POST and not action_failed):
1145 FormSet = self.get_changelist_formset(request)
1146 formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
1147 if formset.is_valid():
1148 changecount = 0
1149 for form in formset.forms:
1150 if form.has_changed():
1151 obj = self.save_form(request, form, change=True)
1152 self.save_model(request, obj, form, change=True)
1153 self.save_related(request, form, formsets=[], change=True)
1154 change_msg = self.construct_change_message(request, form, None)
1155 self.log_change(request, obj, change_msg)
1156 changecount += 1
1157
1158 if changecount:
1159 if changecount == 1:
1160 name = force_unicode(opts.verbose_name)
1161 else:
1162 name = force_unicode(opts.verbose_name_plural)
1163 msg = ungettext("%(count)s %(name)s was changed successfully.",
1164 "%(count)s %(name)s were changed successfully.",
1165 changecount) % {'count': changecount,
1166 'name': name,
1167 'obj': force_unicode(obj)}
1168 self.message_user(request, msg)
1169
1170 return HttpResponseRedirect(request.get_full_path())
1171
1172 # Handle GET -- construct a formset for display.
1173 elif cl.list_editable:
1174 FormSet = self.get_changelist_formset(request)
1175 formset = cl.formset = FormSet(queryset=cl.result_list)
1176
1177 # Build the list of media to be used by the formset.
1178 if formset:
1179 media = self.media + formset.media
1180 else:
1181 media = self.media
1182
1183 # Build the action form and populate it with available actions.
1184 if actions:
1185 action_form = self.action_form(auto_id=None)
1186 action_form.fields['action'].choices = self.get_action_choices(request)
1187 else:
1188 action_form = None
1189
1190 selection_note_all = ungettext('%(total_count)s selected',
1191 'All %(total_count)s selected', cl.result_count)
1192
1193 context = {
1194 'module_name': force_unicode(opts.verbose_name_plural),
1195 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
1196 'selection_note_all': selection_note_all % {'total_count': cl.result_count},
1197 'title': cl.title,
1198 'is_popup': cl.is_popup,
1199 'cl': cl,
1200 'media': media,
1201 'has_add_permission': self.has_add_permission(request),
1202 'app_label': app_label,
1203 'action_form': action_form,
1204 'actions_on_top': self.actions_on_top,
1205 'actions_on_bottom': self.actions_on_bottom,
1206 'actions_selection_counter': self.actions_selection_counter,
1207 }
1208 context.update(extra_context or {})
1209
1210 return TemplateResponse(request, self.change_list_template or [
1211 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
1212 'admin/%s/change_list.html' % app_label,
1213 'admin/change_list.html'
1214 ], context, current_app=self.admin_site.name)
1215
1216 @csrf_protect_m
1217 @transaction.commit_on_success
1218 def delete_view(self, request, object_id, extra_context=None):
1219 "The 'delete' admin view for this model."
1220 opts = self.model._meta
1221 app_label = opts.app_label
1222
1223 obj = self.get_object(request, unquote(object_id))
1224
1225 if not self.has_delete_permission(request, obj):
1226 raise PermissionDenied
1227
1228 if obj is None:
1229 raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
1230
1231 using = router.db_for_write(self.model)
1232
1233 # Populate deleted_objects, a data structure of all related objects that
1234 # will also be deleted.
1235 (deleted_objects, perms_needed, protected) = get_deleted_objects(
1236 [obj], opts, request.user, self.admin_site, using)
1237
1238 if request.POST: # The user has already confirmed the deletion.
1239 if perms_needed:
1240 raise PermissionDenied
1241 obj_display = force_unicode(obj)
1242 self.log_deletion(request, obj, obj_display)
1243 self.delete_model(request, obj)
1244
1245 self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
1246
1247 if not self.has_change_permission(request, None):
1248 return HttpResponseRedirect("../../../../")
1249 return HttpResponseRedirect("../../")
1250
1251 object_name = force_unicode(opts.verbose_name)
1252
1253 if perms_needed or protected:
1254 title = _("Cannot delete %(name)s") % {"name": object_name}
1255 else:
1256 title = _("Are you sure?")
1257
1258 context = {
1259 "title": title,
1260 "object_name": object_name,
1261 "object": obj,
1262 "deleted_objects": deleted_objects,
1263 "perms_lacking": perms_needed,
1264 "protected": protected,
1265 "opts": opts,
1266 "app_label": app_label,
1267 }
1268 context.update(extra_context or {})
1269
1270 return TemplateResponse(request, self.delete_confirmation_template or [
1271 "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
1272 "admin/%s/delete_confirmation.html" % app_label,
1273 "admin/delete_confirmation.html"
1274 ], context, current_app=self.admin_site.name)
1275
1276 def history_view(self, request, object_id, extra_context=None):
1277 "The 'history' admin view for this model."
1278 from django.contrib.admin.models import LogEntry
1279 model = self.model
1280 opts = model._meta
1281 app_label = opts.app_label
1282 action_list = LogEntry.objects.filter(
1283 object_id = object_id,
1284 content_type__id__exact = ContentType.objects.get_for_model(model).id
1285 ).select_related().order_by('action_time')
1286 # If no history was found, see whether this object even exists.
1287 obj = get_object_or_404(model, pk=unquote(object_id))
1288 context = {
1289 'title': _('Change history: %s') % force_unicode(obj),
1290 'action_list': action_list,
1291 'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
1292 'object': obj,
1293 'app_label': app_label,
1294 }
1295 context.update(extra_context or {})
1296 return TemplateResponse(request, self.object_history_template or [
1297 "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
1298 "admin/%s/object_history.html" % app_label,
1299 "admin/object_history.html"
1300 ], context, current_app=self.admin_site.name)
1301
1302class InlineModelAdmin(BaseModelAdmin):
1303 """
1304 Options for inline editing of ``model`` instances.
1305
1306 Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
1307 ``model`` to its parent. This is required if ``model`` has more than one
1308 ``ForeignKey`` to its parent.
1309 """
1310 model = None
1311 fk_name = None
1312 formset = BaseInlineFormSet
1313 extra = 3
1314 max_num = None
1315 template = None
1316 verbose_name = None
1317 verbose_name_plural = None
1318 can_delete = True
1319
1320 def __init__(self, parent_model, admin_site):
1321 self.admin_site = admin_site
1322 self.parent_model = parent_model
1323 self.opts = self.model._meta
1324 super(InlineModelAdmin, self).__init__()
1325 if self.verbose_name is None:
1326 self.verbose_name = self.model._meta.verbose_name
1327 if self.verbose_name_plural is None:
1328 self.verbose_name_plural = self.model._meta.verbose_name_plural
1329
1330 @property
1331 def media(self):
1332 js = ['jquery.min.js', 'jquery.init.js', 'inlines.min.js']
1333 if self.prepopulated_fields:
1334 js.extend(['urlify.js', 'prepopulate.min.js'])
1335 if self.filter_vertical or self.filter_horizontal:
1336 js.extend(['SelectBox.js', 'SelectFilter2.js'])
1337 return forms.Media(js=[static('admin/js/%s' % url) for url in js])
1338
1339 def get_formset(self, request, obj=None, **kwargs):
1340 """Returns a BaseInlineFormSet class for use in admin add/change views."""
1341 if self.declared_fieldsets:
1342 fields = flatten_fieldsets(self.declared_fieldsets)
1343 else:
1344 fields = None
1345 if self.exclude is None:
1346 exclude = []
1347 else:
1348 exclude = list(self.exclude)
1349 exclude.extend(self.get_readonly_fields(request, obj))
1350 if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
1351 # Take the custom ModelForm's Meta.exclude into account only if the
1352 # InlineModelAdmin doesn't define its own.
1353 exclude.extend(self.form._meta.exclude)
1354 # if exclude is an empty list we use None, since that's the actual
1355 # default
1356 exclude = exclude or None
1357 defaults = {
1358 "form": self.form,
1359 "formset": self.formset,
1360 "fk_name": self.fk_name,
1361 "fields": fields,
1362 "exclude": exclude,
1363 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
1364 "extra": self.extra,
1365 "max_num": self.max_num,
1366 "can_delete": self.can_delete,
1367 }
1368 defaults.update(kwargs)
1369 return inlineformset_factory(self.parent_model, self.model, **defaults)
1370
1371 def get_fieldsets(self, request, obj=None):
1372 if self.declared_fieldsets:
1373 return self.declared_fieldsets
1374 form = self.get_formset(request, obj).form
1375 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
1376 return [(None, {'fields': fields})]
1377
1378class StackedInline(InlineModelAdmin):
1379 template = 'admin/edit_inline/stacked.html'
1380
1381class TabularInline(InlineModelAdmin):
1382 template = 'admin/edit_inline/tabular.html'
Back to Top