1 | import copy
|
---|
2 | import operator
|
---|
3 | from collections import OrderedDict
|
---|
4 | from functools import partial, reduce, update_wrapper
|
---|
5 |
|
---|
6 | from django import forms
|
---|
7 | from django.conf import settings
|
---|
8 | from django.contrib import messages
|
---|
9 | from django.contrib.admin import helpers, widgets
|
---|
10 | from django.contrib.admin.checks import (
|
---|
11 | BaseModelAdminChecks, InlineModelAdminChecks, ModelAdminChecks,
|
---|
12 | )
|
---|
13 | from django.contrib.admin.exceptions import DisallowedModelAdminToField
|
---|
14 | from django.contrib.admin.templatetags.admin_static import static
|
---|
15 | from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
|
---|
16 | from django.contrib.admin.utils import (
|
---|
17 | NestedObjects, flatten_fieldsets, get_deleted_objects,
|
---|
18 | lookup_needs_distinct, model_format_dict, quote, unquote,
|
---|
19 | )
|
---|
20 | from django.contrib.auth import get_permission_codename
|
---|
21 | from django.core.exceptions import (
|
---|
22 | FieldDoesNotExist, FieldError, PermissionDenied, ValidationError,
|
---|
23 | )
|
---|
24 | from django.core.paginator import Paginator
|
---|
25 | from django.core.urlresolvers import reverse
|
---|
26 | from django.db import models, router, transaction
|
---|
27 | from django.db.models.constants import LOOKUP_SEP
|
---|
28 | from django.db.models.fields import BLANK_CHOICE_DASH
|
---|
29 | from django.forms.formsets import DELETION_FIELD_NAME, all_valid
|
---|
30 | from django.forms.models import (
|
---|
31 | BaseInlineFormSet, inlineformset_factory, modelform_defines_fields,
|
---|
32 | modelform_factory, modelformset_factory,
|
---|
33 | )
|
---|
34 | from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
|
---|
35 | from django.http import Http404, HttpResponseRedirect
|
---|
36 | from django.http.response import HttpResponseBase
|
---|
37 | from django.template.response import SimpleTemplateResponse, TemplateResponse
|
---|
38 | from django.utils import six
|
---|
39 | from django.utils.decorators import method_decorator
|
---|
40 | from django.utils.encoding import force_text, python_2_unicode_compatible
|
---|
41 | from django.utils.html import escape, escapejs
|
---|
42 | from django.utils.http import urlencode
|
---|
43 | from django.utils.safestring import mark_safe
|
---|
44 | from django.utils.text import capfirst, get_text_list
|
---|
45 | from django.utils.translation import string_concat, ugettext as _, ungettext
|
---|
46 | from django.views.decorators.csrf import csrf_protect
|
---|
47 | from django.views.generic import RedirectView
|
---|
48 |
|
---|
49 | from django.utils.translation import activate, get_language
|
---|
50 |
|
---|
51 | IS_POPUP_VAR = '_popup'
|
---|
52 | TO_FIELD_VAR = '_to_field'
|
---|
53 |
|
---|
54 |
|
---|
55 | HORIZONTAL, VERTICAL = 1, 2
|
---|
56 |
|
---|
57 |
|
---|
58 | def 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 |
|
---|
65 | def get_ul_class(radio_style):
|
---|
66 | return 'radiolist' if radio_style == VERTICAL else 'radiolist inline'
|
---|
67 |
|
---|
68 |
|
---|
69 | class IncorrectLookupParameters(Exception):
|
---|
70 | pass
|
---|
71 |
|
---|
72 | # Defaults for formfield_overrides. ModelAdmin subclasses can change this
|
---|
73 | # by adding to ModelAdmin.formfield_overrides.
|
---|
74 |
|
---|
75 | FORMFIELD_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 |
|
---|
92 | csrf_protect_m = method_decorator(csrf_protect)
|
---|
93 |
|
---|
94 |
|
---|
95 | class 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
|
---|
481 | class 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 |
|
---|
1741 | class 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 |
|
---|
1915 | class StackedInline(InlineModelAdmin):
|
---|
1916 | template = 'admin/edit_inline/stacked.html'
|
---|
1917 |
|
---|
1918 |
|
---|
1919 | class TabularInline(InlineModelAdmin):
|
---|
1920 | template = 'admin/edit_inline/tabular.html'
|
---|