Ticket #8936: options.py

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