Ticket #8936: options.py

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