~canonical-django/canonical-django/project-template

« back to all changes in this revision

Viewing changes to trunk/python-packages/django/contrib/admin/options.py

  • Committer: Matthew Nuzum
  • Date: 2008-11-13 05:46:03 UTC
  • Revision ID: matthew.nuzum@canonical.com-20081113054603-v0kvr6z6xyexvqt3
adding to version control

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from django import forms, template
 
2
from django.forms.formsets import all_valid
 
3
from django.forms.models import modelform_factory, inlineformset_factory
 
4
from django.forms.models import BaseInlineFormSet
 
5
from django.contrib.contenttypes.models import ContentType
 
6
from django.contrib.admin import widgets
 
7
from django.contrib.admin import helpers
 
8
from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects
 
9
from django.core.exceptions import PermissionDenied
 
10
from django.db import models, transaction
 
11
from django.http import Http404, HttpResponse, HttpResponseRedirect
 
12
from django.shortcuts import get_object_or_404, render_to_response
 
13
from django.utils.html import escape
 
14
from django.utils.safestring import mark_safe
 
15
from django.utils.text import capfirst, get_text_list
 
16
from django.utils.translation import ugettext as _
 
17
from django.utils.encoding import force_unicode
 
18
try:
 
19
    set
 
20
except NameError:
 
21
    from sets import Set as set     # Python 2.3 fallback
 
22
 
 
23
HORIZONTAL, VERTICAL = 1, 2
 
24
# returns the <ul> class for a given radio_admin field
 
25
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
 
26
 
 
27
class IncorrectLookupParameters(Exception):
 
28
    pass
 
29
 
 
30
class BaseModelAdmin(object):
 
31
    """Functionality common to both ModelAdmin and InlineAdmin."""
 
32
    raw_id_fields = ()
 
33
    fields = None
 
34
    exclude = None
 
35
    fieldsets = None
 
36
    form = forms.ModelForm
 
37
    filter_vertical = ()
 
38
    filter_horizontal = ()
 
39
    radio_fields = {}
 
40
    prepopulated_fields = {}
 
41
 
 
42
    def formfield_for_dbfield(self, db_field, **kwargs):
 
43
        """
 
44
        Hook for specifying the form Field instance for a given database Field
 
45
        instance.
 
46
 
 
47
        If kwargs are given, they're passed to the form Field's constructor.
 
48
        """
 
49
        
 
50
        # If the field specifies choices, we don't need to look for special
 
51
        # admin widgets - we just need to use a select widget of some kind.
 
52
        if db_field.choices:
 
53
            if db_field.name in self.radio_fields:
 
54
                # If the field is named as a radio_field, use a RadioSelect
 
55
                kwargs['widget'] = widgets.AdminRadioSelect(attrs={
 
56
                    'class': get_ul_class(self.radio_fields[db_field.name]),
 
57
                })
 
58
                kwargs['choices'] = db_field.get_choices(
 
59
                    include_blank = db_field.blank,
 
60
                    blank_choice=[('', _('None'))]
 
61
                )
 
62
                return db_field.formfield(**kwargs)
 
63
            else:
 
64
                # Otherwise, use the default select widget.
 
65
                return db_field.formfield(**kwargs)
 
66
 
 
67
        # For DateTimeFields, use a special field and widget.
 
68
        if isinstance(db_field, models.DateTimeField):
 
69
            kwargs['form_class'] = forms.SplitDateTimeField
 
70
            kwargs['widget'] = widgets.AdminSplitDateTime()
 
71
            return db_field.formfield(**kwargs)
 
72
 
 
73
        # For DateFields, add a custom CSS class.
 
74
        if isinstance(db_field, models.DateField):
 
75
            kwargs['widget'] = widgets.AdminDateWidget
 
76
            return db_field.formfield(**kwargs)
 
77
 
 
78
        # For TimeFields, add a custom CSS class.
 
79
        if isinstance(db_field, models.TimeField):
 
80
            kwargs['widget'] = widgets.AdminTimeWidget
 
81
            return db_field.formfield(**kwargs)
 
82
        
 
83
        # For TextFields, add a custom CSS class.
 
84
        if isinstance(db_field, models.TextField):
 
85
            kwargs['widget'] = widgets.AdminTextareaWidget
 
86
            return db_field.formfield(**kwargs)
 
87
        
 
88
        # For URLFields, add a custom CSS class.
 
89
        if isinstance(db_field, models.URLField):
 
90
            kwargs['widget'] = widgets.AdminURLFieldWidget
 
91
            return db_field.formfield(**kwargs)
 
92
        
 
93
        # For IntegerFields, add a custom CSS class.
 
94
        if isinstance(db_field, models.IntegerField):
 
95
            kwargs['widget'] = widgets.AdminIntegerFieldWidget
 
96
            return db_field.formfield(**kwargs)
 
97
 
 
98
        # For CommaSeparatedIntegerFields, add a custom CSS class.
 
99
        if isinstance(db_field, models.CommaSeparatedIntegerField):
 
100
            kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget
 
101
            return db_field.formfield(**kwargs)
 
102
 
 
103
        # For TextInputs, add a custom CSS class.
 
104
        if isinstance(db_field, models.CharField):
 
105
            kwargs['widget'] = widgets.AdminTextInputWidget
 
106
            return db_field.formfield(**kwargs)
 
107
    
 
108
        # For FileFields and ImageFields add a link to the current file.
 
109
        if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
 
110
            kwargs['widget'] = widgets.AdminFileWidget
 
111
            return db_field.formfield(**kwargs)
 
112
 
 
113
        # For ForeignKey or ManyToManyFields, use a special widget.
 
114
        if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
 
115
            if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
 
116
                kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
 
117
            elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields:
 
118
                kwargs['widget'] = widgets.AdminRadioSelect(attrs={
 
119
                    'class': get_ul_class(self.radio_fields[db_field.name]),
 
120
                })
 
121
                kwargs['empty_label'] = db_field.blank and _('None') or None
 
122
            else:
 
123
                if isinstance(db_field, models.ManyToManyField):
 
124
                    # If it uses an intermediary model, don't show field in admin.
 
125
                    if db_field.rel.through is not None:
 
126
                        return None
 
127
                    elif db_field.name in self.raw_id_fields:
 
128
                        kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
 
129
                        kwargs['help_text'] = ''
 
130
                    elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
 
131
                        kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
 
132
            # Wrap the widget's render() method with a method that adds
 
133
            # extra HTML to the end of the rendered output.
 
134
            formfield = db_field.formfield(**kwargs)
 
135
            # Don't wrap raw_id fields. Their add function is in the popup window.
 
136
            if not db_field.name in self.raw_id_fields:
 
137
                # formfield can be None if it came from a OneToOneField with
 
138
                # parent_link=True
 
139
                if formfield is not None:
 
140
                    formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
 
141
            return formfield
 
142
 
 
143
        # For any other type of field, just call its formfield() method.
 
144
        return db_field.formfield(**kwargs)
 
145
 
 
146
    def _declared_fieldsets(self):
 
147
        if self.fieldsets:
 
148
            return self.fieldsets
 
149
        elif self.fields:
 
150
            return [(None, {'fields': self.fields})]
 
151
        return None
 
152
    declared_fieldsets = property(_declared_fieldsets)
 
153
 
 
154
class ModelAdmin(BaseModelAdmin):
 
155
    "Encapsulates all admin options and functionality for a given model."
 
156
    __metaclass__ = forms.MediaDefiningClass
 
157
 
 
158
    list_display = ('__str__',)
 
159
    list_display_links = ()
 
160
    list_filter = ()
 
161
    list_select_related = False
 
162
    list_per_page = 100
 
163
    search_fields = ()
 
164
    date_hierarchy = None
 
165
    save_as = False
 
166
    save_on_top = False
 
167
    ordering = None
 
168
    inlines = []
 
169
 
 
170
    # Custom templates (designed to be over-ridden in subclasses)
 
171
    change_form_template = None
 
172
    change_list_template = None
 
173
    delete_confirmation_template = None
 
174
    object_history_template = None
 
175
 
 
176
    def __init__(self, model, admin_site):
 
177
        self.model = model
 
178
        self.opts = model._meta
 
179
        self.admin_site = admin_site
 
180
        self.inline_instances = []
 
181
        for inline_class in self.inlines:
 
182
            inline_instance = inline_class(self.model, self.admin_site)
 
183
            self.inline_instances.append(inline_instance)
 
184
        super(ModelAdmin, self).__init__()
 
185
 
 
186
    def __call__(self, request, url):
 
187
        # Delegate to the appropriate method, based on the URL.
 
188
        if url is None:
 
189
            return self.changelist_view(request)
 
190
        elif url == "add":
 
191
            return self.add_view(request)
 
192
        elif url.endswith('/history'):
 
193
            return self.history_view(request, unquote(url[:-8]))
 
194
        elif url.endswith('/delete'):
 
195
            return self.delete_view(request, unquote(url[:-7]))
 
196
        else:
 
197
            return self.change_view(request, unquote(url))
 
198
 
 
199
    def _media(self):
 
200
        from django.conf import settings
 
201
 
 
202
        js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
 
203
        if self.prepopulated_fields:
 
204
            js.append('js/urlify.js')
 
205
        if self.opts.get_ordered_objects():
 
206
            js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
 
207
 
 
208
        return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
 
209
    media = property(_media)
 
210
 
 
211
    def has_add_permission(self, request):
 
212
        "Returns True if the given request has permission to add an object."
 
213
        opts = self.opts
 
214
        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
 
215
 
 
216
    def has_change_permission(self, request, obj=None):
 
217
        """
 
218
        Returns True if the given request has permission to change the given
 
219
        Django model instance.
 
220
 
 
221
        If `obj` is None, this should return True if the given request has
 
222
        permission to change *any* object of the given type.
 
223
        """
 
224
        opts = self.opts
 
225
        return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
 
226
 
 
227
    def has_delete_permission(self, request, obj=None):
 
228
        """
 
229
        Returns True if the given request has permission to change the given
 
230
        Django model instance.
 
231
 
 
232
        If `obj` is None, this should return True if the given request has
 
233
        permission to delete *any* object of the given type.
 
234
        """
 
235
        opts = self.opts
 
236
        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
 
237
 
 
238
    def queryset(self, request):
 
239
        """
 
240
        Returns a QuerySet of all model instances that can be edited by the
 
241
        admin site. This is used by changelist_view.
 
242
        """
 
243
        qs = self.model._default_manager.get_query_set()
 
244
        # TODO: this should be handled by some parameter to the ChangeList.
 
245
        ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
 
246
        if ordering:
 
247
            qs = qs.order_by(*ordering)
 
248
        return qs
 
249
 
 
250
    def get_fieldsets(self, request, obj=None):
 
251
        "Hook for specifying fieldsets for the add form."
 
252
        if self.declared_fieldsets:
 
253
            return self.declared_fieldsets
 
254
        form = self.get_form(request, obj)
 
255
        return [(None, {'fields': form.base_fields.keys()})]
 
256
 
 
257
    def get_form(self, request, obj=None, **kwargs):
 
258
        """
 
259
        Returns a Form class for use in the admin add view. This is used by
 
260
        add_view and change_view.
 
261
        """
 
262
        if self.declared_fieldsets:
 
263
            fields = flatten_fieldsets(self.declared_fieldsets)
 
264
        else:
 
265
            fields = None
 
266
        if self.exclude is None:
 
267
            exclude = []
 
268
        else:
 
269
            exclude = self.exclude
 
270
        defaults = {
 
271
            "form": self.form,
 
272
            "fields": fields,
 
273
            "exclude": exclude + kwargs.get("exclude", []),
 
274
            "formfield_callback": self.formfield_for_dbfield,
 
275
        }
 
276
        defaults.update(kwargs)
 
277
        return modelform_factory(self.model, **defaults)
 
278
 
 
279
    def get_formsets(self, request, obj=None):
 
280
        for inline in self.inline_instances:
 
281
            yield inline.get_formset(request, obj)
 
282
            
 
283
    def log_addition(self, request, object):
 
284
        """
 
285
        Log that an object has been successfully added. 
 
286
        
 
287
        The default implementation creates an admin LogEntry object.
 
288
        """
 
289
        from django.contrib.admin.models import LogEntry, ADDITION
 
290
        LogEntry.objects.log_action(
 
291
            user_id         = request.user.pk, 
 
292
            content_type_id = ContentType.objects.get_for_model(object).pk,
 
293
            object_id       = object.pk,
 
294
            object_repr     = force_unicode(object), 
 
295
            action_flag     = ADDITION
 
296
        )
 
297
        
 
298
    def log_change(self, request, object, message):
 
299
        """
 
300
        Log that an object has been successfully changed. 
 
301
        
 
302
        The default implementation creates an admin LogEntry object.
 
303
        """
 
304
        from django.contrib.admin.models import LogEntry, CHANGE
 
305
        LogEntry.objects.log_action(
 
306
            user_id         = request.user.pk, 
 
307
            content_type_id = ContentType.objects.get_for_model(object).pk, 
 
308
            object_id       = object.pk, 
 
309
            object_repr     = force_unicode(object), 
 
310
            action_flag     = CHANGE, 
 
311
            change_message  = message
 
312
        )
 
313
        
 
314
    def log_deletion(self, request, object, object_repr):
 
315
        """
 
316
        Log that an object has been successfully deleted. Note that since the
 
317
        object is deleted, it might no longer be safe to call *any* methods
 
318
        on the object, hence this method getting object_repr.
 
319
        
 
320
        The default implementation creates an admin LogEntry object.
 
321
        """
 
322
        from django.contrib.admin.models import LogEntry, DELETION
 
323
        LogEntry.objects.log_action(
 
324
            user_id         = request.user.id, 
 
325
            content_type_id = ContentType.objects.get_for_model(self.model).pk, 
 
326
            object_id       = object.pk, 
 
327
            object_repr     = object_repr,
 
328
            action_flag     = DELETION
 
329
        )
 
330
        
 
331
    
 
332
    def construct_change_message(self, request, form, formsets):
 
333
        """
 
334
        Construct a change message from a changed object.
 
335
        """
 
336
        change_message = []
 
337
        if form.changed_data:
 
338
            change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
 
339
 
 
340
        if formsets:
 
341
            for formset in formsets:
 
342
                for added_object in formset.new_objects:
 
343
                    change_message.append(_('Added %(name)s "%(object)s".')
 
344
                                          % {'name': added_object._meta.verbose_name,
 
345
                                             'object': added_object})
 
346
                for changed_object, changed_fields in formset.changed_objects:
 
347
                    change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
 
348
                                          % {'list': get_text_list(changed_fields, _('and')),
 
349
                                             'name': changed_object._meta.verbose_name,
 
350
                                             'object': changed_object})
 
351
                for deleted_object in formset.deleted_objects:
 
352
                    change_message.append(_('Deleted %(name)s "%(object)s".')
 
353
                                          % {'name': deleted_object._meta.verbose_name,
 
354
                                             'object': deleted_object})
 
355
        change_message = ' '.join(change_message)
 
356
        return change_message or _('No fields changed.')
 
357
    
 
358
    def message_user(self, request, message):
 
359
        """
 
360
        Send a message to the user. The default implementation 
 
361
        posts a message using the auth Message object.
 
362
        """
 
363
        request.user.message_set.create(message=message)
 
364
 
 
365
    def save_form(self, request, form, change):
 
366
        """
 
367
        Given a ModelForm return an unsaved instance. ``change`` is True if
 
368
        the object is being changed, and False if it's being added.
 
369
        """
 
370
        return form.save(commit=False)
 
371
    
 
372
    def save_model(self, request, obj, form, change):
 
373
        """
 
374
        Given a model instance save it to the database.
 
375
        """
 
376
        obj.save()
 
377
 
 
378
    def save_formset(self, request, form, formset, change):
 
379
        """
 
380
        Given an inline formset save it to the database.
 
381
        """
 
382
        formset.save()
 
383
 
 
384
    def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
 
385
        opts = self.model._meta
 
386
        app_label = opts.app_label
 
387
        ordered_objects = opts.get_ordered_objects()
 
388
        context.update({
 
389
            'add': add,
 
390
            'change': change,
 
391
            'has_add_permission': self.has_add_permission(request),
 
392
            'has_change_permission': self.has_change_permission(request, obj),
 
393
            'has_delete_permission': self.has_delete_permission(request, obj),
 
394
            'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
 
395
            'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
 
396
            'ordered_objects': ordered_objects,
 
397
            'form_url': mark_safe(form_url),
 
398
            'opts': opts,
 
399
            'content_type_id': ContentType.objects.get_for_model(self.model).id,
 
400
            'save_as': self.save_as,
 
401
            'save_on_top': self.save_on_top,
 
402
            'root_path': self.admin_site.root_path,
 
403
        })
 
404
        return render_to_response(self.change_form_template or [
 
405
            "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
 
406
            "admin/%s/change_form.html" % app_label,
 
407
            "admin/change_form.html"
 
408
        ], context, context_instance=template.RequestContext(request))
 
409
    
 
410
    def response_add(self, request, obj, post_url_continue='../%s/'):
 
411
        """
 
412
        Determines the HttpResponse for the add_view stage.
 
413
        """
 
414
        opts = obj._meta
 
415
        pk_value = obj._get_pk_val()
 
416
        
 
417
        msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
 
418
        # Here, we distinguish between different save types by checking for
 
419
        # the presence of keys in request.POST.
 
420
        if request.POST.has_key("_continue"):
 
421
            self.message_user(request, msg + ' ' + _("You may edit it again below."))
 
422
            if request.POST.has_key("_popup"):
 
423
                post_url_continue += "?_popup=1"
 
424
            return HttpResponseRedirect(post_url_continue % pk_value)
 
425
        
 
426
        if request.POST.has_key("_popup"):
 
427
            return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
 
428
                # escape() calls force_unicode.
 
429
                (escape(pk_value), escape(obj)))
 
430
        elif request.POST.has_key("_addanother"):
 
431
            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
 
432
            return HttpResponseRedirect(request.path)
 
433
        else:
 
434
            self.message_user(request, msg)
 
435
 
 
436
            # Figure out where to redirect. If the user has change permission,
 
437
            # redirect to the change-list page for this object. Otherwise,
 
438
            # redirect to the admin index.
 
439
            if self.has_change_permission(request, None):
 
440
                post_url = '../'
 
441
            else:
 
442
                post_url = '../../../'
 
443
            return HttpResponseRedirect(post_url)
 
444
    
 
445
    def response_change(self, request, obj):
 
446
        """
 
447
        Determines the HttpResponse for the change_view stage.
 
448
        """
 
449
        opts = obj._meta
 
450
        pk_value = obj._get_pk_val()
 
451
        
 
452
        msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
 
453
        if request.POST.has_key("_continue"):
 
454
            self.message_user(request, msg + ' ' + _("You may edit it again below."))
 
455
            if request.REQUEST.has_key('_popup'):
 
456
                return HttpResponseRedirect(request.path + "?_popup=1")
 
457
            else:
 
458
                return HttpResponseRedirect(request.path)
 
459
        elif request.POST.has_key("_saveasnew"):
 
460
            msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj}
 
461
            self.message_user(request, msg)
 
462
            return HttpResponseRedirect("../%s/" % pk_value)
 
463
        elif request.POST.has_key("_addanother"):
 
464
            self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
 
465
            return HttpResponseRedirect("../add/")
 
466
        else:
 
467
            self.message_user(request, msg)
 
468
            return HttpResponseRedirect("../")
 
469
 
 
470
    def add_view(self, request, form_url='', extra_context=None):
 
471
        "The 'add' admin view for this model."
 
472
        model = self.model
 
473
        opts = model._meta
 
474
        app_label = opts.app_label
 
475
 
 
476
        if not self.has_add_permission(request):
 
477
            raise PermissionDenied
 
478
 
 
479
        if self.has_change_permission(request, None):
 
480
            # redirect to list view
 
481
            post_url = '../'
 
482
        else:
 
483
            # Object list will give 'Permission Denied', so go back to admin home
 
484
            post_url = '../../../'
 
485
 
 
486
        ModelForm = self.get_form(request)
 
487
        formsets = []
 
488
        if request.method == 'POST':
 
489
            form = ModelForm(request.POST, request.FILES)
 
490
            if form.is_valid():
 
491
                form_validated = True
 
492
                new_object = self.save_form(request, form, change=False)
 
493
            else:
 
494
                form_validated = False
 
495
                new_object = self.model()
 
496
            for FormSet in self.get_formsets(request):
 
497
                formset = FormSet(data=request.POST, files=request.FILES,
 
498
                                  instance=new_object,
 
499
                                  save_as_new=request.POST.has_key("_saveasnew"))
 
500
                formsets.append(formset)
 
501
            if all_valid(formsets) and form_validated:
 
502
                self.save_model(request, new_object, form, change=False)
 
503
                form.save_m2m()
 
504
                for formset in formsets:
 
505
                    self.save_formset(request, form, formset, change=False)
 
506
                
 
507
                self.log_addition(request, new_object)
 
508
                return self.response_add(request, new_object)
 
509
        else:
 
510
            # Prepare the dict of initial data from the request.
 
511
            # We have to special-case M2Ms as a list of comma-separated PKs.
 
512
            initial = dict(request.GET.items())
 
513
            for k in initial:
 
514
                try:
 
515
                    f = opts.get_field(k)
 
516
                except models.FieldDoesNotExist:
 
517
                    continue
 
518
                if isinstance(f, models.ManyToManyField):
 
519
                    initial[k] = initial[k].split(",")
 
520
            form = ModelForm(initial=initial)
 
521
            for FormSet in self.get_formsets(request):
 
522
                formset = FormSet(instance=self.model())
 
523
                formsets.append(formset)
 
524
 
 
525
        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
 
526
        media = self.media + adminForm.media
 
527
 
 
528
        inline_admin_formsets = []
 
529
        for inline, formset in zip(self.inline_instances, formsets):
 
530
            fieldsets = list(inline.get_fieldsets(request))
 
531
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
 
532
            inline_admin_formsets.append(inline_admin_formset)
 
533
            media = media + inline_admin_formset.media
 
534
 
 
535
        context = {
 
536
            'title': _('Add %s') % force_unicode(opts.verbose_name),
 
537
            'adminform': adminForm,
 
538
            'is_popup': request.REQUEST.has_key('_popup'),
 
539
            'show_delete': False,
 
540
            'media': mark_safe(media),
 
541
            'inline_admin_formsets': inline_admin_formsets,
 
542
            'errors': helpers.AdminErrorList(form, formsets),
 
543
            'root_path': self.admin_site.root_path,
 
544
            'app_label': app_label,
 
545
        }
 
546
        context.update(extra_context or {})
 
547
        return self.render_change_form(request, context, add=True)
 
548
    add_view = transaction.commit_on_success(add_view)
 
549
 
 
550
    def change_view(self, request, object_id, extra_context=None):
 
551
        "The 'change' admin view for this model."
 
552
        model = self.model
 
553
        opts = model._meta
 
554
        app_label = opts.app_label
 
555
 
 
556
        try:
 
557
            obj = model._default_manager.get(pk=object_id)
 
558
        except model.DoesNotExist:
 
559
            # Don't raise Http404 just yet, because we haven't checked
 
560
            # permissions yet. We don't want an unauthenticated user to be able
 
561
            # to determine whether a given object exists.
 
562
            obj = None
 
563
 
 
564
        if not self.has_change_permission(request, obj):
 
565
            raise PermissionDenied
 
566
 
 
567
        if obj is None:
 
568
            raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id)))
 
569
 
 
570
        if request.POST and request.POST.has_key("_saveasnew"):
 
571
            return self.add_view(request, form_url='../../add/')
 
572
 
 
573
        ModelForm = self.get_form(request, obj)
 
574
        formsets = []
 
575
        if request.method == 'POST':
 
576
            form = ModelForm(request.POST, request.FILES, instance=obj)
 
577
            if form.is_valid():
 
578
                form_validated = True
 
579
                new_object = self.save_form(request, form, change=True)
 
580
            else:
 
581
                form_validated = False
 
582
                new_object = obj
 
583
            for FormSet in self.get_formsets(request, new_object):
 
584
                formset = FormSet(request.POST, request.FILES,
 
585
                                  instance=new_object)
 
586
                formsets.append(formset)
 
587
 
 
588
            if all_valid(formsets) and form_validated:
 
589
                self.save_model(request, new_object, form, change=True)
 
590
                form.save_m2m()
 
591
                for formset in formsets:
 
592
                    self.save_formset(request, form, formset, change=True)
 
593
                
 
594
                change_message = self.construct_change_message(request, form, formsets)
 
595
                self.log_change(request, new_object, change_message)
 
596
                return self.response_change(request, new_object)
 
597
        else:
 
598
            form = ModelForm(instance=obj)
 
599
            for FormSet in self.get_formsets(request, obj):
 
600
                formset = FormSet(instance=obj)
 
601
                formsets.append(formset)
 
602
 
 
603
        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
 
604
        media = self.media + adminForm.media
 
605
 
 
606
        inline_admin_formsets = []
 
607
        for inline, formset in zip(self.inline_instances, formsets):
 
608
            fieldsets = list(inline.get_fieldsets(request, obj))
 
609
            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
 
610
            inline_admin_formsets.append(inline_admin_formset)
 
611
            media = media + inline_admin_formset.media
 
612
 
 
613
        context = {
 
614
            'title': _('Change %s') % force_unicode(opts.verbose_name),
 
615
            'adminform': adminForm,
 
616
            'object_id': object_id,
 
617
            'original': obj,
 
618
            'is_popup': request.REQUEST.has_key('_popup'),
 
619
            'media': mark_safe(media),
 
620
            'inline_admin_formsets': inline_admin_formsets,
 
621
            'errors': helpers.AdminErrorList(form, formsets),
 
622
            'root_path': self.admin_site.root_path,
 
623
            'app_label': app_label,
 
624
        }
 
625
        context.update(extra_context or {})
 
626
        return self.render_change_form(request, context, change=True, obj=obj)
 
627
    change_view = transaction.commit_on_success(change_view)
 
628
 
 
629
    def changelist_view(self, request, extra_context=None):
 
630
        "The 'change list' admin view for this model."
 
631
        from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
 
632
        opts = self.model._meta
 
633
        app_label = opts.app_label
 
634
        if not self.has_change_permission(request, None):
 
635
            raise PermissionDenied
 
636
        try:
 
637
            cl = ChangeList(request, self.model, self.list_display, self.list_display_links, self.list_filter,
 
638
                self.date_hierarchy, self.search_fields, self.list_select_related, self.list_per_page, self)
 
639
        except IncorrectLookupParameters:
 
640
            # Wacky lookup parameters were given, so redirect to the main
 
641
            # changelist page, without parameters, and pass an 'invalid=1'
 
642
            # parameter via the query string. If wacky parameters were given and
 
643
            # the 'invalid=1' parameter was already in the query string, something
 
644
            # is screwed up with the database, so display an error page.
 
645
            if ERROR_FLAG in request.GET.keys():
 
646
                return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
 
647
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
 
648
 
 
649
        context = {
 
650
            'title': cl.title,
 
651
            'is_popup': cl.is_popup,
 
652
            'cl': cl,
 
653
            'has_add_permission': self.has_add_permission(request),
 
654
            'root_path': self.admin_site.root_path,
 
655
            'app_label': app_label,
 
656
        }
 
657
        context.update(extra_context or {})
 
658
        return render_to_response(self.change_list_template or [
 
659
            'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
 
660
            'admin/%s/change_list.html' % app_label,
 
661
            'admin/change_list.html'
 
662
        ], context, context_instance=template.RequestContext(request))
 
663
 
 
664
    def delete_view(self, request, object_id, extra_context=None):
 
665
        "The 'delete' admin view for this model."
 
666
        opts = self.model._meta
 
667
        app_label = opts.app_label
 
668
 
 
669
        try:
 
670
            obj = self.model._default_manager.get(pk=object_id)
 
671
        except self.model.DoesNotExist:
 
672
            # Don't raise Http404 just yet, because we haven't checked
 
673
            # permissions yet. We don't want an unauthenticated user to be able
 
674
            # to determine whether a given object exists.
 
675
            obj = None
 
676
 
 
677
        if not self.has_delete_permission(request, obj):
 
678
            raise PermissionDenied
 
679
 
 
680
        if obj is None:
 
681
            raise Http404('%s object with primary key %r does not exist.' % (force_unicode(opts.verbose_name), escape(object_id)))
 
682
 
 
683
        # Populate deleted_objects, a data structure of all related objects that
 
684
        # will also be deleted.
 
685
        deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
 
686
        perms_needed = set()
 
687
        get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
 
688
 
 
689
        if request.POST: # The user has already confirmed the deletion.
 
690
            if perms_needed:
 
691
                raise PermissionDenied
 
692
            obj_display = str(obj)
 
693
            obj.delete()
 
694
            
 
695
            self.log_deletion(request, obj, obj_display)
 
696
            self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
 
697
            
 
698
            if not self.has_change_permission(request, None):
 
699
                return HttpResponseRedirect("../../../../")
 
700
            return HttpResponseRedirect("../../")
 
701
 
 
702
        context = {
 
703
            "title": _("Are you sure?"),
 
704
            "object_name": force_unicode(opts.verbose_name),
 
705
            "object": obj,
 
706
            "deleted_objects": deleted_objects,
 
707
            "perms_lacking": perms_needed,
 
708
            "opts": opts,
 
709
            "root_path": self.admin_site.root_path,
 
710
            "app_label": app_label,
 
711
        }
 
712
        context.update(extra_context or {})
 
713
        return render_to_response(self.delete_confirmation_template or [
 
714
            "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
 
715
            "admin/%s/delete_confirmation.html" % app_label,
 
716
            "admin/delete_confirmation.html"
 
717
        ], context, context_instance=template.RequestContext(request))
 
718
 
 
719
    def history_view(self, request, object_id, extra_context=None):
 
720
        "The 'history' admin view for this model."
 
721
        from django.contrib.admin.models import LogEntry
 
722
        model = self.model
 
723
        opts = model._meta
 
724
        app_label = opts.app_label
 
725
        action_list = LogEntry.objects.filter(
 
726
            object_id = object_id,
 
727
            content_type__id__exact = ContentType.objects.get_for_model(model).id
 
728
        ).select_related().order_by('action_time')
 
729
        # If no history was found, see whether this object even exists.
 
730
        obj = get_object_or_404(model, pk=object_id)
 
731
        context = {
 
732
            'title': _('Change history: %s') % force_unicode(obj),
 
733
            'action_list': action_list,
 
734
            'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
 
735
            'object': obj,
 
736
            'root_path': self.admin_site.root_path,
 
737
            'app_label': app_label,
 
738
        }
 
739
        context.update(extra_context or {})
 
740
        return render_to_response(self.object_history_template or [
 
741
            "admin/%s/%s/object_history.html" % (opts.app_label, opts.object_name.lower()),
 
742
            "admin/%s/object_history.html" % opts.app_label,
 
743
            "admin/object_history.html"
 
744
        ], context, context_instance=template.RequestContext(request))
 
745
 
 
746
class InlineModelAdmin(BaseModelAdmin):
 
747
    """
 
748
    Options for inline editing of ``model`` instances.
 
749
 
 
750
    Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
 
751
    ``model`` to its parent. This is required if ``model`` has more than one
 
752
    ``ForeignKey`` to its parent.
 
753
    """
 
754
    model = None
 
755
    fk_name = None
 
756
    formset = BaseInlineFormSet
 
757
    extra = 3
 
758
    max_num = 0
 
759
    template = None
 
760
    verbose_name = None
 
761
    verbose_name_plural = None
 
762
 
 
763
    def __init__(self, parent_model, admin_site):
 
764
        self.admin_site = admin_site
 
765
        self.parent_model = parent_model
 
766
        self.opts = self.model._meta
 
767
        super(InlineModelAdmin, self).__init__()
 
768
        if self.verbose_name is None:
 
769
            self.verbose_name = self.model._meta.verbose_name
 
770
        if self.verbose_name_plural is None:
 
771
            self.verbose_name_plural = self.model._meta.verbose_name_plural
 
772
    
 
773
    def _media(self):
 
774
        from django.conf import settings
 
775
        js = []
 
776
        if self.prepopulated_fields:
 
777
            js.append('js/urlify.js')
 
778
        if self.filter_vertical or self.filter_horizontal:
 
779
            js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
 
780
        return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
 
781
    media = property(_media)
 
782
 
 
783
    def get_formset(self, request, obj=None, **kwargs):
 
784
        """Returns a BaseInlineFormSet class for use in admin add/change views."""
 
785
        if self.declared_fieldsets:
 
786
            fields = flatten_fieldsets(self.declared_fieldsets)
 
787
        else:
 
788
            fields = None
 
789
        if self.exclude is None:
 
790
            exclude = []
 
791
        else:
 
792
            exclude = self.exclude
 
793
        defaults = {
 
794
            "form": self.form,
 
795
            "formset": self.formset,
 
796
            "fk_name": self.fk_name,
 
797
            "fields": fields,
 
798
            "exclude": exclude + kwargs.get("exclude", []),
 
799
            "formfield_callback": self.formfield_for_dbfield,
 
800
            "extra": self.extra,
 
801
            "max_num": self.max_num,
 
802
        }
 
803
        defaults.update(kwargs)
 
804
        return inlineformset_factory(self.parent_model, self.model, **defaults)
 
805
 
 
806
    def get_fieldsets(self, request, obj=None):
 
807
        if self.declared_fieldsets:
 
808
            return self.declared_fieldsets
 
809
        form = self.get_formset(request).form
 
810
        return [(None, {'fields': form.base_fields.keys()})]
 
811
 
 
812
class StackedInline(InlineModelAdmin):
 
813
    template = 'admin/edit_inline/stacked.html'
 
814
 
 
815
class TabularInline(InlineModelAdmin):
 
816
    template = 'admin/edit_inline/tabular.html'