Django

Code

root/django/trunk/django/contrib/admin/options.py

Revision 10713, 49.0 kB (checked in by jacob, 2 months ago)

Fixed #10448: correcting errors on "save as new" now correctly create a new object instead of modifying the old one. Thanks, bastih.

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