123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556 |
- import copy
- import enum
- import json
- import re
- from functools import partial, update_wrapper
- from urllib.parse import parse_qsl
- from urllib.parse import quote as urlquote
- from urllib.parse import urlparse
- 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, NotRegistered
- from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
- from django.contrib.admin.utils import (
- NestedObjects,
- construct_change_message,
- flatten_fieldsets,
- get_deleted_objects,
- lookup_spawns_duplicates,
- model_format_dict,
- model_ngettext,
- quote,
- unquote,
- )
- from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple
- 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.db import models, router, transaction
- from django.db.models.constants import LOOKUP_SEP
- 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 HttpResponseRedirect
- from django.http.response import HttpResponseBase
- from django.template.response import SimpleTemplateResponse, TemplateResponse
- from django.urls import reverse
- from django.utils.decorators import method_decorator
- from django.utils.html import format_html
- from django.utils.http import urlencode
- from django.utils.safestring import mark_safe
- from django.utils.text import (
- capfirst,
- format_lazy,
- get_text_list,
- smart_split,
- unescape_string_literal,
- )
- from django.utils.translation import gettext as _
- from django.utils.translation import ngettext
- from django.views.decorators.csrf import csrf_protect
- from django.views.generic import RedirectView
- IS_POPUP_VAR = "_popup"
- TO_FIELD_VAR = "_to_field"
- IS_FACETS_VAR = "_facets"
- class ShowFacets(enum.Enum):
- NEVER = "NEVER"
- ALLOW = "ALLOW"
- ALWAYS = "ALWAYS"
- 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},
- models.UUIDField: {"widget": widgets.AdminUUIDInputWidget},
- }
- csrf_protect_m = method_decorator(csrf_protect)
- class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
- """Functionality common to both ModelAdmin and InlineAdmin."""
- autocomplete_fields = ()
- 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
- sortable_by = 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):
- # Merge FORMFIELD_FOR_DBFIELD_DEFAULTS with the formfield_overrides
- # rather than simply overwriting.
- overrides = copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS)
- for k, v in self.formfield_overrides.items():
- overrides.setdefault(k, {}).update(v)
- self.formfield_overrides = overrides
- def formfield_for_dbfield(self, db_field, request, **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.
- """
- # 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 = {**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:
- try:
- related_modeladmin = self.admin_site.get_model_admin(
- db_field.remote_field.model
- )
- except NotRegistered:
- wrapper_kwargs = {}
- else:
- wrapper_kwargs = {
- "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
- ),
- "can_view_related": related_modeladmin.has_view_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 = {**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, **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
- (return None in that case).
- """
- try:
- related_admin = self.admin_site.get_model_admin(db_field.remote_field.model)
- except NotRegistered:
- return None
- else:
- 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, **kwargs):
- """
- Get a form Field for a ForeignKey.
- """
- db = kwargs.get("using")
- if "widget" not in kwargs:
- if db_field.name in self.get_autocomplete_fields(request):
- kwargs["widget"] = AutocompleteSelect(
- db_field, self.admin_site, using=db
- )
- elif 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"] = (
- kwargs.get("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, **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 "widget" not in kwargs:
- autocomplete_fields = self.get_autocomplete_fields(request)
- if db_field.name in autocomplete_fields:
- kwargs["widget"] = AutocompleteSelectMultiple(
- db_field,
- self.admin_site,
- using=db,
- )
- elif db_field.name in self.raw_id_fields:
- kwargs["widget"] = widgets.ManyToManyRawIdWidget(
- db_field.remote_field,
- self.admin_site,
- using=db,
- )
- elif db_field.name in [*self.filter_vertical, *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 form_field.widget.allow_multiple_selected
- and not isinstance(
- form_field.widget, (CheckboxSelectMultiple, AutocompleteSelectMultiple)
- )
- ):
- msg = _(
- "Hold down “Control”, or “Command” on a Mac, to select more than one."
- )
- help_text = form_field.help_text
- form_field.help_text = (
- format_lazy("{} {}", help_text, msg) if help_text else msg
- )
- return form_field
- def get_autocomplete_fields(self, request):
- """
- Return a list of ForeignKey and/or ManyToMany fields which should use
- an autocomplete widget.
- """
- return self.autocomplete_fields
- 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 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,
- },
- current_app=self.admin_site.name,
- )
- 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_exclude(self, request, obj=None):
- """
- Hook for specifying exclude.
- """
- return self.exclude
- def get_fields(self, request, obj=None):
- """
- Hook for specifying fields.
- """
- if self.fields:
- return self.fields
- # _get_form_for_get_fields() is implemented in subclasses.
- form = self._get_form_for_get_fields(request, obj)
- return [*form.base_fields, *self.get_readonly_fields(request, obj)]
- 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_inlines(self, request, obj):
- """Hook for specifying custom inlines."""
- return self.inlines
- 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):
- """
- Return 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 get_sortable_by(self, request):
- """Hook for specifying which fields can be sorted in the changelist."""
- return (
- self.sortable_by
- if self.sortable_by is not None
- else self.get_list_display(request)
- )
- # RemovedInDjango60Warning: when the deprecation ends, replace with:
- # def lookup_allowed(self, lookup, value, request):
- def lookup_allowed(self, lookup, value, request=None):
- 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 fk_lookup in model._meta.related_fkey_lookups:
- # As ``limit_choices_to`` can be a callable, invoke it here.
- if callable(fk_lookup):
- fk_lookup = fk_lookup()
- if (lookup, value) in widgets.url_params_from_lookup_dict(
- fk_lookup
- ).items():
- 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 nonexistent fields are ok, since they're ignored
- # later.
- break
- if not prev_field or (
- prev_field.is_relation
- and field not in model._meta.parents.values()
- and field is not model._meta.auto_field
- and (
- model._meta.auto_field is None
- or part not in getattr(prev_field, "to_fields", [])
- )
- ):
- relation_parts.append(part)
- if not getattr(field, "path_infos", None):
- # This is not a relational field, so further parts
- # must be transforms.
- break
- prev_field = field
- model = field.path_infos[-1].to_opts.model
- if len(relation_parts) <= 1:
- # Either a local field filter, or no fields at all.
- return True
- valid_lookups = {self.date_hierarchy}
- # RemovedInDjango60Warning: when the deprecation ends, replace with:
- # for filter_item in self.get_list_filter(request):
- list_filter = (
- self.get_list_filter(request) if request is not None else self.list_filter
- )
- for filter_item in list_filter:
- if isinstance(filter_item, type) and issubclass(
- filter_item, SimpleListFilter
- ):
- valid_lookups.add(filter_item.parameter_name)
- elif isinstance(filter_item, (list, tuple)):
- valid_lookups.add(filter_item[0])
- else:
- valid_lookups.add(filter_item)
- # Is it a valid relational lookup?
- return not {
- LOOKUP_SEP.join(relation_parts),
- LOOKUP_SEP.join(relation_parts + [part]),
- }.isdisjoint(valid_lookups)
- def to_field_allowed(self, request, to_field):
- """
- Return True if the model associated with this admin should be
- allowed to be referenced by the specified field.
- """
- try:
- field = self.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 self.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 self.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):
- """
- Return 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):
- """
- Return 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):
- """
- Return True if the given request has permission to delete 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_view_permission(self, request, obj=None):
- """
- Return True if the given request has permission to view the given
- Django model instance. The default implementation doesn't examine the
- `obj` parameter.
- If overridden by the user in subclasses, it should return True if the
- given request has permission to view the `obj` model instance. If `obj`
- is None, it should return True if the request has permission to view
- any object of the given type.
- """
- opts = self.opts
- codename_view = get_permission_codename("view", opts)
- codename_change = get_permission_codename("change", opts)
- return request.user.has_perm(
- "%s.%s" % (opts.app_label, codename_view)
- ) or request.user.has_perm("%s.%s" % (opts.app_label, codename_change))
- def has_view_or_change_permission(self, request, obj=None):
- return self.has_view_permission(request, obj) or self.has_change_permission(
- request, obj
- )
- def has_module_permission(self, request):
- """
- Return 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)
- class ModelAdmin(BaseModelAdmin):
- """Encapsulate 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 = ()
- search_help_text = None
- date_hierarchy = None
- save_as = False
- save_as_continue = True
- save_on_top = False
- paginator = Paginator
- preserve_filters = True
- show_facets = ShowFacets.ALLOW
- 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
- popup_response_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().__init__()
- def __str__(self):
- return "%s.%s" % (self.opts.app_label, self.__class__.__name__)
- def __repr__(self):
- return (
- f"<{self.__class__.__qualname__}: model={self.model.__qualname__} "
- f"site={self.admin_site!r}>"
- )
- def get_inline_instances(self, request, obj=None):
- inline_instances = []
- for inline_class in self.get_inlines(request, obj):
- inline = inline_class(self.model, self.admin_site)
- if request:
- if not (
- inline.has_view_or_change_permission(request, obj)
- or inline.has_add_permission(request, obj)
- or inline.has_delete_permission(request, obj)
- ):
- continue
- if not inline.has_add_permission(request, obj):
- inline.max_num = 0
- inline_instances.append(inline)
- return inline_instances
- def get_urls(self):
- from django.urls import path
- 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.opts.app_label, self.opts.model_name
- return [
- path("", wrap(self.changelist_view), name="%s_%s_changelist" % info),
- path("add/", wrap(self.add_view), name="%s_%s_add" % info),
- path(
- "<path:object_id>/history/",
- wrap(self.history_view),
- name="%s_%s_history" % info,
- ),
- path(
- "<path:object_id>/delete/",
- wrap(self.delete_view),
- name="%s_%s_delete" % info,
- ),
- path(
- "<path:object_id>/change/",
- wrap(self.change_view),
- name="%s_%s_change" % info,
- ),
- # For backwards compatibility (was the change url before 1.9)
- path(
- "<path:object_id>/",
- wrap(
- RedirectView.as_view(
- pattern_name="%s:%s_%s_change"
- % ((self.admin_site.name,) + info)
- )
- ),
- ),
- ]
- @property
- def urls(self):
- return self.get_urls()
- @property
- def media(self):
- extra = "" if settings.DEBUG else ".min"
- js = [
- "vendor/jquery/jquery%s.js" % extra,
- "jquery.init.js",
- "core.js",
- "admin/RelatedObjectLookups.js",
- "actions.js",
- "urlify.js",
- "prepopulate.js",
- "vendor/xregexp/xregexp%s.js" % extra,
- ]
- return forms.Media(js=["admin/js/%s" % url for url in js])
- def get_model_perms(self, request):
- """
- Return a dict of all perms for this model. This dict has the keys
- ``add``, ``change``, ``delete``, and ``view`` 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),
- "view": self.has_view_permission(request),
- }
- def _get_form_for_get_fields(self, request, obj):
- return self.get_form(request, obj, fields=None)
- def get_form(self, request, obj=None, change=False, **kwargs):
- """
- Return 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))
- excluded = self.get_exclude(request, obj)
- exclude = [] if excluded is None else list(excluded)
- readonly_fields = self.get_readonly_fields(request, obj)
- exclude.extend(readonly_fields)
- # Exclude all fields if it's a change form and the user doesn't have
- # the change permission.
- if (
- change
- and hasattr(request, "user")
- and not self.has_change_permission(request, obj)
- ):
- exclude.extend(fields)
- if excluded 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 = dict.fromkeys(
- f 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),
- **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):
- """
- Return the ChangeList class for use on the changelist page.
- """
- from django.contrib.admin.views.main import ChangeList
- return ChangeList
- def get_changelist_instance(self, request):
- """
- Return a `ChangeList` instance based on `request`. May raise
- `IncorrectLookupParameters`.
- """
- list_display = self.get_list_display(request)
- list_display_links = self.get_list_display_links(request, list_display)
- # Add the action checkboxes if any actions are available.
- if self.get_actions(request):
- list_display = ["action_checkbox", *list_display]
- sortable_by = self.get_sortable_by(request)
- ChangeList = self.get_changelist(request)
- return ChangeList(
- request,
- self.model,
- list_display,
- list_display_links,
- self.get_list_filter(request),
- self.date_hierarchy,
- self.get_search_fields(request),
- self.get_list_select_related(request),
- self.list_per_page,
- self.list_max_show_all,
- self.list_editable,
- self,
- sortable_by,
- self.search_help_text,
- )
- def get_object(self, request, object_id, from_field=None):
- """
- Return an instance matching the field and value provided, the primary
- key is used if no field is provided. Return ``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):
- """
- Return a Form class for use in the Formset on the changelist page.
- """
- defaults = {
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- **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):
- """
- Return a FormSet class for use on the changelist page if list_editable
- is used.
- """
- defaults = {
- "formfield_callback": partial(self.formfield_for_dbfield, request=request),
- **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):
- """
- Yield 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, obj, message):
- """
- Log that an object has been successfully added.
- The default implementation creates an admin LogEntry object.
- """
- from django.contrib.admin.models import ADDITION, LogEntry
- return LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(obj).pk,
- object_id=obj.pk,
- object_repr=str(obj),
- action_flag=ADDITION,
- change_message=message,
- )
- def log_change(self, request, obj, message):
- """
- Log that an object has been successfully changed.
- The default implementation creates an admin LogEntry object.
- """
- from django.contrib.admin.models import CHANGE, LogEntry
- return LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(obj).pk,
- object_id=obj.pk,
- object_repr=str(obj),
- action_flag=CHANGE,
- change_message=message,
- )
- def log_deletion(self, request, obj, 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 DELETION, LogEntry
- return LogEntry.objects.log_action(
- user_id=request.user.pk,
- content_type_id=get_content_type_for_model(obj).pk,
- object_id=obj.pk,
- object_repr=object_repr,
- action_flag=DELETION,
- )
- def action_checkbox(self, obj):
- """
- A list_display column containing a checkbox widget.
- """
- attrs = {
- "class": "action-select",
- "aria-label": format_html(_("Select this object for an action - {}"), obj),
- }
- checkbox = forms.CheckboxInput(attrs, lambda value: False)
- return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
- @staticmethod
- def _get_action_description(func, name):
- return getattr(func, "short_description", capfirst(name.replace("_", " ")))
- def _get_base_actions(self):
- """Return the list of actions, prior to any request-based filtering."""
- actions = []
- base_actions = (self.get_action(action) for action in self.actions or [])
- # get_action might have returned None, so filter any of those out.
- base_actions = [action for action in base_actions if action]
- base_action_names = {name for _, name, _ in base_actions}
- # Gather actions from the admin site first
- for name, func in self.admin_site.actions:
- if name in base_action_names:
- continue
- description = self._get_action_description(func, name)
- actions.append((func, name, description))
- # Add actions from this ModelAdmin.
- actions.extend(base_actions)
- return actions
- def _filter_actions_by_permissions(self, request, actions):
- """Filter out any actions that the user doesn't have access to."""
- filtered_actions = []
- for action in actions:
- callable = action[0]
- if not hasattr(callable, "allowed_permissions"):
- filtered_actions.append(action)
- continue
- permission_checks = (
- getattr(self, "has_%s_permission" % permission)
- for permission in callable.allowed_permissions
- )
- if any(has_permission(request) for has_permission in permission_checks):
- filtered_actions.append(action)
- return filtered_actions
- 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 set to None that means actions are disabled on
- # this page.
- if self.actions is None or IS_POPUP_VAR in request.GET:
- return {}
- actions = self._filter_actions_by_permissions(request, self._get_base_actions())
- return {name: (func, name, desc) for func, name, desc in actions}
- def get_action_choices(self, request, default_choices=models.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 self.get_actions(request).values():
- 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
- description = self._get_action_description(func, action)
- 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):
- """
- Return 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):
- """
- Return 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):
- """
- Return 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):
- """
- Return 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.removeprefix("^")
- elif field_name.startswith("="):
- return "%s__iexact" % field_name.removeprefix("=")
- elif field_name.startswith("@"):
- return "%s__search" % field_name.removeprefix("@")
- # Use field_name if it includes a lookup.
- opts = queryset.model._meta
- lookup_fields = field_name.split(LOOKUP_SEP)
- # Go through the fields, following all relations.
- prev_field = None
- for path_part in lookup_fields:
- if path_part == "pk":
- path_part = opts.pk.name
- try:
- field = opts.get_field(path_part)
- except FieldDoesNotExist:
- # Use valid query lookups.
- if prev_field and prev_field.get_lookup(path_part):
- return field_name
- else:
- prev_field = field
- if hasattr(field, "path_infos"):
- # Update opts to follow the relation.
- opts = field.path_infos[-1].to_opts
- # Otherwise, use the field with icontains.
- return "%s__icontains" % field_name
- may_have_duplicates = 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
- ]
- term_queries = []
- for bit in smart_split(search_term):
- if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
- bit = unescape_string_literal(bit)
- or_queries = models.Q.create(
- [(orm_lookup, bit) for orm_lookup in orm_lookups],
- connector=models.Q.OR,
- )
- term_queries.append(or_queries)
- queryset = queryset.filter(models.Q.create(term_queries))
- may_have_duplicates |= any(
- lookup_spawns_duplicates(self.opts, search_spec)
- for search_spec in orm_lookups
- )
- return queryset, may_have_duplicates
- def get_preserved_filters(self, request):
- """
- Return the preserved filters querystring.
- """
- match = request.resolver_match
- if self.preserve_filters and match:
- current_url = "%s:%s" % (match.app_name, match.url_name)
- changelist_url = "admin:%s_%s_changelist" % (
- self.opts.app_label,
- self.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 JSON structure describing changes from a changed object.
- """
- return construct_change_message(form, formsets, add)
- 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`" % level for level 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 delete_queryset(self, request, queryset):
- """Given a queryset, delete it from the database."""
- queryset.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
- ):
- app_label = self.opts.app_label
- preserved_filters = self.get_preserved_filters(request)
- form_url = add_preserved_filters(
- {"preserved_filters": preserved_filters, "opts": self.opts}, form_url
- )
- view_on_site_url = self.get_view_on_site_url(obj)
- has_editable_inline_admin_formsets = False
- for inline in context["inline_admin_formsets"]:
- if (
- inline.has_add_permission
- or inline.has_change_permission
- or inline.has_delete_permission
- ):
- has_editable_inline_admin_formsets = True
- break
- context.update(
- {
- "add": add,
- "change": change,
- "has_view_permission": self.has_view_permission(request, obj),
- "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_editable_inline_admin_formsets": (
- has_editable_inline_admin_formsets
- ),
- "has_file_field": context["adminform"].form.is_multipart()
- or any(
- admin_formset.formset.is_multipart()
- for admin_formset in context["inline_admin_formsets"]
- ),
- "has_absolute_url": view_on_site_url is not None,
- "absolute_url": view_on_site_url,
- "form_url": form_url,
- "opts": self.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, self.opts.model_name),
- "admin/%s/change_form.html" % app_label,
- "admin/change_form.html",
- ],
- context,
- )
- def _get_preserved_qsl(self, request, preserved_filters):
- query_string = urlparse(request.build_absolute_uri()).query
- return parse_qsl(query_string.replace(preserved_filters, ""))
- def response_add(self, request, obj, post_url_continue=None):
- """
- Determine the HttpResponse for the add_view stage.
- """
- opts = obj._meta
- preserved_filters = self.get_preserved_filters(request)
- preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
- obj_url = reverse(
- "admin:%s_%s_change" % (opts.app_label, opts.model_name),
- args=(quote(obj.pk),),
- current_app=self.admin_site.name,
- )
- # Add a link to the object's change form if the user can edit the obj.
- if self.has_change_permission(request, obj):
- obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
- else:
- obj_repr = str(obj)
- msg_dict = {
- "name": opts.verbose_name,
- "obj": obj_repr,
- }
- # 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)
- popup_response_data = json.dumps(
- {
- "value": str(value),
- "obj": str(obj),
- }
- )
- return TemplateResponse(
- request,
- self.popup_response_template
- or [
- "admin/%s/%s/popup_response.html"
- % (opts.app_label, opts.model_name),
- "admin/%s/popup_response.html" % opts.app_label,
- "admin/popup_response.html",
- ],
- {
- "popup_response_data": popup_response_data,
- },
- )
- elif "_continue" in request.POST or (
- # Redirecting after "Save as new".
- "_saveasnew" in request.POST
- and self.save_as_continue
- and self.has_change_permission(request, obj)
- ):
- msg = _("The {name} “{obj}” was added successfully.")
- if self.has_change_permission(request, obj):
- msg += " " + _("You may edit it again below.")
- self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
- if post_url_continue is None:
- post_url_continue = obj_url
- post_url_continue = add_preserved_filters(
- {
- "preserved_filters": preserved_filters,
- "preserved_qsl": preserved_qsl,
- "opts": opts,
- },
- post_url_continue,
- )
- return HttpResponseRedirect(post_url_continue)
- elif "_addanother" in request.POST:
- msg = format_html(
- _(
- "The {name} “{obj}” was added successfully. You may add another "
- "{name} below."
- ),
- **msg_dict,
- )
- self.message_user(request, msg, messages.SUCCESS)
- redirect_url = request.path
- redirect_url = add_preserved_filters(
- {
- "preserved_filters": preserved_filters,
- "preserved_qsl": preserved_qsl,
- "opts": opts,
- },
- redirect_url,
- )
- return HttpResponseRedirect(redirect_url)
- else:
- msg = format_html(
- _("The {name} “{obj}” 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):
- """
- Determine the HttpResponse for the change_view stage.
- """
- if IS_POPUP_VAR in request.POST:
- opts = obj._meta
- to_field = request.POST.get(TO_FIELD_VAR)
- attr = str(to_field) if to_field else opts.pk.attname
- value = request.resolver_match.kwargs["object_id"]
- new_value = obj.serializable_value(attr)
- popup_response_data = json.dumps(
- {
- "action": "change",
- "value": str(value),
- "obj": str(obj),
- "new_value": str(new_value),
- }
- )
- return TemplateResponse(
- request,
- self.popup_response_template
- or [
- "admin/%s/%s/popup_response.html"
- % (opts.app_label, opts.model_name),
- "admin/%s/popup_response.html" % opts.app_label,
- "admin/popup_response.html",
- ],
- {
- "popup_response_data": popup_response_data,
- },
- )
- opts = self.opts
- preserved_filters = self.get_preserved_filters(request)
- preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
- msg_dict = {
- "name": opts.verbose_name,
- "obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
- }
- if "_continue" in request.POST:
- msg = format_html(
- _(
- "The {name} “{obj}” 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,
- "preserved_qsl": preserved_qsl,
- "opts": opts,
- },
- redirect_url,
- )
- return HttpResponseRedirect(redirect_url)
- elif "_saveasnew" in request.POST:
- msg = format_html(
- _(
- "The {name} “{obj}” 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=(obj.pk,),
- 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 = format_html(
- _(
- "The {name} “{obj}” was changed successfully. You may add another "
- "{name} 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,
- "preserved_qsl": preserved_qsl,
- "opts": opts,
- },
- redirect_url,
- )
- return HttpResponseRedirect(redirect_url)
- else:
- msg = format_html(
- _("The {name} “{obj}” was changed successfully."), **msg_dict
- )
- self.message_user(request, msg, messages.SUCCESS)
- return self.response_post_save_change(request, obj)
- def _response_post_save(self, request, obj):
- if self.has_view_or_change_permission(request):
- post_url = reverse(
- "admin:%s_%s_changelist" % (self.opts.app_label, self.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": self.opts}, post_url
- )
- else:
- post_url = reverse("admin:index", current_app=self.admin_site.name)
- return HttpResponseRedirect(post_url)
- 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.
- """
- return self._response_post_save(request, obj)
- 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.
- """
- return self._response_post_save(request, obj)
- 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):
- """
- Determine the HttpResponse for the delete_view stage.
- """
- if IS_POPUP_VAR in request.POST:
- popup_response_data = json.dumps(
- {
- "action": "delete",
- "value": str(obj_id),
- }
- )
- return TemplateResponse(
- request,
- self.popup_response_template
- or [
- "admin/%s/%s/popup_response.html"
- % (self.opts.app_label, self.opts.model_name),
- "admin/%s/popup_response.html" % self.opts.app_label,
- "admin/popup_response.html",
- ],
- {
- "popup_response_data": popup_response_data,
- },
- )
- self.message_user(
- request,
- _("The %(name)s “%(obj)s” was deleted successfully.")
- % {
- "name": self.opts.verbose_name,
- "obj": obj_display,
- },
- messages.SUCCESS,
- )
- if self.has_change_permission(request, None):
- post_url = reverse(
- "admin:%s_%s_changelist" % (self.opts.app_label, self.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": self.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):
- app_label = self.opts.app_label
- request.current_app = self.admin_site.name
- context.update(
- to_field_var=TO_FIELD_VAR,
- is_popup_var=IS_POPUP_VAR,
- media=self.media,
- )
- return TemplateResponse(
- request,
- self.delete_confirmation_template
- or [
- "admin/{}/{}/delete_confirmation.html".format(
- app_label, self.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):
- # Edit permissions on parent model are required for editable inlines.
- can_edit_parent = (
- self.has_change_permission(request, obj)
- if obj
- else self.has_add_permission(request)
- )
- 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))
- if can_edit_parent:
- has_add_permission = inline.has_add_permission(request, obj)
- has_change_permission = inline.has_change_permission(request, obj)
- has_delete_permission = inline.has_delete_permission(request, obj)
- else:
- # Disable all edit-permissions, and override formset settings.
- has_add_permission = (
- has_change_permission
- ) = has_delete_permission = False
- formset.extra = formset.max_num = 0
- has_view_permission = inline.has_view_permission(request, obj)
- prepopulated = dict(inline.get_prepopulated_fields(request, obj))
- inline_admin_formset = helpers.InlineAdminFormSet(
- inline,
- formset,
- fieldsets,
- prepopulated,
- readonly,
- model_admin=self,
- has_add_permission=has_add_permission,
- has_change_permission=has_change_permission,
- has_delete_permission=has_delete_permission,
- has_view_permission=has_view_permission,
- )
- inline_admin_formsets.append(inline_admin_formset)
- return inline_admin_formsets
- def get_changeform_initial_data(self, request):
- """
- Get the initial form data from the request's GET params.
- """
- initial = dict(request.GET.items())
- for k in initial:
- try:
- f = self.opts.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
- def _get_obj_does_not_exist_redirect(self, request, opts, object_id):
- """
- Create a message informing the user that the object doesn't exist
- and return a redirect to the admin index page.
- """
- msg = _("%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?") % {
- "name": opts.verbose_name,
- "key": unquote(object_id),
- }
- self.message_user(request, msg, messages.WARNING)
- url = reverse("admin:index", current_app=self.admin_site.name)
- return HttpResponseRedirect(url)
- @csrf_protect_m
- def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
- with transaction.atomic(using=router.db_for_write(self.model)):
- return self._changeform_view(request, object_id, form_url, extra_context)
- def _changeform_view(self, request, object_id, form_url, extra_context):
- 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
- )
- if request.method == "POST" and "_saveasnew" in request.POST:
- object_id = None
- add = object_id is None
- if add:
- if not self.has_add_permission(request):
- raise PermissionDenied
- obj = None
- else:
- obj = self.get_object(request, unquote(object_id), to_field)
- if request.method == "POST":
- if not self.has_change_permission(request, obj):
- raise PermissionDenied
- else:
- if not self.has_view_or_change_permission(request, obj):
- raise PermissionDenied
- if obj is None:
- return self._get_obj_does_not_exist_redirect(
- request, self.opts, object_id
- )
- fieldsets = self.get_fieldsets(request, obj)
- ModelForm = self.get_form(
- request, obj, change=not add, fields=flatten_fieldsets(fieldsets)
- )
- if request.method == "POST":
- form = ModelForm(request.POST, request.FILES, instance=obj)
- formsets, inline_instances = self._create_formsets(
- request,
- form.instance,
- change=not add,
- )
- form_validated = form.is_valid()
- if form_validated:
- new_object = self.save_form(request, form, change=not add)
- else:
- new_object = form.instance
- 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
- )
- if not add and not self.has_change_permission(request, obj):
- readonly_fields = flatten_fieldsets(fieldsets)
- else:
- readonly_fields = self.get_readonly_fields(request, obj)
- admin_form = helpers.AdminForm(
- form,
- list(fieldsets),
- # Clear prepopulated fields on a view-only form to avoid a crash.
- self.get_prepopulated_fields(request, obj)
- if add or self.has_change_permission(request, obj)
- else {},
- readonly_fields,
- model_admin=self,
- )
- media = self.media + admin_form.media
- inline_formsets = self.get_inline_formsets(
- request, formsets, inline_instances, obj
- )
- for inline_formset in inline_formsets:
- media += inline_formset.media
- if add:
- title = _("Add %s")
- elif self.has_change_permission(request, obj):
- title = _("Change %s")
- else:
- title = _("View %s")
- context = {
- **self.admin_site.each_context(request),
- "title": title % self.opts.verbose_name,
- "subtitle": str(obj) if obj else None,
- "adminform": admin_form,
- "object_id": object_id,
- "original": obj,
- "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
- "to_field": to_field,
- "media": media,
- "inline_admin_formsets": inline_formsets,
- "errors": helpers.AdminErrorList(form, formsets),
- "preserved_filters": self.get_preserved_filters(request),
- }
- # Hide the "Save" and "Save and continue" buttons if "Save as New" was
- # previously chosen to prevent the interface from getting confusing.
- if (
- request.method == "POST"
- and not form_validated
- and "_saveasnew" in request.POST
- ):
- context["show_save"] = False
- context["show_save_and_continue"] = False
- # Use the change template instead of the add template.
- add = False
- context.update(extra_context or {})
- return self.render_change_form(
- request, context, add=add, change=not add, obj=obj, form_url=form_url
- )
- def add_view(self, request, form_url="", extra_context=None):
- return self.changeform_view(request, None, form_url, extra_context)
- def change_view(self, request, object_id, form_url="", extra_context=None):
- return self.changeform_view(request, object_id, form_url, extra_context)
- def _get_edited_object_pks(self, request, prefix):
- """Return POST data values of list_editable primary keys."""
- pk_pattern = re.compile(
- r"{}-\d+-{}$".format(re.escape(prefix), self.opts.pk.name)
- )
- return [value for key, value in request.POST.items() if pk_pattern.match(key)]
- def _get_list_editable_queryset(self, request, prefix):
- """
- Based on POST data, return a queryset of the objects that were edited
- via list_editable.
- """
- object_pks = self._get_edited_object_pks(request, prefix)
- queryset = self.get_queryset(request)
- validate = queryset.model._meta.pk.to_python
- try:
- for pk in object_pks:
- validate(pk)
- except ValidationError:
- # Disable the optimization if the POST data was tampered with.
- return queryset
- return queryset.filter(pk__in=object_pks)
- @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
- app_label = self.opts.app_label
- if not self.has_view_or_change_permission(request):
- raise PermissionDenied
- try:
- cl = self.get_changelist_instance(request)
- 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:
- 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 = self.get_actions(request)
- # 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 action_failed:
- # Redirect back to the changelist page to avoid resubmitting the
- # form if the user refreshes the browser or uses the "No, take
- # me back" button on the action confirmation page.
- return HttpResponseRedirect(request.get_full_path())
- # 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:
- if not self.has_change_permission(request):
- raise PermissionDenied
- FormSet = self.get_changelist_formset(request)
- modified_objects = self._get_list_editable_queryset(
- request, FormSet.get_default_prefix()
- )
- formset = cl.formset = FormSet(
- request.POST, request.FILES, queryset=modified_objects
- )
- if formset.is_valid():
- changecount = 0
- with transaction.atomic(using=router.db_for_write(self.model)):
- 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:
- msg = ngettext(
- "%(count)s %(name)s was changed successfully.",
- "%(count)s %(name)s were changed successfully.",
- changecount,
- ) % {
- "count": changecount,
- "name": model_ngettext(self.opts, changecount),
- }
- self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(request.get_full_path())
- # Handle GET -- construct a formset for display.
- elif cl.list_editable and self.has_change_permission(request):
- 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)
- media += action_form.media
- else:
- action_form = None
- selection_note_all = ngettext(
- "%(total_count)s selected", "All %(total_count)s selected", cl.result_count
- )
- context = {
- **self.admin_site.each_context(request),
- "module_name": str(self.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,
- "subtitle": None,
- "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),
- **(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, self.opts.model_name),
- "admin/%s/change_list.html" % app_label,
- "admin/change_list.html",
- ],
- context,
- )
- def get_deleted_objects(self, objs, request):
- """
- Hook for customizing the delete process for the delete view and the
- "delete selected" action.
- """
- return get_deleted_objects(objs, request, self.admin_site)
- @csrf_protect_m
- def delete_view(self, request, object_id, extra_context=None):
- with transaction.atomic(using=router.db_for_write(self.model)):
- return self._delete_view(request, object_id, extra_context)
- def _delete_view(self, request, object_id, extra_context):
- "The 'delete' admin view for this model."
- app_label = self.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:
- return self._get_obj_does_not_exist_redirect(request, self.opts, object_id)
- # Populate deleted_objects, a data structure of all related objects that
- # will also be deleted.
- (
- deleted_objects,
- model_count,
- perms_needed,
- protected,
- ) = self.get_deleted_objects([obj], request)
- if request.POST and not protected: # The user has confirmed the deletion.
- if perms_needed:
- raise PermissionDenied
- obj_display = str(obj)
- attr = str(to_field) if to_field else self.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 = str(self.opts.verbose_name)
- if perms_needed or protected:
- title = _("Cannot delete %(name)s") % {"name": object_name}
- else:
- title = _("Are you sure?")
- context = {
- **self.admin_site.each_context(request),
- "title": title,
- "subtitle": None,
- "object_name": object_name,
- "object": obj,
- "deleted_objects": deleted_objects,
- "model_count": dict(model_count).items(),
- "perms_lacking": perms_needed,
- "protected": protected,
- "opts": self.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,
- **(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
- from django.contrib.admin.views.main import PAGE_VAR
- # First check if the user can see this history.
- model = self.model
- obj = self.get_object(request, unquote(object_id))
- if obj is None:
- return self._get_obj_does_not_exist_redirect(
- request, model._meta, object_id
- )
- if not self.has_view_or_change_permission(request, obj):
- raise PermissionDenied
- # Then get the history for this object.
- app_label = self.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")
- )
- paginator = self.get_paginator(request, action_list, 100)
- page_number = request.GET.get(PAGE_VAR, 1)
- page_obj = paginator.get_page(page_number)
- page_range = paginator.get_elided_page_range(page_obj.number)
- context = {
- **self.admin_site.each_context(request),
- "title": _("Change history: %s") % obj,
- "subtitle": None,
- "action_list": page_obj,
- "page_range": page_range,
- "page_var": PAGE_VAR,
- "pagination_required": paginator.count > 100,
- "module_name": str(capfirst(self.opts.verbose_name_plural)),
- "object": obj,
- "opts": self.opts,
- "preserved_filters": self.get_preserved_filters(request),
- **(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, self.opts.model_name),
- "admin/%s/object_history.html" % app_label,
- "admin/object_history.html",
- ],
- context,
- )
- def get_formset_kwargs(self, request, obj, inline, prefix):
- formset_params = {
- "instance": obj,
- "prefix": prefix,
- "queryset": inline.get_queryset(request),
- }
- if request.method == "POST":
- formset_params.update(
- {
- "data": request.POST.copy(),
- "files": request.FILES,
- "save_as_new": "_saveasnew" in request.POST,
- }
- )
- return formset_params
- 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 = self.get_formset_kwargs(request, obj, inline, prefix)
- formset = FormSet(**formset_params)
- def user_deleted_form(request, obj, formset, index, inline):
- """Return whether or not the user deleted the form."""
- return (
- inline.has_delete_permission(request, obj)
- and "{}-{}-DELETE".format(formset.prefix, index) in request.POST
- )
- # Bypass validation of each view-only inline form (since the form's
- # data won't be in request.POST), unless the form was deleted.
- if not inline.has_change_permission(request, obj if change else None):
- for index, form in enumerate(formset.initial_forms):
- if user_deleted_form(request, obj, formset, index, inline):
- continue
- form._errors = {}
- form.cleaned_data = form.initial
- formsets.append(formset)
- 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
- classes = None
- 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().__init__()
- if self.verbose_name_plural is None:
- if self.verbose_name is None:
- self.verbose_name_plural = self.opts.verbose_name_plural
- else:
- self.verbose_name_plural = format_lazy("{}s", self.verbose_name)
- if self.verbose_name is None:
- self.verbose_name = self.opts.verbose_name
- @property
- def media(self):
- extra = "" if settings.DEBUG else ".min"
- js = ["vendor/jquery/jquery%s.js" % extra, "jquery.init.js", "inlines.js"]
- if self.filter_vertical or self.filter_horizontal:
- js.extend(["SelectBox.js", "SelectFilter2.js"])
- if self.classes and "collapse" in self.classes:
- js.append("collapse.js")
- return forms.Media(js=["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):
- """Return 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))
- excluded = self.get_exclude(request, obj)
- exclude = [] if excluded is None else list(excluded)
- exclude.extend(self.get_readonly_fields(request, obj))
- if excluded 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,
- **kwargs,
- }
- base_model_form = defaults["form"]
- can_change = self.has_change_permission(request, obj) if request else True
- can_add = self.has_add_permission(request, obj) if request else True
- 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._state.adding:
- 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().is_valid()
- self.hand_clean_DELETE()
- return result
- def has_changed(self):
- # Protect against unauthorized edits.
- if not can_change and not self.instance._state.adding:
- return False
- if not can_add and self.instance._state.adding:
- return False
- return super().has_changed()
- 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_form_for_get_fields(self, request, obj=None):
- return self.get_formset(request, obj, fields=None).form
- def get_queryset(self, request):
- queryset = super().get_queryset(request)
- if not self.has_view_or_change_permission(request):
- queryset = queryset.none()
- return queryset
- def _has_any_perms_for_target_model(self, request, perms):
- """
- This method is called only when the ModelAdmin's model is for an
- ManyToManyField's implicit through model (if self.opts.auto_created).
- Return True if the user has any of the given permissions ('add',
- 'change', etc.) for the model that points to the through model.
- """
- opts = self.opts
- # Find the target model of an auto-created many-to-many relationship.
- for field in opts.fields:
- if field.remote_field and field.remote_field.model != self.parent_model:
- opts = field.remote_field.model._meta
- break
- return any(
- request.user.has_perm(
- "%s.%s" % (opts.app_label, get_permission_codename(perm, opts))
- )
- for perm in perms
- )
- def has_add_permission(self, request, obj):
- if self.opts.auto_created:
- # Auto-created intermediate models don't have their own
- # 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_any_perms_for_target_model(request, ["change"])
- return super().has_add_permission(request)
- def has_change_permission(self, request, obj=None):
- if self.opts.auto_created:
- # Same comment as has_add_permission().
- return self._has_any_perms_for_target_model(request, ["change"])
- return super().has_change_permission(request)
- def has_delete_permission(self, request, obj=None):
- if self.opts.auto_created:
- # Same comment as has_add_permission().
- return self._has_any_perms_for_target_model(request, ["change"])
- return super().has_delete_permission(request, obj)
- def has_view_permission(self, request, obj=None):
- if self.opts.auto_created:
- # Same comment as has_add_permission(). The 'change' permission
- # also implies the 'view' permission.
- return self._has_any_perms_for_target_model(request, ["view", "change"])
- return super().has_view_permission(request)
- class StackedInline(InlineModelAdmin):
- template = "admin/edit_inline/stacked.html"
- class TabularInline(InlineModelAdmin):
- template = "admin/edit_inline/tabular.html"
|