Statistics
| Branch: | Revision:

root / env / lib / python2.7 / site-packages / django / contrib / admin / options.py @ 1a305335

History | View | Annotate | Download (62 KB)

1
from functools import update_wrapper, partial
2
from django import forms
3
from django.conf import settings
4
from django.forms.formsets import all_valid
5
from django.forms.models import (modelform_factory, modelformset_factory,
6
    inlineformset_factory, BaseInlineFormSet)
7
from django.contrib.contenttypes.models import ContentType
8
from django.contrib.admin import widgets, helpers
9
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
10
from django.contrib.admin.templatetags.admin_static import static
11
from django.contrib import messages
12
from django.views.decorators.csrf import csrf_protect
13
from django.core.exceptions import PermissionDenied, ValidationError
14
from django.core.paginator import Paginator
15
from django.core.urlresolvers import reverse
16
from django.db import models, transaction, router
17
from django.db.models.related import RelatedObject
18
from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
19
from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
20
from django.http import Http404, HttpResponse, HttpResponseRedirect
21
from django.shortcuts import get_object_or_404
22
from django.template.response import SimpleTemplateResponse, TemplateResponse
23
from django.utils.decorators import method_decorator
24
from django.utils.datastructures import SortedDict
25
from django.utils.html import escape, escapejs
26
from django.utils.safestring import mark_safe
27
from django.utils.text import capfirst, get_text_list
28
from django.utils.translation import ugettext as _
29
from django.utils.translation import ungettext
30
from django.utils.encoding import force_unicode
31

    
32
HORIZONTAL, VERTICAL = 1, 2
33
# returns the <ul> class for a given radio_admin field
34
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
35

    
36
class IncorrectLookupParameters(Exception):
37
    pass
38

    
39
# Defaults for formfield_overrides. ModelAdmin subclasses can change this
40
# by adding to ModelAdmin.formfield_overrides.
41

    
42
FORMFIELD_FOR_DBFIELD_DEFAULTS = {
43
    models.DateTimeField: {
44
        'form_class': forms.SplitDateTimeField,
45
        'widget': widgets.AdminSplitDateTime
46
    },
47
    models.DateField:       {'widget': widgets.AdminDateWidget},
48
    models.TimeField:       {'widget': widgets.AdminTimeWidget},
49
    models.TextField:       {'widget': widgets.AdminTextareaWidget},
50
    models.URLField:        {'widget': widgets.AdminURLFieldWidget},
51
    models.IntegerField:    {'widget': widgets.AdminIntegerFieldWidget},
52
    models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
53
    models.CharField:       {'widget': widgets.AdminTextInputWidget},
54
    models.ImageField:      {'widget': widgets.AdminFileWidget},
55
    models.FileField:       {'widget': widgets.AdminFileWidget},
56
}
57

    
58
csrf_protect_m = method_decorator(csrf_protect)
59

    
60
class BaseModelAdmin(object):
61
    """Functionality common to both ModelAdmin and InlineAdmin."""
62
    __metaclass__ = forms.MediaDefiningClass
63

    
64
    raw_id_fields = ()
65
    fields = None
66
    exclude = None
67
    fieldsets = None
68
    form = forms.ModelForm
69
    filter_vertical = ()
70
    filter_horizontal = ()
71
    radio_fields = {}
72
    prepopulated_fields = {}
73
    formfield_overrides = {}
74
    readonly_fields = ()
75
    ordering = None
76

    
77
    def __init__(self):
78
        overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
79
        overrides.update(self.formfield_overrides)
80
        self.formfield_overrides = overrides
81

    
82
    def formfield_for_dbfield(self, db_field, **kwargs):
83
        """
84
        Hook for specifying the form Field instance for a given database Field
85
        instance.
86

87
        If kwargs are given, they're passed to the form Field's constructor.
88
        """
89
        request = kwargs.pop("request", None)
90

    
91
        # If the field specifies choices, we don't need to look for special
92
        # admin widgets - we just need to use a select widget of some kind.
93
        if db_field.choices:
94
            return self.formfield_for_choice_field(db_field, request, **kwargs)
95

    
96
        # ForeignKey or ManyToManyFields
97
        if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
98
            # Combine the field kwargs with any options for formfield_overrides.
99
            # Make sure the passed in **kwargs override anything in
100
            # formfield_overrides because **kwargs is more specific, and should
101
            # always win.
102
            if db_field.__class__ in self.formfield_overrides:
103
                kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
104

    
105
            # Get the correct formfield.
106
            if isinstance(db_field, models.ForeignKey):
107
                formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
108
            elif isinstance(db_field, models.ManyToManyField):
109
                formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
110

    
111
            # For non-raw_id fields, wrap the widget with a wrapper that adds
112
            # extra HTML -- the "add other" interface -- to the end of the
113
            # rendered output. formfield can be None if it came from a
114
            # OneToOneField with parent_link=True or a M2M intermediary.
115
            if formfield and db_field.name not in self.raw_id_fields:
116
                related_modeladmin = self.admin_site._registry.get(
117
                                                            db_field.rel.to)
118
                can_add_related = bool(related_modeladmin and
119
                            related_modeladmin.has_add_permission(request))
120
                formfield.widget = widgets.RelatedFieldWidgetWrapper(
121
                            formfield.widget, db_field.rel, self.admin_site,
122
                            can_add_related=can_add_related)
123

    
124
            return formfield
125

    
126
        # If we've got overrides for the formfield defined, use 'em. **kwargs
127
        # passed to formfield_for_dbfield override the defaults.
128
        for klass in db_field.__class__.mro():
129
            if klass in self.formfield_overrides:
130
                kwargs = dict(self.formfield_overrides[klass], **kwargs)
131
                return db_field.formfield(**kwargs)
132

    
133
        # For any other type of field, just call its formfield() method.
134
        return db_field.formfield(**kwargs)
135

    
136
    def formfield_for_choice_field(self, db_field, request=None, **kwargs):
137
        """
138
        Get a form Field for a database Field that has declared choices.
139
        """
140
        # If the field is named as a radio_field, use a RadioSelect
141
        if db_field.name in self.radio_fields:
142
            # Avoid stomping on custom widget/choices arguments.
143
            if 'widget' not in kwargs:
144
                kwargs['widget'] = widgets.AdminRadioSelect(attrs={
145
                    'class': get_ul_class(self.radio_fields[db_field.name]),
146
                })
147
            if 'choices' not in kwargs:
148
                kwargs['choices'] = db_field.get_choices(
149
                    include_blank = db_field.blank,
150
                    blank_choice=[('', _('None'))]
151
                )
152
        return db_field.formfield(**kwargs)
153

    
154
    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
155
        """
156
        Get a form Field for a ForeignKey.
157
        """
158
        db = kwargs.get('using')
159
        if db_field.name in self.raw_id_fields:
160
            kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel,
161
                                    self.admin_site, using=db)
162
        elif db_field.name in self.radio_fields:
163
            kwargs['widget'] = widgets.AdminRadioSelect(attrs={
164
                'class': get_ul_class(self.radio_fields[db_field.name]),
165
            })
166
            kwargs['empty_label'] = db_field.blank and _('None') or None
167

    
168
        return db_field.formfield(**kwargs)
169

    
170
    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
171
        """
172
        Get a form Field for a ManyToManyField.
173
        """
174
        # If it uses an intermediary model that isn't auto created, don't show
175
        # a field in admin.
176
        if not db_field.rel.through._meta.auto_created:
177
            return None
178
        db = kwargs.get('using')
179

    
180
        if db_field.name in self.raw_id_fields:
181
            kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel,
182
                                    self.admin_site, using=db)
183
            kwargs['help_text'] = ''
184
        elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
185
            kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
186

    
187
        return db_field.formfield(**kwargs)
188

    
189
    def _declared_fieldsets(self):
190
        if self.fieldsets:
191
            return self.fieldsets
192
        elif self.fields:
193
            return [(None, {'fields': self.fields})]
194
        return None
195
    declared_fieldsets = property(_declared_fieldsets)
196

    
197
    def get_ordering(self, request):
198
        """
199
        Hook for specifying field ordering.
200
        """
201
        return self.ordering or ()  # otherwise we might try to *None, which is bad ;)
202

    
203
    def get_readonly_fields(self, request, obj=None):
204
        """
205
        Hook for specifying custom readonly fields.
206
        """
207
        return self.readonly_fields
208

    
209
    def get_prepopulated_fields(self, request, obj=None):
210
        """
211
        Hook for specifying custom prepopulated fields.
212
        """
213
        return self.prepopulated_fields
214

    
215
    def queryset(self, request):
216
        """
217
        Returns a QuerySet of all model instances that can be edited by the
218
        admin site. This is used by changelist_view.
219
        """
220
        qs = self.model._default_manager.get_query_set()
221
        # TODO: this should be handled by some parameter to the ChangeList.
222
        ordering = self.get_ordering(request)
223
        if ordering:
224
            qs = qs.order_by(*ordering)
225
        return qs
226

    
227
    def lookup_allowed(self, lookup, value):
228
        model = self.model
229
        # Check FKey lookups that are allowed, so that popups produced by
230
        # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
231
        # are allowed to work.
232
        for l in model._meta.related_fkey_lookups:
233
            for k, v in widgets.url_params_from_lookup_dict(l).items():
234
                if k == lookup and v == value:
235
                    return True
236

    
237
        parts = lookup.split(LOOKUP_SEP)
238

    
239
        # Last term in lookup is a query term (__exact, __startswith etc)
240
        # This term can be ignored.
241
        if len(parts) > 1 and parts[-1] in QUERY_TERMS:
242
            parts.pop()
243

    
244
        # Special case -- foo__id__exact and foo__id queries are implied
245
        # if foo has been specificially included in the lookup list; so
246
        # drop __id if it is the last part. However, first we need to find
247
        # the pk attribute name.
248
        rel_name = None
249
        for part in parts[:-1]:
250
            try:
251
                field, _, _, _ = model._meta.get_field_by_name(part)
252
            except FieldDoesNotExist:
253
                # Lookups on non-existants fields are ok, since they're ignored
254
                # later.
255
                return True
256
            if hasattr(field, 'rel'):
257
                model = field.rel.to
258
                rel_name = field.rel.get_related_field().name
259
            elif isinstance(field, RelatedObject):
260
                model = field.model
261
                rel_name = model._meta.pk.name
262
            else:
263
                rel_name = None
264
        if rel_name and len(parts) > 1 and parts[-1] == rel_name:
265
            parts.pop()
266

    
267
        if len(parts) == 1:
268
            return True
269
        clean_lookup = LOOKUP_SEP.join(parts)
270
        return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
271

    
272
    def has_add_permission(self, request):
273
        """
274
        Returns True if the given request has permission to add an object.
275
        Can be overriden by the user in subclasses.
276
        """
277
        opts = self.opts
278
        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
279

    
280
    def has_change_permission(self, request, obj=None):
281
        """
282
        Returns True if the given request has permission to change the given
283
        Django model instance, the default implementation doesn't examine the
284
        `obj` parameter.
285

286
        Can be overriden by the user in subclasses. In such case it should
287
        return True if the given request has permission to change the `obj`
288
        model instance. If `obj` is None, this should return True if the given
289
        request has permission to change *any* object of the given type.
290
        """
291
        opts = self.opts
292
        return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
293

    
294
    def has_delete_permission(self, request, obj=None):
295
        """
296
        Returns True if the given request has permission to change the given
297
        Django model instance, the default implementation doesn't examine the
298
        `obj` parameter.
299

300
        Can be overriden by the user in subclasses. In such case it should
301
        return True if the given request has permission to delete the `obj`
302
        model instance. If `obj` is None, this should return True if the given
303
        request has permission to delete *any* object of the given type.
304
        """
305
        opts = self.opts
306
        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
307

    
308
class ModelAdmin(BaseModelAdmin):
309
    "Encapsulates all admin options and functionality for a given model."
310

    
311
    list_display = ('__str__',)
312
    list_display_links = ()
313
    list_filter = ()
314
    list_select_related = False
315
    list_per_page = 100
316
    list_max_show_all = 200
317
    list_editable = ()
318
    search_fields = ()
319
    date_hierarchy = None
320
    save_as = False
321
    save_on_top = False
322
    paginator = Paginator
323
    inlines = []
324

    
325
    # Custom templates (designed to be over-ridden in subclasses)
326
    add_form_template = None
327
    change_form_template = None
328
    change_list_template = None
329
    delete_confirmation_template = None
330
    delete_selected_confirmation_template = None
331
    object_history_template = None
332

    
333
    # Actions
334
    actions = []
335
    action_form = helpers.ActionForm
336
    actions_on_top = True
337
    actions_on_bottom = False
338
    actions_selection_counter = True
339

    
340
    def __init__(self, model, admin_site):
341
        self.model = model
342
        self.opts = model._meta
343
        self.admin_site = admin_site
344
        super(ModelAdmin, self).__init__()
345

    
346
    def get_inline_instances(self, request):
347
        inline_instances = []
348
        for inline_class in self.inlines:
349
            inline = inline_class(self.model, self.admin_site)
350
            if request:
351
                if not (inline.has_add_permission(request) or
352
                        inline.has_change_permission(request) or
353
                        inline.has_delete_permission(request)):
354
                    continue
355
                if not inline.has_add_permission(request):
356
                    inline.max_num = 0
357
            inline_instances.append(inline)
358

    
359
        return inline_instances
360

    
361
    def get_urls(self):
362
        from django.conf.urls import patterns, url
363

    
364
        def wrap(view):
365
            def wrapper(*args, **kwargs):
366
                return self.admin_site.admin_view(view)(*args, **kwargs)
367
            return update_wrapper(wrapper, view)
368

    
369
        info = self.model._meta.app_label, self.model._meta.module_name
370

    
371
        urlpatterns = patterns('',
372
            url(r'^$',
373
                wrap(self.changelist_view),
374
                name='%s_%s_changelist' % info),
375
            url(r'^add/$',
376
                wrap(self.add_view),
377
                name='%s_%s_add' % info),
378
            url(r'^(.+)/history/$',
379
                wrap(self.history_view),
380
                name='%s_%s_history' % info),
381
            url(r'^(.+)/delete/$',
382
                wrap(self.delete_view),
383
                name='%s_%s_delete' % info),
384
            url(r'^(.+)/$',
385
                wrap(self.change_view),
386
                name='%s_%s_change' % info),
387
        )
388
        return urlpatterns
389

    
390
    def urls(self):
391
        return self.get_urls()
392
    urls = property(urls)
393

    
394
    @property
395
    def media(self):
396
        extra = '' if settings.DEBUG else '.min'
397
        js = [
398
            'core.js',
399
            'admin/RelatedObjectLookups.js',
400
            'jquery%s.js' % extra,
401
            'jquery.init.js'
402
        ]
403
        if self.actions is not None:
404
            js.append('actions%s.js' % extra)
405
        if self.prepopulated_fields:
406
            js.extend(['urlify.js', 'prepopulate%s.js' % extra])
407
        if self.opts.get_ordered_objects():
408
            js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
409
        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
410

    
411
    def get_model_perms(self, request):
412
        """
413
        Returns a dict of all perms for this model. This dict has the keys
414
        ``add``, ``change``, and ``delete`` mapping to the True/False for each
415
        of those actions.
416
        """
417
        return {
418
            'add': self.has_add_permission(request),
419
            'change': self.has_change_permission(request),
420
            'delete': self.has_delete_permission(request),
421
        }
422

    
423
    def get_fieldsets(self, request, obj=None):
424
        "Hook for specifying fieldsets for the add form."
425
        if self.declared_fieldsets:
426
            return self.declared_fieldsets
427
        form = self.get_form(request, obj)
428
        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
429
        return [(None, {'fields': fields})]
430

    
431
    def get_form(self, request, obj=None, **kwargs):
432
        """
433
        Returns a Form class for use in the admin add view. This is used by
434
        add_view and change_view.
435
        """
436
        if self.declared_fieldsets:
437
            fields = flatten_fieldsets(self.declared_fieldsets)
438
        else:
439
            fields = None
440
        if self.exclude is None:
441
            exclude = []
442
        else:
443
            exclude = list(self.exclude)
444
        exclude.extend(self.get_readonly_fields(request, obj))
445
        if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
446
            # Take the custom ModelForm's Meta.exclude into account only if the
447
            # ModelAdmin doesn't define its own.
448
            exclude.extend(self.form._meta.exclude)
449
        # if exclude is an empty list we pass None to be consistant with the
450
        # default on modelform_factory
451
        exclude = exclude or None
452
        defaults = {
453
            "form": self.form,
454
            "fields": fields,
455
            "exclude": exclude,
456
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
457
        }
458
        defaults.update(kwargs)
459
        return modelform_factory(self.model, **defaults)
460

    
461
    def get_changelist(self, request, **kwargs):
462
        """
463
        Returns the ChangeList class for use on the changelist page.
464
        """
465
        from django.contrib.admin.views.main import ChangeList
466
        return ChangeList
467

    
468
    def get_object(self, request, object_id):
469
        """
470
        Returns an instance matching the primary key provided. ``None``  is
471
        returned if no match is found (or the object_id failed validation
472
        against the primary key field).
473
        """
474
        queryset = self.queryset(request)
475
        model = queryset.model
476
        try:
477
            object_id = model._meta.pk.to_python(object_id)
478
            return queryset.get(pk=object_id)
479
        except (model.DoesNotExist, ValidationError):
480
            return None
481

    
482
    def get_changelist_form(self, request, **kwargs):
483
        """
484
        Returns a Form class for use in the Formset on the changelist page.
485
        """
486
        defaults = {
487
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
488
        }
489
        defaults.update(kwargs)
490
        return modelform_factory(self.model, **defaults)
491

    
492
    def get_changelist_formset(self, request, **kwargs):
493
        """
494
        Returns a FormSet class for use on the changelist page if list_editable
495
        is used.
496
        """
497
        defaults = {
498
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
499
        }
500
        defaults.update(kwargs)
501
        return modelformset_factory(self.model,
502
            self.get_changelist_form(request), extra=0,
503
            fields=self.list_editable, **defaults)
504

    
505
    def get_formsets(self, request, obj=None):
506
        for inline in self.get_inline_instances(request):
507
            yield inline.get_formset(request, obj)
508

    
509
    def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
510
        return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
511

    
512
    def log_addition(self, request, object):
513
        """
514
        Log that an object has been successfully added.
515

516
        The default implementation creates an admin LogEntry object.
517
        """
518
        from django.contrib.admin.models import LogEntry, ADDITION
519
        LogEntry.objects.log_action(
520
            user_id         = request.user.pk,
521
            content_type_id = ContentType.objects.get_for_model(object).pk,
522
            object_id       = object.pk,
523
            object_repr     = force_unicode(object),
524
            action_flag     = ADDITION
525
        )
526

    
527
    def log_change(self, request, object, message):
528
        """
529
        Log that an object has been successfully changed.
530

531
        The default implementation creates an admin LogEntry object.
532
        """
533
        from django.contrib.admin.models import LogEntry, CHANGE
534
        LogEntry.objects.log_action(
535
            user_id         = request.user.pk,
536
            content_type_id = ContentType.objects.get_for_model(object).pk,
537
            object_id       = object.pk,
538
            object_repr     = force_unicode(object),
539
            action_flag     = CHANGE,
540
            change_message  = message
541
        )
542

    
543
    def log_deletion(self, request, object, object_repr):
544
        """
545
        Log that an object will be deleted. Note that this method is called
546
        before the deletion.
547

548
        The default implementation creates an admin LogEntry object.
549
        """
550
        from django.contrib.admin.models import LogEntry, DELETION
551
        LogEntry.objects.log_action(
552
            user_id         = request.user.id,
553
            content_type_id = ContentType.objects.get_for_model(self.model).pk,
554
            object_id       = object.pk,
555
            object_repr     = object_repr,
556
            action_flag     = DELETION
557
        )
558

    
559
    def action_checkbox(self, obj):
560
        """
561
        A list_display column containing a checkbox widget.
562
        """
563
        return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
564
    action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
565
    action_checkbox.allow_tags = True
566

    
567
    def get_actions(self, request):
568
        """
569
        Return a dictionary mapping the names of all actions for this
570
        ModelAdmin to a tuple of (callable, name, description) for each action.
571
        """
572
        # If self.actions is explicitally set to None that means that we don't
573
        # want *any* actions enabled on this page.
574
        from django.contrib.admin.views.main import IS_POPUP_VAR
575
        if self.actions is None or IS_POPUP_VAR in request.GET:
576
            return SortedDict()
577

    
578
        actions = []
579

    
580
        # Gather actions from the admin site first
581
        for (name, func) in self.admin_site.actions:
582
            description = getattr(func, 'short_description', name.replace('_', ' '))
583
            actions.append((func, name, description))
584

    
585
        # Then gather them from the model admin and all parent classes,
586
        # starting with self and working back up.
587
        for klass in self.__class__.mro()[::-1]:
588
            class_actions = getattr(klass, 'actions', [])
589
            # Avoid trying to iterate over None
590
            if not class_actions:
591
                continue
592
            actions.extend([self.get_action(action) for action in class_actions])
593

    
594
        # get_action might have returned None, so filter any of those out.
595
        actions = filter(None, actions)
596

    
597
        # Convert the actions into a SortedDict keyed by name.
598
        actions = SortedDict([
599
            (name, (func, name, desc))
600
            for func, name, desc in actions
601
        ])
602

    
603
        return actions
604

    
605
    def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
606
        """
607
        Return a list of choices for use in a form object.  Each choice is a
608
        tuple (name, description).
609
        """
610
        choices = [] + default_choices
611
        for func, name, description in self.get_actions(request).itervalues():
612
            choice = (name, description % model_format_dict(self.opts))
613
            choices.append(choice)
614
        return choices
615

    
616
    def get_action(self, action):
617
        """
618
        Return a given action from a parameter, which can either be a callable,
619
        or the name of a method on the ModelAdmin.  Return is a tuple of
620
        (callable, name, description).
621
        """
622
        # If the action is a callable, just use it.
623
        if callable(action):
624
            func = action
625
            action = action.__name__
626

    
627
        # Next, look for a method. Grab it off self.__class__ to get an unbound
628
        # method instead of a bound one; this ensures that the calling
629
        # conventions are the same for functions and methods.
630
        elif hasattr(self.__class__, action):
631
            func = getattr(self.__class__, action)
632

    
633
        # Finally, look for a named method on the admin site
634
        else:
635
            try:
636
                func = self.admin_site.get_action(action)
637
            except KeyError:
638
                return None
639

    
640
        if hasattr(func, 'short_description'):
641
            description = func.short_description
642
        else:
643
            description = capfirst(action.replace('_', ' '))
644
        return func, action, description
645

    
646
    def get_list_display(self, request):
647
        """
648
        Return a sequence containing the fields to be displayed on the
649
        changelist.
650
        """
651
        return self.list_display
652

    
653
    def get_list_display_links(self, request, list_display):
654
        """
655
        Return a sequence containing the fields to be displayed as links
656
        on the changelist. The list_display parameter is the list of fields
657
        returned by get_list_display().
658
        """
659
        if self.list_display_links or not list_display:
660
            return self.list_display_links
661
        else:
662
            # Use only the first item in list_display as link
663
            return list(list_display)[:1]
664

    
665
    def construct_change_message(self, request, form, formsets):
666
        """
667
        Construct a change message from a changed object.
668
        """
669
        change_message = []
670
        if form.changed_data:
671
            change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
672

    
673
        if formsets:
674
            for formset in formsets:
675
                for added_object in formset.new_objects:
676
                    change_message.append(_('Added %(name)s "%(object)s".')
677
                                          % {'name': force_unicode(added_object._meta.verbose_name),
678
                                             'object': force_unicode(added_object)})
679
                for changed_object, changed_fields in formset.changed_objects:
680
                    change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
681
                                          % {'list': get_text_list(changed_fields, _('and')),
682
                                             'name': force_unicode(changed_object._meta.verbose_name),
683
                                             'object': force_unicode(changed_object)})
684
                for deleted_object in formset.deleted_objects:
685
                    change_message.append(_('Deleted %(name)s "%(object)s".')
686
                                          % {'name': force_unicode(deleted_object._meta.verbose_name),
687
                                             'object': force_unicode(deleted_object)})
688
        change_message = ' '.join(change_message)
689
        return change_message or _('No fields changed.')
690

    
691
    def message_user(self, request, message):
692
        """
693
        Send a message to the user. The default implementation
694
        posts a message using the django.contrib.messages backend.
695
        """
696
        messages.info(request, message)
697

    
698
    def save_form(self, request, form, change):
699
        """
700
        Given a ModelForm return an unsaved instance. ``change`` is True if
701
        the object is being changed, and False if it's being added.
702
        """
703
        return form.save(commit=False)
704

    
705
    def save_model(self, request, obj, form, change):
706
        """
707
        Given a model instance save it to the database.
708
        """
709
        obj.save()
710

    
711
    def delete_model(self, request, obj):
712
        """
713
        Given a model instance delete it from the database.
714
        """
715
        obj.delete()
716

    
717
    def save_formset(self, request, form, formset, change):
718
        """
719
        Given an inline formset save it to the database.
720
        """
721
        formset.save()
722

    
723
    def save_related(self, request, form, formsets, change):
724
        """
725
        Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
726
        list of inline formsets and a boolean value based on whether the
727
        parent is being added or changed, save the related objects to the
728
        database. Note that at this point save_form() and save_model() have
729
        already been called.
730
        """
731
        form.save_m2m()
732
        for formset in formsets:
733
            self.save_formset(request, form, formset, change=change)
734

    
735
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
736
        opts = self.model._meta
737
        app_label = opts.app_label
738
        ordered_objects = opts.get_ordered_objects()
739
        context.update({
740
            'add': add,
741
            'change': change,
742
            'has_add_permission': self.has_add_permission(request),
743
            'has_change_permission': self.has_change_permission(request, obj),
744
            'has_delete_permission': self.has_delete_permission(request, obj),
745
            'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
746
            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
747
            'ordered_objects': ordered_objects,
748
            'form_url': mark_safe(form_url),
749
            'opts': opts,
750
            'content_type_id': ContentType.objects.get_for_model(self.model).id,
751
            'save_as': self.save_as,
752
            'save_on_top': self.save_on_top,
753
        })
754
        if add and self.add_form_template is not None:
755
            form_template = self.add_form_template
756
        else:
757
            form_template = self.change_form_template
758

    
759
        return TemplateResponse(request, form_template or [
760
            "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
761
            "admin/%s/change_form.html" % app_label,
762
            "admin/change_form.html"
763
        ], context, current_app=self.admin_site.name)
764

    
765
    def response_add(self, request, obj, post_url_continue='../%s/'):
766
        """
767
        Determines the HttpResponse for the add_view stage.
768
        """
769
        opts = obj._meta
770
        pk_value = obj._get_pk_val()
771

    
772
        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
773
        # Here, we distinguish between different save types by checking for
774
        # the presence of keys in request.POST.
775
        if "_continue" in request.POST:
776
            self.message_user(request, msg + ' ' + _("You may edit it again below."))
777
            if "_popup" in request.POST:
778
                post_url_continue += "?_popup=1"
779
            return HttpResponseRedirect(post_url_continue % pk_value)
780

    
781
        if "_popup" in request.POST:
782
            return HttpResponse(
783
                '<!DOCTYPE html><html><head><title></title></head><body>'
784
                '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
785
                # escape() calls force_unicode.
786
                (escape(pk_value), escapejs(obj)))
787
        elif "_addanother" in request.POST:
788
            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
789
            return HttpResponseRedirect(request.path)
790
        else:
791
            self.message_user(request, msg)
792

    
793
            # Figure out where to redirect. If the user has change permission,
794
            # redirect to the change-list page for this object. Otherwise,
795
            # redirect to the admin index.
796
            if self.has_change_permission(request, None):
797
                post_url = reverse('admin:%s_%s_changelist' %
798
                                   (opts.app_label, opts.module_name),
799
                                   current_app=self.admin_site.name)
800
            else:
801
                post_url = reverse('admin:index',
802
                                   current_app=self.admin_site.name)
803
            return HttpResponseRedirect(post_url)
804

    
805
    def response_change(self, request, obj):
806
        """
807
        Determines the HttpResponse for the change_view stage.
808
        """
809
        opts = obj._meta
810

    
811
        # Handle proxy models automatically created by .only() or .defer().
812
        # Refs #14529
813
        verbose_name = opts.verbose_name
814
        module_name = opts.module_name
815
        if obj._deferred:
816
            opts_ = opts.proxy_for_model._meta
817
            verbose_name = opts_.verbose_name
818
            module_name = opts_.module_name
819

    
820
        pk_value = obj._get_pk_val()
821

    
822
        msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(verbose_name), 'obj': force_unicode(obj)}
823
        if "_continue" in request.POST:
824
            self.message_user(request, msg + ' ' + _("You may edit it again below."))
825
            if "_popup" in request.REQUEST:
826
                return HttpResponseRedirect(request.path + "?_popup=1")
827
            else:
828
                return HttpResponseRedirect(request.path)
829
        elif "_saveasnew" in request.POST:
830
            msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj}
831
            self.message_user(request, msg)
832
            return HttpResponseRedirect(reverse('admin:%s_%s_change' %
833
                                        (opts.app_label, module_name),
834
                                        args=(pk_value,),
835
                                        current_app=self.admin_site.name))
836
        elif "_addanother" in request.POST:
837
            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name)))
838
            return HttpResponseRedirect(reverse('admin:%s_%s_add' %
839
                                        (opts.app_label, module_name),
840
                                        current_app=self.admin_site.name))
841
        else:
842
            self.message_user(request, msg)
843
            # Figure out where to redirect. If the user has change permission,
844
            # redirect to the change-list page for this object. Otherwise,
845
            # redirect to the admin index.
846
            if self.has_change_permission(request, None):
847
                post_url = reverse('admin:%s_%s_changelist' %
848
                                   (opts.app_label, module_name),
849
                                   current_app=self.admin_site.name)
850
            else:
851
                post_url = reverse('admin:index',
852
                                   current_app=self.admin_site.name)
853
            return HttpResponseRedirect(post_url)
854

    
855
    def response_action(self, request, queryset):
856
        """
857
        Handle an admin action. This is called if a request is POSTed to the
858
        changelist; it returns an HttpResponse if the action was handled, and
859
        None otherwise.
860
        """
861

    
862
        # There can be multiple action forms on the page (at the top
863
        # and bottom of the change list, for example). Get the action
864
        # whose button was pushed.
865
        try:
866
            action_index = int(request.POST.get('index', 0))
867
        except ValueError:
868
            action_index = 0
869

    
870
        # Construct the action form.
871
        data = request.POST.copy()
872
        data.pop(helpers.ACTION_CHECKBOX_NAME, None)
873
        data.pop("index", None)
874

    
875
        # Use the action whose button was pushed
876
        try:
877
            data.update({'action': data.getlist('action')[action_index]})
878
        except IndexError:
879
            # If we didn't get an action from the chosen form that's invalid
880
            # POST data, so by deleting action it'll fail the validation check
881
            # below. So no need to do anything here
882
            pass
883

    
884
        action_form = self.action_form(data, auto_id=None)
885
        action_form.fields['action'].choices = self.get_action_choices(request)
886

    
887
        # If the form's valid we can handle the action.
888
        if action_form.is_valid():
889
            action = action_form.cleaned_data['action']
890
            select_across = action_form.cleaned_data['select_across']
891
            func, name, description = self.get_actions(request)[action]
892

    
893
            # Get the list of selected PKs. If nothing's selected, we can't
894
            # perform an action on it, so bail. Except we want to perform
895
            # the action explicitly on all objects.
896
            selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
897
            if not selected and not select_across:
898
                # Reminder that something needs to be selected or nothing will happen
899
                msg = _("Items must be selected in order to perform "
900
                        "actions on them. No items have been changed.")
901
                self.message_user(request, msg)
902
                return None
903

    
904
            if not select_across:
905
                # Perform the action only on the selected objects
906
                queryset = queryset.filter(pk__in=selected)
907

    
908
            response = func(self, request, queryset)
909

    
910
            # Actions may return an HttpResponse, which will be used as the
911
            # response from the POST. If not, we'll be a good little HTTP
912
            # citizen and redirect back to the changelist page.
913
            if isinstance(response, HttpResponse):
914
                return response
915
            else:
916
                return HttpResponseRedirect(request.get_full_path())
917
        else:
918
            msg = _("No action selected.")
919
            self.message_user(request, msg)
920
            return None
921

    
922
    @csrf_protect_m
923
    @transaction.commit_on_success
924
    def add_view(self, request, form_url='', extra_context=None):
925
        "The 'add' admin view for this model."
926
        model = self.model
927
        opts = model._meta
928

    
929
        if not self.has_add_permission(request):
930
            raise PermissionDenied
931

    
932
        ModelForm = self.get_form(request)
933
        formsets = []
934
        inline_instances = self.get_inline_instances(request)
935
        if request.method == 'POST':
936
            form = ModelForm(request.POST, request.FILES)
937
            if form.is_valid():
938
                new_object = self.save_form(request, form, change=False)
939
                form_validated = True
940
            else:
941
                form_validated = False
942
                new_object = self.model()
943
            prefixes = {}
944
            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
945
                prefix = FormSet.get_default_prefix()
946
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
947
                if prefixes[prefix] != 1 or not prefix:
948
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
949
                formset = FormSet(data=request.POST, files=request.FILES,
950
                                  instance=new_object,
951
                                  save_as_new="_saveasnew" in request.POST,
952
                                  prefix=prefix, queryset=inline.queryset(request))
953
                formsets.append(formset)
954
            if all_valid(formsets) and form_validated:
955
                self.save_model(request, new_object, form, False)
956
                self.save_related(request, form, formsets, False)
957
                self.log_addition(request, new_object)
958
                return self.response_add(request, new_object)
959
        else:
960
            # Prepare the dict of initial data from the request.
961
            # We have to special-case M2Ms as a list of comma-separated PKs.
962
            initial = dict(request.GET.items())
963
            for k in initial:
964
                try:
965
                    f = opts.get_field(k)
966
                except models.FieldDoesNotExist:
967
                    continue
968
                if isinstance(f, models.ManyToManyField):
969
                    initial[k] = initial[k].split(",")
970
            form = ModelForm(initial=initial)
971
            prefixes = {}
972
            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
973
                prefix = FormSet.get_default_prefix()
974
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
975
                if prefixes[prefix] != 1 or not prefix:
976
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
977
                formset = FormSet(instance=self.model(), prefix=prefix,
978
                                  queryset=inline.queryset(request))
979
                formsets.append(formset)
980

    
981
        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
982
            self.get_prepopulated_fields(request),
983
            self.get_readonly_fields(request),
984
            model_admin=self)
985
        media = self.media + adminForm.media
986

    
987
        inline_admin_formsets = []
988
        for inline, formset in zip(inline_instances, formsets):
989
            fieldsets = list(inline.get_fieldsets(request))
990
            readonly = list(inline.get_readonly_fields(request))
991
            prepopulated = dict(inline.get_prepopulated_fields(request))
992
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
993
                fieldsets, prepopulated, readonly, model_admin=self)
994
            inline_admin_formsets.append(inline_admin_formset)
995
            media = media + inline_admin_formset.media
996

    
997
        context = {
998
            'title': _('Add %s') % force_unicode(opts.verbose_name),
999
            'adminform': adminForm,
1000
            'is_popup': "_popup" in request.REQUEST,
1001
            'show_delete': False,
1002
            'media': media,
1003
            'inline_admin_formsets': inline_admin_formsets,
1004
            'errors': helpers.AdminErrorList(form, formsets),
1005
            'app_label': opts.app_label,
1006
        }
1007
        context.update(extra_context or {})
1008
        return self.render_change_form(request, context, form_url=form_url, add=True)
1009

    
1010
    @csrf_protect_m
1011
    @transaction.commit_on_success
1012
    def change_view(self, request, object_id, form_url='', extra_context=None):
1013
        "The 'change' admin view for this model."
1014
        model = self.model
1015
        opts = model._meta
1016

    
1017
        obj = self.get_object(request, unquote(object_id))
1018

    
1019
        if not self.has_change_permission(request, obj):
1020
            raise PermissionDenied
1021

    
1022
        if obj is None:
1023
            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
1024

    
1025
        if request.method == 'POST' and "_saveasnew" in request.POST:
1026
            return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
1027
                                    (opts.app_label, opts.module_name),
1028
                                    current_app=self.admin_site.name))
1029

    
1030
        ModelForm = self.get_form(request, obj)
1031
        formsets = []
1032
        inline_instances = self.get_inline_instances(request)
1033
        if request.method == 'POST':
1034
            form = ModelForm(request.POST, request.FILES, instance=obj)
1035
            if form.is_valid():
1036
                form_validated = True
1037
                new_object = self.save_form(request, form, change=True)
1038
            else:
1039
                form_validated = False
1040
                new_object = obj
1041
            prefixes = {}
1042
            for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
1043
                prefix = FormSet.get_default_prefix()
1044
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
1045
                if prefixes[prefix] != 1 or not prefix:
1046
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
1047
                formset = FormSet(request.POST, request.FILES,
1048
                                  instance=new_object, prefix=prefix,
1049
                                  queryset=inline.queryset(request))
1050

    
1051
                formsets.append(formset)
1052

    
1053
            if all_valid(formsets) and form_validated:
1054
                self.save_model(request, new_object, form, True)
1055
                self.save_related(request, form, formsets, True)
1056
                change_message = self.construct_change_message(request, form, formsets)
1057
                self.log_change(request, new_object, change_message)
1058
                return self.response_change(request, new_object)
1059

    
1060
        else:
1061
            form = ModelForm(instance=obj)
1062
            prefixes = {}
1063
            for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
1064
                prefix = FormSet.get_default_prefix()
1065
                prefixes[prefix] = prefixes.get(prefix, 0) + 1
1066
                if prefixes[prefix] != 1 or not prefix:
1067
                    prefix = "%s-%s" % (prefix, prefixes[prefix])
1068
                formset = FormSet(instance=obj, prefix=prefix,
1069
                                  queryset=inline.queryset(request))
1070
                formsets.append(formset)
1071

    
1072
        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
1073
            self.get_prepopulated_fields(request, obj),
1074
            self.get_readonly_fields(request, obj),
1075
            model_admin=self)
1076
        media = self.media + adminForm.media
1077

    
1078
        inline_admin_formsets = []
1079
        for inline, formset in zip(inline_instances, formsets):
1080
            fieldsets = list(inline.get_fieldsets(request, obj))
1081
            readonly = list(inline.get_readonly_fields(request, obj))
1082
            prepopulated = dict(inline.get_prepopulated_fields(request, obj))
1083
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
1084
                fieldsets, prepopulated, readonly, model_admin=self)
1085
            inline_admin_formsets.append(inline_admin_formset)
1086
            media = media + inline_admin_formset.media
1087

    
1088
        context = {
1089
            'title': _('Change %s') % force_unicode(opts.verbose_name),
1090
            'adminform': adminForm,
1091
            'object_id': object_id,
1092
            'original': obj,
1093
            'is_popup': "_popup" in request.REQUEST,
1094
            'media': media,
1095
            'inline_admin_formsets': inline_admin_formsets,
1096
            'errors': helpers.AdminErrorList(form, formsets),
1097
            'app_label': opts.app_label,
1098
        }
1099
        context.update(extra_context or {})
1100
        return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
1101

    
1102
    @csrf_protect_m
1103
    def changelist_view(self, request, extra_context=None):
1104
        """
1105
        The 'change list' admin view for this model.
1106
        """
1107
        from django.contrib.admin.views.main import ERROR_FLAG
1108
        opts = self.model._meta
1109
        app_label = opts.app_label
1110
        if not self.has_change_permission(request, None):
1111
            raise PermissionDenied
1112

    
1113
        list_display = self.get_list_display(request)
1114
        list_display_links = self.get_list_display_links(request, list_display)
1115

    
1116
        # Check actions to see if any are available on this changelist
1117
        actions = self.get_actions(request)
1118
        if actions:
1119
            # Add the action checkboxes if there are any actions available.
1120
            list_display = ['action_checkbox'] +  list(list_display)
1121

    
1122
        ChangeList = self.get_changelist(request)
1123
        try:
1124
            cl = ChangeList(request, self.model, list_display,
1125
                list_display_links, self.list_filter, self.date_hierarchy,
1126
                self.search_fields, self.list_select_related,
1127
                self.list_per_page, self.list_max_show_all, self.list_editable,
1128
                self)
1129
        except IncorrectLookupParameters:
1130
            # Wacky lookup parameters were given, so redirect to the main
1131
            # changelist page, without parameters, and pass an 'invalid=1'
1132
            # parameter via the query string. If wacky parameters were given
1133
            # and the 'invalid=1' parameter was already in the query string,
1134
            # something is screwed up with the database, so display an error
1135
            # page.
1136
            if ERROR_FLAG in request.GET.keys():
1137
                return SimpleTemplateResponse('admin/invalid_setup.html', {
1138
                    'title': _('Database error'),
1139
                })
1140
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
1141

    
1142
        # If the request was POSTed, this might be a bulk action or a bulk
1143
        # edit. Try to look up an action or confirmation first, but if this
1144
        # isn't an action the POST will fall through to the bulk edit check,
1145
        # below.
1146
        action_failed = False
1147
        selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
1148

    
1149
        # Actions with no confirmation
1150
        if (actions and request.method == 'POST' and
1151
                'index' in request.POST and '_save' not in request.POST):
1152
            if selected:
1153
                response = self.response_action(request, queryset=cl.get_query_set(request))
1154
                if response:
1155
                    return response
1156
                else:
1157
                    action_failed = True
1158
            else:
1159
                msg = _("Items must be selected in order to perform "
1160
                        "actions on them. No items have been changed.")
1161
                self.message_user(request, msg)
1162
                action_failed = True
1163

    
1164
        # Actions with confirmation
1165
        if (actions and request.method == 'POST' and
1166
                helpers.ACTION_CHECKBOX_NAME in request.POST and
1167
                'index' not in request.POST and '_save' not in request.POST):
1168
            if selected:
1169
                response = self.response_action(request, queryset=cl.get_query_set(request))
1170
                if response:
1171
                    return response
1172
                else:
1173
                    action_failed = True
1174

    
1175
        # If we're allowing changelist editing, we need to construct a formset
1176
        # for the changelist given all the fields to be edited. Then we'll
1177
        # use the formset to validate/process POSTed data.
1178
        formset = cl.formset = None
1179

    
1180
        # Handle POSTed bulk-edit data.
1181
        if (request.method == "POST" and cl.list_editable and
1182
                '_save' in request.POST and not action_failed):
1183
            FormSet = self.get_changelist_formset(request)
1184
            formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
1185
            if formset.is_valid():
1186
                changecount = 0
1187
                for form in formset.forms:
1188
                    if form.has_changed():
1189
                        obj = self.save_form(request, form, change=True)
1190
                        self.save_model(request, obj, form, change=True)
1191
                        self.save_related(request, form, formsets=[], change=True)
1192
                        change_msg = self.construct_change_message(request, form, None)
1193
                        self.log_change(request, obj, change_msg)
1194
                        changecount += 1
1195

    
1196
                if changecount:
1197
                    if changecount == 1:
1198
                        name = force_unicode(opts.verbose_name)
1199
                    else:
1200
                        name = force_unicode(opts.verbose_name_plural)
1201
                    msg = ungettext("%(count)s %(name)s was changed successfully.",
1202
                                    "%(count)s %(name)s were changed successfully.",
1203
                                    changecount) % {'count': changecount,
1204
                                                    'name': name,
1205
                                                    'obj': force_unicode(obj)}
1206
                    self.message_user(request, msg)
1207

    
1208
                return HttpResponseRedirect(request.get_full_path())
1209

    
1210
        # Handle GET -- construct a formset for display.
1211
        elif cl.list_editable:
1212
            FormSet = self.get_changelist_formset(request)
1213
            formset = cl.formset = FormSet(queryset=cl.result_list)
1214

    
1215
        # Build the list of media to be used by the formset.
1216
        if formset:
1217
            media = self.media + formset.media
1218
        else:
1219
            media = self.media
1220

    
1221
        # Build the action form and populate it with available actions.
1222
        if actions:
1223
            action_form = self.action_form(auto_id=None)
1224
            action_form.fields['action'].choices = self.get_action_choices(request)
1225
        else:
1226
            action_form = None
1227

    
1228
        selection_note_all = ungettext('%(total_count)s selected',
1229
            'All %(total_count)s selected', cl.result_count)
1230

    
1231
        context = {
1232
            'module_name': force_unicode(opts.verbose_name_plural),
1233
            'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
1234
            'selection_note_all': selection_note_all % {'total_count': cl.result_count},
1235
            'title': cl.title,
1236
            'is_popup': cl.is_popup,
1237
            'cl': cl,
1238
            'media': media,
1239
            'has_add_permission': self.has_add_permission(request),
1240
            'app_label': app_label,
1241
            'action_form': action_form,
1242
            'actions_on_top': self.actions_on_top,
1243
            'actions_on_bottom': self.actions_on_bottom,
1244
            'actions_selection_counter': self.actions_selection_counter,
1245
        }
1246
        context.update(extra_context or {})
1247

    
1248
        return TemplateResponse(request, self.change_list_template or [
1249
            'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
1250
            'admin/%s/change_list.html' % app_label,
1251
            'admin/change_list.html'
1252
        ], context, current_app=self.admin_site.name)
1253

    
1254
    @csrf_protect_m
1255
    @transaction.commit_on_success
1256
    def delete_view(self, request, object_id, extra_context=None):
1257
        "The 'delete' admin view for this model."
1258
        opts = self.model._meta
1259
        app_label = opts.app_label
1260

    
1261
        obj = self.get_object(request, unquote(object_id))
1262

    
1263
        if not self.has_delete_permission(request, obj):
1264
            raise PermissionDenied
1265

    
1266
        if obj is None:
1267
            raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
1268

    
1269
        using = router.db_for_write(self.model)
1270

    
1271
        # Populate deleted_objects, a data structure of all related objects that
1272
        # will also be deleted.
1273
        (deleted_objects, perms_needed, protected) = get_deleted_objects(
1274
            [obj], opts, request.user, self.admin_site, using)
1275

    
1276
        if request.POST: # The user has already confirmed the deletion.
1277
            if perms_needed:
1278
                raise PermissionDenied
1279
            obj_display = force_unicode(obj)
1280
            self.log_deletion(request, obj, obj_display)
1281
            self.delete_model(request, obj)
1282

    
1283
            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
1284

    
1285
            if not self.has_change_permission(request, None):
1286
                return HttpResponseRedirect(reverse('admin:index',
1287
                                                    current_app=self.admin_site.name))
1288
            return HttpResponseRedirect(reverse('admin:%s_%s_changelist' %
1289
                                        (opts.app_label, opts.module_name),
1290
                                        current_app=self.admin_site.name))
1291

    
1292
        object_name = force_unicode(opts.verbose_name)
1293

    
1294
        if perms_needed or protected:
1295
            title = _("Cannot delete %(name)s") % {"name": object_name}
1296
        else:
1297
            title = _("Are you sure?")
1298

    
1299
        context = {
1300
            "title": title,
1301
            "object_name": object_name,
1302
            "object": obj,
1303
            "deleted_objects": deleted_objects,
1304
            "perms_lacking": perms_needed,
1305
            "protected": protected,
1306
            "opts": opts,
1307
            "app_label": app_label,
1308
        }
1309
        context.update(extra_context or {})
1310

    
1311
        return TemplateResponse(request, self.delete_confirmation_template or [
1312
            "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
1313
            "admin/%s/delete_confirmation.html" % app_label,
1314
            "admin/delete_confirmation.html"
1315
        ], context, current_app=self.admin_site.name)
1316

    
1317
    def history_view(self, request, object_id, extra_context=None):
1318
        "The 'history' admin view for this model."
1319
        from django.contrib.admin.models import LogEntry
1320
        model = self.model
1321
        opts = model._meta
1322
        app_label = opts.app_label
1323
        action_list = LogEntry.objects.filter(
1324
            object_id = object_id,
1325
            content_type__id__exact = ContentType.objects.get_for_model(model).id
1326
        ).select_related().order_by('action_time')
1327
        # If no history was found, see whether this object even exists.
1328
        obj = get_object_or_404(model, pk=unquote(object_id))
1329
        context = {
1330
            'title': _('Change history: %s') % force_unicode(obj),
1331
            'action_list': action_list,
1332
            'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
1333
            'object': obj,
1334
            'app_label': app_label,
1335
            'opts': opts,
1336
        }
1337
        context.update(extra_context or {})
1338
        return TemplateResponse(request, self.object_history_template or [
1339
            "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
1340
            "admin/%s/object_history.html" % app_label,
1341
            "admin/object_history.html"
1342
        ], context, current_app=self.admin_site.name)
1343

    
1344
class InlineModelAdmin(BaseModelAdmin):
1345
    """
1346
    Options for inline editing of ``model`` instances.
1347

1348
    Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
1349
    ``model`` to its parent. This is required if ``model`` has more than one
1350
    ``ForeignKey`` to its parent.
1351
    """
1352
    model = None
1353
    fk_name = None
1354
    formset = BaseInlineFormSet
1355
    extra = 3
1356
    max_num = None
1357
    template = None
1358
    verbose_name = None
1359
    verbose_name_plural = None
1360
    can_delete = True
1361

    
1362
    def __init__(self, parent_model, admin_site):
1363
        self.admin_site = admin_site
1364
        self.parent_model = parent_model
1365
        self.opts = self.model._meta
1366
        super(InlineModelAdmin, self).__init__()
1367
        if self.verbose_name is None:
1368
            self.verbose_name = self.model._meta.verbose_name
1369
        if self.verbose_name_plural is None:
1370
            self.verbose_name_plural = self.model._meta.verbose_name_plural
1371

    
1372
    @property
1373
    def media(self):
1374
        extra = '' if settings.DEBUG else '.min'
1375
        js = ['jquery%s.js' % extra, 'jquery.init.js', 'inlines%s.js' % extra]
1376
        if self.prepopulated_fields:
1377
            js.extend(['urlify.js', 'prepopulate%s.js' % extra])
1378
        if self.filter_vertical or self.filter_horizontal:
1379
            js.extend(['SelectBox.js', 'SelectFilter2.js'])
1380
        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
1381

    
1382
    def get_formset(self, request, obj=None, **kwargs):
1383
        """Returns a BaseInlineFormSet class for use in admin add/change views."""
1384
        if self.declared_fieldsets:
1385
            fields = flatten_fieldsets(self.declared_fieldsets)
1386
        else:
1387
            fields = None
1388
        if self.exclude is None:
1389
            exclude = []
1390
        else:
1391
            exclude = list(self.exclude)
1392
        exclude.extend(self.get_readonly_fields(request, obj))
1393
        if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
1394
            # Take the custom ModelForm's Meta.exclude into account only if the
1395
            # InlineModelAdmin doesn't define its own.
1396
            exclude.extend(self.form._meta.exclude)
1397
        # if exclude is an empty list we use None, since that's the actual
1398
        # default
1399
        exclude = exclude or None
1400
        can_delete = self.can_delete and self.has_delete_permission(request, obj)
1401
        defaults = {
1402
            "form": self.form,
1403
            "formset": self.formset,
1404
            "fk_name": self.fk_name,
1405
            "fields": fields,
1406
            "exclude": exclude,
1407
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
1408
            "extra": self.extra,
1409
            "max_num": self.max_num,
1410
            "can_delete": can_delete,
1411
        }
1412
        defaults.update(kwargs)
1413
        return inlineformset_factory(self.parent_model, self.model, **defaults)
1414

    
1415
    def get_fieldsets(self, request, obj=None):
1416
        if self.declared_fieldsets:
1417
            return self.declared_fieldsets
1418
        form = self.get_formset(request, obj).form
1419
        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
1420
        return [(None, {'fields': fields})]
1421

    
1422
    def queryset(self, request):
1423
        queryset = super(InlineModelAdmin, self).queryset(request)
1424
        if not self.has_change_permission(request):
1425
            queryset = queryset.none()
1426
        return queryset
1427

    
1428
    def has_add_permission(self, request):
1429
        if self.opts.auto_created:
1430
            # We're checking the rights to an auto-created intermediate model,
1431
            # which doesn't have its own individual permissions. The user needs
1432
            # to have the change permission for the related model in order to
1433
            # be able to do anything with the intermediate model.
1434
            return self.has_change_permission(request)
1435
        return request.user.has_perm(
1436
            self.opts.app_label + '.' + self.opts.get_add_permission())
1437

    
1438
    def has_change_permission(self, request, obj=None):
1439
        opts = self.opts
1440
        if opts.auto_created:
1441
            # The model was auto-created as intermediary for a
1442
            # ManyToMany-relationship, find the target model
1443
            for field in opts.fields:
1444
                if field.rel and field.rel.to != self.parent_model:
1445
                    opts = field.rel.to._meta
1446
                    break
1447
        return request.user.has_perm(
1448
            opts.app_label + '.' + opts.get_change_permission())
1449

    
1450
    def has_delete_permission(self, request, obj=None):
1451
        if self.opts.auto_created:
1452
            # We're checking the rights to an auto-created intermediate model,
1453
            # which doesn't have its own individual permissions. The user needs
1454
            # to have the change permission for the related model in order to
1455
            # be able to do anything with the intermediate model.
1456
            return self.has_change_permission(request, obj)
1457
        return request.user.has_perm(
1458
            self.opts.app_label + '.' + self.opts.get_delete_permission())
1459

    
1460
class StackedInline(InlineModelAdmin):
1461
    template = 'admin/edit_inline/stacked.html'
1462

    
1463
class TabularInline(InlineModelAdmin):
1464
    template = 'admin/edit_inline/tabular.html'