Ticket #25336: options.py

File options.py, 79.5 KB (added by Zoe, 9 years ago)

the solution is between line 273-278

Line 
1import copy
2import operator
3from collections import OrderedDict
4from functools import partial, reduce, update_wrapper
5
6from django import forms
7from django.conf import settings
8from django.contrib import messages
9from django.contrib.admin import helpers, widgets
10from django.contrib.admin.checks import (
11 BaseModelAdminChecks, InlineModelAdminChecks, ModelAdminChecks,
12)
13from django.contrib.admin.exceptions import DisallowedModelAdminToField
14from django.contrib.admin.templatetags.admin_static import static
15from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
16from django.contrib.admin.utils import (
17 NestedObjects, flatten_fieldsets, get_deleted_objects,
18 lookup_needs_distinct, model_format_dict, quote, unquote,
19)
20from django.contrib.auth import get_permission_codename
21from django.core.exceptions import (
22 FieldDoesNotExist, FieldError, PermissionDenied, ValidationError,
23)
24from django.core.paginator import Paginator
25from django.core.urlresolvers import reverse
26from django.db import models, router, transaction
27from django.db.models.constants import LOOKUP_SEP
28from django.db.models.fields import BLANK_CHOICE_DASH
29from django.forms.formsets import DELETION_FIELD_NAME, all_valid
30from django.forms.models import (
31 BaseInlineFormSet, inlineformset_factory, modelform_defines_fields,
32 modelform_factory, modelformset_factory,
33)
34from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
35from django.http import Http404, HttpResponseRedirect
36from django.http.response import HttpResponseBase
37from django.template.response import SimpleTemplateResponse, TemplateResponse
38from django.utils import six
39from django.utils.decorators import method_decorator
40from django.utils.encoding import force_text, python_2_unicode_compatible
41from django.utils.html import escape, escapejs
42from django.utils.http import urlencode
43from django.utils.safestring import mark_safe
44from django.utils.text import capfirst, get_text_list
45from django.utils.translation import string_concat, ugettext as _, ungettext
46from django.views.decorators.csrf import csrf_protect
47from django.views.generic import RedirectView
48
49from django.utils.translation import activate, get_language
50
51IS_POPUP_VAR = '_popup'
52TO_FIELD_VAR = '_to_field'
53
54
55HORIZONTAL, VERTICAL = 1, 2
56
57
58def get_content_type_for_model(obj):
59 # Since this module gets imported in the application's root package,
60 # it cannot import models from other applications at the module level.
61 from django.contrib.contenttypes.models import ContentType
62 return ContentType.objects.get_for_model(obj, for_concrete_model=False)
63
64
65def get_ul_class(radio_style):
66 return 'radiolist' if radio_style == VERTICAL else 'radiolist inline'
67
68
69class IncorrectLookupParameters(Exception):
70 pass
71
72# Defaults for formfield_overrides. ModelAdmin subclasses can change this
73# by adding to ModelAdmin.formfield_overrides.
74
75FORMFIELD_FOR_DBFIELD_DEFAULTS = {
76 models.DateTimeField: {
77 'form_class': forms.SplitDateTimeField,
78 'widget': widgets.AdminSplitDateTime
79 },
80 models.DateField: {'widget': widgets.AdminDateWidget},
81 models.TimeField: {'widget': widgets.AdminTimeWidget},
82 models.TextField: {'widget': widgets.AdminTextareaWidget},
83 models.URLField: {'widget': widgets.AdminURLFieldWidget},
84 models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
85 models.BigIntegerField: {'widget': widgets.AdminBigIntegerFieldWidget},
86 models.CharField: {'widget': widgets.AdminTextInputWidget},
87 models.ImageField: {'widget': widgets.AdminFileWidget},
88 models.FileField: {'widget': widgets.AdminFileWidget},
89 models.EmailField: {'widget': widgets.AdminEmailInputWidget},
90}
91
92csrf_protect_m = method_decorator(csrf_protect)
93
94
95class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
96 """Functionality common to both ModelAdmin and InlineAdmin."""
97
98 raw_id_fields = ()
99 fields = None
100 exclude = None
101 fieldsets = None
102 form = forms.ModelForm
103 filter_vertical = ()
104 filter_horizontal = ()
105 radio_fields = {}
106 prepopulated_fields = {}
107 formfield_overrides = {}
108 readonly_fields = ()
109 ordering = None
110 view_on_site = True
111 show_full_result_count = True
112 checks_class = BaseModelAdminChecks
113
114 @classmethod
115 def check(cls, model, **kwargs):
116 return cls.checks_class().check(cls, model, **kwargs)
117
118 def __init__(self):
119 overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
120 overrides.update(self.formfield_overrides)
121 self.formfield_overrides = overrides
122
123 def formfield_for_dbfield(self, db_field, **kwargs):
124 """
125 Hook for specifying the form Field instance for a given database Field
126 instance.
127
128 If kwargs are given, they're passed to the form Field's constructor.
129 """
130 request = kwargs.pop("request", None)
131
132 # If the field specifies choices, we don't need to look for special
133 # admin widgets - we just need to use a select widget of some kind.
134 if db_field.choices:
135 return self.formfield_for_choice_field(db_field, request, **kwargs)
136
137 # ForeignKey or ManyToManyFields
138 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
139 # Combine the field kwargs with any options for formfield_overrides.
140 # Make sure the passed in **kwargs override anything in
141 # formfield_overrides because **kwargs is more specific, and should
142 # always win.
143 if db_field.__class__ in self.formfield_overrides:
144 kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
145
146 # Get the correct formfield.
147 if isinstance(db_field, models.ForeignKey):
148 formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
149 elif isinstance(db_field, models.ManyToManyField):
150 formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
151
152 # For non-raw_id fields, wrap the widget with a wrapper that adds
153 # extra HTML -- the "add other" interface -- to the end of the
154 # rendered output. formfield can be None if it came from a
155 # OneToOneField with parent_link=True or a M2M intermediary.
156 if formfield and db_field.name not in self.raw_id_fields:
157 related_modeladmin = self.admin_site._registry.get(db_field.remote_field.model)
158 wrapper_kwargs = {}
159 if related_modeladmin:
160 wrapper_kwargs.update(
161 can_add_related=related_modeladmin.has_add_permission(request),
162 can_change_related=related_modeladmin.has_change_permission(request),
163 can_delete_related=related_modeladmin.has_delete_permission(request),
164 )
165 formfield.widget = widgets.RelatedFieldWidgetWrapper(
166 formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
167 )
168
169 return formfield
170
171 # If we've got overrides for the formfield defined, use 'em. **kwargs
172 # passed to formfield_for_dbfield override the defaults.
173 for klass in db_field.__class__.mro():
174 if klass in self.formfield_overrides:
175 kwargs = dict(copy.deepcopy(self.formfield_overrides[klass]), **kwargs)
176 return db_field.formfield(**kwargs)
177
178 # For any other type of field, just call its formfield() method.
179 return db_field.formfield(**kwargs)
180
181 def formfield_for_choice_field(self, db_field, request=None, **kwargs):
182 """
183 Get a form Field for a database Field that has declared choices.
184 """
185 # If the field is named as a radio_field, use a RadioSelect
186 if db_field.name in self.radio_fields:
187 # Avoid stomping on custom widget/choices arguments.
188 if 'widget' not in kwargs:
189 kwargs['widget'] = widgets.AdminRadioSelect(attrs={
190 'class': get_ul_class(self.radio_fields[db_field.name]),
191 })
192 if 'choices' not in kwargs:
193 kwargs['choices'] = db_field.get_choices(
194 include_blank=db_field.blank,
195 blank_choice=[('', _('None'))]
196 )
197 return db_field.formfield(**kwargs)
198
199 def get_field_queryset(self, db, db_field, request):
200 """
201 If the ModelAdmin specifies ordering, the queryset should respect that
202 ordering. Otherwise don't specify the queryset, let the field decide
203 (returns None in that case).
204 """
205 related_admin = self.admin_site._registry.get(db_field.remote_field.model)
206 if related_admin is not None:
207 ordering = related_admin.get_ordering(request)
208 if ordering is not None and ordering != ():
209 return db_field.remote_field.model._default_manager.using(db).order_by(*ordering)
210 return None
211
212 def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
213 """
214 Get a form Field for a ForeignKey.
215 """
216 db = kwargs.get('using')
217 if db_field.name in self.raw_id_fields:
218 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.remote_field,
219 self.admin_site, using=db)
220 elif db_field.name in self.radio_fields:
221 kwargs['widget'] = widgets.AdminRadioSelect(attrs={
222 'class': get_ul_class(self.radio_fields[db_field.name]),
223 })
224 kwargs['empty_label'] = _('None') if db_field.blank else None
225
226 if 'queryset' not in kwargs:
227 queryset = self.get_field_queryset(db, db_field, request)
228 if queryset is not None:
229 kwargs['queryset'] = queryset
230
231 return db_field.formfield(**kwargs)
232
233 def formfield_for_manytomany(self, db_field, request=None, **kwargs):
234 """
235 Get a form Field for a ManyToManyField.
236 """
237 # If it uses an intermediary model that isn't auto created, don't show
238 # a field in admin.
239 if not db_field.remote_field.through._meta.auto_created:
240 return None
241 db = kwargs.get('using')
242
243 if db_field.name in self.raw_id_fields:
244 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field,
245 self.admin_site, using=db)
246 kwargs['help_text'] = ''
247 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
248 kwargs['widget'] = widgets.FilteredSelectMultiple(
249 db_field.verbose_name,
250 db_field.name in self.filter_vertical
251 )
252
253 if 'queryset' not in kwargs:
254 queryset = self.get_field_queryset(db, db_field, request)
255 if queryset is not None:
256 kwargs['queryset'] = queryset
257
258 form_field = db_field.formfield(**kwargs)
259 if isinstance(form_field.widget, SelectMultiple) and not isinstance(form_field.widget, CheckboxSelectMultiple):
260 msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
261 help_text = form_field.help_text
262 form_field.help_text = string_concat(help_text, ' ', msg) if help_text else msg
263 return form_field
264
265 def get_view_on_site_url(self, obj=None):
266 if obj is None or not self.view_on_site:
267 return None
268
269 if callable(self.view_on_site):
270 return self.view_on_site(obj)
271 elif self.view_on_site and hasattr(obj, 'get_absolute_url'):
272 # use the ContentType lookup if view_on_site is True
273 from django.utils import translation
274 with translation.override(obj.language_code):
275 return reverse('admin:view_on_site', kwargs={
276 'content_type_id': get_content_type_for_model(obj).pk,
277 'object_id': obj.pk
278 })
279
280 def get_empty_value_display(self):
281 """
282 Return the empty_value_display set on ModelAdmin or AdminSite.
283 """
284 try:
285 return mark_safe(self.empty_value_display)
286 except AttributeError:
287 return mark_safe(self.admin_site.empty_value_display)
288
289 def get_fields(self, request, obj=None):
290 """
291 Hook for specifying fields.
292 """
293 return self.fields
294
295 def get_fieldsets(self, request, obj=None):
296 """
297 Hook for specifying fieldsets.
298 """
299 if self.fieldsets:
300 return self.fieldsets
301 return [(None, {'fields': self.get_fields(request, obj)})]
302
303 def get_ordering(self, request):
304 """
305 Hook for specifying field ordering.
306 """
307 return self.ordering or () # otherwise we might try to *None, which is bad ;)
308
309 def get_readonly_fields(self, request, obj=None):
310 """
311 Hook for specifying custom readonly fields.
312 """
313 return self.readonly_fields
314
315 def get_prepopulated_fields(self, request, obj=None):
316 """
317 Hook for specifying custom prepopulated fields.
318 """
319 return self.prepopulated_fields
320
321 def get_queryset(self, request):
322 """
323 Returns a QuerySet of all model instances that can be edited by the
324 admin site. This is used by changelist_view.
325 """
326 qs = self.model._default_manager.get_queryset()
327 # TODO: this should be handled by some parameter to the ChangeList.
328 ordering = self.get_ordering(request)
329 if ordering:
330 qs = qs.order_by(*ordering)
331 return qs
332
333 def lookup_allowed(self, lookup, value):
334 from django.contrib.admin.filters import SimpleListFilter
335
336 model = self.model
337 # Check FKey lookups that are allowed, so that popups produced by
338 # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
339 # are allowed to work.
340 for l in model._meta.related_fkey_lookups:
341 # As ``limit_choices_to`` can be a callable, invoke it here.
342 if callable(l):
343 l = l()
344 for k, v in widgets.url_params_from_lookup_dict(l).items():
345 if k == lookup and v == value:
346 return True
347
348 relation_parts = []
349 prev_field = None
350 for part in lookup.split(LOOKUP_SEP):
351 try:
352 field = model._meta.get_field(part)
353 except FieldDoesNotExist:
354 # Lookups on non-existent fields are ok, since they're ignored
355 # later.
356 break
357 # It is allowed to filter on values that would be found from local
358 # model anyways. For example, if you filter on employee__department__id,
359 # then the id value would be found already from employee__department_id.
360 if not prev_field or (prev_field.concrete and
361 field not in prev_field.get_path_info()[-1].target_fields):
362 relation_parts.append(part)
363 if not getattr(field, 'get_path_info', None):
364 # This is not a relational field, so further parts
365 # must be transforms.
366 break
367 prev_field = field
368 model = field.get_path_info()[-1].to_opts.model
369
370 if len(relation_parts) <= 1:
371 # Either a local field filter, or no fields at all.
372 return True
373 clean_lookup = LOOKUP_SEP.join(relation_parts)
374 valid_lookups = [self.date_hierarchy]
375 for filter_item in self.list_filter:
376 if isinstance(filter_item, type) and issubclass(filter_item, SimpleListFilter):
377 valid_lookups.append(filter_item.parameter_name)
378 elif isinstance(filter_item, (list, tuple)):
379 valid_lookups.append(filter_item[0])
380 else:
381 valid_lookups.append(filter_item)
382 return clean_lookup in valid_lookups
383
384 def to_field_allowed(self, request, to_field):
385 """
386 Returns True if the model associated with this admin should be
387 allowed to be referenced by the specified field.
388 """
389 opts = self.model._meta
390
391 try:
392 field = opts.get_field(to_field)
393 except FieldDoesNotExist:
394 return False
395
396 # Always allow referencing the primary key since it's already possible
397 # to get this information from the change view URL.
398 if field.primary_key:
399 return True
400
401 # Allow reverse relationships to models defining m2m fields if they
402 # target the specified field.
403 for many_to_many in opts.many_to_many:
404 if many_to_many.m2m_target_field_name() == to_field:
405 return True
406
407 # Make sure at least one of the models registered for this site
408 # references this field through a FK or a M2M relationship.
409 registered_models = set()
410 for model, admin in self.admin_site._registry.items():
411 registered_models.add(model)
412 for inline in admin.inlines:
413 registered_models.add(inline.model)
414
415 related_objects = (
416 f for f in opts.get_fields(include_hidden=True)
417 if (f.auto_created and not f.concrete)
418 )
419 for related_object in related_objects:
420 related_model = related_object.related_model
421 if (any(issubclass(model, related_model) for model in registered_models) and
422 related_object.field.remote_field.get_related_field() == field):
423 return True
424
425 return False
426
427 def has_add_permission(self, request):
428 """
429 Returns True if the given request has permission to add an object.
430 Can be overridden by the user in subclasses.
431 """
432 opts = self.opts
433 codename = get_permission_codename('add', opts)
434 return request.user.has_perm("%s.%s" % (opts.app_label, codename))
435
436 def has_change_permission(self, request, obj=None):
437 """
438 Returns True if the given request has permission to change the given
439 Django model instance, the default implementation doesn't examine the
440 `obj` parameter.
441
442 Can be overridden by the user in subclasses. In such case it should
443 return True if the given request has permission to change the `obj`
444 model instance. If `obj` is None, this should return True if the given
445 request has permission to change *any* object of the given type.
446 """
447 opts = self.opts
448 codename = get_permission_codename('change', opts)
449 return request.user.has_perm("%s.%s" % (opts.app_label, codename))
450
451 def has_delete_permission(self, request, obj=None):
452 """
453 Returns True if the given request has permission to change the given
454 Django model instance, the default implementation doesn't examine the
455 `obj` parameter.
456
457 Can be overridden by the user in subclasses. In such case it should
458 return True if the given request has permission to delete the `obj`
459 model instance. If `obj` is None, this should return True if the given
460 request has permission to delete *any* object of the given type.
461 """
462 opts = self.opts
463 codename = get_permission_codename('delete', opts)
464 return request.user.has_perm("%s.%s" % (opts.app_label, codename))
465
466 def has_module_permission(self, request):
467 """
468 Returns True if the given request has any permission in the given
469 app label.
470
471 Can be overridden by the user in subclasses. In such case it should
472 return True if the given request has permission to view the module on
473 the admin index page and access the module's index page. Overriding it
474 does not restrict access to the add, change or delete views. Use
475 `ModelAdmin.has_(add|change|delete)_permission` for that.
476 """
477 return request.user.has_module_perms(self.opts.app_label)
478
479
480@python_2_unicode_compatible
481class ModelAdmin(BaseModelAdmin):
482 "Encapsulates all admin options and functionality for a given model."
483
484 list_display = ('__str__',)
485 list_display_links = ()
486 list_filter = ()
487 list_select_related = False
488 list_per_page = 100
489 list_max_show_all = 200
490 list_editable = ()
491 search_fields = ()
492 date_hierarchy = None
493 save_as = False
494 save_on_top = False
495 paginator = Paginator
496 preserve_filters = True
497 inlines = []
498
499 # Custom templates (designed to be over-ridden in subclasses)
500 add_form_template = None
501 change_form_template = None
502 change_list_template = None
503 delete_confirmation_template = None
504 delete_selected_confirmation_template = None
505 object_history_template = None
506
507 # Actions
508 actions = []
509 action_form = helpers.ActionForm
510 actions_on_top = True
511 actions_on_bottom = False
512 actions_selection_counter = True
513 checks_class = ModelAdminChecks
514
515 def __init__(self, model, admin_site):
516 self.model = model
517 self.opts = model._meta
518 self.admin_site = admin_site
519 super(ModelAdmin, self).__init__()
520
521 def __str__(self):
522 return "%s.%s" % (self.model._meta.app_label, self.__class__.__name__)
523
524 def get_inline_instances(self, request, obj=None):
525 inline_instances = []
526 for inline_class in self.inlines:
527 inline = inline_class(self.model, self.admin_site)
528 if request:
529 if not (inline.has_add_permission(request) or
530 inline.has_change_permission(request, obj) or
531 inline.has_delete_permission(request, obj)):
532 continue
533 if not inline.has_add_permission(request):
534 inline.max_num = 0
535 inline_instances.append(inline)
536
537 return inline_instances
538
539 def get_urls(self):
540 from django.conf.urls import url
541
542 def wrap(view):
543 def wrapper(*args, **kwargs):
544 return self.admin_site.admin_view(view)(*args, **kwargs)
545 wrapper.model_admin = self
546 return update_wrapper(wrapper, view)
547
548 info = self.model._meta.app_label, self.model._meta.model_name
549
550 urlpatterns = [
551 url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info),
552 url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info),
553 url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info),
554 url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info),
555 url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
556 # For backwards compatibility (was the change url before 1.9)
557 url(r'^(.+)/$', wrap(RedirectView.as_view(
558 pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
559 ))),
560 ]
561 return urlpatterns
562
563 def urls(self):
564 return self.get_urls()
565 urls = property(urls)
566
567 @property
568 def media(self):
569 extra = '' if settings.DEBUG else '.min'
570 js = [
571 'core.js',
572 'admin/RelatedObjectLookups.js',
573 'vendor/jquery/jquery%s.js' % extra,
574 'jquery.init.js',
575 'actions%s.js' % extra,
576 'urlify.js',
577 'prepopulate%s.js' % extra,
578 'vendor/xregexp/xregexp.min.js',
579 ]
580 return forms.Media(js=[static('admin/js/%s' % url) for url in js])
581
582 def get_model_perms(self, request):
583 """
584 Returns a dict of all perms for this model. This dict has the keys
585 ``add``, ``change``, and ``delete`` mapping to the True/False for each
586 of those actions.
587 """
588 return {
589 'add': self.has_add_permission(request),
590 'change': self.has_change_permission(request),
591 'delete': self.has_delete_permission(request),
592 }
593
594 def get_fields(self, request, obj=None):
595 if self.fields:
596 return self.fields
597 form = self.get_form(request, obj, fields=None)
598 return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
599
600 def get_form(self, request, obj=None, **kwargs):
601 """
602 Returns a Form class for use in the admin add view. This is used by
603 add_view and change_view.
604 """
605 if 'fields' in kwargs:
606 fields = kwargs.pop('fields')
607 else:
608 fields = flatten_fieldsets(self.get_fieldsets(request, obj))
609 if self.exclude is None:
610 exclude = []
611 else:
612 exclude = list(self.exclude)
613 readonly_fields = self.get_readonly_fields(request, obj)
614 exclude.extend(readonly_fields)
615 if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
616 # Take the custom ModelForm's Meta.exclude into account only if the
617 # ModelAdmin doesn't define its own.
618 exclude.extend(self.form._meta.exclude)
619 # if exclude is an empty list we pass None to be consistent with the
620 # default on modelform_factory
621 exclude = exclude or None
622
623 # Remove declared form fields which are in readonly_fields.
624 new_attrs = OrderedDict(
625 (f, None) for f in readonly_fields
626 if f in self.form.declared_fields
627 )
628 form = type(self.form.__name__, (self.form,), new_attrs)
629
630 defaults = {
631 "form": form,
632 "fields": fields,
633 "exclude": exclude,
634 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
635 }
636 defaults.update(kwargs)
637
638 if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):
639 defaults['fields'] = forms.ALL_FIELDS
640
641 try:
642 return modelform_factory(self.model, **defaults)
643 except FieldError as e:
644 raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.'
645 % (e, self.__class__.__name__))
646
647 def get_changelist(self, request, **kwargs):
648 """
649 Returns the ChangeList class for use on the changelist page.
650 """
651 from django.contrib.admin.views.main import ChangeList
652 return ChangeList
653
654 def get_object(self, request, object_id, from_field=None):
655 """
656 Returns an instance matching the field and value provided, the primary
657 key is used if no field is provided. Returns ``None`` if no match is
658 found or the object_id fails validation.
659 """
660 queryset = self.get_queryset(request)
661 model = queryset.model
662 field = model._meta.pk if from_field is None else model._meta.get_field(from_field)
663 try:
664 object_id = field.to_python(object_id)
665 return queryset.get(**{field.name: object_id})
666 except (model.DoesNotExist, ValidationError, ValueError):
667 return None
668
669 def get_changelist_form(self, request, **kwargs):
670 """
671 Returns a Form class for use in the Formset on the changelist page.
672 """
673 defaults = {
674 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
675 }
676 defaults.update(kwargs)
677 if (defaults.get('fields') is None
678 and not modelform_defines_fields(defaults.get('form'))):
679 defaults['fields'] = forms.ALL_FIELDS
680
681 return modelform_factory(self.model, **defaults)
682
683 def get_changelist_formset(self, request, **kwargs):
684 """
685 Returns a FormSet class for use on the changelist page if list_editable
686 is used.
687 """
688 defaults = {
689 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
690 }
691 defaults.update(kwargs)
692 return modelformset_factory(self.model,
693 self.get_changelist_form(request), extra=0,
694 fields=self.list_editable, **defaults)
695
696 def get_formsets_with_inlines(self, request, obj=None):
697 """
698 Yields formsets and the corresponding inlines.
699 """
700 for inline in self.get_inline_instances(request, obj):
701 yield inline.get_formset(request, obj), inline
702
703 def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
704 return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
705
706 def log_addition(self, request, object, message):
707 """
708 Log that an object has been successfully added.
709
710 The default implementation creates an admin LogEntry object.
711 """
712 from django.contrib.admin.models import LogEntry, ADDITION
713 LogEntry.objects.log_action(
714 user_id=request.user.pk,
715 content_type_id=get_content_type_for_model(object).pk,
716 object_id=object.pk,
717 object_repr=force_text(object),
718 action_flag=ADDITION,
719 change_message=message,
720 )
721
722 def log_change(self, request, object, message):
723 """
724 Log that an object has been successfully changed.
725
726 The default implementation creates an admin LogEntry object.
727 """
728 from django.contrib.admin.models import LogEntry, CHANGE
729 LogEntry.objects.log_action(
730 user_id=request.user.pk,
731 content_type_id=get_content_type_for_model(object).pk,
732 object_id=object.pk,
733 object_repr=force_text(object),
734 action_flag=CHANGE,
735 change_message=message,
736 )
737
738 def log_deletion(self, request, object, object_repr):
739 """
740 Log that an object will be deleted. Note that this method must be
741 called before the deletion.
742
743 The default implementation creates an admin LogEntry object.
744 """
745 from django.contrib.admin.models import LogEntry, DELETION
746 LogEntry.objects.log_action(
747 user_id=request.user.pk,
748 content_type_id=get_content_type_for_model(object).pk,
749 object_id=object.pk,
750 object_repr=object_repr,
751 action_flag=DELETION,
752 )
753
754 def action_checkbox(self, obj):
755 """
756 A list_display column containing a checkbox widget.
757 """
758 return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_text(obj.pk))
759 action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
760 action_checkbox.allow_tags = True
761
762 def get_actions(self, request):
763 """
764 Return a dictionary mapping the names of all actions for this
765 ModelAdmin to a tuple of (callable, name, description) for each action.
766 """
767 # If self.actions is explicitly set to None that means that we don't
768 # want *any* actions enabled on this page.
769 if self.actions is None or IS_POPUP_VAR in request.GET:
770 return OrderedDict()
771
772 actions = []
773
774 # Gather actions from the admin site first
775 for (name, func) in self.admin_site.actions:
776 description = getattr(func, 'short_description', name.replace('_', ' '))
777 actions.append((func, name, description))
778
779 # Then gather them from the model admin and all parent classes,
780 # starting with self and working back up.
781 for klass in self.__class__.mro()[::-1]:
782 class_actions = getattr(klass, 'actions', [])
783 # Avoid trying to iterate over None
784 if not class_actions:
785 continue
786 actions.extend(self.get_action(action) for action in class_actions)
787
788 # get_action might have returned None, so filter any of those out.
789 actions = filter(None, actions)
790
791 # Convert the actions into an OrderedDict keyed by name.
792 actions = OrderedDict(
793 (name, (func, name, desc))
794 for func, name, desc in actions
795 )
796
797 return actions
798
799 def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
800 """
801 Return a list of choices for use in a form object. Each choice is a
802 tuple (name, description).
803 """
804 choices = [] + default_choices
805 for func, name, description in six.itervalues(self.get_actions(request)):
806 choice = (name, description % model_format_dict(self.opts))
807 choices.append(choice)
808 return choices
809
810 def get_action(self, action):
811 """
812 Return a given action from a parameter, which can either be a callable,
813 or the name of a method on the ModelAdmin. Return is a tuple of
814 (callable, name, description).
815 """
816 # If the action is a callable, just use it.
817 if callable(action):
818 func = action
819 action = action.__name__
820
821 # Next, look for a method. Grab it off self.__class__ to get an unbound
822 # method instead of a bound one; this ensures that the calling
823 # conventions are the same for functions and methods.
824 elif hasattr(self.__class__, action):
825 func = getattr(self.__class__, action)
826
827 # Finally, look for a named method on the admin site
828 else:
829 try:
830 func = self.admin_site.get_action(action)
831 except KeyError:
832 return None
833
834 if hasattr(func, 'short_description'):
835 description = func.short_description
836 else:
837 description = capfirst(action.replace('_', ' '))
838 return func, action, description
839
840 def get_list_display(self, request):
841 """
842 Return a sequence containing the fields to be displayed on the
843 changelist.
844 """
845 return self.list_display
846
847 def get_list_display_links(self, request, list_display):
848 """
849 Return a sequence containing the fields to be displayed as links
850 on the changelist. The list_display parameter is the list of fields
851 returned by get_list_display().
852 """
853 if self.list_display_links or self.list_display_links is None or not list_display:
854 return self.list_display_links
855 else:
856 # Use only the first item in list_display as link
857 return list(list_display)[:1]
858
859 def get_list_filter(self, request):
860 """
861 Returns a sequence containing the fields to be displayed as filters in
862 the right sidebar of the changelist page.
863 """
864 return self.list_filter
865
866 def get_list_select_related(self, request):
867 """
868 Returns a list of fields to add to the select_related() part of the
869 changelist items query.
870 """
871 return self.list_select_related
872
873 def get_search_fields(self, request):
874 """
875 Returns a sequence containing the fields to be searched whenever
876 somebody submits a search query.
877 """
878 return self.search_fields
879
880 def get_search_results(self, request, queryset, search_term):
881 """
882 Returns a tuple containing a queryset to implement the search,
883 and a boolean indicating if the results may contain duplicates.
884 """
885 # Apply keyword searches.
886 def construct_search(field_name):
887 if field_name.startswith('^'):
888 return "%s__istartswith" % field_name[1:]
889 elif field_name.startswith('='):
890 return "%s__iexact" % field_name[1:]
891 elif field_name.startswith('@'):
892 return "%s__search" % field_name[1:]
893 else:
894 return "%s__icontains" % field_name
895
896 use_distinct = False
897 search_fields = self.get_search_fields(request)
898 if search_fields and search_term:
899 orm_lookups = [construct_search(str(search_field))
900 for search_field in search_fields]
901 for bit in search_term.split():
902 or_queries = [models.Q(**{orm_lookup: bit})
903 for orm_lookup in orm_lookups]
904 queryset = queryset.filter(reduce(operator.or_, or_queries))
905 if not use_distinct:
906 for search_spec in orm_lookups:
907 if lookup_needs_distinct(self.opts, search_spec):
908 use_distinct = True
909 break
910
911 return queryset, use_distinct
912
913 def get_preserved_filters(self, request):
914 """
915 Returns the preserved filters querystring.
916 """
917 match = request.resolver_match
918 if self.preserve_filters and match:
919 opts = self.model._meta
920 current_url = '%s:%s' % (match.app_name, match.url_name)
921 changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
922 if current_url == changelist_url:
923 preserved_filters = request.GET.urlencode()
924 else:
925 preserved_filters = request.GET.get('_changelist_filters')
926
927 if preserved_filters:
928 return urlencode({'_changelist_filters': preserved_filters})
929 return ''
930
931 def construct_change_message(self, request, form, formsets, add=False):
932 """
933 Construct a change message from a changed object.
934 """
935 change_message = []
936 if add:
937 change_message.append(_('Added.'))
938 elif form.changed_data:
939 change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
940
941 if formsets:
942 for formset in formsets:
943 for added_object in formset.new_objects:
944 change_message.append(_('Added %(name)s "%(object)s".')
945 % {'name': force_text(added_object._meta.verbose_name),
946 'object': force_text(added_object)})
947 for changed_object, changed_fields in formset.changed_objects:
948 change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
949 % {'list': get_text_list(changed_fields, _('and')),
950 'name': force_text(changed_object._meta.verbose_name),
951 'object': force_text(changed_object)})
952 for deleted_object in formset.deleted_objects:
953 change_message.append(_('Deleted %(name)s "%(object)s".')
954 % {'name': force_text(deleted_object._meta.verbose_name),
955 'object': force_text(deleted_object)})
956 change_message = ' '.join(change_message)
957 return change_message or _('No fields changed.')
958
959 def message_user(self, request, message, level=messages.INFO, extra_tags='',
960 fail_silently=False):
961 """
962 Send a message to the user. The default implementation
963 posts a message using the django.contrib.messages backend.
964
965 Exposes almost the same API as messages.add_message(), but accepts the
966 positional arguments in a different order to maintain backwards
967 compatibility. For convenience, it accepts the `level` argument as
968 a string rather than the usual level number.
969 """
970
971 if not isinstance(level, int):
972 # attempt to get the level if passed a string
973 try:
974 level = getattr(messages.constants, level.upper())
975 except AttributeError:
976 levels = messages.constants.DEFAULT_TAGS.values()
977 levels_repr = ', '.join('`%s`' % l for l in levels)
978 raise ValueError('Bad message level string: `%s`. '
979 'Possible values are: %s' % (level, levels_repr))
980
981 messages.add_message(request, level, message, extra_tags=extra_tags,
982 fail_silently=fail_silently)
983
984 def save_form(self, request, form, change):
985 """
986 Given a ModelForm return an unsaved instance. ``change`` is True if
987 the object is being changed, and False if it's being added.
988 """
989 return form.save(commit=False)
990
991 def save_model(self, request, obj, form, change):
992 """
993 Given a model instance save it to the database.
994 """
995 obj.save()
996
997 def delete_model(self, request, obj):
998 """
999 Given a model instance delete it from the database.
1000 """
1001 obj.delete()
1002
1003 def save_formset(self, request, form, formset, change):
1004 """
1005 Given an inline formset save it to the database.
1006 """
1007 formset.save()
1008
1009 def save_related(self, request, form, formsets, change):
1010 """
1011 Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
1012 list of inline formsets and a boolean value based on whether the
1013 parent is being added or changed, save the related objects to the
1014 database. Note that at this point save_form() and save_model() have
1015 already been called.
1016 """
1017 form.save_m2m()
1018 for formset in formsets:
1019 self.save_formset(request, form, formset, change=change)
1020
1021 def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
1022 opts = self.model._meta
1023 app_label = opts.app_label
1024 preserved_filters = self.get_preserved_filters(request)
1025 form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
1026 view_on_site_url = self.get_view_on_site_url(obj)
1027 context.update({
1028 'add': add,
1029 'change': change,
1030 'has_add_permission': self.has_add_permission(request),
1031 'has_change_permission': self.has_change_permission(request, obj),
1032 'has_delete_permission': self.has_delete_permission(request, obj),
1033 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
1034 'has_absolute_url': view_on_site_url is not None,
1035 'absolute_url': view_on_site_url,
1036 'form_url': form_url,
1037 'opts': opts,
1038 'content_type_id': get_content_type_for_model(self.model).pk,
1039 'save_as': self.save_as,
1040 'save_on_top': self.save_on_top,
1041 'to_field_var': TO_FIELD_VAR,
1042 'is_popup_var': IS_POPUP_VAR,
1043 'app_label': app_label,
1044 })
1045 if add and self.add_form_template is not None:
1046 form_template = self.add_form_template
1047 else:
1048 form_template = self.change_form_template
1049
1050 request.current_app = self.admin_site.name
1051
1052 return TemplateResponse(request, form_template or [
1053 "admin/%s/%s/change_form.html" % (app_label, opts.model_name),
1054 "admin/%s/change_form.html" % app_label,
1055 "admin/change_form.html"
1056 ], context)
1057
1058 def response_add(self, request, obj, post_url_continue=None):
1059 """
1060 Determines the HttpResponse for the add_view stage.
1061 """
1062 opts = obj._meta
1063 pk_value = obj._get_pk_val()
1064 preserved_filters = self.get_preserved_filters(request)
1065 msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
1066 # Here, we distinguish between different save types by checking for
1067 # the presence of keys in request.POST.
1068
1069 if IS_POPUP_VAR in request.POST:
1070 to_field = request.POST.get(TO_FIELD_VAR)
1071 if to_field:
1072 attr = str(to_field)
1073 else:
1074 attr = obj._meta.pk.attname
1075 value = obj.serializable_value(attr)
1076 return SimpleTemplateResponse('admin/popup_response.html', {
1077 'value': value,
1078 'obj': obj,
1079 })
1080
1081 elif "_continue" in request.POST:
1082 msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
1083 self.message_user(request, msg, messages.SUCCESS)
1084 if post_url_continue is None:
1085 post_url_continue = reverse('admin:%s_%s_change' %
1086 (opts.app_label, opts.model_name),
1087 args=(quote(pk_value),),
1088 current_app=self.admin_site.name)
1089 post_url_continue = add_preserved_filters(
1090 {'preserved_filters': preserved_filters, 'opts': opts},
1091 post_url_continue
1092 )
1093 return HttpResponseRedirect(post_url_continue)
1094
1095 elif "_addanother" in request.POST:
1096 msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
1097 self.message_user(request, msg, messages.SUCCESS)
1098 redirect_url = request.path
1099 redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
1100 return HttpResponseRedirect(redirect_url)
1101
1102 else:
1103 msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
1104 self.message_user(request, msg, messages.SUCCESS)
1105 return self.response_post_save_add(request, obj)
1106
1107 def response_change(self, request, obj):
1108 """
1109 Determines the HttpResponse for the change_view stage.
1110 """
1111
1112 if IS_POPUP_VAR in request.POST:
1113 to_field = request.POST.get(TO_FIELD_VAR)
1114 attr = str(to_field) if to_field else obj._meta.pk.attname
1115 # Retrieve the `object_id` from the resolved pattern arguments.
1116 value = request.resolver_match.args[0]
1117 new_value = obj.serializable_value(attr)
1118 return SimpleTemplateResponse('admin/popup_response.html', {
1119 'action': 'change',
1120 'value': escape(value),
1121 'obj': escapejs(obj),
1122 'new_value': escape(new_value),
1123 })
1124
1125 opts = self.model._meta
1126 pk_value = obj._get_pk_val()
1127 preserved_filters = self.get_preserved_filters(request)
1128
1129 msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
1130 if "_continue" in request.POST:
1131 msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
1132 self.message_user(request, msg, messages.SUCCESS)
1133 redirect_url = request.path
1134 redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
1135 return HttpResponseRedirect(redirect_url)
1136
1137 elif "_saveasnew" in request.POST:
1138 msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
1139 self.message_user(request, msg, messages.SUCCESS)
1140 redirect_url = reverse('admin:%s_%s_change' %
1141 (opts.app_label, opts.model_name),
1142 args=(pk_value,),
1143 current_app=self.admin_site.name)
1144 redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
1145 return HttpResponseRedirect(redirect_url)
1146
1147 elif "_addanother" in request.POST:
1148 msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
1149 self.message_user(request, msg, messages.SUCCESS)
1150 redirect_url = reverse('admin:%s_%s_add' %
1151 (opts.app_label, opts.model_name),
1152 current_app=self.admin_site.name)
1153 redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
1154 return HttpResponseRedirect(redirect_url)
1155
1156 else:
1157 msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
1158 self.message_user(request, msg, messages.SUCCESS)
1159 return self.response_post_save_change(request, obj)
1160
1161 def response_post_save_add(self, request, obj):
1162 """
1163 Figure out where to redirect after the 'Save' button has been pressed
1164 when adding a new object.
1165 """
1166 opts = self.model._meta
1167 if self.has_change_permission(request, None):
1168 post_url = reverse('admin:%s_%s_changelist' %
1169 (opts.app_label, opts.model_name),
1170 current_app=self.admin_site.name)
1171 preserved_filters = self.get_preserved_filters(request)
1172 post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
1173 else:
1174 post_url = reverse('admin:index',
1175 current_app=self.admin_site.name)
1176 return HttpResponseRedirect(post_url)
1177
1178 def response_post_save_change(self, request, obj):
1179 """
1180 Figure out where to redirect after the 'Save' button has been pressed
1181 when editing an existing object.
1182 """
1183 opts = self.model._meta
1184
1185 if self.has_change_permission(request, None):
1186 post_url = reverse('admin:%s_%s_changelist' %
1187 (opts.app_label, opts.model_name),
1188 current_app=self.admin_site.name)
1189 preserved_filters = self.get_preserved_filters(request)
1190 post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
1191 else:
1192 post_url = reverse('admin:index',
1193 current_app=self.admin_site.name)
1194 return HttpResponseRedirect(post_url)
1195
1196 def response_action(self, request, queryset):
1197 """
1198 Handle an admin action. This is called if a request is POSTed to the
1199 changelist; it returns an HttpResponse if the action was handled, and
1200 None otherwise.
1201 """
1202
1203 # There can be multiple action forms on the page (at the top
1204 # and bottom of the change list, for example). Get the action
1205 # whose button was pushed.
1206 try:
1207 action_index = int(request.POST.get('index', 0))
1208 except ValueError:
1209 action_index = 0
1210
1211 # Construct the action form.
1212 data = request.POST.copy()
1213 data.pop(helpers.ACTION_CHECKBOX_NAME, None)
1214 data.pop("index", None)
1215
1216 # Use the action whose button was pushed
1217 try:
1218 data.update({'action': data.getlist('action')[action_index]})
1219 except IndexError:
1220 # If we didn't get an action from the chosen form that's invalid
1221 # POST data, so by deleting action it'll fail the validation check
1222 # below. So no need to do anything here
1223 pass
1224
1225 action_form = self.action_form(data, auto_id=None)
1226 action_form.fields['action'].choices = self.get_action_choices(request)
1227
1228 # If the form's valid we can handle the action.
1229 if action_form.is_valid():
1230 action = action_form.cleaned_data['action']
1231 select_across = action_form.cleaned_data['select_across']
1232 func = self.get_actions(request)[action][0]
1233
1234 # Get the list of selected PKs. If nothing's selected, we can't
1235 # perform an action on it, so bail. Except we want to perform
1236 # the action explicitly on all objects.
1237 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1238 if not selected and not select_across:
1239 # Reminder that something needs to be selected or nothing will happen
1240 msg = _("Items must be selected in order to perform "
1241 "actions on them. No items have been changed.")
1242 self.message_user(request, msg, messages.WARNING)
1243 return None
1244
1245 if not select_across:
1246 # Perform the action only on the selected objects
1247 queryset = queryset.filter(pk__in=selected)
1248
1249 response = func(self, request, queryset)
1250
1251 # Actions may return an HttpResponse-like object, which will be
1252 # used as the response from the POST. If not, we'll be a good
1253 # little HTTP citizen and redirect back to the changelist page.
1254 if isinstance(response, HttpResponseBase):
1255 return response
1256 else:
1257 return HttpResponseRedirect(request.get_full_path())
1258 else:
1259 msg = _("No action selected.")
1260 self.message_user(request, msg, messages.WARNING)
1261 return None
1262
1263 def response_delete(self, request, obj_display, obj_id):
1264 """
1265 Determines the HttpResponse for the delete_view stage.
1266 """
1267
1268 opts = self.model._meta
1269
1270 if IS_POPUP_VAR in request.POST:
1271 return SimpleTemplateResponse('admin/popup_response.html', {
1272 'action': 'delete',
1273 'value': escape(obj_id),
1274 })
1275
1276 self.message_user(request,
1277 _('The %(name)s "%(obj)s" was deleted successfully.') % {
1278 'name': force_text(opts.verbose_name),
1279 'obj': force_text(obj_display),
1280 }, messages.SUCCESS)
1281
1282 if self.has_change_permission(request, None):
1283 post_url = reverse('admin:%s_%s_changelist' %
1284 (opts.app_label, opts.model_name),
1285 current_app=self.admin_site.name)
1286 preserved_filters = self.get_preserved_filters(request)
1287 post_url = add_preserved_filters(
1288 {'preserved_filters': preserved_filters, 'opts': opts}, post_url
1289 )
1290 else:
1291 post_url = reverse('admin:index',
1292 current_app=self.admin_site.name)
1293 return HttpResponseRedirect(post_url)
1294
1295 def render_delete_form(self, request, context):
1296 opts = self.model._meta
1297 app_label = opts.app_label
1298
1299 request.current_app = self.admin_site.name
1300 context.update(
1301 to_field_var=TO_FIELD_VAR,
1302 is_popup_var=IS_POPUP_VAR,
1303 )
1304
1305 return TemplateResponse(request,
1306 self.delete_confirmation_template or [
1307 "admin/{}/{}/delete_confirmation.html".format(app_label, opts.model_name),
1308 "admin/{}/delete_confirmation.html".format(app_label),
1309 "admin/delete_confirmation.html"
1310 ], context)
1311
1312 def get_inline_formsets(self, request, formsets, inline_instances,
1313 obj=None):
1314 inline_admin_formsets = []
1315 for inline, formset in zip(inline_instances, formsets):
1316 fieldsets = list(inline.get_fieldsets(request, obj))
1317 readonly = list(inline.get_readonly_fields(request, obj))
1318 prepopulated = dict(inline.get_prepopulated_fields(request, obj))
1319 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
1320 fieldsets, prepopulated, readonly, model_admin=self)
1321 inline_admin_formsets.append(inline_admin_formset)
1322 return inline_admin_formsets
1323
1324 def get_changeform_initial_data(self, request):
1325 """
1326 Get the initial form data.
1327 Unless overridden, this populates from the GET params.
1328 """
1329 initial = dict(request.GET.items())
1330 for k in initial:
1331 try:
1332 f = self.model._meta.get_field(k)
1333 except FieldDoesNotExist:
1334 continue
1335 # We have to special-case M2Ms as a list of comma-separated PKs.
1336 if isinstance(f, models.ManyToManyField):
1337 initial[k] = initial[k].split(",")
1338 return initial
1339
1340 @csrf_protect_m
1341 @transaction.atomic
1342 def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
1343
1344 to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
1345 if to_field and not self.to_field_allowed(request, to_field):
1346 raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
1347
1348 model = self.model
1349 opts = model._meta
1350 add = object_id is None
1351
1352 if add:
1353 if not self.has_add_permission(request):
1354 raise PermissionDenied
1355 obj = None
1356
1357 else:
1358 obj = self.get_object(request, unquote(object_id), to_field)
1359
1360 if not self.has_change_permission(request, obj):
1361 raise PermissionDenied
1362
1363 if obj is None:
1364 raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
1365 'name': force_text(opts.verbose_name), 'key': escape(object_id)})
1366
1367 if request.method == 'POST' and "_saveasnew" in request.POST:
1368 object_id = None
1369 obj = None
1370
1371 ModelForm = self.get_form(request, obj)
1372 if request.method == 'POST':
1373 form = ModelForm(request.POST, request.FILES, instance=obj)
1374 if form.is_valid():
1375 form_validated = True
1376 new_object = self.save_form(request, form, change=not add)
1377 else:
1378 form_validated = False
1379 new_object = form.instance
1380 formsets, inline_instances = self._create_formsets(request, new_object, change=not add)
1381 if all_valid(formsets) and form_validated:
1382 self.save_model(request, new_object, form, not add)
1383 self.save_related(request, form, formsets, not add)
1384 change_message = self.construct_change_message(request, form, formsets, add)
1385 if add:
1386 self.log_addition(request, new_object, change_message)
1387 return self.response_add(request, new_object)
1388 else:
1389 self.log_change(request, new_object, change_message)
1390 return self.response_change(request, new_object)
1391 else:
1392 form_validated = False
1393 else:
1394 if add:
1395 initial = self.get_changeform_initial_data(request)
1396 form = ModelForm(initial=initial)
1397 formsets, inline_instances = self._create_formsets(request, form.instance, change=False)
1398 else:
1399 form = ModelForm(instance=obj)
1400 formsets, inline_instances = self._create_formsets(request, obj, change=True)
1401
1402 adminForm = helpers.AdminForm(
1403 form,
1404 list(self.get_fieldsets(request, obj)),
1405 self.get_prepopulated_fields(request, obj),
1406 self.get_readonly_fields(request, obj),
1407 model_admin=self)
1408 media = self.media + adminForm.media
1409
1410 inline_formsets = self.get_inline_formsets(request, formsets, inline_instances, obj)
1411 for inline_formset in inline_formsets:
1412 media = media + inline_formset.media
1413
1414 context = dict(self.admin_site.each_context(request),
1415 title=(_('Add %s') if add else _('Change %s')) % force_text(opts.verbose_name),
1416 adminform=adminForm,
1417 object_id=object_id,
1418 original=obj,
1419 is_popup=(IS_POPUP_VAR in request.POST or
1420 IS_POPUP_VAR in request.GET),
1421 to_field=to_field,
1422 media=media,
1423 inline_admin_formsets=inline_formsets,
1424 errors=helpers.AdminErrorList(form, formsets),
1425 preserved_filters=self.get_preserved_filters(request),
1426 )
1427
1428 # Hide the "Save" and "Save and continue" buttons if "Save as New" was
1429 # previously chosen to prevent the interface from getting confusing.
1430 if request.method == 'POST' and not form_validated and "_saveasnew" in request.POST:
1431 context['show_save'] = False
1432 context['show_save_and_continue'] = False
1433
1434 context.update(extra_context or {})
1435
1436 return self.render_change_form(request, context, add=add, change=not add, obj=obj, form_url=form_url)
1437
1438 def add_view(self, request, form_url='', extra_context=None):
1439 return self.changeform_view(request, None, form_url, extra_context)
1440
1441 def change_view(self, request, object_id, form_url='', extra_context=None):
1442 return self.changeform_view(request, object_id, form_url, extra_context)
1443
1444 @csrf_protect_m
1445 def changelist_view(self, request, extra_context=None):
1446 """
1447 The 'change list' admin view for this model.
1448 """
1449 from django.contrib.admin.views.main import ERROR_FLAG
1450 opts = self.model._meta
1451 app_label = opts.app_label
1452 if not self.has_change_permission(request, None):
1453 raise PermissionDenied
1454
1455 list_display = self.get_list_display(request)
1456 list_display_links = self.get_list_display_links(request, list_display)
1457 list_filter = self.get_list_filter(request)
1458 search_fields = self.get_search_fields(request)
1459 list_select_related = self.get_list_select_related(request)
1460
1461 # Check actions to see if any are available on this changelist
1462 actions = self.get_actions(request)
1463 if actions:
1464 # Add the action checkboxes if there are any actions available.
1465 list_display = ['action_checkbox'] + list(list_display)
1466
1467 ChangeList = self.get_changelist(request)
1468 try:
1469 cl = ChangeList(request, self.model, list_display,
1470 list_display_links, list_filter, self.date_hierarchy,
1471 search_fields, list_select_related, self.list_per_page,
1472 self.list_max_show_all, self.list_editable, self)
1473
1474 except IncorrectLookupParameters:
1475 # Wacky lookup parameters were given, so redirect to the main
1476 # changelist page, without parameters, and pass an 'invalid=1'
1477 # parameter via the query string. If wacky parameters were given
1478 # and the 'invalid=1' parameter was already in the query string,
1479 # something is screwed up with the database, so display an error
1480 # page.
1481 if ERROR_FLAG in request.GET.keys():
1482 return SimpleTemplateResponse('admin/invalid_setup.html', {
1483 'title': _('Database error'),
1484 })
1485 return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
1486
1487 # If the request was POSTed, this might be a bulk action or a bulk
1488 # edit. Try to look up an action or confirmation first, but if this
1489 # isn't an action the POST will fall through to the bulk edit check,
1490 # below.
1491 action_failed = False
1492 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1493
1494 # Actions with no confirmation
1495 if (actions and request.method == 'POST' and
1496 'index' in request.POST and '_save' not in request.POST):
1497 if selected:
1498 response = self.response_action(request, queryset=cl.get_queryset(request))
1499 if response:
1500 return response
1501 else:
1502 action_failed = True
1503 else:
1504 msg = _("Items must be selected in order to perform "
1505 "actions on them. No items have been changed.")
1506 self.message_user(request, msg, messages.WARNING)
1507 action_failed = True
1508
1509 # Actions with confirmation
1510 if (actions and request.method == 'POST' and
1511 helpers.ACTION_CHECKBOX_NAME in request.POST and
1512 'index' not in request.POST and '_save' not in request.POST):
1513 if selected:
1514 response = self.response_action(request, queryset=cl.get_queryset(request))
1515 if response:
1516 return response
1517 else:
1518 action_failed = True
1519
1520 # If we're allowing changelist editing, we need to construct a formset
1521 # for the changelist given all the fields to be edited. Then we'll
1522 # use the formset to validate/process POSTed data.
1523 formset = cl.formset = None
1524
1525 # Handle POSTed bulk-edit data.
1526 if (request.method == "POST" and cl.list_editable and
1527 '_save' in request.POST and not action_failed):
1528 FormSet = self.get_changelist_formset(request)
1529 formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
1530 if formset.is_valid():
1531 changecount = 0
1532 for form in formset.forms:
1533 if form.has_changed():
1534 obj = self.save_form(request, form, change=True)
1535 self.save_model(request, obj, form, change=True)
1536 self.save_related(request, form, formsets=[], change=True)
1537 change_msg = self.construct_change_message(request, form, None)
1538 self.log_change(request, obj, change_msg)
1539 changecount += 1
1540
1541 if changecount:
1542 if changecount == 1:
1543 name = force_text(opts.verbose_name)
1544 else:
1545 name = force_text(opts.verbose_name_plural)
1546 msg = ungettext("%(count)s %(name)s was changed successfully.",
1547 "%(count)s %(name)s were changed successfully.",
1548 changecount) % {'count': changecount,
1549 'name': name,
1550 'obj': force_text(obj)}
1551 self.message_user(request, msg, messages.SUCCESS)
1552
1553 return HttpResponseRedirect(request.get_full_path())
1554
1555 # Handle GET -- construct a formset for display.
1556 elif cl.list_editable:
1557 FormSet = self.get_changelist_formset(request)
1558 formset = cl.formset = FormSet(queryset=cl.result_list)
1559
1560 # Build the list of media to be used by the formset.
1561 if formset:
1562 media = self.media + formset.media
1563 else:
1564 media = self.media
1565
1566 # Build the action form and populate it with available actions.
1567 if actions:
1568 action_form = self.action_form(auto_id=None)
1569 action_form.fields['action'].choices = self.get_action_choices(request)
1570 else:
1571 action_form = None
1572
1573 selection_note_all = ungettext('%(total_count)s selected',
1574 'All %(total_count)s selected', cl.result_count)
1575
1576 context = dict(
1577 self.admin_site.each_context(request),
1578 module_name=force_text(opts.verbose_name_plural),
1579 selection_note=_('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
1580 selection_note_all=selection_note_all % {'total_count': cl.result_count},
1581 title=cl.title,
1582 is_popup=cl.is_popup,
1583 to_field=cl.to_field,
1584 cl=cl,
1585 media=media,
1586 has_add_permission=self.has_add_permission(request),
1587 opts=cl.opts,
1588 action_form=action_form,
1589 actions_on_top=self.actions_on_top,
1590 actions_on_bottom=self.actions_on_bottom,
1591 actions_selection_counter=self.actions_selection_counter,
1592 preserved_filters=self.get_preserved_filters(request),
1593 )
1594 context.update(extra_context or {})
1595
1596 request.current_app = self.admin_site.name
1597
1598 return TemplateResponse(request, self.change_list_template or [
1599 'admin/%s/%s/change_list.html' % (app_label, opts.model_name),
1600 'admin/%s/change_list.html' % app_label,
1601 'admin/change_list.html'
1602 ], context)
1603
1604 @csrf_protect_m
1605 @transaction.atomic
1606 def delete_view(self, request, object_id, extra_context=None):
1607 "The 'delete' admin view for this model."
1608 opts = self.model._meta
1609 app_label = opts.app_label
1610
1611 to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
1612 if to_field and not self.to_field_allowed(request, to_field):
1613 raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
1614
1615 obj = self.get_object(request, unquote(object_id), to_field)
1616
1617 if not self.has_delete_permission(request, obj):
1618 raise PermissionDenied
1619
1620 if obj is None:
1621 raise Http404(
1622 _('%(name)s object with primary key %(key)r does not exist.') %
1623 {'name': force_text(opts.verbose_name), 'key': escape(object_id)}
1624 )
1625
1626 using = router.db_for_write(self.model)
1627
1628 # Populate deleted_objects, a data structure of all related objects that
1629 # will also be deleted.
1630 (deleted_objects, model_count, perms_needed, protected) = get_deleted_objects(
1631 [obj], opts, request.user, self.admin_site, using)
1632
1633 if request.POST: # The user has already confirmed the deletion.
1634 if perms_needed:
1635 raise PermissionDenied
1636 obj_display = force_text(obj)
1637 attr = str(to_field) if to_field else opts.pk.attname
1638 obj_id = obj.serializable_value(attr)
1639 self.log_deletion(request, obj, obj_display)
1640 self.delete_model(request, obj)
1641
1642 return self.response_delete(request, obj_display, obj_id)
1643
1644 object_name = force_text(opts.verbose_name)
1645
1646 if perms_needed or protected:
1647 title = _("Cannot delete %(name)s") % {"name": object_name}
1648 else:
1649 title = _("Are you sure?")
1650
1651 context = dict(
1652 self.admin_site.each_context(request),
1653 title=title,
1654 object_name=object_name,
1655 object=obj,
1656 deleted_objects=deleted_objects,
1657 model_count=dict(model_count).items(),
1658 perms_lacking=perms_needed,
1659 protected=protected,
1660 opts=opts,
1661 app_label=app_label,
1662 preserved_filters=self.get_preserved_filters(request),
1663 is_popup=(IS_POPUP_VAR in request.POST or
1664 IS_POPUP_VAR in request.GET),
1665 to_field=to_field,
1666 )
1667 context.update(extra_context or {})
1668
1669 return self.render_delete_form(request, context)
1670
1671 def history_view(self, request, object_id, extra_context=None):
1672 "The 'history' admin view for this model."
1673 from django.contrib.admin.models import LogEntry
1674 # First check if the user can see this history.
1675 model = self.model
1676 obj = self.get_object(request, unquote(object_id))
1677 if obj is None:
1678 raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
1679 'name': force_text(model._meta.verbose_name),
1680 'key': escape(object_id),
1681 })
1682
1683 if not self.has_change_permission(request, obj):
1684 raise PermissionDenied
1685
1686 # Then get the history for this object.
1687 opts = model._meta
1688 app_label = opts.app_label
1689 action_list = LogEntry.objects.filter(
1690 object_id=unquote(object_id),
1691 content_type=get_content_type_for_model(model)
1692 ).select_related().order_by('action_time')
1693
1694 context = dict(self.admin_site.each_context(request),
1695 title=_('Change history: %s') % force_text(obj),
1696 action_list=action_list,
1697 module_name=capfirst(force_text(opts.verbose_name_plural)),
1698 object=obj,
1699 opts=opts,
1700 preserved_filters=self.get_preserved_filters(request),
1701 )
1702 context.update(extra_context or {})
1703
1704 request.current_app = self.admin_site.name
1705
1706 return TemplateResponse(request, self.object_history_template or [
1707 "admin/%s/%s/object_history.html" % (app_label, opts.model_name),
1708 "admin/%s/object_history.html" % app_label,
1709 "admin/object_history.html"
1710 ], context)
1711
1712 def _create_formsets(self, request, obj, change):
1713 "Helper function to generate formsets for add/change_view."
1714 formsets = []
1715 inline_instances = []
1716 prefixes = {}
1717 get_formsets_args = [request]
1718 if change:
1719 get_formsets_args.append(obj)
1720 for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
1721 prefix = FormSet.get_default_prefix()
1722 prefixes[prefix] = prefixes.get(prefix, 0) + 1
1723 if prefixes[prefix] != 1 or not prefix:
1724 prefix = "%s-%s" % (prefix, prefixes[prefix])
1725 formset_params = {
1726 'instance': obj,
1727 'prefix': prefix,
1728 'queryset': inline.get_queryset(request),
1729 }
1730 if request.method == 'POST':
1731 formset_params.update({
1732 'data': request.POST,
1733 'files': request.FILES,
1734 'save_as_new': '_saveasnew' in request.POST
1735 })
1736 formsets.append(FormSet(**formset_params))
1737 inline_instances.append(inline)
1738 return formsets, inline_instances
1739
1740
1741class InlineModelAdmin(BaseModelAdmin):
1742 """
1743 Options for inline editing of ``model`` instances.
1744
1745 Provide ``fk_name`` to specify the attribute name of the ``ForeignKey``
1746 from ``model`` to its parent. This is required if ``model`` has more than
1747 one ``ForeignKey`` to its parent.
1748 """
1749 model = None
1750 fk_name = None
1751 formset = BaseInlineFormSet
1752 extra = 3
1753 min_num = None
1754 max_num = None
1755 template = None
1756 verbose_name = None
1757 verbose_name_plural = None
1758 can_delete = True
1759 show_change_link = False
1760 checks_class = InlineModelAdminChecks
1761
1762 def __init__(self, parent_model, admin_site):
1763 self.admin_site = admin_site
1764 self.parent_model = parent_model
1765 self.opts = self.model._meta
1766 self.has_registered_model = admin_site.is_registered(self.model)
1767 super(InlineModelAdmin, self).__init__()
1768 if self.verbose_name is None:
1769 self.verbose_name = self.model._meta.verbose_name
1770 if self.verbose_name_plural is None:
1771 self.verbose_name_plural = self.model._meta.verbose_name_plural
1772
1773 @property
1774 def media(self):
1775 extra = '' if settings.DEBUG else '.min'
1776 js = ['vendor/jquery/jquery%s.js' % extra, 'jquery.init.js',
1777 'inlines%s.js' % extra]
1778 if self.filter_vertical or self.filter_horizontal:
1779 js.extend(['SelectBox.js', 'SelectFilter2.js'])
1780 return forms.Media(js=[static('admin/js/%s' % url) for url in js])
1781
1782 def get_extra(self, request, obj=None, **kwargs):
1783 """Hook for customizing the number of extra inline forms."""
1784 return self.extra
1785
1786 def get_min_num(self, request, obj=None, **kwargs):
1787 """Hook for customizing the min number of inline forms."""
1788 return self.min_num
1789
1790 def get_max_num(self, request, obj=None, **kwargs):
1791 """Hook for customizing the max number of extra inline forms."""
1792 return self.max_num
1793
1794 def get_formset(self, request, obj=None, **kwargs):
1795 """Returns a BaseInlineFormSet class for use in admin add/change views."""
1796 if 'fields' in kwargs:
1797 fields = kwargs.pop('fields')
1798 else:
1799 fields = flatten_fieldsets(self.get_fieldsets(request, obj))
1800 if self.exclude is None:
1801 exclude = []
1802 else:
1803 exclude = list(self.exclude)
1804 exclude.extend(self.get_readonly_fields(request, obj))
1805 if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
1806 # Take the custom ModelForm's Meta.exclude into account only if the
1807 # InlineModelAdmin doesn't define its own.
1808 exclude.extend(self.form._meta.exclude)
1809 # If exclude is an empty list we use None, since that's the actual
1810 # default.
1811 exclude = exclude or None
1812 can_delete = self.can_delete and self.has_delete_permission(request, obj)
1813 defaults = {
1814 "form": self.form,
1815 "formset": self.formset,
1816 "fk_name": self.fk_name,
1817 "fields": fields,
1818 "exclude": exclude,
1819 "formfield_callback": partial(self.formfield_for_dbfield, request=request),
1820 "extra": self.get_extra(request, obj, **kwargs),
1821 "min_num": self.get_min_num(request, obj, **kwargs),
1822 "max_num": self.get_max_num(request, obj, **kwargs),
1823 "can_delete": can_delete,
1824 }
1825
1826 defaults.update(kwargs)
1827 base_model_form = defaults['form']
1828
1829 class DeleteProtectedModelForm(base_model_form):
1830 def hand_clean_DELETE(self):
1831 """
1832 We don't validate the 'DELETE' field itself because on
1833 templates it's not rendered using the field information, but
1834 just using a generic "deletion_field" of the InlineModelAdmin.
1835 """
1836 if self.cleaned_data.get(DELETION_FIELD_NAME, False):
1837 using = router.db_for_write(self._meta.model)
1838 collector = NestedObjects(using=using)
1839 if self.instance.pk is None:
1840 return
1841 collector.collect([self.instance])
1842 if collector.protected:
1843 objs = []
1844 for p in collector.protected:
1845 objs.append(
1846 # Translators: Model verbose name and instance representation,
1847 # suitable to be an item in a list.
1848 _('%(class_name)s %(instance)s') % {
1849 'class_name': p._meta.verbose_name,
1850 'instance': p}
1851 )
1852 params = {'class_name': self._meta.model._meta.verbose_name,
1853 'instance': self.instance,
1854 'related_objects': get_text_list(objs, _('and'))}
1855 msg = _("Deleting %(class_name)s %(instance)s would require "
1856 "deleting the following protected related objects: "
1857 "%(related_objects)s")
1858 raise ValidationError(msg, code='deleting_protected', params=params)
1859
1860 def is_valid(self):
1861 result = super(DeleteProtectedModelForm, self).is_valid()
1862 self.hand_clean_DELETE()
1863 return result
1864
1865 defaults['form'] = DeleteProtectedModelForm
1866
1867 if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):
1868 defaults['fields'] = forms.ALL_FIELDS
1869
1870 return inlineformset_factory(self.parent_model, self.model, **defaults)
1871
1872 def get_fields(self, request, obj=None):
1873 if self.fields:
1874 return self.fields
1875 form = self.get_formset(request, obj, fields=None).form
1876 return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
1877
1878 def get_queryset(self, request):
1879 queryset = super(InlineModelAdmin, self).get_queryset(request)
1880 if not self.has_change_permission(request):
1881 queryset = queryset.none()
1882 return queryset
1883
1884 def has_add_permission(self, request):
1885 if self.opts.auto_created:
1886 # We're checking the rights to an auto-created intermediate model,
1887 # which doesn't have its own individual permissions. The user needs
1888 # to have the change permission for the related model in order to
1889 # be able to do anything with the intermediate model.
1890 return self.has_change_permission(request)
1891 return super(InlineModelAdmin, self).has_add_permission(request)
1892
1893 def has_change_permission(self, request, obj=None):
1894 opts = self.opts
1895 if opts.auto_created:
1896 # The model was auto-created as intermediary for a
1897 # ManyToMany-relationship, find the target model
1898 for field in opts.fields:
1899 if field.remote_field and field.remote_field.model != self.parent_model:
1900 opts = field.remote_field.model._meta
1901 break
1902 codename = get_permission_codename('change', opts)
1903 return request.user.has_perm("%s.%s" % (opts.app_label, codename))
1904
1905 def has_delete_permission(self, request, obj=None):
1906 if self.opts.auto_created:
1907 # We're checking the rights to an auto-created intermediate model,
1908 # which doesn't have its own individual permissions. The user needs
1909 # to have the change permission for the related model in order to
1910 # be able to do anything with the intermediate model.
1911 return self.has_change_permission(request, obj)
1912 return super(InlineModelAdmin, self).has_delete_permission(request, obj)
1913
1914
1915class StackedInline(InlineModelAdmin):
1916 template = 'admin/edit_inline/stacked.html'
1917
1918
1919class TabularInline(InlineModelAdmin):
1920 template = 'admin/edit_inline/tabular.html'
Back to Top