Django

Code

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

Revision 7507, 33.8 kB (checked in by brosner, 2 weeks ago)

newforms-admin: Fixed #6117 -- Implemented change history for the admin. This includes the ability to track changes on a newform. Model formsets now only return the changed/new objects saved. A big thanks to Karen Tracey and Alex Gaynor.

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