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