Django

Code

root/django/branches/newforms-admin/django/contrib/admin/options.py

Revision 7935, 34.9 kB (checked in by brosner, 4 months ago)

newforms-admin: Fixed #5490 -- Properly quote special characters in primary keys in the admin. Added tests to ensure functionality. This also moves quote and unquote to django/contrib/admin/util.py. Thanks jdetaeye and shanx for all your help.

Line 
1 from django import oldforms, template
2 from django import newforms as forms
3 from django.newforms.formsets import all_valid
4 from django.newforms.models import modelform_factory, inlineformset_factory
5 from django.newforms.models import BaseInlineFormset
6 from django.contrib.contenttypes.models import ContentType
7 from django.contrib.admin import widgets
8 from django.contrib.admin.util import quote, unquote, get_deleted_objects
9 from django.core.exceptions import ImproperlyConfigured, 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 import sets
19
20 HORIZONTAL, VERTICAL = 1, 2
21 # returns the <ul> class for a given radio_admin field
22 get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
23
24 class IncorrectLookupParameters(Exception):
25     pass
26
27 def flatten_fieldsets(fieldsets):
28     """Returns a list of field names from an admin fieldsets structure."""
29     field_names = []
30     for name, opts in fieldsets:
31         for field in opts['fields']:
32             # type checking feels dirty, but it seems like the best way here
33             if type(field) == tuple:
34                 field_names.extend(field)
35             else:
36                 field_names.append(field)
37     return field_names
38
39 class AdminForm(object):
40     def __init__(self, form, fieldsets, prepopulated_fields):
41         self.form, self.fieldsets = form, fieldsets
42         self.prepopulated_fields = [{
43             'field': form[field_name],
44             'dependencies': [form[f] for f in dependencies]
45         } for field_name, dependencies in prepopulated_fields.items()]
46
47     def __iter__(self):
48         for name, options in self.fieldsets:
49             yield Fieldset(self.form, name, **options)
50
51     def first_field(self):
52         for bf in self.form:
53             return bf
54
55     def _media(self):
56         media = self.form.media
57         for fs in self:
58             media = media + fs.media
59         return media
60     media = property(_media)
61
62 class Fieldset(object):
63     def __init__(self, form, name=None, fields=(), classes=(), description=None):
64         self.form = form
65         self.name, self.fields = name, fields
66         self.classes = u' '.join(classes)
67         self.description = description
68
69     def _media(self):
70         from django.conf import settings
71         if 'collapse' in self.classes:
72             return forms.Media(js=['%sjs/admin/CollapsedFieldsets.js' % settings.ADMIN_MEDIA_PREFIX])
73         return forms.Media()
74     media = property(_media)
75
76     def __iter__(self):
77         for field in self.fields:
78             yield Fieldline(self.form, field)
79
80 class Fieldline(object):
81     def __init__(self, form, field):
82         self.form = form # A django.forms.Form instance
83         if isinstance(field, basestring):
84             self.fields = [field]
85         else:
86             self.fields = field
87
88     def __iter__(self):
89         for i, field in enumerate(self.fields):
90             yield AdminField(self.form, field, is_first=(i == 0))
91
92     def errors(self):
93         return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]))
94
95 class AdminField(object):
96     def __init__(self, form, field, is_first):
97         self.field = form[field] # A django.forms.BoundField instance
98         self.is_first = is_first # Whether this field is first on the line
99         self.is_checkbox = isinstance(self.field.field.widget, forms.CheckboxInput)
100
101     def label_tag(self):
102         classes = []
103         if self.is_checkbox:
104             classes.append(u'vCheckboxLabel')
105             contents = escape(self.field.label)
106         else:
107             contents = force_unicode(escape(self.field.label)) + u':'
108         if self.field.field.required:
109             classes.append(u'required')
110         if not self.is_first:
111             classes.append(u'inline')
112         attrs = classes and {'class': u' '.join(classes)} or {}
113         return self.field.label_tag(contents=contents, attrs=attrs)
114
115 class BaseModelAdmin(object):
116     """Functionality common to both ModelAdmin and InlineAdmin."""
117     raw_id_fields = ()
118     fields = None
119     fieldsets = None
120     form = forms.ModelForm
121     filter_vertical = ()
122     filter_horizontal = ()
123     radio_fields = {}
124     prepopulated_fields = {}
125
126     def formfield_for_dbfield(self, db_field, **kwargs):
127         """
128         Hook for specifying the form Field instance for a given database Field
129         instance.
130
131         If kwargs are given, they're passed to the form Field's constructor.
132         """
133         # For DateTimeFields, use a special field and widget.
134         if isinstance(db_field, models.DateTimeField):
135             kwargs['form_class'] = forms.SplitDateTimeField
136             kwargs['widget'] = widgets.AdminSplitDateTime()
137             return db_field.formfield(**kwargs)
138
139         # For DateFields, add a custom CSS class.
140         if isinstance(db_field, models.DateField):
141             kwargs['widget'] = widgets.AdminDateWidget
142             return db_field.formfield(**kwargs)
143
144         # For TimeFields, add a custom CSS class.
145         if isinstance(db_field, models.TimeField):
146             kwargs['widget'] = widgets.AdminTimeWidget
147             return db_field.formfield(**kwargs)
148
149         # For FileFields and ImageFields add a link to the current file.
150         if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
151             kwargs['widget'] = widgets.AdminFileWidget
152             return db_field.formfield(**kwargs)
153
154         # For ForeignKey or ManyToManyFields, use a special widget.
155         if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
156             if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
157                 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
158             elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields:
159                 kwargs['widget'] = widgets.AdminRadioSelect(attrs={
160                     'class': get_ul_class(self.radio_fields[db_field.name]),
161                 })
162                 kwargs['empty_label'] = db_field.blank and _('None') or None
163             else:
164                 if isinstance(db_field, models.ManyToManyField):
165                     if db_field.name in self.raw_id_fields:
166                         kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
167                         kwargs['help_text'] = ''
168                     elif db_field.name in (self.filter_vertical + self.filter_horizontal):
169                         kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
170             # Wrap the widget's render() method with a method that adds
171             # extra HTML to the end of the rendered output.
172             formfield = db_field.formfield(**kwargs)
173             # Don't wrap raw_id fields. Their add function is in the popup window.
174             if not db_field.name in self.raw_id_fields:
175                 formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
176             return formfield
177        
178         if db_field.choices and db_field.name in self.radio_fields:
179             kwargs['widget'] = widgets.AdminRadioSelect(
180                 choices=db_field.get_choices(include_blank=db_field.blank,
181                     blank_choice=[('', _('None'))]),
182                 attrs={
183                     'class': get_ul_class(self.radio_fields[db_field.name]),
184                 }
185             )
186
187         # For any other type of field, just call its formfield() method.
188         return db_field.formfield(**kwargs)
189
190     def _declared_fieldsets(self):
191         if self.fieldsets:
192             return self.fieldsets
193         elif self.fields:
194             return [(None, {'fields': self.fields})]
195         return None
196     declared_fieldsets = property(_declared_fieldsets)
197
198 class ModelAdmin(BaseModelAdmin):
199     "Encapsulates all admin options and functionality for a given model."
200     __metaclass__ = forms.MediaDefiningClass
201
202     list_display = ('__str__',)
203     list_display_links = ()
204     list_filter = ()
205     list_select_related = False
206     list_per_page = 100
207     search_fields = ()
208     date_hierarchy = None
209     save_as = False
210     save_on_top = False
211     ordering = None
212     inlines = []
213    
214     # Custom templates (designed to be over-ridden in subclasses)
215     change_form_template = None
216     change_list_template = None
217     delete_confirmation_template = None
218     object_history_template = None
219
220     def __init__(self, model, admin_site):
221         self.model = model
222         self.opts = model._meta
223         self.admin_site = admin_site
224         self.inline_instances = []
225         for inline_class in self.inlines:
226             inline_instance = inline_class(self.model, self.admin_site)
227             self.inline_instances.append(inline_instance)
228         super(ModelAdmin, self).__init__()
229
230     def __call__(self, request, url):
231         # Check that LogEntry, ContentType and the auth context processor are installed.
232         from django.conf import settings
233         if settings.DEBUG:
234             from django.contrib.admin.models import LogEntry
235             if not LogEntry._meta.installed:
236                 raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.")
237             if not ContentType._meta.installed:
238                 raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.")
239             if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
240                 raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
241
242         # Delegate to the appropriate method, based on the URL.
243         if url is None:
244             return self.changelist_view(request)
245         elif url.endswith('add'):
246             return self.add_view(request)
247         elif url.endswith('history'):
248             return self.history_view(request, unquote(url[:-8]))
249         elif url.endswith('delete'):
250             return self.delete_view(request, unquote(url[:-7]))
251         else:
252             return self.change_view(request, unquote(url))
253
254     def _media(self):
255         from django.conf import settings
256
257         js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
258         if self.prepopulated_fields:
259             js.append('js/urlify.js')
260         if self.opts.get_ordered_objects():
261             js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
262         if self.filter_vertical or self.filter_horizontal:
263             js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
264        
265         return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
266     media = property(_media)
267
268     def has_add_permission(self, request):
269         "Returns True if the given request has permission to add an object."
270         opts = self.opts
271         return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
272
273     def has_change_permission(self, request, obj=None):
274         """
275         Returns True if the given request has permission to change the given
276         Django model instance.
277
278         If `obj` is None, this should return True if the given request has
279         permission to change *any* object of the given type.
280         """
281         opts = self.opts
282         return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
283
284     def has_delete_permission(self, request, obj=None):
285         """
286         Returns True if the given request has permission to change the given
287         Django model instance.
288
289         If `obj` is None, this should return True if the given request has
290         permission to delete *any* object of the given type.
291         """
292         opts = self.opts
293         return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
294
295     def queryset(self, request):
296         """
297         Returns a QuerySet of all model instances that can be edited by the
298         admin site. This is used by changelist_view.
299         """
300         qs = self.model._default_manager.get_query_set()
301         # TODO: this should be handled by some parameter to the ChangeList.
302         ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
303         if ordering:
304             qs = qs.order_by(*ordering)
305         return qs
306
307     def get_fieldsets(self, request, obj=None):
308         "Hook for specifying fieldsets for the add form."
309         if self.declared_fieldsets:
310             return self.declared_fieldsets
311         form = self.get_form(request)
312         return [(None, {'fields': form.base_fields.keys()})]
313
314     def get_form(self, request, obj=None):
315         """
316         Returns a Form class for use in the admin add view. This is used by
317         add_view and change_view.
318         """
319         if self.declared_fieldsets:
320             fields = flatten_fieldsets(self.declared_fieldsets)
321         else:
322             fields = None
323         return modelform_factory(self.model, form=self.form, fields=fields, formfield_callback=self.formfield_for_dbfield)
324
325     def get_formsets(self, request, obj=None):
326         for inline in self.inline_instances:
327             yield inline.get_formset(request, obj)
328
329     def save_add(self, request, form, formsets, post_url_continue):
330         """
331         Saves the object in the "add" stage and returns an HttpResponseRedirect.
332
333         `form` is a bound Form instance that's verified to be valid.
334         """
335         from django.contrib.admin.models import LogEntry, ADDITION
336         opts = self.model._meta
337         new_object = form.save(commit=True)
338
339         if formsets:
340             for formset in formsets:
341                 # HACK: it seems like the parent obejct should be passed into
342                 # a method of something, not just set as an attribute
343                 formset.instance = new_object
344                 formset.save()
345
346         pk_value = new_object._get_pk_val()
347         LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), ADDITION)
348         msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': opts.verbose_name, 'obj': new_object}
349         # Here, we distinguish between different save types by checking for
350         # the presence of keys in request.POST.
351         if request.POST.has_key("_continue"):
352             request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
353             if request.POST.has_key("_popup"):
354                 post_url_continue += "?_popup=1"
355             return HttpResponseRedirect(post_url_continue % pk_value)
356
357         if request.POST.has_key("_popup"):
358             return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
359                 # escape() calls force_unicode.
360                 (escape(pk_value), escape(new_object)))
361         elif request.POST.has_key("_addanother"):
362             request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))
363             return HttpResponseRedirect(request.path)
364         else:
365             request.user.message_set.create(message=msg)
366             # Figure out where to redirect. If the user has change permission,
367             # redirect to the change-list page for this object. Otherwise,
368             # redirect to the admin index.
369             if self.has_change_permission(request, None):
370                 post_url = '../'
371             else:
372                 post_url = '../../../'
373             return HttpResponseRedirect(post_url)
374     save_add = transaction.commit_on_success(save_add)
375
376     def save_change(self, request, form, formsets=None):
377         """
378         Saves the object in the "change" stage and returns an HttpResponseRedirect.
379
380         `form` is a bound Form instance that's verified to be valid.
381         
382         `formsets` is a sequence of InlineFormSet instances that are verified to be valid.
383         """
384         from django.contrib.admin.models import LogEntry, CHANGE
385         opts = self.model._meta
386         new_object = form.save(commit=True)
387         pk_value = new_object._get_pk_val()
388
389         if formsets:
390             for formset in formsets:
391                 formset.save()
392
393         # Construct the change message.
394         change_message = []
395         if form.changed_data:
396             change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
397            
398         if formsets:
399             for formset in formsets:
400                 for added_object in formset.new_objects:
401                     change_message.append(_('Added %(name)s "%(object)s".')
402                                           % {'name': added_object._meta.verbose_name,
403                                              'object': added_object})
404                 for changed_object, changed_fields in formset.changed_objects:
405                     change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
406                                           % {'list': get_text_list(changed_fields, _('and')),
407                                              'name': changed_object._meta.verbose_name,
408                                              'object': changed_object})
409                 for deleted_object in formset.deleted_objects:
410                     change_message.append(_('Deleted %(name)s "%(object)s".')
411                                           % {'name': deleted_object._meta.verbose_name,
412                                              'object': deleted_object})
413         change_message = ' '.join(change_message)
414         if not change_message:
415             change_message = _('No fields changed.')
416         LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(self.model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
417
418         msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': opts.verbose_name, 'obj': new_object}
419         if request.POST.has_key("_continue"):
420             request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
421             if request.REQUEST.has_key('_popup'):
422                 return HttpResponseRedirect(request.path + "?_popup=1")
423             else:
424                 return HttpResponseRedirect(request.path)
425         elif request.POST.has_key("_saveasnew"):
426             request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': opts.verbose_name, 'obj': new_object})
427             return HttpResponseRedirect("../%s/" % pk_value)
428         elif request.POST.has_key("_addanother"):
429             request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % opts.verbose_name))
430             return HttpResponseRedirect("../add/")
431         else:
432             request.user.message_set.create(message=msg)
433             return HttpResponseRedirect("../")
434     save_change = transaction.commit_on_success(save_change)
435
436     def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
437         opts = self.model._meta
438         app_label = opts.app_label
439         ordered_objects = opts.get_ordered_objects()
440         context.update({
441             'add': add,
442             'change': change,
443             'has_add_permission': self.has_add_permission(request),
444             'has_change_permission': self.has_change_permission(request, obj),
445             'has_delete_permission': self.has_delete_permission(request, obj),
446             'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
447             'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
448             'ordered_objects': ordered_objects,
449             'form_url': mark_safe(form_url),
450             'opts': opts,
451             'content_type_id':