Django

Code

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

Revision 9383, 35.0 kB (checked in by kmtracey, 2 weeks ago)

Fixed #8910 -- Added force_unicode during admin log message creation to avoid triggering a Python 2.3 bug. Thanks for the report joshg and patch nfg.

Line 
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 = list(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': force_unicode(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': force_unicode(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': force_unicode(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
475         if not self.has_add_permission(request):