diff options
author | kinitrupti | 2016-11-21 12:16:09 +0530 |
---|---|---|
committer | kinitrupti | 2016-11-21 12:16:09 +0530 |
commit | dbaf7af91132ff93718678f2a5b42fee3d1b6acd (patch) | |
tree | aec4d3f94c7423280499ad6264d24f586881b08f /tbc/static/admin/options.py | |
parent | 19793cdf2025bb2a4560a0718ed5d6bce175e7b8 (diff) | |
download | Python-TBC-Interface-dbaf7af91132ff93718678f2a5b42fee3d1b6acd.tar.gz Python-TBC-Interface-dbaf7af91132ff93718678f2a5b42fee3d1b6acd.tar.bz2 Python-TBC-Interface-dbaf7af91132ff93718678f2a5b42fee3d1b6acd.zip |
Admin css fixed
Diffstat (limited to 'tbc/static/admin/options.py')
-rw-r--r-- | tbc/static/admin/options.py | 1916 |
1 files changed, 1916 insertions, 0 deletions
diff --git a/tbc/static/admin/options.py b/tbc/static/admin/options.py new file mode 100644 index 0000000..c7ac2ff --- /dev/null +++ b/tbc/static/admin/options.py @@ -0,0 +1,1916 @@ +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, escapejs +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', + 'admin/RelatedObjectLookups.js', + 'vendor/jquery/jquery%s.js' % extra, + 'jquery.init.js', + 'actions%s.js' % extra, + 'urlify.js', + 'prepopulate%s.js' % extra, + 'vendor/xregexp/xregexp.min.js', + ] + 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': escape(value), + 'obj': escapejs(obj), + 'new_value': escape(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 + 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)}) + + if request.method == 'POST' and "_saveasnew" in request.POST: + object_id = None + obj = None + + 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 + + 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' |