Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: jphalip/django
...
head fork: jphalip/django
Checking mergeability… Don't worry, you can still create the pull request.
  • 8 commits
  • 12 files changed
  • 1 commit comment
  • 2 contributors
Commits on May 27, 2012
@jphalip Converted the admin change view to CBV. eb8ea16
@jphalip Converted the admin delete and add views to CBV. a47bf38
@jphalip Converted the admin change list view to CBV. 55c91c7
@jphalip Converted the admin history view to CBV. 519c630
@jphalip Restored the admin views' decorators exactly to what they were before…
… the CBV conversion.
808e9ad
@jphalip Moved the admin CBVs into logically-named modules. 27f8d4b
Commits on May 28, 2012
@rasca rasca Refactored how the formsets are constructed
Merged all the code in a Mixin that looks for self.object and
instantiates the formsets with it or with the base model. This replaces
all the code that previously managed the formsets and their prefixes.
3c1c283
Commits on Jun 02, 2012
@jphalip Merge pull request #1 from rasca/admin-cbv
Simplified the construction of admin inline formsets.
68ebca9
View
4 django/contrib/admin/filters.py
@@ -186,7 +186,7 @@ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, cl):
- from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ from django.contrib.admin.views.list import EMPTY_CHANGELIST_VALUE
yield {
'selected': self.lookup_val is None and not self.lookup_val_isnull,
'query_string': cl.get_query_string({},
@@ -368,7 +368,7 @@ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, cl):
- from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ from django.contrib.admin.views.list import EMPTY_CHANGELIST_VALUE
yield {
'selected': (self.lookup_val is None
and self.lookup_val_isnull is None),
View
2  django/contrib/admin/helpers.py
@@ -169,7 +169,7 @@ def label_tag(self):
def contents(self):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
- from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ from django.contrib.admin.views.list import EMPTY_CHANGELIST_VALUE
field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
try:
f, attr, value = lookup_field(field, obj, model_admin)
View
449 django/contrib/admin/options.py
@@ -1,32 +1,31 @@
from functools import update_wrapper, partial
from django import forms
from django.conf import settings
-from django.forms.formsets import all_valid
from django.forms.models import (modelform_factory, modelformset_factory,
inlineformset_factory, BaseInlineFormSet)
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets, helpers
-from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
+from django.contrib.admin.util import flatten_fieldsets, model_format_dict
from django.contrib.admin.templatetags.admin_static import static
+from django.contrib.admin.views import (AdminChangeView, AdminAddView,
+ AdminDeleteView, AdminChangeListView, AdminHistoryView)
from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import ValidationError
from django.core.paginator import Paginator
from django.core.urlresolvers import reverse
-from django.db import models, transaction, router
+from django.db import models, transaction
from django.db.models.related import RelatedObject
from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
-from django.http import Http404, HttpResponse, HttpResponseRedirect
-from django.shortcuts import get_object_or_404
-from django.template.response import SimpleTemplateResponse, TemplateResponse
+from django.http import HttpResponse, HttpResponseRedirect
+from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.datastructures import SortedDict
from django.utils.html import escape, escapejs
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
-from django.utils.translation import ungettext
from django.utils.encoding import force_unicode
HORIZONTAL, VERTICAL = 1, 2
@@ -462,16 +461,16 @@ def get_changelist(self, request, **kwargs):
"""
Returns the ChangeList class for use on the changelist page.
"""
- from django.contrib.admin.views.main import ChangeList
+ from django.contrib.admin.views.list import ChangeList
return ChangeList
- def get_object(self, request, object_id):
+ def get_object(self, request, object_id, queryset=None):
"""
Returns an instance matching the primary key provided. ``None`` is
returned if no match is found (or the object_id failed validation
against the primary key field).
"""
- queryset = self.queryset(request)
+ queryset = queryset or self.queryset(request)
model = queryset.model
try:
object_id = model._meta.pk.to_python(object_id)
@@ -571,7 +570,7 @@ def get_actions(self, request):
"""
# If self.actions is explicitally set to None that means that we don't
# want *any* actions enabled on this page.
- from django.contrib.admin.views.main import IS_POPUP_VAR
+ from django.contrib.admin.views.list import IS_POPUP_VAR
if self.actions is None or IS_POPUP_VAR in request.GET:
return SortedDict()
@@ -922,424 +921,46 @@ def response_action(self, request, queryset):
@csrf_protect_m
@transaction.commit_on_success
def add_view(self, request, form_url='', extra_context=None):
- "The 'add' admin view for this model."
- model = self.model
- opts = model._meta
-
- if not self.has_add_permission(request):
- raise PermissionDenied
-
- ModelForm = self.get_form(request)
- formsets = []
- inline_instances = self.get_inline_instances(request)
- if request.method == 'POST':
- form = ModelForm(request.POST, request.FILES)
- if form.is_valid():
- new_object = self.save_form(request, form, change=False)
- form_validated = True
- else:
- form_validated = False
- new_object = self.model()
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(data=request.POST, files=request.FILES,
- instance=new_object,
- save_as_new="_saveasnew" in request.POST,
- prefix=prefix, queryset=inline.queryset(request))
- formsets.append(formset)
- if all_valid(formsets) and form_validated:
- self.save_model(request, new_object, form, False)
- self.save_related(request, form, formsets, False)
- self.log_addition(request, new_object)
- return self.response_add(request, new_object)
- else:
- # Prepare the dict of initial data from the request.
- # We have to special-case M2Ms as a list of comma-separated PKs.
- initial = dict(request.GET.items())
- for k in initial:
- try:
- f = opts.get_field(k)
- except models.FieldDoesNotExist:
- continue
- if isinstance(f, models.ManyToManyField):
- initial[k] = initial[k].split(",")
- form = ModelForm(initial=initial)
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(instance=self.model(), prefix=prefix,
- queryset=inline.queryset(request))
- formsets.append(formset)
-
- adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
- self.get_prepopulated_fields(request),
- self.get_readonly_fields(request),
- model_admin=self)
- media = self.media + adminForm.media
-
- inline_admin_formsets = []
- for inline, formset in zip(inline_instances, formsets):
- fieldsets = list(inline.get_fieldsets(request))
- readonly = list(inline.get_readonly_fields(request))
- prepopulated = dict(inline.get_prepopulated_fields(request))
- inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
- fieldsets, prepopulated, readonly, model_admin=self)
- inline_admin_formsets.append(inline_admin_formset)
- media = media + inline_admin_formset.media
-
- context = {
- 'title': _('Add %s') % force_unicode(opts.verbose_name),
- 'adminform': adminForm,
- 'is_popup': "_popup" in request.REQUEST,
- 'show_delete': False,
- 'media': media,
- 'inline_admin_formsets': inline_admin_formsets,
- 'errors': helpers.AdminErrorList(form, formsets),
- 'app_label': opts.app_label,
- }
- context.update(extra_context or {})
- return self.render_change_form(request, context, form_url=form_url, add=True)
+ """
+ The 'add' admin view for this model.
+ """
+ return AdminAddView(
+ admin_opts=self, form_url=form_url,
+ extra_context=extra_context).dispatch(request)
@csrf_protect_m
@transaction.commit_on_success
def change_view(self, request, object_id, form_url='', extra_context=None):
- "The 'change' admin view for this model."
- model = self.model
- opts = model._meta
-
- obj = self.get_object(request, unquote(object_id))
-
- if not self.has_change_permission(request, obj):
- raise PermissionDenied
-
- if obj is None:
- raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
-
- if request.method == 'POST' and "_saveasnew" in request.POST:
- return self.add_view(request, form_url=reverse('admin:%s_%s_add' %
- (opts.app_label, opts.module_name),
- current_app=self.admin_site.name))
-
- ModelForm = self.get_form(request, obj)
- formsets = []
- inline_instances = self.get_inline_instances(request)
- if request.method == 'POST':
- form = ModelForm(request.POST, request.FILES, instance=obj)
- if form.is_valid():
- form_validated = True
- new_object = self.save_form(request, form, change=True)
- else:
- form_validated = False
- new_object = obj
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(request.POST, request.FILES,
- instance=new_object, prefix=prefix,
- queryset=inline.queryset(request))
-
- formsets.append(formset)
-
- if all_valid(formsets) and form_validated:
- self.save_model(request, new_object, form, True)
- self.save_related(request, form, formsets, True)
- change_message = self.construct_change_message(request, form, formsets)
- self.log_change(request, new_object, change_message)
- return self.response_change(request, new_object)
-
- else:
- form = ModelForm(instance=obj)
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(instance=obj, prefix=prefix,
- queryset=inline.queryset(request))
- formsets.append(formset)
-
- adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
- self.get_prepopulated_fields(request, obj),
- self.get_readonly_fields(request, obj),
- model_admin=self)
- media = self.media + adminForm.media
-
- inline_admin_formsets = []
- for inline, formset in zip(inline_instances, formsets):
- fieldsets = list(inline.get_fieldsets(request, obj))
- readonly = list(inline.get_readonly_fields(request, obj))
- prepopulated = dict(inline.get_prepopulated_fields(request, obj))
- inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
- fieldsets, prepopulated, readonly, model_admin=self)
- inline_admin_formsets.append(inline_admin_formset)
- media = media + inline_admin_formset.media
-
- context = {
- 'title': _('Change %s') % force_unicode(opts.verbose_name),
- 'adminform': adminForm,
- 'object_id': object_id,
- 'original': obj,
- 'is_popup': "_popup" in request.REQUEST,
- 'media': media,
- 'inline_admin_formsets': inline_admin_formsets,
- 'errors': helpers.AdminErrorList(form, formsets),
- 'app_label': opts.app_label,
- }
- context.update(extra_context or {})
- return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
+ """
+ The 'change' admin view for this model.
+ """
+ return AdminChangeView(
+ admin_opts=self, form_url=form_url, extra_context=extra_context,
+ object_id=object_id).dispatch(request)
@csrf_protect_m
def changelist_view(self, request, extra_context=None):
"""
The 'change list' admin view for this model.
"""
- from django.contrib.admin.views.main import ERROR_FLAG
- opts = self.model._meta
- app_label = opts.app_label
- if not self.has_change_permission(request, None):
- raise PermissionDenied
-
- list_display = self.get_list_display(request)
- list_display_links = self.get_list_display_links(request, list_display)
-
- # Check actions to see if any are available on this changelist
- actions = self.get_actions(request)
- if actions:
- # Add the action checkboxes if there are any actions available.
- list_display = ['action_checkbox'] + list(list_display)
-
- ChangeList = self.get_changelist(request)
- try:
- cl = ChangeList(request, self.model, list_display,
- list_display_links, self.list_filter, self.date_hierarchy,
- self.search_fields, self.list_select_related,
- self.list_per_page, self.list_max_show_all, self.list_editable,
- self)
- 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 SimpleTemplateResponse('admin/invalid_setup.html', {
- 'title': _('Database error'),
- })
- return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
-
- # If the request was POSTed, this might be a bulk action or a bulk
- # edit. Try to look up an action or confirmation first, but if this
- # isn't an action the POST will fall through to the bulk edit check,
- # below.
- action_failed = False
- selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
-
- # Actions with no confirmation
- if (actions and request.method == 'POST' and
- 'index' in request.POST and '_save' not in request.POST):
- if selected:
- response = self.response_action(request, queryset=cl.get_query_set(request))
- if response:
- return response
- else:
- action_failed = True
- else:
- msg = _("Items must be selected in order to perform "
- "actions on them. No items have been changed.")
- self.message_user(request, msg)
- action_failed = True
-
- # Actions with confirmation
- if (actions and request.method == 'POST' and
- helpers.ACTION_CHECKBOX_NAME in request.POST and
- 'index' not in request.POST and '_save' not in request.POST):
- if selected:
- response = self.response_action(request, queryset=cl.get_query_set(request))
- if response:
- return response
- else:
- action_failed = True
-
- # If we're allowing changelist editing, we need to construct a formset
- # for the changelist given all the fields to be edited. Then we'll
- # use the formset to validate/process POSTed data.
- formset = cl.formset = None
-
- # Handle POSTed bulk-edit data.
- if (request.method == "POST" and cl.list_editable and
- '_save' in request.POST and not action_failed):
- FormSet = self.get_changelist_formset(request)
- formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
- if formset.is_valid():
- changecount = 0
- for form in formset.forms:
- if form.has_changed():
- obj = self.save_form(request, form, change=True)
- self.save_model(request, obj, form, change=True)
- self.save_related(request, form, formsets=[], change=True)
- change_msg = self.construct_change_message(request, form, None)
- self.log_change(request, obj, change_msg)
- changecount += 1
-
- if changecount:
- if changecount == 1:
- name = force_unicode(opts.verbose_name)
- else:
- name = force_unicode(opts.verbose_name_plural)
- msg = ungettext("%(count)s %(name)s was changed successfully.",
- "%(count)s %(name)s were changed successfully.",
- changecount) % {'count': changecount,
- 'name': name,
- 'obj': force_unicode(obj)}
- self.message_user(request, msg)
-
- return HttpResponseRedirect(request.get_full_path())
-
- # Handle GET -- construct a formset for display.
- elif cl.list_editable:
- FormSet = self.get_changelist_formset(request)
- formset = cl.formset = FormSet(queryset=cl.result_list)
-
- # Build the list of media to be used by the formset.
- if formset:
- media = self.media + formset.media
- else:
- media = self.media
-
- # Build the action form and populate it with available actions.
- if actions:
- action_form = self.action_form(auto_id=None)
- action_form.fields['action'].choices = self.get_action_choices(request)
- else:
- action_form = None
-
- selection_note_all = ungettext('%(total_count)s selected',
- 'All %(total_count)s selected', cl.result_count)
-
- context = {
- 'module_name': force_unicode(opts.verbose_name_plural),
- 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
- 'selection_note_all': selection_note_all % {'total_count': cl.result_count},
- 'title': cl.title,
- 'is_popup': cl.is_popup,
- 'cl': cl,
- 'media': media,
- 'has_add_permission': self.has_add_permission(request),
- 'app_label': app_label,
- 'action_form': action_form,
- 'actions_on_top': self.actions_on_top,
- 'actions_on_bottom': self.actions_on_bottom,
- 'actions_selection_counter': self.actions_selection_counter,
- }
- context.update(extra_context or {})
-
- return TemplateResponse(request, self.change_list_template or [
- 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
- 'admin/%s/change_list.html' % app_label,
- 'admin/change_list.html'
- ], context, current_app=self.admin_site.name)
+ return AdminChangeListView(
+ admin_opts=self, extra_context=extra_context).dispatch(request)
@csrf_protect_m
@transaction.commit_on_success
def delete_view(self, request, object_id, extra_context=None):
- "The 'delete' admin view for this model."
- opts = self.model._meta
- app_label = opts.app_label
-
- obj = self.get_object(request, unquote(object_id))
-
- if not self.has_delete_permission(request, obj):
- raise PermissionDenied
-
- if obj is None:
- raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
-
- using = router.db_for_write(self.model)
-
- # Populate deleted_objects, a data structure of all related objects that
- # will also be deleted.
- (deleted_objects, perms_needed, protected) = get_deleted_objects(
- [obj], opts, request.user, self.admin_site, using)
-
- if request.POST: # The user has already confirmed the deletion.
- if perms_needed:
- raise PermissionDenied
- obj_display = force_unicode(obj)
- self.log_deletion(request, obj, obj_display)
- self.delete_model(request, obj)
-
- self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
-
- if not self.has_change_permission(request, None):
- return HttpResponseRedirect(reverse('admin:index',
- current_app=self.admin_site.name))
- return HttpResponseRedirect(reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.module_name),
- current_app=self.admin_site.name))
-
- object_name = force_unicode(opts.verbose_name)
-
- if perms_needed or protected:
- title = _("Cannot delete %(name)s") % {"name": object_name}
- else:
- title = _("Are you sure?")
-
- context = {
- "title": title,
- "object_name": object_name,
- "object": obj,
- "deleted_objects": deleted_objects,
- "perms_lacking": perms_needed,
- "protected": protected,
- "opts": opts,
- "app_label": app_label,
- }
- context.update(extra_context or {})
-
- return TemplateResponse(request, self.delete_confirmation_template or [
- "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
- "admin/%s/delete_confirmation.html" % app_label,
- "admin/delete_confirmation.html"
- ], context, current_app=self.admin_site.name)
+ """The 'delete' admin view for this model."""
+ return AdminDeleteView(
+ admin_opts=self, extra_context=extra_context,
+ object_id=object_id).dispatch(request)
def history_view(self, request, object_id, extra_context=None):
- "The 'history' admin view for this model."
- from django.contrib.admin.models import LogEntry
- model = self.model
- opts = model._meta
- app_label = opts.app_label
- 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=unquote(object_id))
- context = {
- 'title': _('Change history: %s') % force_unicode(obj),
- 'action_list': action_list,
- 'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
- 'object': obj,
- 'app_label': app_label,
- 'opts': opts,
- }
- context.update(extra_context or {})
- return TemplateResponse(request, self.object_history_template or [
- "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
- "admin/%s/object_history.html" % app_label,
- "admin/object_history.html"
- ], context, current_app=self.admin_site.name)
+ """
+ The 'history' admin view for this model.
+ """
+ return AdminHistoryView(
+ admin_opts=self, extra_context=extra_context,
+ object_id=object_id).dispatch(request)
class InlineModelAdmin(BaseModelAdmin):
"""
View
4 django/contrib/admin/templatetags/admin_list.py
@@ -2,7 +2,7 @@
from django.contrib.admin.util import (lookup_field, display_for_field,
display_for_value, label_for_field)
-from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
+from django.contrib.admin.views.list import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
ORDER_VAR, PAGE_VAR, SEARCH_VAR)
from django.contrib.admin.templatetags.admin_static import static
from django.core.exceptions import ObjectDoesNotExist
@@ -12,7 +12,7 @@
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
-from django.utils.encoding import smart_unicode, force_unicode
+from django.utils.encoding import force_unicode
from django.template import Library
from django.template.loader import get_template
from django.template.context import Context
View
4 django/contrib/admin/util.py
@@ -314,7 +314,7 @@ def help_text_for_field(name, model):
def display_for_field(value, field):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
- from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ from django.contrib.admin.views.list import EMPTY_CHANGELIST_VALUE
if field.flatchoices:
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
@@ -338,7 +338,7 @@ def display_for_field(value, field):
def display_for_value(value, boolean=False):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
- from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
+ from django.contrib.admin.views.list import EMPTY_CHANGELIST_VALUE
if boolean:
return _boolean_icon(value)
View
4 django/contrib/admin/views/__init__.py
@@ -0,0 +1,4 @@
+from django.contrib.admin.views.edit import (
+ AdminDeleteView, AdminAddView, AdminChangeView)
+from django.contrib.admin.views.list import AdminChangeListView
+from django.contrib.admin.views.history import AdminHistoryView
View
10 django/contrib/admin/views/base.py
@@ -0,0 +1,10 @@
+
+class AdminViewMixin(object):
+
+ def __init__(self, **kwargs):
+ super(AdminViewMixin, self).__init__(**kwargs)
+ self.model = self.admin_opts.model
+ self.model_opts = self.model._meta
+
+ def get_queryset(self):
+ return self.admin_opts.queryset(self.request)
View
328 django/contrib/admin/views/edit.py
@@ -0,0 +1,328 @@
+from django.core.exceptions import PermissionDenied
+from django.core.urlresolvers import reverse
+from django.http import Http404, HttpResponseRedirect
+from django.utils.encoding import force_unicode
+from django.utils.translation import ugettext as _
+from django.views.generic.edit import UpdateView, CreateView, DeleteView
+from django.contrib.admin.util import unquote, get_deleted_objects
+from django.contrib.admin.views.base import AdminViewMixin
+from django.forms.formsets import all_valid
+from django.contrib.admin import helpers
+from django.utils.html import escape
+from django.db import models, router
+
+
+class FormSetsMixin(object):
+ def construct_formsets(self, **kwargs):
+ """
+ Constructs the formsets taking care of any clashing prefixes.
+
+ It accepts kwargs for the FormSet instantiating and adds the POST and
+ FILES if available.
+ """
+
+ prefixes = {}
+ # Check if we have an instance or if we are creating a new one
+ object = getattr(self, 'object', None)
+
+ for FormSet, inline in zip(self.admin_opts.get_formsets(
+ self.request, object), self.inline_instances):
+
+ prefix = FormSet.get_default_prefix()
+ prefixes[prefix] = prefixes.get(prefix, 0) + 1
+ if prefixes[prefix] != 1 or not prefix:
+ prefix = "%s-%s" % (prefix, prefixes[prefix])
+
+ formset_kwargs = {
+ 'prefix': prefix,
+ 'queryset': inline.queryset(self.request),
+ 'instance': object or self.model()
+ }
+
+ if self.request.method in ('POST', 'PUT'):
+ formset_kwargs.update({
+ 'data': self.request.POST,
+ 'files': self.request.FILES,
+ })
+
+ formset_kwargs.update(kwargs)
+
+ self.formsets.append(FormSet(**formset_kwargs))
+
+
+class AdminDeleteView(AdminViewMixin, DeleteView):
+
+ def dispatch(self, request, *args, **kwargs):
+ return super(AdminDeleteView, self).dispatch(request, *args, **kwargs)
+
+ def get_object(self, queryset=None):
+ object = self.admin_opts.get_object(
+ self.request, unquote(self.object_id), queryset=queryset)
+ if not self.admin_opts.has_delete_permission(self.request, object):
+ raise PermissionDenied
+ if object is None:
+ raise Http404(
+ _('%(name)s object with primary key %(key)r does not exist.') % {
+ 'name': force_unicode(self.model_opts.verbose_name),
+ 'key': escape(self.object_id)})
+ using = router.db_for_write(self.model)
+ # Populate deleted_objects, a data structure of all related objects that
+ # will also be deleted.
+ (self.deleted_objects, self.perms_needed, self.protected) = get_deleted_objects(
+ [object], self.model_opts, self.request.user, self.admin_opts.admin_site, using)
+ return object
+
+ def post(self, *args, **kwargs):
+ self.object = self.get_object()
+ # The user has already confirmed the deletion.
+ if self.perms_needed:
+ raise PermissionDenied
+ obj_display = force_unicode(self.object)
+ self.admin_opts.log_deletion(self.request, self.object, obj_display)
+ self.admin_opts.delete_model(self.request, self.object)
+
+ self.admin_opts.message_user(
+ self.request, _('The %(name)s "%(obj)s" was deleted successfully.') % {
+ 'name': force_unicode(self.model_opts.verbose_name),
+ 'obj': force_unicode(obj_display)})
+
+ if not self.admin_opts.has_change_permission(self.request, None):
+ return HttpResponseRedirect(
+ reverse('admin:index', current_app=self.admin_opts.admin_site.name))
+ return HttpResponseRedirect(
+ reverse('admin:%s_%s_changelist' % (
+ self.model_opts.app_label, self.model_opts.module_name),
+ current_app=self.admin_opts.admin_site.name))
+
+ def get_context_data(self, **kwargs):
+ context = super(AdminDeleteView, self).get_context_data(**kwargs)
+
+ object_name = force_unicode(self.model_opts.verbose_name)
+
+ if self.perms_needed or self.protected:
+ title = _("Cannot delete %(name)s") % {"name": object_name}
+ else:
+ title = _("Are you sure?")
+
+ context.update({
+ "title": title,
+ "object_name": object_name,
+ "object": self.object,
+ "deleted_objects": self.deleted_objects,
+ "perms_lacking": self.perms_needed,
+ "protected": self.protected,
+ "opts": self.model_opts,
+ "app_label": self.model_opts.app_label,
+ })
+
+ context.update(self.extra_context or {})
+ return context
+
+ def get_template_names(self):
+ form_template = self.admin_opts.delete_confirmation_template
+ if form_template:
+ return [form_template]
+ else:
+ return [
+ "admin/%s/%s/delete_confirmation.html" % (self.model_opts.app_label, self.model_opts.object_name.lower()),
+ "admin/%s/delete_confirmation.html" % self.model_opts.app_label,
+ "admin/delete_confirmation.html"
+ ]
+
+
+class AdminAddView(AdminViewMixin, FormSetsMixin, CreateView):
+
+ def dispatch(self, request, *args, **kwargs):
+ if not self.admin_opts.has_add_permission(request):
+ raise PermissionDenied
+
+ self.formsets = []
+ self.inline_instances = self.admin_opts.get_inline_instances(request)
+ return super(AdminAddView, self).dispatch(request, *args, **kwargs)
+
+ def get_form_class(self):
+ return self.admin_opts.get_form(self.request, self.object)
+
+ def get_template_names(self):
+ form_template = self.admin_opts.change_form_template
+ if form_template:
+ return [form_template]
+ else:
+ return [
+ "admin/%s/%s/change_form.html" % (self.model_opts.app_label, self.model_opts.object_name.lower()),
+ "admin/%s/change_form.html" % self.model_opts.app_label,
+ "admin/change_form.html"
+ ]
+
+ def form_valid(self, form):
+ self.object = self.admin_opts.save_form(self.request, form, change=False)
+ self.construct_formsets(save_as_new="_saveasnew" in self.request.POST)
+ if all_valid(self.formsets):
+ self.admin_opts.save_model(self.request, self.object, form, False)
+ self.admin_opts.save_related(self.request, form, self.formsets, False)
+ self.admin_opts.log_addition(self.request, self.object)
+ return self.admin_opts.response_add(self.request, self.object)
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def form_invalid(self, form):
+ self.object = self.model()
+ self.construct_formsets(save_as_new="_saveasnew" in self.request.POST)
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def get_form_kwargs(self):
+ kwargs = super(AdminAddView, self).get_form_kwargs()
+
+ # Prepare the dict of initial data from the request.
+ # We have to special-case M2Ms as a list of comma-separated PKs.
+ initial = dict(self.request.GET.items())
+ for k in initial:
+ try:
+ f = self.model_opts.get_field(k)
+ except models.FieldDoesNotExist:
+ continue
+ if isinstance(f, models.ManyToManyField):
+ initial[k] = initial[k].split(",")
+ kwargs.update({'initial': initial})
+ return kwargs
+
+ def render_to_response(self, context, **response_kwargs):
+ return self.admin_opts.render_change_form(
+ self.request, context, add=True, obj=self.object,
+ form_url=self.form_url)
+
+ def get(self, request, *args, **kwargs):
+ self.construct_formsets()
+ return super(CreateView, self).get(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super(AdminAddView, self).get_context_data(**kwargs)
+
+ adminForm = helpers.AdminForm(
+ kwargs['form'], list(self.admin_opts.get_fieldsets(self.request)),
+ self.admin_opts.get_prepopulated_fields(self.request),
+ self.admin_opts.get_readonly_fields(self.request),
+ model_admin=self.admin_opts)
+ media = self.admin_opts.media + adminForm.media
+
+ inline_admin_formsets = []
+ for inline, formset in zip(self.inline_instances, self.formsets):
+ fieldsets = list(inline.get_fieldsets(self.request))
+ readonly = list(inline.get_readonly_fields(self.request))
+ prepopulated = dict(inline.get_prepopulated_fields(self.request))
+ inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
+ fieldsets, prepopulated, readonly, model_admin=self.admin_opts)
+ inline_admin_formsets.append(inline_admin_formset)
+ media = media + inline_admin_formset.media
+
+ context.update({
+ 'title': _('Add %s') % force_unicode(self.model_opts.verbose_name),
+ 'adminform': adminForm,
+ 'is_popup': "_popup" in self.request.REQUEST,
+ 'show_delete': False,
+ 'media': media,
+ 'inline_admin_formsets': inline_admin_formsets,
+ 'errors': helpers.AdminErrorList(adminForm.form, self.formsets),
+ 'app_label': self.model_opts.app_label,
+ })
+ context.update(self.extra_context or {})
+ return context
+
+
+class AdminChangeView(AdminViewMixin, FormSetsMixin, UpdateView):
+
+ def dispatch(self, request, *args, **kwargs):
+ if request.method == 'POST' and "_saveasnew" in request.POST:
+ return self.admin_opts.add_view(
+ request, form_url=reverse('admin:%s_%s_add' %
+ (self.model_opts.app_label, self.model_opts.module_name),
+ current_app=self.admin_opts.admin_site.name))
+
+ self.formsets = []
+ self.inline_instances = self.admin_opts.get_inline_instances(request)
+ return super(AdminChangeView, self).dispatch(request, *args, **kwargs)
+
+ def get_form_class(self):
+ return self.admin_opts.get_form(self.request, self.object)
+
+ def get_template_names(self):
+ form_template = self.admin_opts.change_form_template
+ if form_template:
+ return [form_template]
+ else:
+ return [
+ "admin/%s/%s/change_form.html" % (self.model_opts.app_label, self.model_opts.object_name.lower()),
+ "admin/%s/change_form.html" % self.model_opts.app_label,
+ "admin/change_form.html"
+ ]
+
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object()
+ self.construct_formsets()
+ return super(UpdateView, self).get(request, *args, **kwargs)
+
+ def get_object(self, queryset=None):
+ obj = self.admin_opts.get_object(self.request, unquote(self.object_id), queryset=queryset)
+
+ if not self.admin_opts.has_change_permission(self.request, obj):
+ raise PermissionDenied
+
+ if obj is None:
+ raise Http404(
+ _('%(name)s object with primary key %(key)r does not exist.') % {
+ 'name': force_unicode(self.model_opts.verbose_name),
+ 'key': escape(self.object_id)})
+ return obj
+
+ def render_to_response(self, context, **response_kwargs):
+ return self.admin_opts.render_change_form(
+ self.request, context, change=True, obj=self.object,
+ form_url=self.form_url)
+
+ def form_valid(self, form):
+ self.object = self.admin_opts.save_form(self.request, form, change=True)
+ self.construct_formsets()
+ if all_valid(self.formsets):
+ self.admin_opts.save_model(self.request, self.object, form, True)
+ self.admin_opts.save_related(self.request, form, self.formsets, True)
+ change_message = self.admin_opts.construct_change_message(self.request, form, self.formsets)
+ self.admin_opts.log_change(self.request, self.object, change_message)
+ return self.admin_opts.response_change(self.request, self.object)
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def form_invalid(self, form):
+ self.construct_formsets()
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def get_context_data(self, **kwargs):
+ context = super(AdminChangeView, self).get_context_data(**kwargs)
+
+ adminform = helpers.AdminForm(
+ kwargs['form'], self.admin_opts.get_fieldsets(self.request, self.object),
+ self.admin_opts.get_prepopulated_fields(self.request, self.object),
+ self.admin_opts.get_readonly_fields(self.request, self.object),
+ model_admin=self.admin_opts)
+ media = self.admin_opts.media + adminform.media
+
+ inline_admin_formsets = []
+ for inline, formset in zip(self.inline_instances, self.formsets):
+ fieldsets = list(inline.get_fieldsets(self.request, self.object))
+ readonly = list(inline.get_readonly_fields(self.request, self.object))
+ prepopulated = dict(inline.get_prepopulated_fields(self.request, self.object))
+ inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
+ fieldsets, prepopulated, readonly, model_admin=self.admin_opts)
+ inline_admin_formsets.append(inline_admin_formset)
+ media = media + inline_admin_formset.media
+
+ context.update({
+ 'title': _('Change %s') % force_unicode(self.model_opts.verbose_name),
+ 'object_id': self.object_id,
+ 'is_popup': "_popup" in self.request.REQUEST,
+ 'adminform': adminform,
+ 'original': self.object,
+ 'media': media,
+ 'inline_admin_formsets': inline_admin_formsets,
+ 'errors': helpers.AdminErrorList(adminform.form, self.formsets),
+ 'app_label': self.model_opts.app_label,
+ })
+ context.update(self.extra_context or {})
+ return context
View
46 django/contrib/admin/views/history.py
@@ -0,0 +1,46 @@
+from django.shortcuts import get_object_or_404
+from django.utils.encoding import force_unicode
+from django.views.generic.base import TemplateView
+from django.utils.translation import ugettext as _
+from django.contrib.admin.util import unquote
+from django.utils.text import capfirst
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.admin.views.base import AdminViewMixin
+
+
+class AdminHistoryView(AdminViewMixin, TemplateView):
+
+ def get_context_data(self, **kwargs):
+ from django.contrib.admin.models import LogEntry
+
+ context = super(AdminHistoryView, self).get_context_data(**kwargs)
+
+ action_list = LogEntry.objects.filter(
+ object_id = self.object_id,
+ content_type__id__exact = ContentType.objects.get_for_model(self.model).id
+ ).select_related().order_by('action_time')
+ # If no history was found, see whether this object even exists.
+ obj = get_object_or_404(self.model, pk=unquote(self.object_id))
+
+ context.update({
+ 'title': _('Change history: %s') % force_unicode(obj),
+ 'action_list': action_list,
+ 'module_name': capfirst(force_unicode(self.model_opts.verbose_name_plural)),
+ 'object': obj,
+ 'app_label': self.model_opts.app_label,
+ 'opts': self.model_opts,
+ })
+
+ context.update(self.extra_context or {})
+ return context
+
+ def get_template_names(self):
+ form_template = self.admin_opts.object_history_template
+ if form_template:
+ return [form_template]
+ else:
+ return [
+ "admin/%s/%s/object_history.html" % (self.model_opts.app_label, self.model_opts.object_name.lower()),
+ "admin/%s/object_history.html" % self.model_opts.app_label,
+ "admin/object_history.html"
+ ]
View
561 django/contrib/admin/views/list.py
@@ -0,0 +1,561 @@
+import operator
+from functools import reduce
+
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponseRedirect
+from django.utils.encoding import force_unicode
+from django.views.generic.base import TemplateView
+from django.utils.translation import ugettext as _, ungettext
+from django.template.response import SimpleTemplateResponse
+from django.contrib.admin import helpers
+from django.contrib.admin.views.base import AdminViewMixin
+from django.contrib.admin.util import (quote, get_fields_from_path,
+ lookup_needs_distinct, prepare_lookup_value)
+from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
+from django.core.paginator import InvalidPage
+from django.db import models
+from django.db.models.fields import FieldDoesNotExist
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import smart_str
+from django.utils.translation import ugettext, ugettext_lazy
+from django.utils.http import urlencode
+
+
+# Changelist settings
+ALL_VAR = 'all'
+ORDER_VAR = 'o'
+ORDER_TYPE_VAR = 'ot'
+PAGE_VAR = 'p'
+SEARCH_VAR = 'q'
+TO_FIELD_VAR = 't'
+IS_POPUP_VAR = 'pop'
+ERROR_FLAG = 'e'
+
+IGNORED_PARAMS = (
+ ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
+
+# Text to display within change-list table cells if the value is blank.
+EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
+
+
+class ChangeList(object):
+ def __init__(self, request, model, list_display, list_display_links,
+ list_filter, date_hierarchy, search_fields, list_select_related,
+ list_per_page, list_max_show_all, list_editable, model_admin):
+ self.model = model
+ self.opts = model._meta
+ self.lookup_opts = self.opts
+ self.root_query_set = model_admin.queryset(request)
+ self.list_display = list_display
+ self.list_display_links = list_display_links
+ self.list_filter = list_filter
+ self.date_hierarchy = date_hierarchy
+ self.search_fields = search_fields
+ self.list_select_related = list_select_related
+ self.list_per_page = list_per_page
+ self.list_max_show_all = list_max_show_all
+ self.model_admin = model_admin
+
+ # 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.to_field = request.GET.get(TO_FIELD_VAR)
+ 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]
+
+ if self.is_popup:
+ self.list_editable = ()
+ else:
+ self.list_editable = list_editable
+ self.query = request.GET.get(SEARCH_VAR, '')
+ self.query_set = self.get_query_set(request)
+ self.get_results(request)
+ if self.is_popup:
+ title = ugettext('Select %s')
+ else:
+ title = ugettext('Select %s to change')
+ self.title = title % force_unicode(self.opts.verbose_name)
+ self.pk_attname = self.lookup_opts.pk.attname
+
+ def get_filters(self, request):
+ from django.contrib.admin import FieldListFilter
+ from django.contrib.admin.options import IncorrectLookupParameters
+
+ lookup_params = self.params.copy() # a dictionary of the query string
+ use_distinct = False
+
+ # Remove all the parameters that are globally and systematically
+ # ignored.
+ for ignored in IGNORED_PARAMS:
+ if ignored in lookup_params:
+ del lookup_params[ignored]
+
+ # Normalize the types of keys
+ 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
+
+ if not self.model_admin.lookup_allowed(key, value):
+ raise SuspiciousOperation("Filtering by %s not allowed" % key)
+
+ filter_specs = []
+ if self.list_filter:
+ for list_filter in self.list_filter:
+ if callable(list_filter):
+ # This is simply a custom list filter class.
+ spec = list_filter(request, lookup_params,
+ self.model, self.model_admin)
+ else:
+ field_path = None
+ if isinstance(list_filter, (tuple, list)):
+ # This is a custom FieldListFilter class for a given field.
+ field, field_list_filter_class = list_filter
+ else:
+ # This is simply a field name, so use the default
+ # FieldListFilter class that has been registered for
+ # the type of the given field.
+ field, field_list_filter_class = list_filter, FieldListFilter.create
+ if not isinstance(field, models.Field):
+ field_path = field
+ field = get_fields_from_path(self.model, field_path)[-1]
+ spec = field_list_filter_class(field, request, lookup_params,
+ self.model, self.model_admin, field_path=field_path)
+ # Check if we need to use distinct()
+ use_distinct = (use_distinct or
+ lookup_needs_distinct(self.lookup_opts,
+ field_path))
+ if spec and spec.has_output():
+ filter_specs.append(spec)
+
+ # At this point, all the parameters used by the various ListFilters
+ # have been removed from lookup_params, which now only contains other
+ # parameters passed via the query string. We now loop through the
+ # remaining parameters both to ensure that all the parameters are valid
+ # fields and to determine if at least one of them needs distinct(). If
+ # the lookup parameters aren't real fields, then bail out.
+ try:
+ for key, value in lookup_params.items():
+ lookup_params[key] = prepare_lookup_value(key, value)
+ use_distinct = (use_distinct or
+ lookup_needs_distinct(self.lookup_opts, key))
+ return filter_specs, bool(filter_specs), lookup_params, use_distinct
+ except FieldDoesNotExist as e:
+ raise IncorrectLookupParameters(e)
+
+ 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 v is None:
+ if k in p:
+ del p[k]
+ else:
+ p[k] = v
+ return '?%s' % urlencode(p)
+
+ def get_results(self, request):
+ from django.contrib.admin.options import IncorrectLookupParameters
+
+ paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
+ # Get the number of objects, with admin filters applied.
+ result_count = paginator.count
+
+ # 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.root_query_set.count()
+
+ can_show_all = result_count <= self.list_max_show_all
+ multi_page = result_count > self.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 = self.query_set._clone()
+ else:
+ try:
+ result_list = paginator.page(self.page_num+1).object_list
+ except InvalidPage:
+ raise IncorrectLookupParameters
+
+ 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_default_ordering(self):
+ ordering = []
+ if self.model_admin.ordering:
+ ordering = self.model_admin.ordering
+ elif self.lookup_opts.ordering:
+ ordering = self.lookup_opts.ordering
+ return ordering
+
+ def get_ordering_field(self, field_name):
+ """
+ Returns the proper model field name corresponding to the given
+ field_name to use for ordering. field_name may either be the name of a
+ proper model field or the name of a method (on the admin or model) or a
+ callable with the 'admin_order_field' attribute. Returns None if no
+ proper model field name can be matched.
+ """
+ try:
+ field = self.lookup_opts.get_field(field_name)
+ return field.name
+ except models.FieldDoesNotExist:
+ # See whether field_name is a name of a non-field
+ # that allows sorting.
+ if callable(field_name):
+ attr = field_name
+ elif hasattr(self.model_admin, field_name):
+ attr = getattr(self.model_admin, field_name)
+ else:
+ attr = getattr(self.model, field_name)
+ return getattr(attr, 'admin_order_field', None)
+
+ def get_ordering(self, request, queryset):
+ """
+ Returns the list of ordering fields for the change list.
+ First we check the get_ordering() method in model admin, then we check
+ the object's default ordering. Then, any manually-specified ordering
+ from the query string overrides anything. Finally, a deterministic
+ order is guaranteed by ensuring the primary key is used as the last
+ ordering field.
+ """
+ params = self.params
+ ordering = list(self.model_admin.get_ordering(request)
+ or self._get_default_ordering())
+ if ORDER_VAR in params:
+ # Clear ordering and used params
+ ordering = []
+ order_params = params[ORDER_VAR].split('.')
+ for p in order_params:
+ try:
+ none, pfx, idx = p.rpartition('-')
+ field_name = self.list_display[int(idx)]
+ order_field = self.get_ordering_field(field_name)
+ if not order_field:
+ continue # No 'admin_order_field', skip it
+ ordering.append(pfx + order_field)
+ except (IndexError, ValueError):
+ continue # Invalid ordering specified, skip it.
+
+ # Add the given query's ordering fields, if any.
+ ordering.extend(queryset.query.order_by)
+
+ # Ensure that the primary key is systematically present in the list of
+ # ordering fields so we can guarantee a deterministic order across all
+ # database backends.
+ pk_name = self.lookup_opts.pk.name
+ if not (set(ordering) & set(['pk', '-pk', pk_name, '-' + pk_name])):
+ # The two sets do not intersect, meaning the pk isn't present. So
+ # we add it.
+ ordering.append('-pk')
+
+ return ordering
+
+ def get_ordering_field_columns(self):
+ """
+ Returns a SortedDict of ordering field column numbers and asc/desc
+ """
+
+ # We must cope with more than one column having the same underlying sort
+ # field, so we base things on column numbers.
+ ordering = self._get_default_ordering()
+ ordering_fields = SortedDict()
+ if ORDER_VAR not in self.params:
+ # for ordering specified on ModelAdmin or model Meta, we don't know
+ # the right column numbers absolutely, because there might be more
+ # than one column associated with that ordering, so we guess.
+ for field in ordering:
+ if field.startswith('-'):
+ field = field[1:]
+ order_type = 'desc'
+ else:
+ order_type = 'asc'
+ for index, attr in enumerate(self.list_display):
+ if self.get_ordering_field(attr) == field:
+ ordering_fields[index] = order_type
+ break
+ else:
+ for p in self.params[ORDER_VAR].split('.'):
+ none, pfx, idx = p.rpartition('-')
+ try:
+ idx = int(idx)
+ except ValueError:
+ continue # skip it
+ ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
+ return ordering_fields
+
+ def get_query_set(self, request):
+ from django.contrib.admin.options import IncorrectLookupParameters
+
+ # First, we collect all the declared list filters.
+ (self.filter_specs, self.has_filters, remaining_lookup_params,
+ use_distinct) = self.get_filters(request)
+
+ # Then, we let every list filter modify the queryset to its liking.
+ qs = self.root_query_set
+ for filter_spec in self.filter_specs:
+ new_qs = filter_spec.queryset(request, qs)
+ if new_qs is not None:
+ qs = new_qs
+
+ try:
+ # Finally, we apply the remaining lookup parameters from the query
+ # string (i.e. those that haven't already been processed by the
+ # filters).
+ qs = qs.filter(**remaining_lookup_params)
+ except (SuspiciousOperation, ImproperlyConfigured):
+ # Allow certain types of errors to be re-raised as-is so that the
+ # caller can treat them in a special way.
+ raise
+ except Exception as e:
+ # Every other error is caught with a naked except, because we don't
+ # have any other way of validating lookup parameters. They might be
+ # invalid if the keyword arguments are incorrect, or if the values
+ # are not in the correct type, so we might get FieldError,
+ # ValueError, ValidationError, or ?.
+ raise IncorrectLookupParameters(e)
+
+ # Use select_related() if one of the list_display options is a field
+ # with a relationship and the provided queryset doesn't already have
+ # select_related defined.
+ if not qs.query.select_related:
+ if self.list_select_related:
+ qs = qs.select_related()
+ else:
+ for field_name in self.list_display:
+ try:
+ field = self.lookup_opts.get_field(field_name)
+ except models.FieldDoesNotExist:
+ pass
+ else:
+ if isinstance(field.rel, models.ManyToOneRel):
+ qs = qs.select_related()
+ break
+
+ # Set ordering.
+ ordering = self.get_ordering(request, qs)
+ qs = qs.order_by(*ordering)
+
+ # 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.search_fields and self.query:
+ orm_lookups = [construct_search(str(search_field))
+ for search_field in self.search_fields]
+ for bit in self.query.split():
+ or_queries = [models.Q(**{orm_lookup: bit})
+ for orm_lookup in orm_lookups]
+ qs = qs.filter(reduce(operator.or_, or_queries))
+ if not use_distinct:
+ for search_spec in orm_lookups:
+ if lookup_needs_distinct(self.lookup_opts, search_spec):
+ use_distinct = True
+ break
+
+ if use_distinct:
+ return qs.distinct()
+ else:
+ return qs
+
+ def url_for_result(self, result):
+ return "%s/" % quote(getattr(result, self.pk_attname))
+
+
+class AdminChangeListView(AdminViewMixin, TemplateView):
+
+ def dispatch(self, request, *args, **kwargs):
+ from django.contrib.admin.options import IncorrectLookupParameters
+
+ if not self.admin_opts.has_change_permission(request, None):
+ raise PermissionDenied
+
+ list_display = self.admin_opts.get_list_display(request)
+ list_display_links = self.admin_opts.get_list_display_links(request, list_display)
+
+ # Check actions to see if any are available on this changelist
+ self.actions = self.admin_opts.get_actions(request)
+ if self.actions:
+ # Add the action checkboxes if there are any actions available.
+ list_display = ['action_checkbox'] + list(list_display)
+
+ ChangeList = self.admin_opts.get_changelist(request)
+ try:
+ self.changelist = ChangeList(request, self.admin_opts.model, list_display,
+ list_display_links, self.admin_opts.list_filter, self.admin_opts.date_hierarchy,
+ self.admin_opts.search_fields, self.admin_opts.list_select_related,
+ self.admin_opts.list_per_page, self.admin_opts.list_max_show_all, self.admin_opts.list_editable,
+ self.admin_opts)
+ 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 SimpleTemplateResponse('admin/invalid_setup.html', {
+ 'title': _('Database error'),
+ })
+ return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
+
+ # If we're allowing changelist editing, we need to construct a formset
+ # for the changelist given all the fields to be edited. Then we'll
+ # use the formset to validate/process POSTed data.
+ self.formset = self.changelist.formset = None
+
+ return super(AdminChangeListView, self).dispatch(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ # If the request was POSTed, this might be a bulk action or a bulk
+ # edit. Try to look up an action or confirmation first, but if this
+ # isn't an action the POST will fall through to the bulk edit check,
+ # below.
+ action_failed = False
+ selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
+
+ # Actions with no confirmation
+ if (self.actions and request.method == 'POST' and
+ 'index' in request.POST and '_save' not in request.POST):
+ if selected:
+ response = self.admin_opts.response_action(request, queryset=self.changelist.get_query_set(request))
+ if response:
+ return response
+ else:
+ action_failed = True
+ else:
+ msg = _("Items must be selected in order to perform "
+ "actions on them. No items have been changed.")
+ self.admin_opts.message_user(request, msg)
+ action_failed = True
+
+ # Actions with confirmation
+ if (self.actions and
+ helpers.ACTION_CHECKBOX_NAME in request.POST and
+ 'index' not in request.POST and
+ '_save' not in request.POST and
+ selected):
+ response = self.admin_opts.response_action(request, queryset=self.changelist.get_query_set(request))
+ if response:
+ return response
+ else:
+ action_failed = True
+
+ # Handle POSTed bulk-edit data.
+ if (self.changelist.list_editable and
+ '_save' in request.POST and not action_failed):
+ FormSet = self.admin_opts.get_changelist_formset(request)
+ self.formset = self.changelist.formset = FormSet(request.POST, request.FILES, queryset=self.changelist.result_list)
+ if self.formset.is_valid():
+ changecount = 0
+ for form in self.formset.forms:
+ if form.has_changed():
+ obj = self.admin_opts.save_form(request, form, change=True)
+ self.admin_opts.save_model(request, obj, form, change=True)
+ self.admin_opts.save_related(request, form, formsets=[], change=True)
+ change_msg = self.admin_opts.construct_change_message(request, form, None)
+ self.admin_opts.log_change(request, obj, change_msg)
+ changecount += 1
+
+ if changecount:
+ if changecount == 1:
+ name = force_unicode(self.model_opts.verbose_name)
+ else:
+ name = force_unicode(self.model_opts.verbose_name_plural)
+ msg = ungettext("%(count)s %(name)s was changed successfully.",
+ "%(count)s %(name)s were changed successfully.",
+ changecount) % {'count': changecount,
+ 'name': name,
+ 'obj': force_unicode(obj)}
+ self.admin_opts.message_user(request, msg)
+
+ return HttpResponseRedirect(request.get_full_path())
+
+ return self.render_to_response(self.get_context_data(), current_app=self.admin_opts.admin_site.name)
+
+ def get(self, request, *args, **kwargs):
+ # Handle GET -- construct a formset for display.
+ if self.changelist.list_editable:
+ FormSet = self.admin_opts.get_changelist_formset(request)
+ self.formset = self.changelist.formset = FormSet(queryset=self.changelist.result_list)
+ return self.render_to_response(self.get_context_data(), current_app=self.admin_opts.admin_site.name)
+
+ def get_template_names(self):
+ form_template = self.admin_opts.change_list_template
+ if form_template:
+ return [form_template]
+ else:
+ return [
+ "admin/%s/%s/change_list.html" % (self.model_opts.app_label, self.model_opts.object_name.lower()),
+ "admin/%s/change_list.html" % self.model_opts.app_label,
+ "admin/change_list.html"
+ ]
+
+ def get_context_data(self, **kwargs):
+ context = super(AdminChangeListView, self).get_context_data(**kwargs)
+
+ # Build the list of media to be used by the formset.
+ if self.formset:
+ media = self.admin_opts.media + self.formset.media
+ else:
+ media = self.admin_opts.media
+
+ # Build the action form and populate it with available actions.
+ if self.actions:
+ self.action_form = self.admin_opts.action_form(auto_id=None)
+ self.action_form.fields['action'].choices = self.admin_opts.get_action_choices(self.request)
+ else:
+ self.action_form = None
+
+ selection_note_all = ungettext('%(total_count)s selected',
+ 'All %(total_count)s selected', self.changelist.result_count)
+
+ context.update({
+ 'module_name': force_unicode(self.model_opts.verbose_name_plural),
+ 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(self.changelist.result_list)},
+ 'selection_note_all': selection_note_all % {'total_count': self.changelist.result_count},
+ 'title': self.changelist.title,
+ 'is_popup': self.changelist.is_popup,
+ 'cl': self.changelist,
+ 'media': media,
+ 'has_add_permission': self.admin_opts.has_add_permission(self.request),
+ 'app_label': self.model_opts.app_label,
+ 'action_form': self.action_form,
+ 'actions_on_top': self.admin_opts.actions_on_top,
+ 'actions_on_bottom': self.admin_opts.actions_on_bottom,
+ 'actions_selection_counter': self.admin_opts.actions_selection_counter,
+ })
+
+ context.update(self.extra_context or {})
+ return context
+
View
384 django/contrib/admin/views/main.py
@@ -1,379 +1,5 @@
-import operator
-from functools import reduce
-
-from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
-from django.core.paginator import InvalidPage
-from django.db import models
-from django.db.models.fields import FieldDoesNotExist
-from django.utils.datastructures import SortedDict
-from django.utils.encoding import force_unicode, smart_str
-from django.utils.translation import ugettext, ugettext_lazy
-from django.utils.http import urlencode
-
-from django.contrib.admin import FieldListFilter
-from django.contrib.admin.options import IncorrectLookupParameters
-from django.contrib.admin.util import (quote, get_fields_from_path,
- lookup_needs_distinct, prepare_lookup_value)
-
-# Changelist settings
-ALL_VAR = 'all'
-ORDER_VAR = 'o'
-ORDER_TYPE_VAR = 'ot'
-PAGE_VAR = 'p'
-SEARCH_VAR = 'q'
-TO_FIELD_VAR = 't'
-IS_POPUP_VAR = 'pop'
-ERROR_FLAG = 'e'
-
-IGNORED_PARAMS = (
- ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
-
-# Text to display within change-list table cells if the value is blank.
-EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
-
-
-class ChangeList(object):
- def __init__(self, request, model, list_display, list_display_links,
- list_filter, date_hierarchy, search_fields, list_select_related,
- list_per_page, list_max_show_all, list_editable, model_admin):
- self.model = model
- self.opts = model._meta
- self.lookup_opts = self.opts
- self.root_query_set = model_admin.queryset(request)
- self.list_display = list_display
- self.list_display_links = list_display_links
- self.list_filter = list_filter
- self.date_hierarchy = date_hierarchy
- self.search_fields = search_fields
- self.list_select_related = list_select_related
- self.list_per_page = list_per_page
- self.list_max_show_all = list_max_show_all
- self.model_admin = model_admin
-
- # 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.to_field = request.GET.get(TO_FIELD_VAR)
- 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]
-
- if self.is_popup:
- self.list_editable = ()
- else:
- self.list_editable = list_editable
- self.query = request.GET.get(SEARCH_VAR, '')
- self.query_set = self.get_query_set(request)
- self.get_results(request)
- if self.is_popup:
- title = ugettext('Select %s')
- else:
- title = ugettext('Select %s to change')
- self.title = title % force_unicode(self.opts.verbose_name)
- self.pk_attname = self.lookup_opts.pk.attname
-
- def get_filters(self, request):
- lookup_params = self.params.copy() # a dictionary of the query string
- use_distinct = False
-
- # Remove all the parameters that are globally and systematically
- # ignored.
- for ignored in IGNORED_PARAMS:
- if ignored in lookup_params:
- del lookup_params[ignored]
-
- # Normalize the types of keys
- 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
-
- if not self.model_admin.lookup_allowed(key, value):
- raise SuspiciousOperation("Filtering by %s not allowed" % key)
-
- filter_specs = []
- if self.list_filter:
- for list_filter in self.list_filter:
- if callable(list_filter):
- # This is simply a custom list filter class.
- spec = list_filter(request, lookup_params,
- self.model, self.model_admin)
- else:
- field_path = None
- if isinstance(list_filter, (tuple, list)):
- # This is a custom FieldListFilter class for a given field.
- field, field_list_filter_class = list_filter
- else:
- # This is simply a field name, so use the default
- # FieldListFilter class that has been registered for
- # the type of the given field.
- field, field_list_filter_class = list_filter, FieldListFilter.create
- if not isinstance(field, models.Field):
- field_path = field
- field = get_fields_from_path(self.model, field_path)[-1]
- spec = field_list_filter_class(field, request, lookup_params,
- self.model, self.model_admin, field_path=field_path)
- # Check if we need to use distinct()
- use_distinct = (use_distinct or
- lookup_needs_distinct(self.lookup_opts,
- field_path))
- if spec and spec.has_output():
- filter_specs.append(spec)
-
- # At this point, all the parameters used by the various ListFilters
- # have been removed from lookup_params, which now only contains other
- # parameters passed via the query string. We now loop through the
- # remaining parameters both to ensure that all the parameters are valid
- # fields and to determine if at least one of them needs distinct(). If
- # the lookup parameters aren't real fields, then bail out.
- try:
- for key, value in lookup_params.items():
- lookup_params[key] = prepare_lookup_value(key, value)
- use_distinct = (use_distinct or
- lookup_needs_distinct(self.lookup_opts, key))
- return filter_specs, bool(filter_specs), lookup_params, use_distinct
- except FieldDoesNotExist as e:
- raise IncorrectLookupParameters(e)
-
- 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 v is None:
- if k in p:
- del p[k]
- else:
- p[k] = v
- return '?%s' % urlencode(p)
-
- def get_results(self, request):
- paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
- # Get the number of objects, with admin filters applied.
- result_count = paginator.count
-
- # 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.root_query_set.count()
-
- can_show_all = result_count <= self.list_max_show_all
- multi_page = result_count > self.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 = self.query_set._clone()
- else:
- try:
- result_list = paginator.page(self.page_num+1).object_list
- except InvalidPage:
- raise IncorrectLookupParameters
-
- 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_default_ordering(self):
- ordering = []
- if self.model_admin.ordering:
- ordering = self.model_admin.ordering
- elif self.lookup_opts.ordering:
- ordering = self.lookup_opts.ordering
- return ordering
-
- def get_ordering_field(self, field_name):
- """
- Returns the proper model field name corresponding to the given
- field_name to use for ordering. field_name may either be the name of a
- proper model field or the name of a method (on the admin or model) or a
- callable with the 'admin_order_field' attribute. Returns None if no
- proper model field name can be matched.
- """
- try:
- field = self.lookup_opts.get_field(field_name)
- return field.name
- except models.FieldDoesNotExist:
- # See whether field_name is a name of a non-field
- # that allows sorting.
- if callable(field_name):
- attr = field_name
- elif hasattr(self.model_admin, field_name):
- attr = getattr(self.model_admin, field_name)
- else:
- attr = getattr(self.model, field_name)
- return getattr(attr, 'admin_order_field', None)
-
- def get_ordering(self, request, queryset):
- """
- Returns the list of ordering fields for the change list.
- First we check the get_ordering() method in model admin, then we check
- the object's default ordering. Then, any manually-specified ordering
- from the query string overrides anything. Finally, a deterministic
- order is guaranteed by ensuring the primary key is used as the last
- ordering field.
- """
- params = self.params
- ordering = list(self.model_admin.get_ordering(request)
- or self._get_default_ordering())
- if ORDER_VAR in params:
- # Clear ordering and used params
- ordering = []
- order_params = params[ORDER_VAR].split('.')
- for p in order_params:
- try:
- none, pfx, idx = p.rpartition('-')
- field_name = self.list_display[int(idx)]
- order_field = self.get_ordering_field(field_name)
- if not order_field:
- continue # No 'admin_order_field', skip it
- ordering.append(pfx + order_field)
- except (IndexError, ValueError):
- continue # Invalid ordering specified, skip it.
-
- # Add the given query's ordering fields, if any.
- ordering.extend(queryset.query.order_by)
-
- # Ensure that the primary key is systematically present in the list of
- # ordering fields so we can guarantee a deterministic order across all
- # database backends.
- pk_name = self.lookup_opts.pk.name
- if not (set(ordering) & set(['pk', '-pk', pk_name, '-' + pk_name])):
- # The two sets do not intersect, meaning the pk isn't present. So
- # we add it.
- ordering.append('-pk')
-
- return ordering
-
- def get_ordering_field_columns(self):
- """
- Returns a SortedDict of ordering field column numbers and asc/desc
- """
-
- # We must cope with more than one column having the same underlying sort
- # field, so we base things on column numbers.
- ordering = self._get_default_ordering()
- ordering_fields = SortedDict()
- if ORDER_VAR not in self.params:
- # for ordering specified on ModelAdmin or model Meta, we don't know
- # the right column numbers absolutely, because there might be more
- # than one column associated with that ordering, so we guess.
- for field in ordering:
- if field.startswith('-'):
- field = field[1:]
- order_type = 'desc'
- else:
- order_type = 'asc'
- for index, attr in enumerate(self.list_display):
- if self.get_ordering_field(attr) == field:
- ordering_fields[index] = order_type
- break
- else:
- for p in self.params[ORDER_VAR].split('.'):
- none, pfx, idx = p.rpartition('-')
- try:
- idx = int(idx)
- except ValueError:
- continue # skip it
- ordering_fields[idx] = 'desc' if pfx == '-' else 'asc'
- return ordering_fields
-
- def get_query_set(self, request):
- # First, we collect all the declared list filters.
- (self.filter_specs, self.has_filters, remaining_lookup_params,
- use_distinct) = self.get_filters(request)
-
- # Then, we let every list filter modify the queryset to its liking.
- qs = self.root_query_set
- for filter_spec in self.filter_specs:
- new_qs = filter_spec.queryset(request, qs)
- if new_qs is not None:
- qs = new_qs
-
- try:
- # Finally, we apply the remaining lookup parameters from the query
- # string (i.e. those that haven't already been processed by the
- # filters).
- qs = qs.filter(**remaining_lookup_params)
- except (SuspiciousOperation, ImproperlyConfigured):
- # Allow certain types of errors to be re-raised as-is so that the
- # caller can treat them in a special way.
- raise
- except Exception as e:
- # Every other error is caught with a naked except, because we don't
- # have any other way of validating lookup parameters. They might be
- # invalid if the keyword arguments are incorrect, or if the values
- # are not in the correct type, so we might get FieldError,
- # ValueError, ValidationError, or ?.
- raise IncorrectLookupParameters(e)
-
- # Use select_related() if one of the list_display options is a field
- # with a relationship and the provided queryset doesn't already have
- # select_related defined.
- if not qs.query.select_related:
- if self.list_select_related:
- qs = qs.select_related()
- else:
- for field_name in self.list_display:
- try:
- field = self.lookup_opts.get_field(field_name)
- except models.FieldDoesNotExist:
- pass
- else:
- if isinstance(field.rel, models.ManyToOneRel):
- qs = qs.select_related()
- break
-
- # Set ordering.
- ordering = self.get_ordering(request, qs)
- qs = qs.order_by(*ordering)
-
- # 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.search_fields and self.query:
- orm_lookups = [construct_search(str(search_field))
- for search_field in self.search_fields]
- for bit in self.query.split():
- or_queries = [models.Q(**{orm_lookup: bit})
- for orm_lookup in orm_lookups]
- qs = qs.filter(reduce(operator.or_, or_queries))
- if not use_distinct:
- for search_spec in orm_lookups:
- if lookup_needs_distinct(self.lookup_opts, search_spec):
- use_distinct = True
- break
-
- if use_distinct:
- return qs.distinct()
- else:
- return qs
-
- def url_for_result(self, result):
- return "%s/" % quote(getattr(result, self.pk_attname))
+# The following imports are here for backwards compatibility reasons.
+from django.contrib.admin.views.list import (
+ ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR, TO_FIELD_VAR,
+ IS_POPUP_VAR, ERROR_FLAG, IGNORED_PARAMS, EMPTY_CHANGELIST_VALUE,
+ ChangeList)
View
2  django/contrib/admin/widgets.py
@@ -168,7 +168,7 @@ def base_url_parameters(self):
return url_params_from_lookup_dict(self.rel.limit_choices_to)
def url_parameters(self):
- from django.contrib.admin.views.main import TO_FIELD_VAR
+ from django.contrib.admin.views.list import TO_FIELD_VAR
params = self.base_url_parameters()
params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
return params

Showing you all comments on commits in this comparison.

@charettes

Since you removed decorators you don't have to override dispatch anymore. Good work BTW :).

Something went wrong with that request. Please try again.