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