|
- import copy
- import operator
- from collections import OrderedDict
- from functools import partial, reduce, update_wrapper
-
- from django import forms
- from django.conf import settings
- from django.contrib import messages
- from django.contrib.admin import helpers, widgets
- from django.contrib.admin.checks import (
- BaseModelAdminChecks, InlineModelAdminChecks, ModelAdminChecks,
- )
- from django.contrib.admin.exceptions import DisallowedModelAdminToField
- from django.contrib.admin.templatetags.admin_static import static
- from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
- from django.contrib.admin.utils import (
- NestedObjects, flatten_fieldsets, get_deleted_objects,
- lookup_needs_distinct, model_format_dict, quote, unquote,
- )
- from django.contrib.auth import get_permission_codename
- from django.core.exceptions import (
- FieldDoesNotExist, FieldError, PermissionDenied, ValidationError,
- )
- from django.core.paginator import Paginator
- from django.core.urlresolvers import reverse
- from django.db import models, router, transaction
- from django.db.models.constants import LOOKUP_SEP
- from django.db.models.fields import BLANK_CHOICE_DASH
- from django.forms.formsets import DELETION_FIELD_NAME, all_valid
- from django.forms.models import (
- BaseInlineFormSet, inlineformset_factory, modelform_defines_fields,
- modelform_factory, modelformset_factory,
- )
- from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
- from django.http import Http404, HttpResponseRedirect
- from django.http.response import HttpResponseBase
- from django.template.response import SimpleTemplateResponse, TemplateResponse
- from django.utils import six
- from django.utils.decorators import method_decorator
- from django.utils.encoding import force_text, python_2_unicode_compatible
- from django.utils.html import escape
- from django.utils.http import urlencode
- from django.utils.safestring import mark_safe
- from django.utils.text import capfirst, get_text_list
- from django.utils.translation import string_concat, ugettext as _, ungettext
- from django.views.decorators.csrf import csrf_protect
- from django.views.generic import RedirectView
-
- IS_POPUP_VAR = '_popup'
- TO_FIELD_VAR = '_to_field'
-
-
- HORIZONTAL, VERTICAL = 1, 2
-
-
- def get_content_type_for_model(obj):
- # Since this module gets imported in the application's root package,
- # it cannot import models from other applications at the module level.
- from django.contrib.contenttypes.models import ContentType
- return ContentType.objects.get_for_model(obj, for_concrete_model=False)
-
-
- def get_ul_class(radio_style):
- return 'radiolist' if radio_style == VERTICAL else 'radiolist inline'
-
-
- class IncorrectLookupParameters(Exception):
- pass
-
- # Defaults for formfield_overrides. ModelAdmin subclasses can change this
- # by adding to ModelAdmin.formfield_overrides.
-
- FORMFIELD_FOR_DBFIELD_DEFAULTS = {
- models.DateTimeField: {
- 'form_class': forms.SplitDateTimeField,
- 'widget': widgets.AdminSplitDateTime
- },
- models.DateField: {'widget': widgets.AdminDateWidget},
- models.TimeField: {'widget': widgets.AdminTimeWidget},
- models.TextField: {'widget': widgets.AdminTextareaWidget},
- models.URLField: {'widget': widgets.AdminURLFieldWidget},
- models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
- models.BigIntegerField: {'widget': widgets.AdminBigIntegerFieldWidget},
- models.CharField: {'widget': widgets.AdminTextInputWidget},
- models.ImageField: {'widget': widgets.AdminFileWidget},
- models.FileField: {'widget': widgets.AdminFileWidget},
- models.EmailField: {'widget': widgets.AdminEmailInputWidget},
- }
-
- csrf_protect_m = method_decorator(csrf_protect)
-
-
- class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
- """Functionality common to both ModelAdmin and InlineAdmin."""
-
- raw_id_fields = ()
- fields = None
- exclude = None
- fieldsets = None
- form = forms.ModelForm
- filter_vertical = ()
- filter_horizontal = ()
- radio_fields = {}
- prepopulated_fields = {}
- formfield_overrides = {}
- readonly_fields = ()
- ordering = None
- view_on_site = True
- show_full_result_count = True
- checks_class = BaseModelAdminChecks
-
- def check(self, **kwargs):
- return self.checks_class().check(self, **kwargs)
-
- def __init__(self):
- overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
- overrides.update(self.formfield_overrides)
- self.formfield_overrides = overrides
-
- def formfield_for_dbfield(self, db_field, **kwargs):
- """
- Hook for specifying the form Field instance for a given database Field
- instance.
-
- If kwargs are given, they're passed to the form Field's constructor.
- """
- request = kwargs.pop("request", None)
-
- # If the field specifies choices, we don't need to look for special
- # admin widgets - we just need to use a select widget of some kind.
- if db_field.choices:
- return self.formfield_for_choice_field(db_field, request, **kwargs)
-
- # ForeignKey or ManyToManyFields
- if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
- # Combine the field kwargs with any options for formfield_overrides.
- # Make sure the passed in **kwargs override anything in
- # formfield_overrides because **kwargs is more specific, and should
- # always win.
- if db_field.__class__ in self.formfield_overrides:
- kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
-
- # Get the correct formfield.
- if isinstance(db_field, models.ForeignKey):
- formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
- elif isinstance(db_field, models.ManyToManyField):
- formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
-
- # For non-raw_id fields, wrap the widget with a wrapper that adds
- # extra HTML -- the "add other" interface -- to the end of the
- # rendered output. formfield can be None if it came from a
- # OneToOneField with parent_link=True or a M2M intermediary.
- if formfield and db_field.name not in self.raw_id_fields:
- related_modeladmin = self.admin_site._registry.get(db_field.remote_field.model)
- wrapper_kwargs = {}
- if related_modeladmin:
- wrapper_kwargs.update(
- can_add_related=related_modeladmin.has_add_permission(request),
- can_change_related=related_modeladmin.has_change_permission(request),
- can_delete_related=related_modeladmin.has_delete_permission(request),
- )
- formfield.widget = widgets.RelatedFieldWidgetWrapper(
- formfield.widget, db_field.remote_field, self.admin_site, **wrapper_kwargs
- )
-
- return formfield
-
- # If we've got overrides for the formfield defined, use 'em. **kwargs
- # passed to formfield_for_dbfield override the defaults.
- for klass in db_field.__class__.mro():
- if klass in self.formfield_overrides:
- kwargs = dict(copy.deepcopy(self.formfield_overrides[klass]), **kwargs)
- return db_field.formfield(**kwargs)
-
- # For any other type of field, just call its formfield() method.
- return db_field.formfield(**kwargs)
-
- def formfield_for_choice_field(self, db_field, request=None, **kwargs):
- """
- Get a form Field for a database Field that has declared choices.
- """
- # If the field is named as a radio_field, use a RadioSelect
- if db_field.name in self.radio_fields:
- # Avoid stomping on custom widget/choices arguments.
- if 'widget' not in kwargs:
- kwargs['widget'] = widgets.AdminRadioSelect(attrs={
- 'class': get_ul_class(self.radio_fields[db_field.name]),
- })
- if 'choices' not in kwargs:
- kwargs['choices'] = db_field.get_choices(
- include_blank=db_field.blank,
- blank_choice=[('', _('None'))]
- )
- return db_field.formfield(**kwargs)
-
- def get_field_queryset(self, db, db_field, request):
- """
- If the ModelAdmin specifies ordering, the queryset should respect that
- ordering. Otherwise don't specify the queryset, let the field decide
- (returns None in that case).
- """
- related_admin = self.admin_site._registry.get(db_field.remote_field.model)
- if related_admin is not None:
- ordering = related_admin.get_ordering(request)
- if ordering is not None and ordering != ():
- return db_field.remote_field.model._default_manager.using(db).order_by(*ordering)
- return None
-
- def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
- """
- Get a form Field for a ForeignKey.
- """
- db = kwargs.get('using')
- if db_field.name in self.raw_id_fields:
- kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.remote_field,
- self.admin_site, using=db)
- elif db_field.name in self.radio_fields:
- kwargs['widget'] = widgets.AdminRadioSelect(attrs={
- 'class': get_ul_class(self.radio_fields[db_field.name]),
- })
- kwargs['empty_label'] = _('None') if db_field.blank else None
-
- if 'queryset' not in kwargs:
- queryset = self.get_field_queryset(db, db_field, request)
- if queryset is not None:
- kwargs['queryset'] = queryset
-
- return db_field.formfield(**kwargs)
-
- def formfield_for_manytomany(self, db_field, request=None, **kwargs):
- """
- Get a form Field for a ManyToManyField.
- """
- # If it uses an intermediary model that isn't auto created, don't show
- # a field in admin.
- if not db_field.remote_field.through._meta.auto_created:
- return None
- db = kwargs.get('using')
-
- if db_field.name in self.raw_id_fields:
- kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.remote_field,
- self.admin_site, using=db)
- kwargs['help_text'] = ''
- elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
- kwargs['widget'] = widgets.FilteredSelectMultiple(
- db_field.verbose_name,
- db_field.name in self.filter_vertical
- )
-
- if 'queryset' not in kwargs:
- queryset = self.get_field_queryset(db, db_field, request)
- if queryset is not None:
- kwargs['queryset'] = queryset
-
- form_field = db_field.formfield(**kwargs)
- if isinstance(form_field.widget, SelectMultiple) and not isinstance(form_field.widget, CheckboxSelectMultiple):
- msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
- help_text = form_field.help_text
- form_field.help_text = string_concat(help_text, ' ', msg) if help_text else msg
- return form_field
-
- def get_view_on_site_url(self, obj=None):
- if obj is None or not self.view_on_site:
- return None
-
- if callable(self.view_on_site):
- return self.view_on_site(obj)
- elif self.view_on_site and hasattr(obj, 'get_absolute_url'):
- # use the ContentType lookup if view_on_site is True
- return reverse('admin:view_on_site', kwargs={
- 'content_type_id': get_content_type_for_model(obj).pk,
- 'object_id': obj.pk
- })
-
- def get_empty_value_display(self):
- """
- Return the empty_value_display set on ModelAdmin or AdminSite.
- """
- try:
- return mark_safe(self.empty_value_display)
- except AttributeError:
- return mark_safe(self.admin_site.empty_value_display)
-
- def get_fields(self, request, obj=None):
- """
- Hook for specifying fields.
- """
- return self.fields
-
- def get_fieldsets(self, request, obj=None):
- """
- Hook for specifying fieldsets.
- """
- if self.fieldsets:
- return self.fieldsets
- return [(None, {'fields': self.get_fields(request, obj)})]
-
- def get_ordering(self, request):
- """
- Hook for specifying field ordering.
- """
- return self.ordering or () # otherwise we might try to *None, which is bad ;)
-
- def get_readonly_fields(self, request, obj=None):
- """
- Hook for specifying custom readonly fields.
- """
- return self.readonly_fields
-
- def get_prepopulated_fields(self, request, obj=None):
- """
- Hook for specifying custom prepopulated fields.
- """
- return self.prepopulated_fields
-
- def get_queryset(self, request):
- """
- Returns a QuerySet of all model instances that can be edited by the
- admin site. This is used by changelist_view.
- """
- qs = self.model._default_manager.get_queryset()
- # TODO: this should be handled by some parameter to the ChangeList.
- ordering = self.get_ordering(request)
- if ordering:
- qs = qs.order_by(*ordering)
- return qs
-
- def lookup_allowed(self, lookup, value):
- from django.contrib.admin.filters import SimpleListFilter
-
- model = self.model
- # Check FKey lookups that are allowed, so that popups produced by
- # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
- # are allowed to work.
- for l in model._meta.related_fkey_lookups:
- # As ``limit_choices_to`` can be a callable, invoke it here.
- if callable(l):
- l = l()
- for k, v in widgets.url_params_from_lookup_dict(l).items():
- if k == lookup and v == value:
- return True
-
- relation_parts = []
- prev_field = None
- for part in lookup.split(LOOKUP_SEP):
- try:
- field = model._meta.get_field(part)
- except FieldDoesNotExist:
- # Lookups on non-existent fields are ok, since they're ignored
- # later.
- break
- # It is allowed to filter on values that would be found from local
- # model anyways. For example, if you filter on employee__department__id,
- # then the id value would be found already from employee__department_id.
- if not prev_field or (prev_field.concrete and
- field not in prev_field.get_path_info()[-1].target_fields):
- relation_parts.append(part)
- if not getattr(field, 'get_path_info', None):
- # This is not a relational field, so further parts
- # must be transforms.
- break
- prev_field = field
- model = field.get_path_info()[-1].to_opts.model
-
- if len(relation_parts) <= 1:
- # Either a local field filter, or no fields at all.
- return True
- clean_lookup = LOOKUP_SEP.join(relation_parts)
- valid_lookups = [self.date_hierarchy]
- for filter_item in self.list_filter:
- if isinstance(filter_item, type) and issubclass(filter_item, SimpleListFilter):
- valid_lookups.append(filter_item.parameter_name)
- elif isinstance(filter_item, (list, tuple)):
- valid_lookups.append(filter_item[0])
- else:
- valid_lookups.append(filter_item)
- return clean_lookup in valid_lookups
-
- def to_field_allowed(self, request, to_field):
- """
- Returns True if the model associated with this admin should be
- allowed to be referenced by the specified field.
- """
- opts = self.model._meta
-
- try:
- field = opts.get_field(to_field)
- except FieldDoesNotExist:
- return False
-
- # Always allow referencing the primary key since it's already possible
- # to get this information from the change view URL.
- if field.primary_key:
- return True
-
- # Allow reverse relationships to models defining m2m fields if they
- # target the specified field.
- for many_to_many in opts.many_to_many:
- if many_to_many.m2m_target_field_name() == to_field:
- return True
-
- # Make sure at least one of the models registered for this site
- # references this field through a FK or a M2M relationship.
- registered_models = set()
- for model, admin in self.admin_site._registry.items():
- registered_models.add(model)
- for inline in admin.inlines:
- registered_models.add(inline.model)
-
- related_objects = (
- f for f in opts.get_fields(include_hidden=True)
- if (f.auto_created and not f.concrete)
- )
- for related_object in related_objects:
- related_model = related_object.related_model
- remote_field = related_object.field.remote_field
- if (any(issubclass(model, related_model) for model in registered_models) and
- hasattr(remote_field, 'get_related_field') and
- remote_field.get_related_field() == field):
- return True
-
- return False
-
- def has_add_permission(self, request):
- """
- Returns True if the given request has permission to add an object.
- Can be overridden by the user in subclasses.
- """
- opts = self.opts
- codename = get_permission_codename('add', opts)
- return request.user.has_perm("%s.%s" % (opts.app_label, codename))
-
- def has_change_permission(self, request, obj=None):
- """
- Returns True if the given request has permission to change the given
- Django model instance, the default implementation doesn't examine the
- `obj` parameter.
-
- Can be overridden by the user in subclasses. In such case it should
- return True if the given request has permission to change the `obj`
- model instance. If `obj` is None, this should return True if the given
- request has permission to change *any* object of the given type.
- """
- opts = self.opts
- codename = get_permission_codename('change', opts)
- return request.user.has_perm("%s.%s" % (opts.app_label, codename))
-
- def has_delete_permission(self, request, obj=None):
- """
- Returns True if the given request has permission to change the given
- Django model instance, the default implementation doesn't examine the
- `obj` parameter.
-
- Can be overridden by the user in subclasses. In such case it should
- return True if the given request has permission to delete the `obj`
- model instance. If `obj` is None, this should return True if the given
- request has permission to delete *any* object of the given type.
- """
- opts = self.opts
- codename = get_permission_codename('delete', opts)
- return request.user.has_perm("%s.%s" % (opts.app_label, codename))
-
- def has_module_permission(self, request):
- """
- Returns True if the given request has any permission in the given
- app label.
-
- Can be overridden by the user in subclasses. In such case it should
- return True if the given request has permission to view the module on
- the admin index page and access the module's index page. Overriding it
- does not restrict access to the add, change or delete views. Use
- `ModelAdmin.has_(add|change|delete)_permission` for that.
- """
- return request.user.has_module_perms(self.opts.app_label)
-
-
- @python_2_unicode_compatible
- class ModelAdmin(BaseModelAdmin):
- "Encapsulates all admin options and functionality for a given model."
-
- list_display = ('__str__',)
- list_display_links = ()
- list_filter = ()
- list_select_related = False
- list_per_page = 100
- list_max_show_all = 200
- list_editable = ()
- search_fields = ()
- date_hierarchy = None
- save_as = False
- save_on_top = False
- paginator = Paginator
- preserve_filters = True
- inlines = []
-
- # Custom templates (designed to be over-ridden in subclasses)
- add_form_template = None
- change_form_template = None
- change_list_template = None
- delete_confirmation_template = None
- delete_selected_confirmation_template = None
- object_history_template = None
-
- # Actions
- actions = []
- action_form = helpers.ActionForm
- actions_on_top = True
- actions_on_bottom = False
- actions_selection_counter = True
- checks_class = ModelAdminChecks
-
- def __init__(self, model, admin_site):
- self.model = model
- self.opts = model._meta
- self.admin_site = admin_site
- super(ModelAdmin, self).__init__()
-
- def __str__(self):
- return "%s.%s" % (self.model._meta.app_label, self.__class__.__name__)
-
- def get_inline_instances(self, request, obj=None):
- inline_instances = []
- for inline_class in self.inlines:
- inline = inline_class(self.model, self.admin_site)
- if request:
- if not (inline.has_add_permission(request) or
- inline.has_change_permission(request, obj) or
- inline.has_delete_permission(request, obj)):
- continue
- if not inline.has_add_permission(request):
- inline.max_num = 0
- inline_instances.append(inline)
-
- return inline_instances
-
- def get_urls(self):
- from django.conf.urls import url
-
- def wrap(view):
- def wrapper(*args, **kwargs):
- return self.admin_site.admin_view(view)(*args, **kwargs)
- wrapper.model_admin = self
- return update_wrapper(wrapper, view)
-
- info = self.model._meta.app_label, self.model._meta.model_name
-
- urlpatterns = [
- url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info),
- url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info),
- url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info),
- url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info),
- url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
- # For backwards compatibility (was the change url before 1.9)
- url(r'^(.+)/$', wrap(RedirectView.as_view(
- pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
- ))),
- ]
- return urlpatterns
-
- def urls(self):
- return self.get_urls()
- urls = property(urls)
-
- @property
- def media(self):
- extra = '' if settings.DEBUG else '.min'
- js = [
- 'core.js',
- 'vendor/jquery/jquery%s.js' % extra,
- 'jquery.init.js',
- 'admin/RelatedObjectLookups.js',
- 'actions%s.js' % extra,
- 'urlify.js',
- 'prepopulate%s.js' % extra,
- 'vendor/xregexp/xregexp%s.js' % extra,
- ]
- return forms.Media(js=[static('admin/js/%s' % url) for url in js])
-
- def get_model_perms(self, request):
- """
- Returns a dict of all perms for this model. This dict has the keys
- ``add``, ``change``, and ``delete`` mapping to the True/False for each
- of those actions.
- """
- return {
- 'add': self.has_add_permission(request),
- 'change': self.has_change_permission(request),
- 'delete': self.has_delete_permission(request),
- }
-
- def get_fields(self, request, obj=None):
- if self.fields:
- return self.fields
- form = self.get_form(request, obj, fields=None)
- return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
-
- def get_form(self, request, obj=None, **kwargs):
- """
- Returns a Form class for use in the admin add view. This is used by
- add_view and change_view.
- """
- if 'fields' in kwargs:
- fields = kwargs.pop('fields')
- else:
- fields = flatten_fieldsets(self.get_fieldsets(request, obj))
- if self.exclude is None:
- exclude = []
- else:
- exclude = list(self.exclude)
- readonly_fields = self.get_readonly_fields(request, obj)
- exclude.extend(readonly_fields)
- if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
- # Take the custom ModelForm's Meta.exclude into account only if the
- # ModelAdmin doesn't define its own.
- exclude.extend(self.form._meta.exclude)
- # if exclude is an empty list we pass None to be consistent with the
- # default on modelform_factory
- exclude = exclude or None
-
- # Remove declared form fields which are in readonly_fields.
- new_attrs = OrderedDict(
- (f, None) for f in readonly_fields
- if f in self.form.declared_fields
- )
- form = type(self.form.__name__, (self.form,), new_attrs)
-
- defaults = {
- "form": form,
- "fields": fields,
- "exclude": exclude,
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- }
- defaults.update(kwargs)
-
- if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):
- defaults['fields'] = forms.ALL_FIELDS
-
- try:
- return modelform_factory(self.model, **defaults)
- except FieldError as e:
- raise FieldError('%s. Check fields/fieldsets/exclude attributes of class %s.'
- % (e, self.__class__.__name__))
-
- def get_changelist(self, request, **kwargs):
- """
- Returns the ChangeList class for use on the changelist page.
- """
- from django.contrib.admin.views.main import ChangeList
- return ChangeList
-
- def get_object(self, request, object_id, from_field=None):
- """
- Returns an instance matching the field and value provided, the primary
- key is used if no field is provided. Returns ``None`` if no match is
- found or the object_id fails validation.
- """
- queryset = self.get_queryset(request)
- model = queryset.model
- field = model._meta.pk if from_field is None else model._meta.get_field(from_field)
- try:
- object_id = field.to_python(object_id)
- return queryset.get(**{field.name: object_id})
- except (model.DoesNotExist, ValidationError, ValueError):
- return None
-
- def get_changelist_form(self, request, **kwargs):
- """
- Returns a Form class for use in the Formset on the changelist page.
- """
- defaults = {
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- }
- defaults.update(kwargs)
- if (defaults.get('fields') is None
- and not modelform_defines_fields(defaults.get('form'))):
- defaults['fields'] = forms.ALL_FIELDS
-
- return modelform_factory(self.model, **defaults)
-
- def get_changelist_formset(self, request, **kwargs):
- """
- Returns a FormSet class for use on the changelist page if list_editable
- is used.
- """
- defaults = {
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- }
- defaults.update(kwargs)
- return modelformset_factory(self.model,
- self.get_changelist_form(request), extra=0,
- fields=self.list_editable, **defaults)
-
- def get_formsets_with_inlines(self, request, obj=None):
- """
- Yields formsets and the corresponding inlines.
- """
- for inline in self.get_inline_instances(request, obj):
- yield inline.get_formset(request, obj), inline
-
- def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
- return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
-
- def log_addition(self, request, object, message):
- """
- Log that an object has been successfully added.
-
- The default implementation creates an admin LogEntry object.
- """
- from django.contrib.admin.models import LogEntry, ADDITION
- LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(object).pk,
- object_id=object.pk,
- object_repr=force_text(object),
- action_flag=ADDITION,
- change_message=message,
- )
-
- def log_change(self, request, object, message):
- """
- Log that an object has been successfully changed.
-
- The default implementation creates an admin LogEntry object.
- """
- from django.contrib.admin.models import LogEntry, CHANGE
- LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(object).pk,
- object_id=object.pk,
- object_repr=force_text(object),
- action_flag=CHANGE,
- change_message=message,
- )
-
- def log_deletion(self, request, object, object_repr):
- """
- Log that an object will be deleted. Note that this method must be
- called before the deletion.
-
- The default implementation creates an admin LogEntry object.
- """
- from django.contrib.admin.models import LogEntry, DELETION
- LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(object).pk,
- object_id=object.pk,
- object_repr=object_repr,
- action_flag=DELETION,
- )
-
- def action_checkbox(self, obj):
- """
- A list_display column containing a checkbox widget.
- """
- return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_text(obj.pk))
- action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
-
- def get_actions(self, request):
- """
- Return a dictionary mapping the names of all actions for this
- ModelAdmin to a tuple of (callable, name, description) for each action.
- """
- # If self.actions is explicitly set to None that means that we don't
- # want *any* actions enabled on this page.
- if self.actions is None or IS_POPUP_VAR in request.GET:
- return OrderedDict()
-
- actions = []
-
- # Gather actions from the admin site first
- for (name, func) in self.admin_site.actions:
- description = getattr(func, 'short_description', name.replace('_', ' '))
- actions.append((func, name, description))
-
- # Then gather them from the model admin and all parent classes,
- # starting with self and working back up.
- for klass in self.__class__.mro()[::-1]:
- class_actions = getattr(klass, 'actions', [])
- # Avoid trying to iterate over None
- if not class_actions:
- continue
- actions.extend(self.get_action(action) for action in class_actions)
-
- # get_action might have returned None, so filter any of those out.
- actions = filter(None, actions)
-
- # Convert the actions into an OrderedDict keyed by name.
- actions = OrderedDict(
- (name, (func, name, desc))
- for func, name, desc in actions
- )
-
- return actions
-
- def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
- """
- Return a list of choices for use in a form object. Each choice is a
- tuple (name, description).
- """
- choices = [] + default_choices
- for func, name, description in six.itervalues(self.get_actions(request)):
- choice = (name, description % model_format_dict(self.opts))
- choices.append(choice)
- return choices
-
- def get_action(self, action):
- """
- Return a given action from a parameter, which can either be a callable,
- or the name of a method on the ModelAdmin. Return is a tuple of
- (callable, name, description).
- """
- # If the action is a callable, just use it.
- if callable(action):
- func = action
- action = action.__name__
-
- # Next, look for a method. Grab it off self.__class__ to get an unbound
- # method instead of a bound one; this ensures that the calling
- # conventions are the same for functions and methods.
- elif hasattr(self.__class__, action):
- func = getattr(self.__class__, action)
-
- # Finally, look for a named method on the admin site
- else:
- try:
- func = self.admin_site.get_action(action)
- except KeyError:
- return None
-
- if hasattr(func, 'short_description'):
- description = func.short_description
- else:
- description = capfirst(action.replace('_', ' '))
- return func, action, description
-
- def get_list_display(self, request):
- """
- Return a sequence containing the fields to be displayed on the
- changelist.
- """
- return self.list_display
-
- def get_list_display_links(self, request, list_display):
- """
- Return a sequence containing the fields to be displayed as links
- on the changelist. The list_display parameter is the list of fields
- returned by get_list_display().
- """
- if self.list_display_links or self.list_display_links is None or not list_display:
- return self.list_display_links
- else:
- # Use only the first item in list_display as link
- return list(list_display)[:1]
-
- def get_list_filter(self, request):
- """
- Returns a sequence containing the fields to be displayed as filters in
- the right sidebar of the changelist page.
- """
- return self.list_filter
-
- def get_list_select_related(self, request):
- """
- Returns a list of fields to add to the select_related() part of the
- changelist items query.
- """
- return self.list_select_related
-
- def get_search_fields(self, request):
- """
- Returns a sequence containing the fields to be searched whenever
- somebody submits a search query.
- """
- return self.search_fields
-
- def get_search_results(self, request, queryset, search_term):
- """
- Returns a tuple containing a queryset to implement the search,
- and a boolean indicating if the results may contain duplicates.
- """
- # 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
-
- use_distinct = False
- search_fields = self.get_search_fields(request)
- if search_fields and search_term:
- orm_lookups = [construct_search(str(search_field))
- for search_field in search_fields]
- for bit in search_term.split():
- or_queries = [models.Q(**{orm_lookup: bit})
- for orm_lookup in orm_lookups]
- queryset = queryset.filter(reduce(operator.or_, or_queries))
- if not use_distinct:
- for search_spec in orm_lookups:
- if lookup_needs_distinct(self.opts, search_spec):
- use_distinct = True
- break
-
- return queryset, use_distinct
-
- def get_preserved_filters(self, request):
- """
- Returns the preserved filters querystring.
- """
- match = request.resolver_match
- if self.preserve_filters and match:
- opts = self.model._meta
- current_url = '%s:%s' % (match.app_name, match.url_name)
- changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
- if current_url == changelist_url:
- preserved_filters = request.GET.urlencode()
- else:
- preserved_filters = request.GET.get('_changelist_filters')
-
- if preserved_filters:
- return urlencode({'_changelist_filters': preserved_filters})
- return ''
-
- def construct_change_message(self, request, form, formsets, add=False):
- """
- Construct a change message from a changed object.
- """
- change_message = []
- if add:
- change_message.append(_('Added.'))
- elif form.changed_data:
- change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
-
- if formsets:
- for formset in formsets:
- for added_object in formset.new_objects:
- change_message.append(_('Added %(name)s "%(object)s".')
- % {'name': force_text(added_object._meta.verbose_name),
- 'object': force_text(added_object)})
- for changed_object, changed_fields in formset.changed_objects:
- change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
- % {'list': get_text_list(changed_fields, _('and')),
- 'name': force_text(changed_object._meta.verbose_name),
- 'object': force_text(changed_object)})
- for deleted_object in formset.deleted_objects:
- change_message.append(_('Deleted %(name)s "%(object)s".')
- % {'name': force_text(deleted_object._meta.verbose_name),
- 'object': force_text(deleted_object)})
- change_message = ' '.join(change_message)
- return change_message or _('No fields changed.')
-
- def message_user(self, request, message, level=messages.INFO, extra_tags='',
- fail_silently=False):
- """
- Send a message to the user. The default implementation
- posts a message using the django.contrib.messages backend.
-
- Exposes almost the same API as messages.add_message(), but accepts the
- positional arguments in a different order to maintain backwards
- compatibility. For convenience, it accepts the `level` argument as
- a string rather than the usual level number.
- """
-
- if not isinstance(level, int):
- # attempt to get the level if passed a string
- try:
- level = getattr(messages.constants, level.upper())
- except AttributeError:
- levels = messages.constants.DEFAULT_TAGS.values()
- levels_repr = ', '.join('`%s`' % l for l in levels)
- raise ValueError('Bad message level string: `%s`. '
- 'Possible values are: %s' % (level, levels_repr))
-
- messages.add_message(request, level, message, extra_tags=extra_tags,
- fail_silently=fail_silently)
-
- def save_form(self, request, form, change):
- """
- Given a ModelForm return an unsaved instance. ``change`` is True if
- the object is being changed, and False if it's being added.
- """
- return form.save(commit=False)
-
- def save_model(self, request, obj, form, change):
- """
- Given a model instance save it to the database.
- """
- obj.save()
-
- def delete_model(self, request, obj):
- """
- Given a model instance delete it from the database.
- """
- obj.delete()
-
- def save_formset(self, request, form, formset, change):
- """
- Given an inline formset save it to the database.
- """
- formset.save()
-
- def save_related(self, request, form, formsets, change):
- """
- Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
- list of inline formsets and a boolean value based on whether the
- parent is being added or changed, save the related objects to the
- database. Note that at this point save_form() and save_model() have
- already been called.
- """
- form.save_m2m()
- for formset in formsets:
- self.save_formset(request, form, formset, change=change)
-
- def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
- opts = self.model._meta
- app_label = opts.app_label
- preserved_filters = self.get_preserved_filters(request)
- form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
- view_on_site_url = self.get_view_on_site_url(obj)
- context.update({
- 'add': add,
- 'change': change,
- 'has_add_permission': self.has_add_permission(request),
- 'has_change_permission': self.has_change_permission(request, obj),
- 'has_delete_permission': self.has_delete_permission(request, obj),
- 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
- 'has_absolute_url': view_on_site_url is not None,
- 'absolute_url': view_on_site_url,
- 'form_url': form_url,
- 'opts': opts,
- 'content_type_id': get_content_type_for_model(self.model).pk,
- 'save_as': self.save_as,
- 'save_on_top': self.save_on_top,
- 'to_field_var': TO_FIELD_VAR,
- 'is_popup_var': IS_POPUP_VAR,
- 'app_label': app_label,
- })
- if add and self.add_form_template is not None:
- form_template = self.add_form_template
- else:
- form_template = self.change_form_template
-
- request.current_app = self.admin_site.name
-
- return TemplateResponse(request, form_template or [
- "admin/%s/%s/change_form.html" % (app_label, opts.model_name),
- "admin/%s/change_form.html" % app_label,
- "admin/change_form.html"
- ], context)
-
- def response_add(self, request, obj, post_url_continue=None):
- """
- Determines the HttpResponse for the add_view stage.
- """
- opts = obj._meta
- pk_value = obj._get_pk_val()
- preserved_filters = self.get_preserved_filters(request)
- msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
- # Here, we distinguish between different save types by checking for
- # the presence of keys in request.POST.
-
- if IS_POPUP_VAR in request.POST:
- to_field = request.POST.get(TO_FIELD_VAR)
- if to_field:
- attr = str(to_field)
- else:
- attr = obj._meta.pk.attname
- value = obj.serializable_value(attr)
- return SimpleTemplateResponse('admin/popup_response.html', {
- 'value': value,
- 'obj': obj,
- })
-
- elif "_continue" in request.POST:
- msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- if post_url_continue is None:
- post_url_continue = reverse('admin:%s_%s_change' %
- (opts.app_label, opts.model_name),
- args=(quote(pk_value),),
- current_app=self.admin_site.name)
- post_url_continue = add_preserved_filters(
- {'preserved_filters': preserved_filters, 'opts': opts},
- post_url_continue
- )
- return HttpResponseRedirect(post_url_continue)
-
- elif "_addanother" in request.POST:
- msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- redirect_url = request.path
- redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
- return HttpResponseRedirect(redirect_url)
-
- else:
- msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- return self.response_post_save_add(request, obj)
-
- def response_change(self, request, obj):
- """
- Determines the HttpResponse for the change_view stage.
- """
-
- if IS_POPUP_VAR in request.POST:
- to_field = request.POST.get(TO_FIELD_VAR)
- attr = str(to_field) if to_field else obj._meta.pk.attname
- # Retrieve the `object_id` from the resolved pattern arguments.
- value = request.resolver_match.args[0]
- new_value = obj.serializable_value(attr)
- return SimpleTemplateResponse('admin/popup_response.html', {
- 'action': 'change',
- 'value': value,
- 'obj': obj,
- 'new_value': new_value,
- })
-
- opts = self.model._meta
- pk_value = obj._get_pk_val()
- preserved_filters = self.get_preserved_filters(request)
-
- msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
- if "_continue" in request.POST:
- msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- redirect_url = request.path
- redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
- return HttpResponseRedirect(redirect_url)
-
- elif "_saveasnew" in request.POST:
- msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- redirect_url = reverse('admin:%s_%s_change' %
- (opts.app_label, opts.model_name),
- args=(pk_value,),
- current_app=self.admin_site.name)
- redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
- return HttpResponseRedirect(redirect_url)
-
- elif "_addanother" in request.POST:
- msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- redirect_url = reverse('admin:%s_%s_add' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name)
- redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
- return HttpResponseRedirect(redirect_url)
-
- else:
- msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
- self.message_user(request, msg, messages.SUCCESS)
- return self.response_post_save_change(request, obj)
-
- def response_post_save_add(self, request, obj):
- """
- Figure out where to redirect after the 'Save' button has been pressed
- when adding a new object.
- """
- opts = self.model._meta
- if self.has_change_permission(request, None):
- post_url = reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name)
- preserved_filters = self.get_preserved_filters(request)
- post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
- else:
- post_url = reverse('admin:index',
- current_app=self.admin_site.name)
- return HttpResponseRedirect(post_url)
-
- def response_post_save_change(self, request, obj):
- """
- Figure out where to redirect after the 'Save' button has been pressed
- when editing an existing object.
- """
- opts = self.model._meta
-
- if self.has_change_permission(request, None):
- post_url = reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name)
- preserved_filters = self.get_preserved_filters(request)
- post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
- else:
- post_url = reverse('admin:index',
- current_app=self.admin_site.name)
- return HttpResponseRedirect(post_url)
-
- def response_action(self, request, queryset):
- """
- Handle an admin action. This is called if a request is POSTed to the
- changelist; it returns an HttpResponse if the action was handled, and
- None otherwise.
- """
-
- # There can be multiple action forms on the page (at the top
- # and bottom of the change list, for example). Get the action
- # whose button was pushed.
- try:
- action_index = int(request.POST.get('index', 0))
- except ValueError:
- action_index = 0
-
- # Construct the action form.
- data = request.POST.copy()
- data.pop(helpers.ACTION_CHECKBOX_NAME, None)
- data.pop("index", None)
-
- # Use the action whose button was pushed
- try:
- data.update({'action': data.getlist('action')[action_index]})
- except IndexError:
- # If we didn't get an action from the chosen form that's invalid
- # POST data, so by deleting action it'll fail the validation check
- # below. So no need to do anything here
- pass
-
- action_form = self.action_form(data, auto_id=None)
- action_form.fields['action'].choices = self.get_action_choices(request)
-
- # If the form's valid we can handle the action.
- if action_form.is_valid():
- action = action_form.cleaned_data['action']
- select_across = action_form.cleaned_data['select_across']
- func = self.get_actions(request)[action][0]
-
- # Get the list of selected PKs. If nothing's selected, we can't
- # perform an action on it, so bail. Except we want to perform
- # the action explicitly on all objects.
- selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
- if not selected and not select_across:
- # Reminder that something needs to be selected or nothing will happen
- msg = _("Items must be selected in order to perform "
- "actions on them. No items have been changed.")
- self.message_user(request, msg, messages.WARNING)
- return None
-
- if not select_across:
- # Perform the action only on the selected objects
- queryset = queryset.filter(pk__in=selected)
-
- response = func(self, request, queryset)
-
- # Actions may return an HttpResponse-like object, which will be
- # used as the response from the POST. If not, we'll be a good
- # little HTTP citizen and redirect back to the changelist page.
- if isinstance(response, HttpResponseBase):
- return response
- else:
- return HttpResponseRedirect(request.get_full_path())
- else:
- msg = _("No action selected.")
- self.message_user(request, msg, messages.WARNING)
- return None
-
- def response_delete(self, request, obj_display, obj_id):
- """
- Determines the HttpResponse for the delete_view stage.
- """
-
- opts = self.model._meta
-
- if IS_POPUP_VAR in request.POST:
- return SimpleTemplateResponse('admin/popup_response.html', {
- 'action': 'delete',
- 'value': escape(obj_id),
- })
-
- self.message_user(request,
- _('The %(name)s "%(obj)s" was deleted successfully.') % {
- 'name': force_text(opts.verbose_name),
- 'obj': force_text(obj_display),
- }, messages.SUCCESS)
-
- if self.has_change_permission(request, None):
- post_url = reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name)
- preserved_filters = self.get_preserved_filters(request)
- post_url = add_preserved_filters(
- {'preserved_filters': preserved_filters, 'opts': opts}, post_url
- )
- else:
- post_url = reverse('admin:index',
- current_app=self.admin_site.name)
- return HttpResponseRedirect(post_url)
-
- def render_delete_form(self, request, context):
- opts = self.model._meta
- app_label = opts.app_label
-
- request.current_app = self.admin_site.name
- context.update(
- to_field_var=TO_FIELD_VAR,
- is_popup_var=IS_POPUP_VAR,
- )
-
- return TemplateResponse(request,
- self.delete_confirmation_template or [
- "admin/{}/{}/delete_confirmation.html".format(app_label, opts.model_name),
- "admin/{}/delete_confirmation.html".format(app_label),
- "admin/delete_confirmation.html"
- ], context)
-
- def get_inline_formsets(self, request, formsets, inline_instances,
- obj=None):
- 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)
- return inline_admin_formsets
-
- def get_changeform_initial_data(self, request):
- """
- Get the initial form data.
- Unless overridden, this populates from the GET params.
- """
- initial = dict(request.GET.items())
- for k in initial:
- try:
- f = self.model._meta.get_field(k)
- except FieldDoesNotExist:
- continue
- # We have to special-case M2Ms as a list of comma-separated PKs.
- if isinstance(f, models.ManyToManyField):
- initial[k] = initial[k].split(",")
- return initial
-
- @csrf_protect_m
- @transaction.atomic
- def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
-
- to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
- if to_field and not self.to_field_allowed(request, to_field):
- raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
-
- model = self.model
- opts = model._meta
-
- if request.method == 'POST' and '_saveasnew' in request.POST:
- object_id = None
-
- add = object_id is None
-
- if add:
- if not self.has_add_permission(request):
- raise PermissionDenied
- obj = None
-
- else:
- obj = self.get_object(request, unquote(object_id), to_field)
-
- 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_text(opts.verbose_name), 'key': escape(object_id)})
-
- ModelForm = self.get_form(request, obj)
- 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=not add)
- else:
- form_validated = False
- new_object = form.instance
- formsets, inline_instances = self._create_formsets(request, new_object, change=not add)
- if all_valid(formsets) and form_validated:
- self.save_model(request, new_object, form, not add)
- self.save_related(request, form, formsets, not add)
- change_message = self.construct_change_message(request, form, formsets, add)
- if add:
- self.log_addition(request, new_object, change_message)
- return self.response_add(request, new_object)
- else:
- self.log_change(request, new_object, change_message)
- return self.response_change(request, new_object)
- else:
- form_validated = False
- else:
- if add:
- initial = self.get_changeform_initial_data(request)
- form = ModelForm(initial=initial)
- formsets, inline_instances = self._create_formsets(request, form.instance, change=False)
- else:
- form = ModelForm(instance=obj)
- formsets, inline_instances = self._create_formsets(request, obj, change=True)
-
- adminForm = helpers.AdminForm(
- form,
- list(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_formsets = self.get_inline_formsets(request, formsets, inline_instances, obj)
- for inline_formset in inline_formsets:
- media = media + inline_formset.media
-
- context = dict(self.admin_site.each_context(request),
- title=(_('Add %s') if add else _('Change %s')) % force_text(opts.verbose_name),
- adminform=adminForm,
- object_id=object_id,
- original=obj,
- is_popup=(IS_POPUP_VAR in request.POST or
- IS_POPUP_VAR in request.GET),
- to_field=to_field,
- media=media,
- inline_admin_formsets=inline_formsets,
- errors=helpers.AdminErrorList(form, formsets),
- preserved_filters=self.get_preserved_filters(request),
- )
-
- # Hide the "Save" and "Save and continue" buttons if "Save as New" was
- # previously chosen to prevent the interface from getting confusing.
- if request.method == 'POST' and not form_validated and "_saveasnew" in request.POST:
- context['show_save'] = False
- context['show_save_and_continue'] = False
- # Use the change template instead of the add template.
- add = False
-
- context.update(extra_context or {})
-
- return self.render_change_form(request, context, add=add, change=not add, obj=obj, form_url=form_url)
-
- def add_view(self, request, form_url='', extra_context=None):
- return self.changeform_view(request, None, form_url, extra_context)
-
- def change_view(self, request, object_id, form_url='', extra_context=None):
- return self.changeform_view(request, object_id, form_url, extra_context)
-
- @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)
- list_filter = self.get_list_filter(request)
- search_fields = self.get_search_fields(request)
- list_select_related = self.get_list_select_related(request)
-
- # 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, list_filter, self.date_hierarchy,
- search_fields, 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_queryset(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, messages.WARNING)
- 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_queryset(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_text(opts.verbose_name)
- else:
- name = force_text(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_text(obj)}
- self.message_user(request, msg, messages.SUCCESS)
-
- 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 = dict(
- self.admin_site.each_context(request),
- module_name=force_text(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,
- to_field=cl.to_field,
- cl=cl,
- media=media,
- has_add_permission=self.has_add_permission(request),
- opts=cl.opts,
- 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,
- preserved_filters=self.get_preserved_filters(request),
- )
- context.update(extra_context or {})
-
- request.current_app = self.admin_site.name
-
- return TemplateResponse(request, self.change_list_template or [
- 'admin/%s/%s/change_list.html' % (app_label, opts.model_name),
- 'admin/%s/change_list.html' % app_label,
- 'admin/change_list.html'
- ], context)
-
- @csrf_protect_m
- @transaction.atomic
- 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
-
- to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
- if to_field and not self.to_field_allowed(request, to_field):
- raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field)
-
- obj = self.get_object(request, unquote(object_id), to_field)
-
- 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_text(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, model_count, 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_text(obj)
- attr = str(to_field) if to_field else opts.pk.attname
- obj_id = obj.serializable_value(attr)
- self.log_deletion(request, obj, obj_display)
- self.delete_model(request, obj)
-
- return self.response_delete(request, obj_display, obj_id)
-
- object_name = force_text(opts.verbose_name)
-
- if perms_needed or protected:
- title = _("Cannot delete %(name)s") % {"name": object_name}
- else:
- title = _("Are you sure?")
-
- context = dict(
- self.admin_site.each_context(request),
- title=title,
- object_name=object_name,
- object=obj,
- deleted_objects=deleted_objects,
- model_count=dict(model_count).items(),
- perms_lacking=perms_needed,
- protected=protected,
- opts=opts,
- app_label=app_label,
- preserved_filters=self.get_preserved_filters(request),
- is_popup=(IS_POPUP_VAR in request.POST or
- IS_POPUP_VAR in request.GET),
- to_field=to_field,
- )
- context.update(extra_context or {})
-
- return self.render_delete_form(request, context)
-
- def history_view(self, request, object_id, extra_context=None):
- "The 'history' admin view for this model."
- from django.contrib.admin.models import LogEntry
- # First check if the user can see this history.
- model = self.model
- obj = self.get_object(request, unquote(object_id))
- if obj is None:
- raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
- 'name': force_text(model._meta.verbose_name),
- 'key': escape(object_id),
- })
-
- if not self.has_change_permission(request, obj):
- raise PermissionDenied
-
- # Then get the history for this object.
- opts = model._meta
- app_label = opts.app_label
- action_list = LogEntry.objects.filter(
- object_id=unquote(object_id),
- content_type=get_content_type_for_model(model)
- ).select_related().order_by('action_time')
-
- context = dict(self.admin_site.each_context(request),
- title=_('Change history: %s') % force_text(obj),
- action_list=action_list,
- module_name=capfirst(force_text(opts.verbose_name_plural)),
- object=obj,
- opts=opts,
- preserved_filters=self.get_preserved_filters(request),
- )
- context.update(extra_context or {})
-
- request.current_app = self.admin_site.name
-
- return TemplateResponse(request, self.object_history_template or [
- "admin/%s/%s/object_history.html" % (app_label, opts.model_name),
- "admin/%s/object_history.html" % app_label,
- "admin/object_history.html"
- ], context)
-
- def _create_formsets(self, request, obj, change):
- "Helper function to generate formsets for add/change_view."
- formsets = []
- inline_instances = []
- prefixes = {}
- get_formsets_args = [request]
- if change:
- get_formsets_args.append(obj)
- for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
- 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_params = {
- 'instance': obj,
- 'prefix': prefix,
- 'queryset': inline.get_queryset(request),
- }
- if request.method == 'POST':
- formset_params.update({
- 'data': request.POST,
- 'files': request.FILES,
- 'save_as_new': '_saveasnew' in request.POST
- })
- formsets.append(FormSet(**formset_params))
- inline_instances.append(inline)
- return formsets, inline_instances
-
-
- class InlineModelAdmin(BaseModelAdmin):
- """
- Options for inline editing of ``model`` instances.
-
- Provide ``fk_name`` to specify the attribute name of the ``ForeignKey``
- from ``model`` to its parent. This is required if ``model`` has more than
- one ``ForeignKey`` to its parent.
- """
- model = None
- fk_name = None
- formset = BaseInlineFormSet
- extra = 3
- min_num = None
- max_num = None
- template = None
- verbose_name = None
- verbose_name_plural = None
- can_delete = True
- show_change_link = False
- checks_class = InlineModelAdminChecks
-
- def __init__(self, parent_model, admin_site):
- self.admin_site = admin_site
- self.parent_model = parent_model
- self.opts = self.model._meta
- self.has_registered_model = admin_site.is_registered(self.model)
- super(InlineModelAdmin, self).__init__()
- if self.verbose_name is None:
- self.verbose_name = self.model._meta.verbose_name
- if self.verbose_name_plural is None:
- self.verbose_name_plural = self.model._meta.verbose_name_plural
-
- @property
- def media(self):
- extra = '' if settings.DEBUG else '.min'
- js = ['vendor/jquery/jquery%s.js' % extra, 'jquery.init.js',
- 'inlines%s.js' % extra]
- if self.filter_vertical or self.filter_horizontal:
- js.extend(['SelectBox.js', 'SelectFilter2.js'])
- return forms.Media(js=[static('admin/js/%s' % url) for url in js])
-
- def get_extra(self, request, obj=None, **kwargs):
- """Hook for customizing the number of extra inline forms."""
- return self.extra
-
- def get_min_num(self, request, obj=None, **kwargs):
- """Hook for customizing the min number of inline forms."""
- return self.min_num
-
- def get_max_num(self, request, obj=None, **kwargs):
- """Hook for customizing the max number of extra inline forms."""
- return self.max_num
-
- def get_formset(self, request, obj=None, **kwargs):
- """Returns a BaseInlineFormSet class for use in admin add/change views."""
- if 'fields' in kwargs:
- fields = kwargs.pop('fields')
- else:
- fields = flatten_fieldsets(self.get_fieldsets(request, obj))
- if self.exclude is None:
- exclude = []
- else:
- exclude = list(self.exclude)
- exclude.extend(self.get_readonly_fields(request, obj))
- if self.exclude is None and hasattr(self.form, '_meta') and self.form._meta.exclude:
- # Take the custom ModelForm's Meta.exclude into account only if the
- # InlineModelAdmin doesn't define its own.
- exclude.extend(self.form._meta.exclude)
- # If exclude is an empty list we use None, since that's the actual
- # default.
- exclude = exclude or None
- can_delete = self.can_delete and self.has_delete_permission(request, obj)
- defaults = {
- "form": self.form,
- "formset": self.formset,
- "fk_name": self.fk_name,
- "fields": fields,
- "exclude": exclude,
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- "extra": self.get_extra(request, obj, **kwargs),
- "min_num": self.get_min_num(request, obj, **kwargs),
- "max_num": self.get_max_num(request, obj, **kwargs),
- "can_delete": can_delete,
- }
-
- defaults.update(kwargs)
- base_model_form = defaults['form']
-
- class DeleteProtectedModelForm(base_model_form):
- def hand_clean_DELETE(self):
- """
- We don't validate the 'DELETE' field itself because on
- templates it's not rendered using the field information, but
- just using a generic "deletion_field" of the InlineModelAdmin.
- """
- if self.cleaned_data.get(DELETION_FIELD_NAME, False):
- using = router.db_for_write(self._meta.model)
- collector = NestedObjects(using=using)
- if self.instance.pk is None:
- return
- collector.collect([self.instance])
- if collector.protected:
- objs = []
- for p in collector.protected:
- objs.append(
- # Translators: Model verbose name and instance representation,
- # suitable to be an item in a list.
- _('%(class_name)s %(instance)s') % {
- 'class_name': p._meta.verbose_name,
- 'instance': p}
- )
- params = {'class_name': self._meta.model._meta.verbose_name,
- 'instance': self.instance,
- 'related_objects': get_text_list(objs, _('and'))}
- msg = _("Deleting %(class_name)s %(instance)s would require "
- "deleting the following protected related objects: "
- "%(related_objects)s")
- raise ValidationError(msg, code='deleting_protected', params=params)
-
- def is_valid(self):
- result = super(DeleteProtectedModelForm, self).is_valid()
- self.hand_clean_DELETE()
- return result
-
- defaults['form'] = DeleteProtectedModelForm
-
- if defaults['fields'] is None and not modelform_defines_fields(defaults['form']):
- defaults['fields'] = forms.ALL_FIELDS
-
- return inlineformset_factory(self.parent_model, self.model, **defaults)
-
- def get_fields(self, request, obj=None):
- if self.fields:
- return self.fields
- form = self.get_formset(request, obj, fields=None).form
- return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
-
- def get_queryset(self, request):
- queryset = super(InlineModelAdmin, self).get_queryset(request)
- if not self.has_change_permission(request):
- queryset = queryset.none()
- return queryset
-
- def has_add_permission(self, request):
- if self.opts.auto_created:
- # We're checking the rights to an auto-created intermediate model,
- # which doesn't have its own individual permissions. The user needs
- # to have the change permission for the related model in order to
- # be able to do anything with the intermediate model.
- return self.has_change_permission(request)
- return super(InlineModelAdmin, self).has_add_permission(request)
-
- def has_change_permission(self, request, obj=None):
- opts = self.opts
- if opts.auto_created:
- # The model was auto-created as intermediary for a
- # ManyToMany-relationship, find the target model
- for field in opts.fields:
- if field.remote_field and field.remote_field.model != self.parent_model:
- opts = field.remote_field.model._meta
- break
- codename = get_permission_codename('change', opts)
- return request.user.has_perm("%s.%s" % (opts.app_label, codename))
-
- def has_delete_permission(self, request, obj=None):
- if self.opts.auto_created:
- # We're checking the rights to an auto-created intermediate model,
- # which doesn't have its own individual permissions. The user needs
- # to have the change permission for the related model in order to
- # be able to do anything with the intermediate model.
- return self.has_change_permission(request, obj)
- return super(InlineModelAdmin, self).has_delete_permission(request, obj)
-
-
- class StackedInline(InlineModelAdmin):
- template = 'admin/edit_inline/stacked.html'
-
-
- class TabularInline(InlineModelAdmin):
- template = 'admin/edit_inline/tabular.html'
|