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