app/django/contrib/admin/views/main.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/contrib/admin/views/main.py	Fri Jul 18 18:22:23 2008 +0000
@@ -0,0 +1,776 @@
+from django import oldforms, template
+from django.conf import settings
+from django.contrib.admin.filterspecs import FilterSpec
+from django.contrib.admin.views.decorators import staff_member_required
+from django.views.decorators.cache import never_cache
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
+from django.core.paginator import QuerySetPaginator, InvalidPage
+from django.shortcuts import get_object_or_404, render_to_response
+from django.db import models
+from django.db.models.query import QuerySet
+from django.http import Http404, HttpResponse, HttpResponseRedirect
+from django.utils.html import escape
+from django.utils.text import capfirst, get_text_list
+from django.utils.encoding import force_unicode, smart_str
+from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
+import operator
+
+try:
+    set
+except NameError:
+    from sets import Set as set   # Python 2.3 fallback
+
+from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
+if not LogEntry._meta.installed:
+    raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application."
+
+if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
+    raise ImproperlyConfigured, "You'll need to put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting before you can use the admin application."
+
+# The system will display a "Show all" link on the change list only if the
+# total result count is less than or equal to this setting.
+MAX_SHOW_ALL_ALLOWED = 200
+
+# Changelist settings
+ALL_VAR = 'all'
+ORDER_VAR = 'o'
+ORDER_TYPE_VAR = 'ot'
+PAGE_VAR = 'p'
+SEARCH_VAR = 'q'
+IS_POPUP_VAR = 'pop'
+ERROR_FLAG = 'e'
+
+# Text to display within change-list table cells if the value is blank.
+EMPTY_CHANGELIST_VALUE = '(None)'
+
+use_raw_id_admin = lambda field: isinstance(field.rel, (models.ManyToOneRel, models.ManyToManyRel)) and field.rel.raw_id_admin
+
+class IncorrectLookupParameters(Exception):
+    pass
+
+def quote(s):
+    """
+    Ensure that primary key values do not confuse the admin URLs by escaping
+    any '/', '_' and ':' characters. Similar to urllib.quote, except that the
+    quoting is slightly different so that it doesn't get automatically
+    unquoted by the Web browser.
+    """
+    if type(s) != type(''):
+        return s
+    res = list(s)
+    for i in range(len(res)):
+        c = res[i]
+        if c in ':/_':
+            res[i] = '_%02X' % ord(c)
+    return ''.join(res)
+
+def unquote(s):
+    """
+    Undo the effects of quote(). Based heavily on urllib.unquote().
+    """
+    mychr = chr
+    myatoi = int
+    list = s.split('_')
+    res = [list[0]]
+    myappend = res.append
+    del list[0]
+    for item in list:
+        if item[1:2]:
+            try:
+                myappend(mychr(myatoi(item[:2], 16)) + item[2:])
+            except ValueError:
+                myappend('_' + item)
+        else:
+            myappend('_' + item)
+    return "".join(res)
+
+def get_javascript_imports(opts, auto_populated_fields, field_sets):
+# Put in any necessary JavaScript imports.
+    js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
+    if auto_populated_fields:
+        js.append('js/urlify.js')
+    if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField):
+        js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js'])
+    if opts.get_ordered_objects():
+        js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
+    if opts.admin.js:
+        js.extend(opts.admin.js)
+    seen_collapse = False
+    for field_set in field_sets:
+        if not seen_collapse and 'collapse' in field_set.classes:
+            seen_collapse = True
+            js.append('js/admin/CollapsedFieldsets.js')
+
+        for field_line in field_set:
+            try:
+                for f in field_line:
+                    if f.rel and isinstance(f, models.ManyToManyField) and f.rel.filter_interface:
+                        js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
+                        raise StopIteration
+            except StopIteration:
+                break
+    return js
+
+class AdminBoundField(object):
+    def __init__(self, field, field_mapping, original):
+        self.field = field
+        self.original = original
+        self.form_fields = [field_mapping[name] for name in self.field.get_manipulator_field_names('')]
+        self.element_id = self.form_fields[0].get_id()
+        self.has_label_first = not isinstance(self.field, models.BooleanField)
+        self.raw_id_admin = use_raw_id_admin(field)
+        self.is_date_time = isinstance(field, models.DateTimeField)
+        self.is_file_field = isinstance(field, models.FileField)
+        self.needs_add_label = field.rel and (isinstance(field.rel, models.ManyToOneRel) or isinstance(field.rel, models.ManyToManyRel)) and field.rel.to._meta.admin
+        self.hidden = isinstance(self.field, models.AutoField)
+        self.first = False
+
+        classes = []
+        if self.raw_id_admin:
+            classes.append('nowrap')
+        if max([bool(f.errors()) for f in self.form_fields]):
+            classes.append('error')
+        if classes:
+            self.cell_class_attribute = u' class="%s" ' % ' '.join(classes)
+        self._repr_filled = False
+
+        if field.rel:
+            self.related_url = mark_safe(u'../../../%s/%s/'
+                    % (field.rel.to._meta.app_label,
+                        field.rel.to._meta.object_name.lower()))
+
+    def original_value(self):
+        if self.original:
+            return self.original.__dict__[self.field.attname]
+
+    def existing_display(self):
+        try:
+            return self._display
+        except AttributeError:
+            if isinstance(self.field.rel, models.ManyToOneRel):
+                self._display = force_unicode(getattr(self.original, self.field.name), strings_only=True)
+            elif isinstance(self.field.rel, models.ManyToManyRel):
+                self._display = u", ".join([force_unicode(obj) for obj in getattr(self.original, self.field.name).all()])
+            return self._display
+
+    def __repr__(self):
+        return repr(self.__dict__)
+
+    def html_error_list(self):
+        return mark_safe(" ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]))
+
+    def original_url(self):
+        if self.is_file_field and self.original and self.field.attname:
+            url_method = getattr(self.original, 'get_%s_url' % self.field.attname)
+            if callable(url_method):
+                return url_method()
+        return ''
+
+class AdminBoundFieldLine(object):
+    def __init__(self, field_line, field_mapping, original):
+        self.bound_fields = [field.bind(field_mapping, original, AdminBoundField) for field in field_line]
+        for bound_field in self:
+            bound_field.first = True
+            break
+
+    def __iter__(self):
+        for bound_field in self.bound_fields:
+            yield bound_field
+
+    def __len__(self):
+        return len(self.bound_fields)
+
+class AdminBoundFieldSet(object):
+    def __init__(self, field_set, field_mapping, original):
+        self.name = field_set.name
+        self.classes = field_set.classes
+        self.description = field_set.description
+        self.bound_field_lines = [field_line.bind(field_mapping, original, AdminBoundFieldLine) for field_line in field_set]
+
+    def __iter__(self):
+        for bound_field_line in self.bound_field_lines:
+            yield bound_field_line
+
+    def __len__(self):
+        return len(self.bound_field_lines)
+
+def render_change_form(model, manipulator, context, add=False, change=False, form_url=''):
+    opts = model._meta
+    app_label = opts.app_label
+    auto_populated_fields = [f for f in opts.fields if f.prepopulate_from]
+    field_sets = opts.admin.get_field_sets(opts)
+    original = getattr(manipulator, 'original_object', None)
+    bound_field_sets = [field_set.bind(context['form'], original, AdminBoundFieldSet) for field_set in field_sets]
+    first_form_field_id = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id();
+    ordered_objects = opts.get_ordered_objects()
+    inline_related_objects = opts.get_followed_related_objects(manipulator.follow)
+    extra_context = {
+        'add': add,
+        'change': change,
+        'has_delete_permission': context['perms'][app_label][opts.get_delete_permission()],
+        'has_change_permission': context['perms'][app_label][opts.get_change_permission()],
+        'has_file_field': opts.has_field_type(models.FileField),
+        'has_absolute_url': hasattr(model, 'get_absolute_url'),
+        'auto_populated_fields': auto_populated_fields,
+        'bound_field_sets': bound_field_sets,
+        'first_form_field_id': first_form_field_id,
+        'javascript_imports': get_javascript_imports(opts, auto_populated_fields, field_sets),
+        'ordered_objects': ordered_objects,
+        'inline_related_objects': inline_related_objects,
+        'form_url': mark_safe(form_url),
+        'opts': opts,
+        'content_type_id': ContentType.objects.get_for_model(model).id,
+    }
+    context.update(extra_context)
+    return render_to_response([
+        "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
+        "admin/%s/change_form.html" % app_label,
+        "admin/change_form.html"], context_instance=context)
+
+def index(request):
+    return render_to_response('admin/index.html', {'title': _('Site administration')}, context_instance=template.RequestContext(request))
+index = staff_member_required(never_cache(index))
+
+def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None):
+    model = models.get_model(app_label, model_name)
+    if model is None:
+        raise Http404("App %r, model %r, not found" % (app_label, model_name))
+    opts = model._meta
+
+    if not request.user.has_perm(app_label + '.' + opts.get_add_permission()):
+        raise PermissionDenied
+
+    if post_url is None:
+        if request.user.has_perm(app_label + '.' + opts.get_change_permission()):
+            # redirect to list view
+            post_url = '../'
+        else:
+            # Object list will give 'Permission Denied', so go back to admin home
+            post_url = '../../../'
+
+    manipulator = model.AddManipulator()
+    if request.POST:
+        new_data = request.POST.copy()
+
+        if opts.has_field_type(models.FileField):
+            new_data.update(request.FILES)
+
+        errors = manipulator.get_validation_errors(new_data)
+        manipulator.do_html2python(new_data)
+
+        if not errors:
+            new_object = manipulator.save(new_data)
+            pk_value = new_object._get_pk_val()
+            LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), ADDITION)
+            msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
+            # Here, we distinguish between different save types by checking for
+            # the presence of keys in request.POST.
+            if "_continue" in request.POST:
+                request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
+                if "_popup" in request.POST:
+                    post_url_continue += "?_popup=1"
+                return HttpResponseRedirect(post_url_continue % pk_value)
+            if "_popup" in request.POST:
+                return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
+                    # escape() calls force_unicode.
+                    (escape(pk_value), escape(new_object)))
+            elif "_addanother" in request.POST:
+                request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
+                return HttpResponseRedirect(request.path)
+            else:
+                request.user.message_set.create(message=msg)
+                return HttpResponseRedirect(post_url)
+    else:
+        # Add default data.
+        new_data = manipulator.flatten_data()
+
+        # Override the defaults with GET params, if they exist.
+        new_data.update(dict(request.GET.items()))
+
+        errors = {}
+
+    # Populate the FormWrapper.
+    form = oldforms.FormWrapper(manipulator, new_data, errors)
+
+    c = template.RequestContext(request, {
+        'title': _('Add %s') % force_unicode(opts.verbose_name),
+        'form': form,
+        'is_popup': '_popup' in request.REQUEST,
+        'show_delete': show_delete,
+    })
+
+    if object_id_override is not None:
+        c['object_id'] = object_id_override
+
+    return render_change_form(model, manipulator, c, add=True)
+add_stage = staff_member_required(never_cache(add_stage))
+
+def change_stage(request, app_label, model_name, object_id):
+    model = models.get_model(app_label, model_name)
+    object_id = unquote(object_id)
+    if model is None:
+        raise Http404("App %r, model %r, not found" % (app_label, model_name))
+    opts = model._meta
+
+    if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
+        raise PermissionDenied
+
+    if request.POST and "_saveasnew" in request.POST:
+        return add_stage(request, app_label, model_name, form_url='../../add/')
+
+    try:
+        manipulator = model.ChangeManipulator(object_id)
+    except model.DoesNotExist:
+        raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id)))
+
+    if request.POST:
+        new_data = request.POST.copy()
+
+        if opts.has_field_type(models.FileField):
+            new_data.update(request.FILES)
+
+        errors = manipulator.get_validation_errors(new_data)
+        manipulator.do_html2python(new_data)
+
+        if not errors:
+            new_object = manipulator.save(new_data)
+            pk_value = new_object._get_pk_val()
+
+            # Construct the change message.
+            change_message = []
+            if manipulator.fields_added:
+                change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and')))
+            if manipulator.fields_changed:
+                change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and')))
+            if manipulator.fields_deleted:
+                change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and')))
+            change_message = ' '.join(change_message)
+            if not change_message:
+                change_message = _('No fields changed.')
+            LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
+
+            msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
+            if "_continue" in request.POST:
+                request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
+                if '_popup' in request.REQUEST:
+                    return HttpResponseRedirect(request.path + "?_popup=1")
+                else:
+                    return HttpResponseRedirect(request.path)
+            elif "_saveasnew" in request.POST:
+                request.user.message_set.create(message=_('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)})
+                return HttpResponseRedirect("../%s/" % pk_value)
+            elif "_addanother" in request.POST:
+                request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
+                return HttpResponseRedirect("../add/")
+            else:
+                request.user.message_set.create(message=msg)
+                return HttpResponseRedirect("../")
+    else:
+        # Populate new_data with a "flattened" version of the current data.
+        new_data = manipulator.flatten_data()
+
+        # TODO: do this in flatten_data...
+        # If the object has ordered objects on its admin page, get the existing
+        # order and flatten it into a comma-separated list of IDs.
+
+        id_order_list = []
+        for rel_obj in opts.get_ordered_objects():
+            id_order_list.extend(getattr(manipulator.original_object, 'get_%s_order' % rel_obj.object_name.lower())())
+        if id_order_list:
+            new_data['order_'] = ','.join(map(str, id_order_list))
+        errors = {}
+
+    # Populate the FormWrapper.
+    form = oldforms.FormWrapper(manipulator, new_data, errors)
+    form.original = manipulator.original_object
+    form.order_objects = []
+
+    #TODO Should be done in flatten_data  / FormWrapper construction
+    for related in opts.get_followed_related_objects():
+        wrt = related.opts.order_with_respect_to
+        if wrt and wrt.rel and wrt.rel.to == opts:
+            func = getattr(manipulator.original_object, 'get_%s_list' %
+                    related.get_accessor_name())
+            orig_list = func()
+            form.order_objects.extend(orig_list)
+
+    c = template.RequestContext(request, {
+        'title': _('Change %s') % force_unicode(opts.verbose_name),
+        'form': form,
+        'object_id': object_id,
+        'original': manipulator.original_object,
+        'is_popup': '_popup' in request.REQUEST,
+    })
+    return render_change_form(model, manipulator, c, change=True)
+change_stage = staff_member_required(never_cache(change_stage))
+
+def _nest_help(obj, depth, val):
+    current = obj
+    for i in range(depth):
+        current = current[-1]
+    current.append(val)
+
+def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth):
+    "Helper function that recursively populates deleted_objects."
+    nh = _nest_help # Bind to local variable for performance
+    if current_depth > 16:
+        return # Avoid recursing too deep.
+    opts_seen = []
+    for related in opts.get_all_related_objects():
+        if related.opts in opts_seen:
+            continue
+        opts_seen.append(related.opts)
+        rel_opts_name = related.get_accessor_name()
+        if isinstance(related.field.rel, models.OneToOneRel):
+            try:
+                sub_obj = getattr(obj, rel_opts_name)
+            except ObjectDoesNotExist:
+                pass
+            else:
+                if related.opts.admin:
+                    p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
+                    if not user.has_perm(p):
+                        perms_needed.add(related.opts.verbose_name)
+                        # We don't care about populating deleted_objects now.
+                        continue
+                if related.field.rel.edit_inline or not related.opts.admin:
+                    # Don't display link to edit, because it either has no
+                    # admin or is edited inline.
+                    nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []])
+                else:
+                    # Display a link to the admin page.
+                    nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
+                        (escape(force_unicode(capfirst(related.opts.verbose_name))),
+                            related.opts.app_label,
+                            related.opts.object_name.lower(),
+                            sub_obj._get_pk_val(), sub_obj)), []])
+                _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
+        else:
+            has_related_objs = False
+            for sub_obj in getattr(obj, rel_opts_name).all():
+                has_related_objs = True
+                if related.field.rel.edit_inline or not related.opts.admin:
+                    # Don't display link to edit, because it either has no
+                    # admin or is edited inline.
+                    nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []])
+                else:
+                    # Display a link to the admin page.
+                    nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
+                        (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []])
+                _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
+            # If there were related objects, and the user doesn't have
+            # permission to delete them, add the missing perm to perms_needed.
+            if related.opts.admin and has_related_objs:
+                p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
+                if not user.has_perm(p):
+                    perms_needed.add(related.opts.verbose_name)
+    for related in opts.get_all_related_many_to_many_objects():
+        if related.opts in opts_seen:
+            continue
+        opts_seen.append(related.opts)
+        rel_opts_name = related.get_accessor_name()
+        has_related_objs = False
+
+        # related.get_accessor_name() could return None for symmetrical relationships
+        if rel_opts_name:
+            rel_objs = getattr(obj, rel_opts_name, None)
+            if rel_objs:
+                has_related_objs = True
+
+        if has_related_objs:
+            for sub_obj in rel_objs.all():
+                if related.field.rel.edit_inline or not related.opts.admin:
+                    # Don't display link to edit, because it either has no
+                    # admin or is edited inline.
+                    nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
+                        {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
+                else:
+                    # Display a link to the admin page.
+                    nh(deleted_objects, current_depth, [
+                        mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
+                        (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
+                            (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
+        # If there were related objects, and the user doesn't have
+        # permission to change them, add the missing perm to perms_needed.
+        if related.opts.admin and has_related_objs:
+            p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
+            if not user.has_perm(p):
+                perms_needed.add(related.opts.verbose_name)
+
+def delete_stage(request, app_label, model_name, object_id):
+    model = models.get_model(app_label, model_name)
+    object_id = unquote(object_id)
+    if model is None:
+        raise Http404("App %r, model %r, not found" % (app_label, model_name))
+    opts = model._meta
+    if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()):
+        raise PermissionDenied
+    obj = get_object_or_404(model, pk=object_id)
+
+    # Populate deleted_objects, a data structure of all related objects that
+    # will also be deleted.
+    deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []]
+    perms_needed = set()
+    _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
+
+    if request.POST: # The user has already confirmed the deletion.
+        if perms_needed:
+            raise PermissionDenied
+        obj_display = force_unicode(obj)
+        obj.delete()
+        LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, object_id, obj_display, DELETION)
+        request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': obj_display})
+        return HttpResponseRedirect("../../")
+    extra_context = {
+        "title": _("Are you sure?"),
+        "object_name": force_unicode(opts.verbose_name),
+        "object": obj,
+        "deleted_objects": deleted_objects,
+        "perms_lacking": perms_needed,
+        "opts": model._meta,
+    }
+    return render_to_response(["admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower() ),
+                               "admin/%s/delete_confirmation.html" % app_label ,
+                               "admin/delete_confirmation.html"], extra_context, context_instance=template.RequestContext(request))
+delete_stage = staff_member_required(never_cache(delete_stage))
+
+def history(request, app_label, model_name, object_id):
+    model = models.get_model(app_label, model_name)
+    object_id = unquote(object_id)
+    if model is None:
+        raise Http404("App %r, model %r, not found" % (app_label, model_name))
+    action_list = LogEntry.objects.filter(object_id=object_id,
+        content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time')
+    # If no history was found, see whether this object even exists.
+    obj = get_object_or_404(model, pk=object_id)
+    extra_context = {
+        'title': _('Change history: %s') % obj,
+        'action_list': action_list,
+        'module_name': force_unicode(capfirst(model._meta.verbose_name_plural)),
+        'object': obj,
+    }
+    return render_to_response(["admin/%s/%s/object_history.html" % (app_label, model._meta.object_name.lower()),
+                               "admin/%s/object_history.html" % app_label ,
+                               "admin/object_history.html"], extra_context, context_instance=template.RequestContext(request))
+history = staff_member_required(never_cache(history))
+
+class ChangeList(object):
+    def __init__(self, request, model):
+        self.model = model
+        self.opts = model._meta
+        self.lookup_opts = self.opts
+        self.manager = self.opts.admin.manager
+
+        # Get search parameters from the query string.
+        try:
+            self.page_num = int(request.GET.get(PAGE_VAR, 0))
+        except ValueError:
+            self.page_num = 0
+        self.show_all = ALL_VAR in request.GET
+        self.is_popup = IS_POPUP_VAR in request.GET
+        self.params = dict(request.GET.items())
+        if PAGE_VAR in self.params:
+            del self.params[PAGE_VAR]
+        if ERROR_FLAG in self.params:
+            del self.params[ERROR_FLAG]
+
+        self.order_field, self.order_type = self.get_ordering()
+        self.query = request.GET.get(SEARCH_VAR, '')
+        self.query_set = self.get_query_set()
+        self.get_results(request)
+        self.title = (self.is_popup and _('Select %s') % force_unicode(self.opts.verbose_name) or _('Select %s to change') % force_unicode(self.opts.verbose_name))
+        self.filter_specs, self.has_filters = self.get_filters(request)
+        self.pk_attname = self.lookup_opts.pk.attname
+
+    def get_filters(self, request):
+        filter_specs = []
+        if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field:
+            filter_fields = [self.lookup_opts.get_field(field_name) \
+                              for field_name in self.lookup_opts.admin.list_filter]
+            for f in filter_fields:
+                spec = FilterSpec.create(f, request, self.params, self.model)
+                if spec and spec.has_output():
+                    filter_specs.append(spec)
+        return filter_specs, bool(filter_specs)
+
+    def get_query_string(self, new_params=None, remove=None):
+        if new_params is None: new_params = {}
+        if remove is None: remove = []
+        p = self.params.copy()
+        for r in remove:
+            for k in p.keys():
+                if k.startswith(r):
+                    del p[k]
+        for k, v in new_params.items():
+            if k in p and v is None:
+                del p[k]
+            elif v is not None:
+                p[k] = v
+        return mark_safe('?' + '&amp;'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20'))
+
+    def get_results(self, request):
+        paginator = QuerySetPaginator(self.query_set, self.lookup_opts.admin.list_per_page)
+
+        # Get the number of objects, with admin filters applied.
+        try:
+            result_count = paginator.count
+        # Naked except! Because we don't have any other way of validating
+        # "params". They might be invalid if the keyword arguments are
+        # incorrect, or if the values are not in the correct type (which would
+        # result in a database error).
+        except:
+            raise IncorrectLookupParameters
+
+        # Get the total number of objects, with no admin filters applied.
+        # Perform a slight optimization: Check to see whether any filters were
+        # given. If not, use paginator.hits to calculate the number of objects,
+        # because we've already done paginator.hits and the value is cached.
+        if not self.query_set.query.where:
+            full_result_count = result_count
+        else:
+            full_result_count = self.manager.count()
+
+        can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
+        multi_page = result_count > self.lookup_opts.admin.list_per_page
+
+        # Get the list of objects to display on this page.
+        if (self.show_all and can_show_all) or not multi_page:
+            result_list = list(self.query_set)
+        else:
+            try:
+                result_list = paginator.page(self.page_num+1).object_list
+            except InvalidPage:
+                result_list = ()
+
+        self.result_count = result_count
+        self.full_result_count = full_result_count
+        self.result_list = result_list
+        self.can_show_all = can_show_all
+        self.multi_page = multi_page
+        self.paginator = paginator
+
+    def get_ordering(self):
+        lookup_opts, params = self.lookup_opts, self.params
+        # For ordering, first check the "ordering" parameter in the admin
+        # options, then check the object's default ordering. If neither of
+        # those exist, order descending by ID by default. Finally, look for
+        # manually-specified ordering from the query string.
+        ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
+
+        if ordering[0].startswith('-'):
+            order_field, order_type = ordering[0][1:], 'desc'
+        else:
+            order_field, order_type = ordering[0], 'asc'
+        if ORDER_VAR in params:
+            try:
+                field_name = lookup_opts.admin.list_display[int(params[ORDER_VAR])]
+                try:
+                    f = lookup_opts.get_field(field_name)
+                except models.FieldDoesNotExist:
+                    # see if field_name is a name of a non-field
+                    # that allows sorting
+                    try:
+                        attr = getattr(lookup_opts.admin.manager.model, field_name)
+                        order_field = attr.admin_order_field
+                    except AttributeError:
+                        pass
+                else:
+                    if not isinstance(f.rel, models.ManyToOneRel) or not f.null:
+                        order_field = f.name
+            except (IndexError, ValueError):
+                pass # Invalid ordering specified. Just use the default.
+        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
+            order_type = params[ORDER_TYPE_VAR]
+        return order_field, order_type
+
+    def get_query_set(self):
+        qs = self.manager.get_query_set()
+        lookup_params = self.params.copy() # a dictionary of the query string
+        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
+            if i in lookup_params:
+                del lookup_params[i]
+        for key, value in lookup_params.items():
+            if not isinstance(key, str):
+                # 'key' will be used as a keyword argument later, so Python
+                # requires it to be a string.
+                del lookup_params[key]
+                lookup_params[smart_str(key)] = value
+
+        # Apply lookup parameters from the query string.
+        qs = qs.filter(**lookup_params)
+
+        # Use select_related() if one of the list_display options is a field
+        # with a relationship.
+        if self.lookup_opts.admin.list_select_related:
+            qs = qs.select_related()
+        else:
+            for field_name in self.lookup_opts.admin.list_display:
+                try:
+                    f = self.lookup_opts.get_field(field_name)
+                except models.FieldDoesNotExist:
+                    pass
+                else:
+                    if isinstance(f.rel, models.ManyToOneRel):
+                        qs = qs.select_related()
+                        break
+
+        # Set ordering.
+        if self.order_field:
+            qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
+
+        # Apply keyword searches.
+        def construct_search(field_name):
+            if field_name.startswith('^'):
+                return "%s__istartswith" % field_name[1:]
+            elif field_name.startswith('='):
+                return "%s__iexact" % field_name[1:]
+            elif field_name.startswith('@'):
+                return "%s__search" % field_name[1:]
+            else:
+                return "%s__icontains" % field_name
+
+        if self.lookup_opts.admin.search_fields and self.query:
+            for bit in self.query.split():
+                or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields]
+                other_qs = QuerySet(self.model)
+                other_qs.dup_select_related(qs)
+                other_qs = other_qs.filter(reduce(operator.or_, or_queries))
+                qs = qs & other_qs
+
+        if self.opts.one_to_one_field:
+            qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to)
+
+        return qs
+
+    def url_for_result(self, result):
+        return "%s/" % quote(getattr(result, self.pk_attname))
+
+def change_list(request, app_label, model_name):
+    model = models.get_model(app_label, model_name)
+    if model is None:
+        raise Http404("App %r, model %r, not found" % (app_label, model_name))
+    if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
+        raise PermissionDenied
+    try:
+        cl = ChangeList(request, model)
+    except IncorrectLookupParameters:
+        # Wacky lookup parameters were given, so redirect to the main
+        # changelist page, without parameters, and pass an 'invalid=1'
+        # parameter via the query string. If wacky parameters were given and
+        # the 'invalid=1' parameter was already in the query string, something
+        # is screwed up with the database, so display an error page.
+        if ERROR_FLAG in request.GET.keys():
+            return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
+        return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+    c = template.RequestContext(request, {
+        'title': cl.title,
+        'is_popup': cl.is_popup,
+        'cl': cl,
+    })
+    c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}),
+    return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()),
+                               'admin/%s/change_list.html' % app_label,
+                               'admin/change_list.html'], context_instance=c)
+change_list = staff_member_required(never_cache(change_list))