diff options
author | ttt | 2017-05-13 00:29:47 +0530 |
---|---|---|
committer | ttt | 2017-05-13 00:29:47 +0530 |
commit | abf599be33b383a6a5baf9493093b2126a622ac8 (patch) | |
tree | 4c5ab6e0d935d5e65fabcf0258e4a00dd20a5afa /lib/python2.7/site-packages/django/contrib/admin/options.py | |
download | SBHS-2018-Rpi-abf599be33b383a6a5baf9493093b2126a622ac8.tar.gz SBHS-2018-Rpi-abf599be33b383a6a5baf9493093b2126a622ac8.tar.bz2 SBHS-2018-Rpi-abf599be33b383a6a5baf9493093b2126a622ac8.zip |
added all server files
Diffstat (limited to 'lib/python2.7/site-packages/django/contrib/admin/options.py')
-rw-r--r-- | lib/python2.7/site-packages/django/contrib/admin/options.py | 1718 |
1 files changed, 1718 insertions, 0 deletions
diff --git a/lib/python2.7/site-packages/django/contrib/admin/options.py b/lib/python2.7/site-packages/django/contrib/admin/options.py new file mode 100644 index 0000000..9dddcb0 --- /dev/null +++ b/lib/python2.7/site-packages/django/contrib/admin/options.py @@ -0,0 +1,1718 @@ +import copy +import operator +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 widgets, helpers +from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, + model_format_dict, NestedObjects, lookup_needs_distinct) +from django.contrib.admin import validation +from django.contrib.admin.templatetags.admin_static import static +from django.contrib.admin.templatetags.admin_urls import add_preserved_filters +from django.contrib.auth import get_permission_codename +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied, ValidationError, FieldError +from django.core.paginator import Paginator +from django.core.urlresolvers import reverse +from django.db import models, transaction, router +from django.db.models.constants import LOOKUP_SEP +from django.db.models.related import RelatedObject +from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist +from django.db.models.sql.constants import QUERY_TERMS +from django.forms.formsets import all_valid, DELETION_FIELD_NAME +from django.forms.models import (modelform_factory, modelformset_factory, + inlineformset_factory, BaseInlineFormSet, modelform_defines_fields) +from django.http import Http404, HttpResponseRedirect +from django.http.response import HttpResponseBase +from django.shortcuts import get_object_or_404 +from django.template.response import SimpleTemplateResponse, TemplateResponse +from django.utils.decorators import method_decorator +from django.utils.datastructures import SortedDict +from django.utils.html import escape, escapejs +from django.utils.safestring import mark_safe +from django.utils import six +from django.utils.deprecation import RenameMethodsBase +from django.utils.http import urlencode +from django.utils.text import capfirst, get_text_list +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +from django.utils.encoding import force_text +from django.views.decorators.csrf import csrf_protect + + +IS_POPUP_VAR = '_popup' + +HORIZONTAL, VERTICAL = 1, 2 +# returns the <ul> class for a given radio_admin field +get_ul_class = lambda x: 'radiolist%s' % (' inline' if x == HORIZONTAL else '') + + +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 RenameBaseModelAdminMethods(forms.MediaDefiningClass, RenameMethodsBase): + renamed_methods = ( + ('queryset', 'get_queryset', PendingDeprecationWarning), + ) + + +class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): + """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 + + # validation + validator_class = validation.BaseValidator + + @classmethod + def validate(cls, model): + validator = cls.validator_class() + validator.validate(cls, model) + + 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.rel.to) + can_add_related = bool(related_modeladmin and + related_modeladmin.has_add_permission(request)) + formfield.widget = widgets.RelatedFieldWidgetWrapper( + formfield.widget, db_field.rel, self.admin_site, + can_add_related=can_add_related) + + 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.rel.to, None) + if related_admin is not None: + ordering = related_admin.get_ordering(request) + if ordering is not None and ordering != (): + return db_field.rel.to._default_manager.using(db).order_by(*ordering).complex_filter(db_field.rel.limit_choices_to) + 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.rel, + 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 not 'queryset' 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.rel.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.rel, + 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 not 'queryset' 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 _declared_fieldsets(self): + if self.fieldsets: + return self.fieldsets + elif self.fields: + return [(None, {'fields': self.fields})] + return None + declared_fieldsets = property(_declared_fieldsets) + + 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): + 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: + for k, v in widgets.url_params_from_lookup_dict(l).items(): + if k == lookup and v == value: + return True + + parts = lookup.split(LOOKUP_SEP) + + # Last term in lookup is a query term (__exact, __startswith etc) + # This term can be ignored. + if len(parts) > 1 and parts[-1] in QUERY_TERMS: + parts.pop() + + # Special case -- foo__id__exact and foo__id queries are implied + # if foo has been specifically included in the lookup list; so + # drop __id if it is the last part. However, first we need to find + # the pk attribute name. + rel_name = None + for part in parts[:-1]: + try: + field, _, _, _ = model._meta.get_field_by_name(part) + except FieldDoesNotExist: + # Lookups on non-existent fields are ok, since they're ignored + # later. + return True + if hasattr(field, 'rel'): + if field.rel is None: + # This property or relation doesn't exist, but it's allowed + # since it's ignored in ChangeList.get_filters(). + return True + model = field.rel.to + rel_name = field.rel.get_related_field().name + elif isinstance(field, RelatedObject): + model = field.model + rel_name = model._meta.pk.name + else: + rel_name = None + if rel_name and len(parts) > 1 and parts[-1] == rel_name: + parts.pop() + + if len(parts) == 1: + return True + clean_lookup = LOOKUP_SEP.join(parts) + return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy + + 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)) + + +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 + + # validation + validator_class = validation.ModelAdminValidator + + def __init__(self, model, admin_site): + self.model = model + self.opts = model._meta + self.admin_site = admin_site + super(ModelAdmin, self).__init__() + + 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 patterns, url + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + return update_wrapper(wrapper, view) + + info = self.model._meta.app_label, self.model._meta.model_name + + urlpatterns = patterns('', + 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'^(.+)/$', + wrap(self.change_view), + name='%s_%s_change' % 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', + 'jquery%s.js' % extra, + 'jquery.init.js' + ] + if self.actions is not None: + js.append('actions%s.js' % extra) + if self.prepopulated_fields: + js.extend(['urlify.js', 'prepopulate%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_fieldsets(self, request, obj=None): + "Hook for specifying fieldsets for the add form." + if self.declared_fieldsets: + return self.declared_fieldsets + form = self.get_form(request, obj, fields=None) + fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) + return [(None, {'fields': fields})] + + 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) + 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 + # 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 + defaults = { + "form": self.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): + """ + Returns an instance matching the primary key provided. ``None`` is + returned if no match is found (or the object_id failed validation + against the primary key field). + """ + queryset = self.get_queryset(request) + model = queryset.model + try: + object_id = model._meta.pk.to_python(object_id) + return queryset.get(pk=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(self, request, obj=None): + for inline in self.get_inline_instances(request, obj): + yield inline.get_formset(request, obj) + + 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): + """ + 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=ContentType.objects.get_for_model(object).pk, + object_id=object.pk, + object_repr=force_text(object), + action_flag=ADDITION + ) + + 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=ContentType.objects.get_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 is 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=ContentType.objects.get_for_model(self.model).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" />') + action_checkbox.allow_tags = True + + 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. + from django.contrib.admin.views.main import _is_changelist_popup + if self.actions is None or _is_changelist_popup(request): + return SortedDict() + + 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 a SortedDict keyed by name. + actions = SortedDict([ + (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 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_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 + if self.search_fields and search_term: + orm_lookups = [construct_search(str(search_field)) + for search_field in self.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): + """ + Construct a change message from a changed object. + """ + change_message = [] + if 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) + 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': hasattr(self.model, 'get_absolute_url'), + 'form_url': form_url, + 'opts': opts, + 'content_type_id': ContentType.objects.get_for_model(self.model).id, + 'save_as': self.save_as, + 'save_on_top': self.save_on_top, + }) + if add and self.add_form_template is not None: + form_template = self.add_form_template + else: + form_template = self.change_form_template + + 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, current_app=self.admin_site.name) + + 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: + return SimpleTemplateResponse('admin/popup_response.html', { + 'pk_value': escape(pk_value), + 'obj': escapejs(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=(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. + """ + 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 + + @csrf_protect_m + @transaction.atomic + def add_view(self, request, form_url='', extra_context=None): + "The 'add' admin view for this model." + model = self.model + opts = model._meta + + if not self.has_add_permission(request): + raise PermissionDenied + + ModelForm = self.get_form(request) + formsets = [] + inline_instances = self.get_inline_instances(request, None) + if request.method == 'POST': + form = ModelForm(request.POST, request.FILES) + if form.is_valid(): + new_object = self.save_form(request, form, change=False) + form_validated = True + else: + form_validated = False + new_object = self.model() + prefixes = {} + for FormSet, inline in zip(self.get_formsets(request), inline_instances): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1 or not prefix: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(data=request.POST, files=request.FILES, + instance=new_object, + save_as_new="_saveasnew" in request.POST, + prefix=prefix, queryset=inline.get_queryset(request)) + formsets.append(formset) + if all_valid(formsets) and form_validated: + self.save_model(request, new_object, form, False) + self.save_related(request, form, formsets, False) + self.log_addition(request, new_object) + return self.response_add(request, new_object) + else: + # Prepare the dict of initial data from the request. + # We have to special-case M2Ms as a list of comma-separated PKs. + initial = dict(request.GET.items()) + for k in initial: + try: + f = opts.get_field(k) + except models.FieldDoesNotExist: + continue + if isinstance(f, models.ManyToManyField): + initial[k] = initial[k].split(",") + form = ModelForm(initial=initial) + prefixes = {} + for FormSet, inline in zip(self.get_formsets(request), inline_instances): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1 or not prefix: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(instance=self.model(), prefix=prefix, + queryset=inline.get_queryset(request)) + formsets.append(formset) + + adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), + self.get_prepopulated_fields(request), + self.get_readonly_fields(request), + model_admin=self) + media = self.media + adminForm.media + + inline_admin_formsets = [] + for inline, formset in zip(inline_instances, formsets): + fieldsets = list(inline.get_fieldsets(request)) + readonly = list(inline.get_readonly_fields(request)) + prepopulated = dict(inline.get_prepopulated_fields(request)) + inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, + fieldsets, prepopulated, readonly, model_admin=self) + inline_admin_formsets.append(inline_admin_formset) + media = media + inline_admin_formset.media + + context = { + 'title': _('Add %s') % force_text(opts.verbose_name), + 'adminform': adminForm, + 'is_popup': IS_POPUP_VAR in request.REQUEST, + 'media': media, + 'inline_admin_formsets': inline_admin_formsets, + 'errors': helpers.AdminErrorList(form, formsets), + 'app_label': opts.app_label, + 'preserved_filters': self.get_preserved_filters(request), + } + context.update(extra_context or {}) + return self.render_change_form(request, context, form_url=form_url, add=True) + + @csrf_protect_m + @transaction.atomic + def change_view(self, request, object_id, form_url='', extra_context=None): + "The 'change' admin view for this model." + model = self.model + opts = model._meta + + obj = self.get_object(request, unquote(object_id)) + + if not self.has_change_permission(request, obj): + raise PermissionDenied + + if obj is None: + raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_text(opts.verbose_name), 'key': escape(object_id)}) + + if request.method == 'POST' and "_saveasnew" in request.POST: + return self.add_view(request, form_url=reverse('admin:%s_%s_add' % + (opts.app_label, opts.model_name), + current_app=self.admin_site.name)) + + ModelForm = self.get_form(request, obj) + formsets = [] + inline_instances = self.get_inline_instances(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=True) + else: + form_validated = False + new_object = obj + prefixes = {} + for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1 or not prefix: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(request.POST, request.FILES, + instance=new_object, prefix=prefix, + queryset=inline.get_queryset(request)) + + formsets.append(formset) + + if all_valid(formsets) and form_validated: + self.save_model(request, new_object, form, True) + self.save_related(request, form, formsets, True) + change_message = self.construct_change_message(request, form, formsets) + self.log_change(request, new_object, change_message) + return self.response_change(request, new_object) + + else: + form = ModelForm(instance=obj) + prefixes = {} + for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances): + prefix = FormSet.get_default_prefix() + prefixes[prefix] = prefixes.get(prefix, 0) + 1 + if prefixes[prefix] != 1 or not prefix: + prefix = "%s-%s" % (prefix, prefixes[prefix]) + formset = FormSet(instance=obj, prefix=prefix, + queryset=inline.get_queryset(request)) + formsets.append(formset) + + adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), + self.get_prepopulated_fields(request, obj), + self.get_readonly_fields(request, obj), + model_admin=self) + media = self.media + adminForm.media + + inline_admin_formsets = [] + for inline, formset in zip(inline_instances, formsets): + fieldsets = list(inline.get_fieldsets(request, obj)) + readonly = list(inline.get_readonly_fields(request, obj)) + prepopulated = dict(inline.get_prepopulated_fields(request, obj)) + inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, + fieldsets, prepopulated, readonly, model_admin=self) + inline_admin_formsets.append(inline_admin_formset) + media = media + inline_admin_formset.media + + context = { + 'title': _('Change %s') % force_text(opts.verbose_name), + 'adminform': adminForm, + 'object_id': object_id, + 'original': obj, + 'is_popup': IS_POPUP_VAR in request.REQUEST, + 'media': media, + 'inline_admin_formsets': inline_admin_formsets, + 'errors': helpers.AdminErrorList(form, formsets), + 'app_label': opts.app_label, + 'preserved_filters': self.get_preserved_filters(request), + } + context.update(extra_context or {}) + return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url) + + @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) + + # 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, + self.search_fields, self.list_select_related, + self.list_per_page, self.list_max_show_all, self.list_editable, + self) + except IncorrectLookupParameters: + # Wacky lookup parameters were given, so redirect to the main + # changelist page, without parameters, and pass an 'invalid=1' + # parameter via the query string. If wacky parameters were given + # and the 'invalid=1' parameter was already in the query string, + # something is screwed up with the database, so display an error + # page. + if ERROR_FLAG in request.GET.keys(): + return SimpleTemplateResponse('admin/invalid_setup.html', { + 'title': _('Database error'), + }) + return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') + + # If the request was POSTed, this might be a bulk action or a bulk + # edit. Try to look up an action or confirmation first, but if this + # isn't an action the POST will fall through to the bulk edit check, + # below. + action_failed = False + selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) + + # Actions with no confirmation + if (actions and request.method == 'POST' and + 'index' in request.POST and '_save' not in request.POST): + if selected: + response = self.response_action(request, queryset=cl.get_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 = { + '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, + 'cl': cl, + 'media': media, + 'has_add_permission': self.has_add_permission(request), + 'opts': cl.opts, + 'app_label': app_label, + 'action_form': action_form, + 'actions_on_top': self.actions_on_top, + 'actions_on_bottom': self.actions_on_bottom, + 'actions_selection_counter': self.actions_selection_counter, + 'preserved_filters': self.get_preserved_filters(request), + } + context.update(extra_context or {}) + + 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, current_app=self.admin_site.name) + + @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 + + obj = self.get_object(request, unquote(object_id)) + + if not self.has_delete_permission(request, obj): + raise PermissionDenied + + if obj is None: + raise Http404( + _('%(name)s object with primary key %(key)r does not exist.') % + {'name': force_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, 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) + self.log_deletion(request, obj, obj_display) + self.delete_model(request, obj) + + self.message_user(request, _( + 'The %(name)s "%(obj)s" was deleted successfully.') % { + 'name': force_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) + + 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 = { + "title": title, + "object_name": object_name, + "object": obj, + "deleted_objects": deleted_objects, + "perms_lacking": perms_needed, + "protected": protected, + "opts": opts, + "app_label": app_label, + 'preserved_filters': self.get_preserved_filters(request), + } + context.update(extra_context or {}) + + return TemplateResponse(request, self.delete_confirmation_template or [ + "admin/%s/%s/delete_confirmation.html" % (app_label, opts.model_name), + "admin/%s/delete_confirmation.html" % app_label, + "admin/delete_confirmation.html" + ], context, current_app=self.admin_site.name) + + 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 = get_object_or_404(model, pk=unquote(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__id__exact=ContentType.objects.get_for_model(model).id + ).select_related().order_by('action_time') + + context = { + 'title': _('Change history: %s') % force_text(obj), + 'action_list': action_list, + 'module_name': capfirst(force_text(opts.verbose_name_plural)), + 'object': obj, + 'app_label': app_label, + 'opts': opts, + 'preserved_filters': self.get_preserved_filters(request), + } + context.update(extra_context or {}) + 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, current_app=self.admin_site.name) + + +class InlineModelAdmin(BaseModelAdmin): + """ + Options for inline editing of ``model`` instances. + + Provide ``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 + max_num = None + template = None + verbose_name = None + verbose_name_plural = None + can_delete = True + + # validation + validator_class = validation.InlineValidator + + def __init__(self, parent_model, admin_site): + self.admin_site = admin_site + self.parent_model = parent_model + self.opts = self.model._meta + 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 = ['jquery%s.js' % extra, 'jquery.init.js', 'inlines%s.js' % extra] + if self.prepopulated_fields: + js.extend(['urlify.js', 'prepopulate%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_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), + "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) + 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_fieldsets(self, request, obj=None): + if self.declared_fieldsets: + return self.declared_fieldsets + form = self.get_formset(request, obj, fields=None).form + fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) + return [(None, {'fields': fields})] + + 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.rel and field.rel.to != self.parent_model: + opts = field.rel.to._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' |