app/django/contrib/admin/views/main.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     1 from django import oldforms, template
       
     2 from django.conf import settings
       
     3 from django.contrib.admin.filterspecs import FilterSpec
       
     4 from django.contrib.admin.views.decorators import staff_member_required
       
     5 from django.views.decorators.cache import never_cache
       
     6 from django.contrib.contenttypes.models import ContentType
       
     7 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied
       
     8 from django.core.paginator import QuerySetPaginator, InvalidPage
       
     9 from django.shortcuts import get_object_or_404, render_to_response
       
    10 from django.db import models
       
    11 from django.db.models.query import QuerySet
       
    12 from django.http import Http404, HttpResponse, HttpResponseRedirect
       
    13 from django.utils.html import escape
       
    14 from django.utils.text import capfirst, get_text_list
       
    15 from django.utils.encoding import force_unicode, smart_str
       
    16 from django.utils.translation import ugettext as _
       
    17 from django.utils.safestring import mark_safe
       
    18 import operator
       
    19 
       
    20 try:
       
    21     set
       
    22 except NameError:
       
    23     from sets import Set as set   # Python 2.3 fallback
       
    24 
       
    25 from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
       
    26 if not LogEntry._meta.installed:
       
    27     raise ImproperlyConfigured, "You'll need to put 'django.contrib.admin' in your INSTALLED_APPS setting before you can use the admin application."
       
    28 
       
    29 if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
       
    30     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."
       
    31 
       
    32 # The system will display a "Show all" link on the change list only if the
       
    33 # total result count is less than or equal to this setting.
       
    34 MAX_SHOW_ALL_ALLOWED = 200
       
    35 
       
    36 # Changelist settings
       
    37 ALL_VAR = 'all'
       
    38 ORDER_VAR = 'o'
       
    39 ORDER_TYPE_VAR = 'ot'
       
    40 PAGE_VAR = 'p'
       
    41 SEARCH_VAR = 'q'
       
    42 IS_POPUP_VAR = 'pop'
       
    43 ERROR_FLAG = 'e'
       
    44 
       
    45 # Text to display within change-list table cells if the value is blank.
       
    46 EMPTY_CHANGELIST_VALUE = '(None)'
       
    47 
       
    48 use_raw_id_admin = lambda field: isinstance(field.rel, (models.ManyToOneRel, models.ManyToManyRel)) and field.rel.raw_id_admin
       
    49 
       
    50 class IncorrectLookupParameters(Exception):
       
    51     pass
       
    52 
       
    53 def quote(s):
       
    54     """
       
    55     Ensure that primary key values do not confuse the admin URLs by escaping
       
    56     any '/', '_' and ':' characters. Similar to urllib.quote, except that the
       
    57     quoting is slightly different so that it doesn't get automatically
       
    58     unquoted by the Web browser.
       
    59     """
       
    60     if type(s) != type(''):
       
    61         return s
       
    62     res = list(s)
       
    63     for i in range(len(res)):
       
    64         c = res[i]
       
    65         if c in ':/_':
       
    66             res[i] = '_%02X' % ord(c)
       
    67     return ''.join(res)
       
    68 
       
    69 def unquote(s):
       
    70     """
       
    71     Undo the effects of quote(). Based heavily on urllib.unquote().
       
    72     """
       
    73     mychr = chr
       
    74     myatoi = int
       
    75     list = s.split('_')
       
    76     res = [list[0]]
       
    77     myappend = res.append
       
    78     del list[0]
       
    79     for item in list:
       
    80         if item[1:2]:
       
    81             try:
       
    82                 myappend(mychr(myatoi(item[:2], 16)) + item[2:])
       
    83             except ValueError:
       
    84                 myappend('_' + item)
       
    85         else:
       
    86             myappend('_' + item)
       
    87     return "".join(res)
       
    88 
       
    89 def get_javascript_imports(opts, auto_populated_fields, field_sets):
       
    90 # Put in any necessary JavaScript imports.
       
    91     js = ['js/core.js', 'js/admin/RelatedObjectLookups.js']
       
    92     if auto_populated_fields:
       
    93         js.append('js/urlify.js')
       
    94     if opts.has_field_type(models.DateTimeField) or opts.has_field_type(models.TimeField) or opts.has_field_type(models.DateField):
       
    95         js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js'])
       
    96     if opts.get_ordered_objects():
       
    97         js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
       
    98     if opts.admin.js:
       
    99         js.extend(opts.admin.js)
       
   100     seen_collapse = False
       
   101     for field_set in field_sets:
       
   102         if not seen_collapse and 'collapse' in field_set.classes:
       
   103             seen_collapse = True
       
   104             js.append('js/admin/CollapsedFieldsets.js')
       
   105 
       
   106         for field_line in field_set:
       
   107             try:
       
   108                 for f in field_line:
       
   109                     if f.rel and isinstance(f, models.ManyToManyField) and f.rel.filter_interface:
       
   110                         js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
       
   111                         raise StopIteration
       
   112             except StopIteration:
       
   113                 break
       
   114     return js
       
   115 
       
   116 class AdminBoundField(object):
       
   117     def __init__(self, field, field_mapping, original):
       
   118         self.field = field
       
   119         self.original = original
       
   120         self.form_fields = [field_mapping[name] for name in self.field.get_manipulator_field_names('')]
       
   121         self.element_id = self.form_fields[0].get_id()
       
   122         self.has_label_first = not isinstance(self.field, models.BooleanField)
       
   123         self.raw_id_admin = use_raw_id_admin(field)
       
   124         self.is_date_time = isinstance(field, models.DateTimeField)
       
   125         self.is_file_field = isinstance(field, models.FileField)
       
   126         self.needs_add_label = field.rel and (isinstance(field.rel, models.ManyToOneRel) or isinstance(field.rel, models.ManyToManyRel)) and field.rel.to._meta.admin
       
   127         self.hidden = isinstance(self.field, models.AutoField)
       
   128         self.first = False
       
   129 
       
   130         classes = []
       
   131         if self.raw_id_admin:
       
   132             classes.append('nowrap')
       
   133         if max([bool(f.errors()) for f in self.form_fields]):
       
   134             classes.append('error')
       
   135         if classes:
       
   136             self.cell_class_attribute = u' class="%s" ' % ' '.join(classes)
       
   137         self._repr_filled = False
       
   138 
       
   139         if field.rel:
       
   140             self.related_url = mark_safe(u'../../../%s/%s/'
       
   141                     % (field.rel.to._meta.app_label,
       
   142                         field.rel.to._meta.object_name.lower()))
       
   143 
       
   144     def original_value(self):
       
   145         if self.original:
       
   146             return self.original.__dict__[self.field.attname]
       
   147 
       
   148     def existing_display(self):
       
   149         try:
       
   150             return self._display
       
   151         except AttributeError:
       
   152             if isinstance(self.field.rel, models.ManyToOneRel):
       
   153                 self._display = force_unicode(getattr(self.original, self.field.name), strings_only=True)
       
   154             elif isinstance(self.field.rel, models.ManyToManyRel):
       
   155                 self._display = u", ".join([force_unicode(obj) for obj in getattr(self.original, self.field.name).all()])
       
   156             return self._display
       
   157 
       
   158     def __repr__(self):
       
   159         return repr(self.__dict__)
       
   160 
       
   161     def html_error_list(self):
       
   162         return mark_safe(" ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]))
       
   163 
       
   164     def original_url(self):
       
   165         if self.is_file_field and self.original and self.field.attname:
       
   166             url_method = getattr(self.original, 'get_%s_url' % self.field.attname)
       
   167             if callable(url_method):
       
   168                 return url_method()
       
   169         return ''
       
   170 
       
   171 class AdminBoundFieldLine(object):
       
   172     def __init__(self, field_line, field_mapping, original):
       
   173         self.bound_fields = [field.bind(field_mapping, original, AdminBoundField) for field in field_line]
       
   174         for bound_field in self:
       
   175             bound_field.first = True
       
   176             break
       
   177 
       
   178     def __iter__(self):
       
   179         for bound_field in self.bound_fields:
       
   180             yield bound_field
       
   181 
       
   182     def __len__(self):
       
   183         return len(self.bound_fields)
       
   184 
       
   185 class AdminBoundFieldSet(object):
       
   186     def __init__(self, field_set, field_mapping, original):
       
   187         self.name = field_set.name
       
   188         self.classes = field_set.classes
       
   189         self.description = field_set.description
       
   190         self.bound_field_lines = [field_line.bind(field_mapping, original, AdminBoundFieldLine) for field_line in field_set]
       
   191 
       
   192     def __iter__(self):
       
   193         for bound_field_line in self.bound_field_lines:
       
   194             yield bound_field_line
       
   195 
       
   196     def __len__(self):
       
   197         return len(self.bound_field_lines)
       
   198 
       
   199 def render_change_form(model, manipulator, context, add=False, change=False, form_url=''):
       
   200     opts = model._meta
       
   201     app_label = opts.app_label
       
   202     auto_populated_fields = [f for f in opts.fields if f.prepopulate_from]
       
   203     field_sets = opts.admin.get_field_sets(opts)
       
   204     original = getattr(manipulator, 'original_object', None)
       
   205     bound_field_sets = [field_set.bind(context['form'], original, AdminBoundFieldSet) for field_set in field_sets]
       
   206     first_form_field_id = bound_field_sets[0].bound_field_lines[0].bound_fields[0].form_fields[0].get_id();
       
   207     ordered_objects = opts.get_ordered_objects()
       
   208     inline_related_objects = opts.get_followed_related_objects(manipulator.follow)
       
   209     extra_context = {
       
   210         'add': add,
       
   211         'change': change,
       
   212         'has_delete_permission': context['perms'][app_label][opts.get_delete_permission()],
       
   213         'has_change_permission': context['perms'][app_label][opts.get_change_permission()],
       
   214         'has_file_field': opts.has_field_type(models.FileField),
       
   215         'has_absolute_url': hasattr(model, 'get_absolute_url'),
       
   216         'auto_populated_fields': auto_populated_fields,
       
   217         'bound_field_sets': bound_field_sets,
       
   218         'first_form_field_id': first_form_field_id,
       
   219         'javascript_imports': get_javascript_imports(opts, auto_populated_fields, field_sets),
       
   220         'ordered_objects': ordered_objects,
       
   221         'inline_related_objects': inline_related_objects,
       
   222         'form_url': mark_safe(form_url),
       
   223         'opts': opts,
       
   224         'content_type_id': ContentType.objects.get_for_model(model).id,
       
   225     }
       
   226     context.update(extra_context)
       
   227     return render_to_response([
       
   228         "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
       
   229         "admin/%s/change_form.html" % app_label,
       
   230         "admin/change_form.html"], context_instance=context)
       
   231 
       
   232 def index(request):
       
   233     return render_to_response('admin/index.html', {'title': _('Site administration')}, context_instance=template.RequestContext(request))
       
   234 index = staff_member_required(never_cache(index))
       
   235 
       
   236 def add_stage(request, app_label, model_name, show_delete=False, form_url='', post_url=None, post_url_continue='../%s/', object_id_override=None):
       
   237     model = models.get_model(app_label, model_name)
       
   238     if model is None:
       
   239         raise Http404("App %r, model %r, not found" % (app_label, model_name))
       
   240     opts = model._meta
       
   241 
       
   242     if not request.user.has_perm(app_label + '.' + opts.get_add_permission()):
       
   243         raise PermissionDenied
       
   244 
       
   245     if post_url is None:
       
   246         if request.user.has_perm(app_label + '.' + opts.get_change_permission()):
       
   247             # redirect to list view
       
   248             post_url = '../'
       
   249         else:
       
   250             # Object list will give 'Permission Denied', so go back to admin home
       
   251             post_url = '../../../'
       
   252 
       
   253     manipulator = model.AddManipulator()
       
   254     if request.POST:
       
   255         new_data = request.POST.copy()
       
   256 
       
   257         if opts.has_field_type(models.FileField):
       
   258             new_data.update(request.FILES)
       
   259 
       
   260         errors = manipulator.get_validation_errors(new_data)
       
   261         manipulator.do_html2python(new_data)
       
   262 
       
   263         if not errors:
       
   264             new_object = manipulator.save(new_data)
       
   265             pk_value = new_object._get_pk_val()
       
   266             LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), ADDITION)
       
   267             msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
       
   268             # Here, we distinguish between different save types by checking for
       
   269             # the presence of keys in request.POST.
       
   270             if "_continue" in request.POST:
       
   271                 request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
       
   272                 if "_popup" in request.POST:
       
   273                     post_url_continue += "?_popup=1"
       
   274                 return HttpResponseRedirect(post_url_continue % pk_value)
       
   275             if "_popup" in request.POST:
       
   276                 return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
       
   277                     # escape() calls force_unicode.
       
   278                     (escape(pk_value), escape(new_object)))
       
   279             elif "_addanother" in request.POST:
       
   280                 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
       
   281                 return HttpResponseRedirect(request.path)
       
   282             else:
       
   283                 request.user.message_set.create(message=msg)
       
   284                 return HttpResponseRedirect(post_url)
       
   285     else:
       
   286         # Add default data.
       
   287         new_data = manipulator.flatten_data()
       
   288 
       
   289         # Override the defaults with GET params, if they exist.
       
   290         new_data.update(dict(request.GET.items()))
       
   291 
       
   292         errors = {}
       
   293 
       
   294     # Populate the FormWrapper.
       
   295     form = oldforms.FormWrapper(manipulator, new_data, errors)
       
   296 
       
   297     c = template.RequestContext(request, {
       
   298         'title': _('Add %s') % force_unicode(opts.verbose_name),
       
   299         'form': form,
       
   300         'is_popup': '_popup' in request.REQUEST,
       
   301         'show_delete': show_delete,
       
   302     })
       
   303 
       
   304     if object_id_override is not None:
       
   305         c['object_id'] = object_id_override
       
   306 
       
   307     return render_change_form(model, manipulator, c, add=True)
       
   308 add_stage = staff_member_required(never_cache(add_stage))
       
   309 
       
   310 def change_stage(request, app_label, model_name, object_id):
       
   311     model = models.get_model(app_label, model_name)
       
   312     object_id = unquote(object_id)
       
   313     if model is None:
       
   314         raise Http404("App %r, model %r, not found" % (app_label, model_name))
       
   315     opts = model._meta
       
   316 
       
   317     if not request.user.has_perm(app_label + '.' + opts.get_change_permission()):
       
   318         raise PermissionDenied
       
   319 
       
   320     if request.POST and "_saveasnew" in request.POST:
       
   321         return add_stage(request, app_label, model_name, form_url='../../add/')
       
   322 
       
   323     try:
       
   324         manipulator = model.ChangeManipulator(object_id)
       
   325     except model.DoesNotExist:
       
   326         raise Http404('%s object with primary key %r does not exist' % (model_name, escape(object_id)))
       
   327 
       
   328     if request.POST:
       
   329         new_data = request.POST.copy()
       
   330 
       
   331         if opts.has_field_type(models.FileField):
       
   332             new_data.update(request.FILES)
       
   333 
       
   334         errors = manipulator.get_validation_errors(new_data)
       
   335         manipulator.do_html2python(new_data)
       
   336 
       
   337         if not errors:
       
   338             new_object = manipulator.save(new_data)
       
   339             pk_value = new_object._get_pk_val()
       
   340 
       
   341             # Construct the change message.
       
   342             change_message = []
       
   343             if manipulator.fields_added:
       
   344                 change_message.append(_('Added %s.') % get_text_list(manipulator.fields_added, _('and')))
       
   345             if manipulator.fields_changed:
       
   346                 change_message.append(_('Changed %s.') % get_text_list(manipulator.fields_changed, _('and')))
       
   347             if manipulator.fields_deleted:
       
   348                 change_message.append(_('Deleted %s.') % get_text_list(manipulator.fields_deleted, _('and')))
       
   349             change_message = ' '.join(change_message)
       
   350             if not change_message:
       
   351                 change_message = _('No fields changed.')
       
   352             LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, pk_value, force_unicode(new_object), CHANGE, change_message)
       
   353 
       
   354             msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(new_object)}
       
   355             if "_continue" in request.POST:
       
   356                 request.user.message_set.create(message=msg + ' ' + _("You may edit it again below."))
       
   357                 if '_popup' in request.REQUEST:
       
   358                     return HttpResponseRedirect(request.path + "?_popup=1")
       
   359                 else:
       
   360                     return HttpResponseRedirect(request.path)
       
   361             elif "_saveasnew" in request.POST:
       
   362                 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)})
       
   363                 return HttpResponseRedirect("../%s/" % pk_value)
       
   364             elif "_addanother" in request.POST:
       
   365                 request.user.message_set.create(message=msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
       
   366                 return HttpResponseRedirect("../add/")
       
   367             else:
       
   368                 request.user.message_set.create(message=msg)
       
   369                 return HttpResponseRedirect("../")
       
   370     else:
       
   371         # Populate new_data with a "flattened" version of the current data.
       
   372         new_data = manipulator.flatten_data()
       
   373 
       
   374         # TODO: do this in flatten_data...
       
   375         # If the object has ordered objects on its admin page, get the existing
       
   376         # order and flatten it into a comma-separated list of IDs.
       
   377 
       
   378         id_order_list = []
       
   379         for rel_obj in opts.get_ordered_objects():
       
   380             id_order_list.extend(getattr(manipulator.original_object, 'get_%s_order' % rel_obj.object_name.lower())())
       
   381         if id_order_list:
       
   382             new_data['order_'] = ','.join(map(str, id_order_list))
       
   383         errors = {}
       
   384 
       
   385     # Populate the FormWrapper.
       
   386     form = oldforms.FormWrapper(manipulator, new_data, errors)
       
   387     form.original = manipulator.original_object
       
   388     form.order_objects = []
       
   389 
       
   390     #TODO Should be done in flatten_data  / FormWrapper construction
       
   391     for related in opts.get_followed_related_objects():
       
   392         wrt = related.opts.order_with_respect_to
       
   393         if wrt and wrt.rel and wrt.rel.to == opts:
       
   394             func = getattr(manipulator.original_object, 'get_%s_list' %
       
   395                     related.get_accessor_name())
       
   396             orig_list = func()
       
   397             form.order_objects.extend(orig_list)
       
   398 
       
   399     c = template.RequestContext(request, {
       
   400         'title': _('Change %s') % force_unicode(opts.verbose_name),
       
   401         'form': form,
       
   402         'object_id': object_id,
       
   403         'original': manipulator.original_object,
       
   404         'is_popup': '_popup' in request.REQUEST,
       
   405     })
       
   406     return render_change_form(model, manipulator, c, change=True)
       
   407 change_stage = staff_member_required(never_cache(change_stage))
       
   408 
       
   409 def _nest_help(obj, depth, val):
       
   410     current = obj
       
   411     for i in range(depth):
       
   412         current = current[-1]
       
   413     current.append(val)
       
   414 
       
   415 def _get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth):
       
   416     "Helper function that recursively populates deleted_objects."
       
   417     nh = _nest_help # Bind to local variable for performance
       
   418     if current_depth > 16:
       
   419         return # Avoid recursing too deep.
       
   420     opts_seen = []
       
   421     for related in opts.get_all_related_objects():
       
   422         if related.opts in opts_seen:
       
   423             continue
       
   424         opts_seen.append(related.opts)
       
   425         rel_opts_name = related.get_accessor_name()
       
   426         if isinstance(related.field.rel, models.OneToOneRel):
       
   427             try:
       
   428                 sub_obj = getattr(obj, rel_opts_name)
       
   429             except ObjectDoesNotExist:
       
   430                 pass
       
   431             else:
       
   432                 if related.opts.admin:
       
   433                     p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
       
   434                     if not user.has_perm(p):
       
   435                         perms_needed.add(related.opts.verbose_name)
       
   436                         # We don't care about populating deleted_objects now.
       
   437                         continue
       
   438                 if related.field.rel.edit_inline or not related.opts.admin:
       
   439                     # Don't display link to edit, because it either has no
       
   440                     # admin or is edited inline.
       
   441                     nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []])
       
   442                 else:
       
   443                     # Display a link to the admin page.
       
   444                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
       
   445                         (escape(force_unicode(capfirst(related.opts.verbose_name))),
       
   446                             related.opts.app_label,
       
   447                             related.opts.object_name.lower(),
       
   448                             sub_obj._get_pk_val(), sub_obj)), []])
       
   449                 _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
       
   450         else:
       
   451             has_related_objs = False
       
   452             for sub_obj in getattr(obj, rel_opts_name).all():
       
   453                 has_related_objs = True
       
   454                 if related.field.rel.edit_inline or not related.opts.admin:
       
   455                     # Don't display link to edit, because it either has no
       
   456                     # admin or is edited inline.
       
   457                     nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []])
       
   458                 else:
       
   459                     # Display a link to the admin page.
       
   460                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
       
   461                         (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))), []])
       
   462                 _get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2)
       
   463             # If there were related objects, and the user doesn't have
       
   464             # permission to delete them, add the missing perm to perms_needed.
       
   465             if related.opts.admin and has_related_objs:
       
   466                 p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
       
   467                 if not user.has_perm(p):
       
   468                     perms_needed.add(related.opts.verbose_name)
       
   469     for related in opts.get_all_related_many_to_many_objects():
       
   470         if related.opts in opts_seen:
       
   471             continue
       
   472         opts_seen.append(related.opts)
       
   473         rel_opts_name = related.get_accessor_name()
       
   474         has_related_objs = False
       
   475 
       
   476         # related.get_accessor_name() could return None for symmetrical relationships
       
   477         if rel_opts_name:
       
   478             rel_objs = getattr(obj, rel_opts_name, None)
       
   479             if rel_objs:
       
   480                 has_related_objs = True
       
   481 
       
   482         if has_related_objs:
       
   483             for sub_obj in rel_objs.all():
       
   484                 if related.field.rel.edit_inline or not related.opts.admin:
       
   485                     # Don't display link to edit, because it either has no
       
   486                     # admin or is edited inline.
       
   487                     nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
       
   488                         {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
       
   489                 else:
       
   490                     # Display a link to the admin page.
       
   491                     nh(deleted_objects, current_depth, [
       
   492                         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))}) + \
       
   493                         (u' <a href="../../../../%s/%s/%s/">%s</a>' % \
       
   494                             (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
       
   495         # If there were related objects, and the user doesn't have
       
   496         # permission to change them, add the missing perm to perms_needed.
       
   497         if related.opts.admin and has_related_objs:
       
   498             p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
       
   499             if not user.has_perm(p):
       
   500                 perms_needed.add(related.opts.verbose_name)
       
   501 
       
   502 def delete_stage(request, app_label, model_name, object_id):
       
   503     model = models.get_model(app_label, model_name)
       
   504     object_id = unquote(object_id)
       
   505     if model is None:
       
   506         raise Http404("App %r, model %r, not found" % (app_label, model_name))
       
   507     opts = model._meta
       
   508     if not request.user.has_perm(app_label + '.' + opts.get_delete_permission()):
       
   509         raise PermissionDenied
       
   510     obj = get_object_or_404(model, pk=object_id)
       
   511 
       
   512     # Populate deleted_objects, a data structure of all related objects that
       
   513     # will also be deleted.
       
   514     deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []]
       
   515     perms_needed = set()
       
   516     _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
       
   517 
       
   518     if request.POST: # The user has already confirmed the deletion.
       
   519         if perms_needed:
       
   520             raise PermissionDenied
       
   521         obj_display = force_unicode(obj)
       
   522         obj.delete()
       
   523         LogEntry.objects.log_action(request.user.id, ContentType.objects.get_for_model(model).id, object_id, obj_display, DELETION)
       
   524         request.user.message_set.create(message=_('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': obj_display})
       
   525         return HttpResponseRedirect("../../")
       
   526     extra_context = {
       
   527         "title": _("Are you sure?"),
       
   528         "object_name": force_unicode(opts.verbose_name),
       
   529         "object": obj,
       
   530         "deleted_objects": deleted_objects,
       
   531         "perms_lacking": perms_needed,
       
   532         "opts": model._meta,
       
   533     }
       
   534     return render_to_response(["admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower() ),
       
   535                                "admin/%s/delete_confirmation.html" % app_label ,
       
   536                                "admin/delete_confirmation.html"], extra_context, context_instance=template.RequestContext(request))
       
   537 delete_stage = staff_member_required(never_cache(delete_stage))
       
   538 
       
   539 def history(request, app_label, model_name, object_id):
       
   540     model = models.get_model(app_label, model_name)
       
   541     object_id = unquote(object_id)
       
   542     if model is None:
       
   543         raise Http404("App %r, model %r, not found" % (app_label, model_name))
       
   544     action_list = LogEntry.objects.filter(object_id=object_id,
       
   545         content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time')
       
   546     # If no history was found, see whether this object even exists.
       
   547     obj = get_object_or_404(model, pk=object_id)
       
   548     extra_context = {
       
   549         'title': _('Change history: %s') % obj,
       
   550         'action_list': action_list,
       
   551         'module_name': force_unicode(capfirst(model._meta.verbose_name_plural)),
       
   552         'object': obj,
       
   553     }
       
   554     return render_to_response(["admin/%s/%s/object_history.html" % (app_label, model._meta.object_name.lower()),
       
   555                                "admin/%s/object_history.html" % app_label ,
       
   556                                "admin/object_history.html"], extra_context, context_instance=template.RequestContext(request))
       
   557 history = staff_member_required(never_cache(history))
       
   558 
       
   559 class ChangeList(object):
       
   560     def __init__(self, request, model):
       
   561         self.model = model
       
   562         self.opts = model._meta
       
   563         self.lookup_opts = self.opts
       
   564         self.manager = self.opts.admin.manager
       
   565 
       
   566         # Get search parameters from the query string.
       
   567         try:
       
   568             self.page_num = int(request.GET.get(PAGE_VAR, 0))
       
   569         except ValueError:
       
   570             self.page_num = 0
       
   571         self.show_all = ALL_VAR in request.GET
       
   572         self.is_popup = IS_POPUP_VAR in request.GET
       
   573         self.params = dict(request.GET.items())
       
   574         if PAGE_VAR in self.params:
       
   575             del self.params[PAGE_VAR]
       
   576         if ERROR_FLAG in self.params:
       
   577             del self.params[ERROR_FLAG]
       
   578 
       
   579         self.order_field, self.order_type = self.get_ordering()
       
   580         self.query = request.GET.get(SEARCH_VAR, '')
       
   581         self.query_set = self.get_query_set()
       
   582         self.get_results(request)
       
   583         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))
       
   584         self.filter_specs, self.has_filters = self.get_filters(request)
       
   585         self.pk_attname = self.lookup_opts.pk.attname
       
   586 
       
   587     def get_filters(self, request):
       
   588         filter_specs = []
       
   589         if self.lookup_opts.admin.list_filter and not self.opts.one_to_one_field:
       
   590             filter_fields = [self.lookup_opts.get_field(field_name) \
       
   591                               for field_name in self.lookup_opts.admin.list_filter]
       
   592             for f in filter_fields:
       
   593                 spec = FilterSpec.create(f, request, self.params, self.model)
       
   594                 if spec and spec.has_output():
       
   595                     filter_specs.append(spec)
       
   596         return filter_specs, bool(filter_specs)
       
   597 
       
   598     def get_query_string(self, new_params=None, remove=None):
       
   599         if new_params is None: new_params = {}
       
   600         if remove is None: remove = []
       
   601         p = self.params.copy()
       
   602         for r in remove:
       
   603             for k in p.keys():
       
   604                 if k.startswith(r):
       
   605                     del p[k]
       
   606         for k, v in new_params.items():
       
   607             if k in p and v is None:
       
   608                 del p[k]
       
   609             elif v is not None:
       
   610                 p[k] = v
       
   611         return mark_safe('?' + '&amp;'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20'))
       
   612 
       
   613     def get_results(self, request):
       
   614         paginator = QuerySetPaginator(self.query_set, self.lookup_opts.admin.list_per_page)
       
   615 
       
   616         # Get the number of objects, with admin filters applied.
       
   617         try:
       
   618             result_count = paginator.count
       
   619         # Naked except! Because we don't have any other way of validating
       
   620         # "params". They might be invalid if the keyword arguments are
       
   621         # incorrect, or if the values are not in the correct type (which would
       
   622         # result in a database error).
       
   623         except:
       
   624             raise IncorrectLookupParameters
       
   625 
       
   626         # Get the total number of objects, with no admin filters applied.
       
   627         # Perform a slight optimization: Check to see whether any filters were
       
   628         # given. If not, use paginator.hits to calculate the number of objects,
       
   629         # because we've already done paginator.hits and the value is cached.
       
   630         if not self.query_set.query.where:
       
   631             full_result_count = result_count
       
   632         else:
       
   633             full_result_count = self.manager.count()
       
   634 
       
   635         can_show_all = result_count <= MAX_SHOW_ALL_ALLOWED
       
   636         multi_page = result_count > self.lookup_opts.admin.list_per_page
       
   637 
       
   638         # Get the list of objects to display on this page.
       
   639         if (self.show_all and can_show_all) or not multi_page:
       
   640             result_list = list(self.query_set)
       
   641         else:
       
   642             try:
       
   643                 result_list = paginator.page(self.page_num+1).object_list
       
   644             except InvalidPage:
       
   645                 result_list = ()
       
   646 
       
   647         self.result_count = result_count
       
   648         self.full_result_count = full_result_count
       
   649         self.result_list = result_list
       
   650         self.can_show_all = can_show_all
       
   651         self.multi_page = multi_page
       
   652         self.paginator = paginator
       
   653 
       
   654     def get_ordering(self):
       
   655         lookup_opts, params = self.lookup_opts, self.params
       
   656         # For ordering, first check the "ordering" parameter in the admin
       
   657         # options, then check the object's default ordering. If neither of
       
   658         # those exist, order descending by ID by default. Finally, look for
       
   659         # manually-specified ordering from the query string.
       
   660         ordering = lookup_opts.admin.ordering or lookup_opts.ordering or ['-' + lookup_opts.pk.name]
       
   661 
       
   662         if ordering[0].startswith('-'):
       
   663             order_field, order_type = ordering[0][1:], 'desc'
       
   664         else:
       
   665             order_field, order_type = ordering[0], 'asc'
       
   666         if ORDER_VAR in params:
       
   667             try:
       
   668                 field_name = lookup_opts.admin.list_display[int(params[ORDER_VAR])]
       
   669                 try:
       
   670                     f = lookup_opts.get_field(field_name)
       
   671                 except models.FieldDoesNotExist:
       
   672                     # see if field_name is a name of a non-field
       
   673                     # that allows sorting
       
   674                     try:
       
   675                         attr = getattr(lookup_opts.admin.manager.model, field_name)
       
   676                         order_field = attr.admin_order_field
       
   677                     except AttributeError:
       
   678                         pass
       
   679                 else:
       
   680                     if not isinstance(f.rel, models.ManyToOneRel) or not f.null:
       
   681                         order_field = f.name
       
   682             except (IndexError, ValueError):
       
   683                 pass # Invalid ordering specified. Just use the default.
       
   684         if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
       
   685             order_type = params[ORDER_TYPE_VAR]
       
   686         return order_field, order_type
       
   687 
       
   688     def get_query_set(self):
       
   689         qs = self.manager.get_query_set()
       
   690         lookup_params = self.params.copy() # a dictionary of the query string
       
   691         for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
       
   692             if i in lookup_params:
       
   693                 del lookup_params[i]
       
   694         for key, value in lookup_params.items():
       
   695             if not isinstance(key, str):
       
   696                 # 'key' will be used as a keyword argument later, so Python
       
   697                 # requires it to be a string.
       
   698                 del lookup_params[key]
       
   699                 lookup_params[smart_str(key)] = value
       
   700 
       
   701         # Apply lookup parameters from the query string.
       
   702         qs = qs.filter(**lookup_params)
       
   703 
       
   704         # Use select_related() if one of the list_display options is a field
       
   705         # with a relationship.
       
   706         if self.lookup_opts.admin.list_select_related:
       
   707             qs = qs.select_related()
       
   708         else:
       
   709             for field_name in self.lookup_opts.admin.list_display:
       
   710                 try:
       
   711                     f = self.lookup_opts.get_field(field_name)
       
   712                 except models.FieldDoesNotExist:
       
   713                     pass
       
   714                 else:
       
   715                     if isinstance(f.rel, models.ManyToOneRel):
       
   716                         qs = qs.select_related()
       
   717                         break
       
   718 
       
   719         # Set ordering.
       
   720         if self.order_field:
       
   721             qs = qs.order_by('%s%s' % ((self.order_type == 'desc' and '-' or ''), self.order_field))
       
   722 
       
   723         # Apply keyword searches.
       
   724         def construct_search(field_name):
       
   725             if field_name.startswith('^'):
       
   726                 return "%s__istartswith" % field_name[1:]
       
   727             elif field_name.startswith('='):
       
   728                 return "%s__iexact" % field_name[1:]
       
   729             elif field_name.startswith('@'):
       
   730                 return "%s__search" % field_name[1:]
       
   731             else:
       
   732                 return "%s__icontains" % field_name
       
   733 
       
   734         if self.lookup_opts.admin.search_fields and self.query:
       
   735             for bit in self.query.split():
       
   736                 or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields]
       
   737                 other_qs = QuerySet(self.model)
       
   738                 other_qs.dup_select_related(qs)
       
   739                 other_qs = other_qs.filter(reduce(operator.or_, or_queries))
       
   740                 qs = qs & other_qs
       
   741 
       
   742         if self.opts.one_to_one_field:
       
   743             qs = qs.complex_filter(self.opts.one_to_one_field.rel.limit_choices_to)
       
   744 
       
   745         return qs
       
   746 
       
   747     def url_for_result(self, result):
       
   748         return "%s/" % quote(getattr(result, self.pk_attname))
       
   749 
       
   750 def change_list(request, app_label, model_name):
       
   751     model = models.get_model(app_label, model_name)
       
   752     if model is None:
       
   753         raise Http404("App %r, model %r, not found" % (app_label, model_name))
       
   754     if not request.user.has_perm(app_label + '.' + model._meta.get_change_permission()):
       
   755         raise PermissionDenied
       
   756     try:
       
   757         cl = ChangeList(request, model)
       
   758     except IncorrectLookupParameters:
       
   759         # Wacky lookup parameters were given, so redirect to the main
       
   760         # changelist page, without parameters, and pass an 'invalid=1'
       
   761         # parameter via the query string. If wacky parameters were given and
       
   762         # the 'invalid=1' parameter was already in the query string, something
       
   763         # is screwed up with the database, so display an error page.
       
   764         if ERROR_FLAG in request.GET.keys():
       
   765             return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
       
   766         return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
       
   767     c = template.RequestContext(request, {
       
   768         'title': cl.title,
       
   769         'is_popup': cl.is_popup,
       
   770         'cl': cl,
       
   771     })
       
   772     c.update({'has_add_permission': c['perms'][app_label][cl.opts.get_add_permission()]}),
       
   773     return render_to_response(['admin/%s/%s/change_list.html' % (app_label, cl.opts.object_name.lower()),
       
   774                                'admin/%s/change_list.html' % app_label,
       
   775                                'admin/change_list.html'], context_instance=c)
       
   776 change_list = staff_member_required(never_cache(change_list))