options.py 98 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574
  1. import copy
  2. import enum
  3. import json
  4. import re
  5. import warnings
  6. from functools import partial, update_wrapper
  7. from urllib.parse import parse_qsl
  8. from urllib.parse import quote as urlquote
  9. from urllib.parse import urlparse
  10. from django import forms
  11. from django.conf import settings
  12. from django.contrib import messages
  13. from django.contrib.admin import helpers, widgets
  14. from django.contrib.admin.checks import (
  15. BaseModelAdminChecks,
  16. InlineModelAdminChecks,
  17. ModelAdminChecks,
  18. )
  19. from django.contrib.admin.exceptions import DisallowedModelAdminToField, NotRegistered
  20. from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
  21. from django.contrib.admin.utils import (
  22. NestedObjects,
  23. construct_change_message,
  24. flatten_fieldsets,
  25. get_deleted_objects,
  26. lookup_spawns_duplicates,
  27. model_format_dict,
  28. model_ngettext,
  29. quote,
  30. unquote,
  31. )
  32. from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple
  33. from django.contrib.auth import get_permission_codename
  34. from django.core.exceptions import (
  35. FieldDoesNotExist,
  36. FieldError,
  37. PermissionDenied,
  38. ValidationError,
  39. )
  40. from django.core.paginator import Paginator
  41. from django.db import models, router, transaction
  42. from django.db.models.constants import LOOKUP_SEP
  43. from django.forms.formsets import DELETION_FIELD_NAME, all_valid
  44. from django.forms.models import (
  45. BaseInlineFormSet,
  46. inlineformset_factory,
  47. modelform_defines_fields,
  48. modelform_factory,
  49. modelformset_factory,
  50. )
  51. from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
  52. from django.http import HttpResponseRedirect
  53. from django.http.response import HttpResponseBase
  54. from django.template.response import SimpleTemplateResponse, TemplateResponse
  55. from django.urls import reverse
  56. from django.utils.decorators import method_decorator
  57. from django.utils.deprecation import RemovedInDjango60Warning
  58. from django.utils.html import format_html
  59. from django.utils.http import urlencode
  60. from django.utils.safestring import mark_safe
  61. from django.utils.text import (
  62. capfirst,
  63. format_lazy,
  64. get_text_list,
  65. smart_split,
  66. unescape_string_literal,
  67. )
  68. from django.utils.translation import gettext as _
  69. from django.utils.translation import ngettext
  70. from django.views.decorators.csrf import csrf_protect
  71. from django.views.generic import RedirectView
  72. IS_POPUP_VAR = "_popup"
  73. TO_FIELD_VAR = "_to_field"
  74. IS_FACETS_VAR = "_facets"
  75. class ShowFacets(enum.Enum):
  76. NEVER = "NEVER"
  77. ALLOW = "ALLOW"
  78. ALWAYS = "ALWAYS"
  79. HORIZONTAL, VERTICAL = 1, 2
  80. def get_content_type_for_model(obj):
  81. # Since this module gets imported in the application's root package,
  82. # it cannot import models from other applications at the module level.
  83. from django.contrib.contenttypes.models import ContentType
  84. return ContentType.objects.get_for_model(obj, for_concrete_model=False)
  85. def get_ul_class(radio_style):
  86. return "radiolist" if radio_style == VERTICAL else "radiolist inline"
  87. class IncorrectLookupParameters(Exception):
  88. pass
  89. # Defaults for formfield_overrides. ModelAdmin subclasses can change this
  90. # by adding to ModelAdmin.formfield_overrides.
  91. FORMFIELD_FOR_DBFIELD_DEFAULTS = {
  92. models.DateTimeField: {
  93. "form_class": forms.SplitDateTimeField,
  94. "widget": widgets.AdminSplitDateTime,
  95. },
  96. models.DateField: {"widget": widgets.AdminDateWidget},
  97. models.TimeField: {"widget": widgets.AdminTimeWidget},
  98. models.TextField: {"widget": widgets.AdminTextareaWidget},
  99. models.URLField: {"widget": widgets.AdminURLFieldWidget},
  100. models.IntegerField: {"widget": widgets.AdminIntegerFieldWidget},
  101. models.BigIntegerField: {"widget": widgets.AdminBigIntegerFieldWidget},
  102. models.CharField: {"widget": widgets.AdminTextInputWidget},
  103. models.ImageField: {"widget": widgets.AdminFileWidget},
  104. models.FileField: {"widget": widgets.AdminFileWidget},
  105. models.EmailField: {"widget": widgets.AdminEmailInputWidget},
  106. models.UUIDField: {"widget": widgets.AdminUUIDInputWidget},
  107. }
  108. csrf_protect_m = method_decorator(csrf_protect)
  109. class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
  110. """Functionality common to both ModelAdmin and InlineAdmin."""
  111. autocomplete_fields = ()
  112. raw_id_fields = ()
  113. fields = None
  114. exclude = None
  115. fieldsets = None
  116. form = forms.ModelForm
  117. filter_vertical = ()
  118. filter_horizontal = ()
  119. radio_fields = {}
  120. prepopulated_fields = {}
  121. formfield_overrides = {}
  122. readonly_fields = ()
  123. ordering = None
  124. sortable_by = None
  125. view_on_site = True
  126. show_full_result_count = True
  127. checks_class = BaseModelAdminChecks
  128. def check(self, **kwargs):
  129. return self.checks_class().check(self, **kwargs)
  130. def __init__(self):
  131. # Merge FORMFIELD_FOR_DBFIELD_DEFAULTS with the formfield_overrides
  132. # rather than simply overwriting.
  133. overrides = copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS)
  134. for k, v in self.formfield_overrides.items():
  135. overrides.setdefault(k, {}).update(v)
  136. self.formfield_overrides = overrides
  137. def formfield_for_dbfield(self, db_field, request, **kwargs):
  138. """
  139. Hook for specifying the form Field instance for a given database Field
  140. instance.
  141. If kwargs are given, they're passed to the form Field's constructor.
  142. """
  143. # If the field specifies choices, we don't need to look for special
  144. # admin widgets - we just need to use a select widget of some kind.
  145. if db_field.choices:
  146. return self.formfield_for_choice_field(db_field, request, **kwargs)
  147. # ForeignKey or ManyToManyFields
  148. if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
  149. # Combine the field kwargs with any options for formfield_overrides.
  150. # Make sure the passed in **kwargs override anything in
  151. # formfield_overrides because **kwargs is more specific, and should
  152. # always win.
  153. if db_field.__class__ in self.formfield_overrides:
  154. kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs}
  155. # Get the correct formfield.
  156. if isinstance(db_field, models.ForeignKey):
  157. formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
  158. elif isinstance(db_field, models.ManyToManyField):
  159. formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
  160. # For non-raw_id fields, wrap the widget with a wrapper that adds
  161. # extra HTML -- the "add other" interface -- to the end of the
  162. # rendered output. formfield can be None if it came from a
  163. # OneToOneField with parent_link=True or a M2M intermediary.
  164. if formfield and db_field.name not in self.raw_id_fields:
  165. try:
  166. related_modeladmin = self.admin_site.get_model_admin(
  167. db_field.remote_field.model
  168. )
  169. except NotRegistered:
  170. wrapper_kwargs = {}
  171. else:
  172. wrapper_kwargs = {
  173. "can_add_related": related_modeladmin.has_add_permission(
  174. request
  175. ),
  176. "can_change_related": related_modeladmin.has_change_permission(
  177. request
  178. ),
  179. "can_delete_related": related_modeladmin.has_delete_permission(
  180. request
  181. ),
  182. "can_view_related": related_modeladmin.has_view_permission(
  183. request
  184. ),
  185. }
  186. formfield.widget = widgets.RelatedFieldWidgetWrapper(
  187. formfield.widget,
  188. db_field.remote_field,
  189. self.admin_site,
  190. **wrapper_kwargs,
  191. )
  192. return formfield
  193. # If we've got overrides for the formfield defined, use 'em. **kwargs
  194. # passed to formfield_for_dbfield override the defaults.
  195. for klass in db_field.__class__.mro():
  196. if klass in self.formfield_overrides:
  197. kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs}
  198. return db_field.formfield(**kwargs)
  199. # For any other type of field, just call its formfield() method.
  200. return db_field.formfield(**kwargs)
  201. def formfield_for_choice_field(self, db_field, request, **kwargs):
  202. """
  203. Get a form Field for a database Field that has declared choices.
  204. """
  205. # If the field is named as a radio_field, use a RadioSelect
  206. if db_field.name in self.radio_fields:
  207. # Avoid stomping on custom widget/choices arguments.
  208. if "widget" not in kwargs:
  209. kwargs["widget"] = widgets.AdminRadioSelect(
  210. attrs={
  211. "class": get_ul_class(self.radio_fields[db_field.name]),
  212. }
  213. )
  214. if "choices" not in kwargs:
  215. kwargs["choices"] = db_field.get_choices(
  216. include_blank=db_field.blank, blank_choice=[("", _("None"))]
  217. )
  218. return db_field.formfield(**kwargs)
  219. def get_field_queryset(self, db, db_field, request):
  220. """
  221. If the ModelAdmin specifies ordering, the queryset should respect that
  222. ordering. Otherwise don't specify the queryset, let the field decide
  223. (return None in that case).
  224. """
  225. try:
  226. related_admin = self.admin_site.get_model_admin(db_field.remote_field.model)
  227. except NotRegistered:
  228. return None
  229. else:
  230. ordering = related_admin.get_ordering(request)
  231. if ordering is not None and ordering != ():
  232. return db_field.remote_field.model._default_manager.using(db).order_by(
  233. *ordering
  234. )
  235. return None
  236. def formfield_for_foreignkey(self, db_field, request, **kwargs):
  237. """
  238. Get a form Field for a ForeignKey.
  239. """
  240. db = kwargs.get("using")
  241. if "widget" not in kwargs:
  242. if db_field.name in self.get_autocomplete_fields(request):
  243. kwargs["widget"] = AutocompleteSelect(
  244. db_field, self.admin_site, using=db
  245. )
  246. elif db_field.name in self.raw_id_fields:
  247. kwargs["widget"] = widgets.ForeignKeyRawIdWidget(
  248. db_field.remote_field, self.admin_site, using=db
  249. )
  250. elif db_field.name in self.radio_fields:
  251. kwargs["widget"] = widgets.AdminRadioSelect(
  252. attrs={
  253. "class": get_ul_class(self.radio_fields[db_field.name]),
  254. }
  255. )
  256. kwargs["empty_label"] = (
  257. kwargs.get("empty_label", _("None")) if db_field.blank else None
  258. )
  259. if "queryset" not in kwargs:
  260. queryset = self.get_field_queryset(db, db_field, request)
  261. if queryset is not None:
  262. kwargs["queryset"] = queryset
  263. return db_field.formfield(**kwargs)
  264. def formfield_for_manytomany(self, db_field, request, **kwargs):
  265. """
  266. Get a form Field for a ManyToManyField.
  267. """
  268. # If it uses an intermediary model that isn't auto created, don't show
  269. # a field in admin.
  270. if not db_field.remote_field.through._meta.auto_created:
  271. return None
  272. db = kwargs.get("using")
  273. if "widget" not in kwargs:
  274. autocomplete_fields = self.get_autocomplete_fields(request)
  275. if db_field.name in autocomplete_fields:
  276. kwargs["widget"] = AutocompleteSelectMultiple(
  277. db_field,
  278. self.admin_site,
  279. using=db,
  280. )
  281. elif db_field.name in self.raw_id_fields:
  282. kwargs["widget"] = widgets.ManyToManyRawIdWidget(
  283. db_field.remote_field,
  284. self.admin_site,
  285. using=db,
  286. )
  287. elif db_field.name in [*self.filter_vertical, *self.filter_horizontal]:
  288. kwargs["widget"] = widgets.FilteredSelectMultiple(
  289. db_field.verbose_name, db_field.name in self.filter_vertical
  290. )
  291. if "queryset" not in kwargs:
  292. queryset = self.get_field_queryset(db, db_field, request)
  293. if queryset is not None:
  294. kwargs["queryset"] = queryset
  295. form_field = db_field.formfield(**kwargs)
  296. if (
  297. isinstance(form_field.widget, SelectMultiple)
  298. and form_field.widget.allow_multiple_selected
  299. and not isinstance(
  300. form_field.widget, (CheckboxSelectMultiple, AutocompleteSelectMultiple)
  301. )
  302. ):
  303. msg = _(
  304. "Hold down “Control”, or “Command” on a Mac, to select more than one."
  305. )
  306. help_text = form_field.help_text
  307. form_field.help_text = (
  308. format_lazy("{} {}", help_text, msg) if help_text else msg
  309. )
  310. return form_field
  311. def get_autocomplete_fields(self, request):
  312. """
  313. Return a list of ForeignKey and/or ManyToMany fields which should use
  314. an autocomplete widget.
  315. """
  316. return self.autocomplete_fields
  317. def get_view_on_site_url(self, obj=None):
  318. if obj is None or not self.view_on_site:
  319. return None
  320. if callable(self.view_on_site):
  321. return self.view_on_site(obj)
  322. elif hasattr(obj, "get_absolute_url"):
  323. # use the ContentType lookup if view_on_site is True
  324. return reverse(
  325. "admin:view_on_site",
  326. kwargs={
  327. "content_type_id": get_content_type_for_model(obj).pk,
  328. "object_id": obj.pk,
  329. },
  330. current_app=self.admin_site.name,
  331. )
  332. def get_empty_value_display(self):
  333. """
  334. Return the empty_value_display set on ModelAdmin or AdminSite.
  335. """
  336. try:
  337. return mark_safe(self.empty_value_display)
  338. except AttributeError:
  339. return mark_safe(self.admin_site.empty_value_display)
  340. def get_exclude(self, request, obj=None):
  341. """
  342. Hook for specifying exclude.
  343. """
  344. return self.exclude
  345. def get_fields(self, request, obj=None):
  346. """
  347. Hook for specifying fields.
  348. """
  349. if self.fields:
  350. return self.fields
  351. # _get_form_for_get_fields() is implemented in subclasses.
  352. form = self._get_form_for_get_fields(request, obj)
  353. return [*form.base_fields, *self.get_readonly_fields(request, obj)]
  354. def get_fieldsets(self, request, obj=None):
  355. """
  356. Hook for specifying fieldsets.
  357. """
  358. if self.fieldsets:
  359. return self.fieldsets
  360. return [(None, {"fields": self.get_fields(request, obj)})]
  361. def get_inlines(self, request, obj):
  362. """Hook for specifying custom inlines."""
  363. return self.inlines
  364. def get_ordering(self, request):
  365. """
  366. Hook for specifying field ordering.
  367. """
  368. return self.ordering or () # otherwise we might try to *None, which is bad ;)
  369. def get_readonly_fields(self, request, obj=None):
  370. """
  371. Hook for specifying custom readonly fields.
  372. """
  373. return self.readonly_fields
  374. def get_prepopulated_fields(self, request, obj=None):
  375. """
  376. Hook for specifying custom prepopulated fields.
  377. """
  378. return self.prepopulated_fields
  379. def get_queryset(self, request):
  380. """
  381. Return a QuerySet of all model instances that can be edited by the
  382. admin site. This is used by changelist_view.
  383. """
  384. qs = self.model._default_manager.get_queryset()
  385. # TODO: this should be handled by some parameter to the ChangeList.
  386. ordering = self.get_ordering(request)
  387. if ordering:
  388. qs = qs.order_by(*ordering)
  389. return qs
  390. def get_sortable_by(self, request):
  391. """Hook for specifying which fields can be sorted in the changelist."""
  392. return (
  393. self.sortable_by
  394. if self.sortable_by is not None
  395. else self.get_list_display(request)
  396. )
  397. # RemovedInDjango60Warning: when the deprecation ends, replace with:
  398. # def lookup_allowed(self, lookup, value, request):
  399. def lookup_allowed(self, lookup, value, request=None):
  400. from django.contrib.admin.filters import SimpleListFilter
  401. model = self.model
  402. # Check FKey lookups that are allowed, so that popups produced by
  403. # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
  404. # are allowed to work.
  405. for fk_lookup in model._meta.related_fkey_lookups:
  406. # As ``limit_choices_to`` can be a callable, invoke it here.
  407. if callable(fk_lookup):
  408. fk_lookup = fk_lookup()
  409. if (lookup, value) in widgets.url_params_from_lookup_dict(
  410. fk_lookup
  411. ).items():
  412. return True
  413. relation_parts = []
  414. prev_field = None
  415. parts = lookup.split(LOOKUP_SEP)
  416. for part in parts:
  417. try:
  418. field = model._meta.get_field(part)
  419. except FieldDoesNotExist:
  420. # Lookups on nonexistent fields are ok, since they're ignored
  421. # later.
  422. break
  423. if not prev_field or (
  424. prev_field.is_relation
  425. and field not in model._meta.parents.values()
  426. and field is not model._meta.auto_field
  427. and (
  428. model._meta.auto_field is None
  429. or part not in getattr(prev_field, "to_fields", [])
  430. )
  431. and (field.is_relation or not field.primary_key)
  432. ):
  433. relation_parts.append(part)
  434. if not getattr(field, "path_infos", None):
  435. # This is not a relational field, so further parts
  436. # must be transforms.
  437. break
  438. prev_field = field
  439. model = field.path_infos[-1].to_opts.model
  440. if len(relation_parts) <= 1:
  441. # Either a local field filter, or no fields at all.
  442. return True
  443. valid_lookups = {self.date_hierarchy}
  444. # RemovedInDjango60Warning: when the deprecation ends, replace with:
  445. # for filter_item in self.get_list_filter(request):
  446. list_filter = (
  447. self.get_list_filter(request) if request is not None else self.list_filter
  448. )
  449. for filter_item in list_filter:
  450. if isinstance(filter_item, type) and issubclass(
  451. filter_item, SimpleListFilter
  452. ):
  453. valid_lookups.add(filter_item.parameter_name)
  454. elif isinstance(filter_item, (list, tuple)):
  455. valid_lookups.add(filter_item[0])
  456. else:
  457. valid_lookups.add(filter_item)
  458. # Is it a valid relational lookup?
  459. return not {
  460. LOOKUP_SEP.join(relation_parts),
  461. LOOKUP_SEP.join(relation_parts + [part]),
  462. }.isdisjoint(valid_lookups)
  463. def to_field_allowed(self, request, to_field):
  464. """
  465. Return True if the model associated with this admin should be
  466. allowed to be referenced by the specified field.
  467. """
  468. try:
  469. field = self.opts.get_field(to_field)
  470. except FieldDoesNotExist:
  471. return False
  472. # Always allow referencing the primary key since it's already possible
  473. # to get this information from the change view URL.
  474. if field.primary_key:
  475. return True
  476. # Allow reverse relationships to models defining m2m fields if they
  477. # target the specified field.
  478. for many_to_many in self.opts.many_to_many:
  479. if many_to_many.m2m_target_field_name() == to_field:
  480. return True
  481. # Make sure at least one of the models registered for this site
  482. # references this field through a FK or a M2M relationship.
  483. registered_models = set()
  484. for model, admin in self.admin_site._registry.items():
  485. registered_models.add(model)
  486. for inline in admin.inlines:
  487. registered_models.add(inline.model)
  488. related_objects = (
  489. f
  490. for f in self.opts.get_fields(include_hidden=True)
  491. if (f.auto_created and not f.concrete)
  492. )
  493. for related_object in related_objects:
  494. related_model = related_object.related_model
  495. remote_field = related_object.field.remote_field
  496. if (
  497. any(issubclass(model, related_model) for model in registered_models)
  498. and hasattr(remote_field, "get_related_field")
  499. and remote_field.get_related_field() == field
  500. ):
  501. return True
  502. return False
  503. def has_add_permission(self, request):
  504. """
  505. Return True if the given request has permission to add an object.
  506. Can be overridden by the user in subclasses.
  507. """
  508. opts = self.opts
  509. codename = get_permission_codename("add", opts)
  510. return request.user.has_perm("%s.%s" % (opts.app_label, codename))
  511. def has_change_permission(self, request, obj=None):
  512. """
  513. Return True if the given request has permission to change the given
  514. Django model instance, the default implementation doesn't examine the
  515. `obj` parameter.
  516. Can be overridden by the user in subclasses. In such case it should
  517. return True if the given request has permission to change the `obj`
  518. model instance. If `obj` is None, this should return True if the given
  519. request has permission to change *any* object of the given type.
  520. """
  521. opts = self.opts
  522. codename = get_permission_codename("change", opts)
  523. return request.user.has_perm("%s.%s" % (opts.app_label, codename))
  524. def has_delete_permission(self, request, obj=None):
  525. """
  526. Return True if the given request has permission to delete the given
  527. Django model instance, the default implementation doesn't examine the
  528. `obj` parameter.
  529. Can be overridden by the user in subclasses. In such case it should
  530. return True if the given request has permission to delete the `obj`
  531. model instance. If `obj` is None, this should return True if the given
  532. request has permission to delete *any* object of the given type.
  533. """
  534. opts = self.opts
  535. codename = get_permission_codename("delete", opts)
  536. return request.user.has_perm("%s.%s" % (opts.app_label, codename))
  537. def has_view_permission(self, request, obj=None):
  538. """
  539. Return True if the given request has permission to view the given
  540. Django model instance. The default implementation doesn't examine the
  541. `obj` parameter.
  542. If overridden by the user in subclasses, it should return True if the
  543. given request has permission to view the `obj` model instance. If `obj`
  544. is None, it should return True if the request has permission to view
  545. any object of the given type.
  546. """
  547. opts = self.opts
  548. codename_view = get_permission_codename("view", opts)
  549. codename_change = get_permission_codename("change", opts)
  550. return request.user.has_perm(
  551. "%s.%s" % (opts.app_label, codename_view)
  552. ) or request.user.has_perm("%s.%s" % (opts.app_label, codename_change))
  553. def has_view_or_change_permission(self, request, obj=None):
  554. return self.has_view_permission(request, obj) or self.has_change_permission(
  555. request, obj
  556. )
  557. def has_module_permission(self, request):
  558. """
  559. Return True if the given request has any permission in the given
  560. app label.
  561. Can be overridden by the user in subclasses. In such case it should
  562. return True if the given request has permission to view the module on
  563. the admin index page and access the module's index page. Overriding it
  564. does not restrict access to the add, change or delete views. Use
  565. `ModelAdmin.has_(add|change|delete)_permission` for that.
  566. """
  567. return request.user.has_module_perms(self.opts.app_label)
  568. class ModelAdmin(BaseModelAdmin):
  569. """Encapsulate all admin options and functionality for a given model."""
  570. list_display = ("__str__",)
  571. list_display_links = ()
  572. list_filter = ()
  573. list_select_related = False
  574. list_per_page = 100
  575. list_max_show_all = 200
  576. list_editable = ()
  577. search_fields = ()
  578. search_help_text = None
  579. date_hierarchy = None
  580. save_as = False
  581. save_as_continue = True
  582. save_on_top = False
  583. paginator = Paginator
  584. preserve_filters = True
  585. show_facets = ShowFacets.ALLOW
  586. inlines = ()
  587. # Custom templates (designed to be over-ridden in subclasses)
  588. add_form_template = None
  589. change_form_template = None
  590. change_list_template = None
  591. delete_confirmation_template = None
  592. delete_selected_confirmation_template = None
  593. object_history_template = None
  594. popup_response_template = None
  595. # Actions
  596. actions = ()
  597. action_form = helpers.ActionForm
  598. actions_on_top = True
  599. actions_on_bottom = False
  600. actions_selection_counter = True
  601. checks_class = ModelAdminChecks
  602. def __init__(self, model, admin_site):
  603. self.model = model
  604. self.opts = model._meta
  605. self.admin_site = admin_site
  606. super().__init__()
  607. def __str__(self):
  608. return "%s.%s" % (self.opts.app_label, self.__class__.__name__)
  609. def __repr__(self):
  610. return (
  611. f"<{self.__class__.__qualname__}: model={self.model.__qualname__} "
  612. f"site={self.admin_site!r}>"
  613. )
  614. def get_inline_instances(self, request, obj=None):
  615. inline_instances = []
  616. for inline_class in self.get_inlines(request, obj):
  617. inline = inline_class(self.model, self.admin_site)
  618. if request:
  619. if not (
  620. inline.has_view_or_change_permission(request, obj)
  621. or inline.has_add_permission(request, obj)
  622. or inline.has_delete_permission(request, obj)
  623. ):
  624. continue
  625. if not inline.has_add_permission(request, obj):
  626. inline.max_num = 0
  627. inline_instances.append(inline)
  628. return inline_instances
  629. def get_urls(self):
  630. from django.urls import path
  631. def wrap(view):
  632. def wrapper(*args, **kwargs):
  633. return self.admin_site.admin_view(view)(*args, **kwargs)
  634. wrapper.model_admin = self
  635. return update_wrapper(wrapper, view)
  636. info = self.opts.app_label, self.opts.model_name
  637. return [
  638. path("", wrap(self.changelist_view), name="%s_%s_changelist" % info),
  639. path("add/", wrap(self.add_view), name="%s_%s_add" % info),
  640. path(
  641. "<path:object_id>/history/",
  642. wrap(self.history_view),
  643. name="%s_%s_history" % info,
  644. ),
  645. path(
  646. "<path:object_id>/delete/",
  647. wrap(self.delete_view),
  648. name="%s_%s_delete" % info,
  649. ),
  650. path(
  651. "<path:object_id>/change/",
  652. wrap(self.change_view),
  653. name="%s_%s_change" % info,
  654. ),
  655. # For backwards compatibility (was the change url before 1.9)
  656. path(
  657. "<path:object_id>/",
  658. wrap(
  659. RedirectView.as_view(
  660. pattern_name="%s:%s_%s_change"
  661. % ((self.admin_site.name,) + info)
  662. )
  663. ),
  664. ),
  665. ]
  666. @property
  667. def urls(self):
  668. return self.get_urls()
  669. @property
  670. def media(self):
  671. extra = "" if settings.DEBUG else ".min"
  672. js = [
  673. "vendor/jquery/jquery%s.js" % extra,
  674. "jquery.init.js",
  675. "core.js",
  676. "admin/RelatedObjectLookups.js",
  677. "actions.js",
  678. "urlify.js",
  679. "prepopulate.js",
  680. "vendor/xregexp/xregexp%s.js" % extra,
  681. ]
  682. return forms.Media(js=["admin/js/%s" % url for url in js])
  683. def get_model_perms(self, request):
  684. """
  685. Return a dict of all perms for this model. This dict has the keys
  686. ``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
  687. for each of those actions.
  688. """
  689. return {
  690. "add": self.has_add_permission(request),
  691. "change": self.has_change_permission(request),
  692. "delete": self.has_delete_permission(request),
  693. "view": self.has_view_permission(request),
  694. }
  695. def _get_form_for_get_fields(self, request, obj):
  696. return self.get_form(request, obj, fields=None)
  697. def get_form(self, request, obj=None, change=False, **kwargs):
  698. """
  699. Return a Form class for use in the admin add view. This is used by
  700. add_view and change_view.
  701. """
  702. if "fields" in kwargs:
  703. fields = kwargs.pop("fields")
  704. else:
  705. fields = flatten_fieldsets(self.get_fieldsets(request, obj))
  706. excluded = self.get_exclude(request, obj)
  707. exclude = [] if excluded is None else list(excluded)
  708. readonly_fields = self.get_readonly_fields(request, obj)
  709. exclude.extend(readonly_fields)
  710. # Exclude all fields if it's a change form and the user doesn't have
  711. # the change permission.
  712. if (
  713. change
  714. and hasattr(request, "user")
  715. and not self.has_change_permission(request, obj)
  716. ):
  717. exclude.extend(fields)
  718. if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
  719. # Take the custom ModelForm's Meta.exclude into account only if the
  720. # ModelAdmin doesn't define its own.
  721. exclude.extend(self.form._meta.exclude)
  722. # if exclude is an empty list we pass None to be consistent with the
  723. # default on modelform_factory
  724. exclude = exclude or None
  725. # Remove declared form fields which are in readonly_fields.
  726. new_attrs = dict.fromkeys(
  727. f for f in readonly_fields if f in self.form.declared_fields
  728. )
  729. form = type(self.form.__name__, (self.form,), new_attrs)
  730. defaults = {
  731. "form": form,
  732. "fields": fields,
  733. "exclude": exclude,
  734. "formfield_callback": partial(self.formfield_for_dbfield, request=request),
  735. **kwargs,
  736. }
  737. if defaults["fields"] is None and not modelform_defines_fields(
  738. defaults["form"]
  739. ):
  740. defaults["fields"] = forms.ALL_FIELDS
  741. try:
  742. return modelform_factory(self.model, **defaults)
  743. except FieldError as e:
  744. raise FieldError(
  745. "%s. Check fields/fieldsets/exclude attributes of class %s."
  746. % (e, self.__class__.__name__)
  747. )
  748. def get_changelist(self, request, **kwargs):
  749. """
  750. Return the ChangeList class for use on the changelist page.
  751. """
  752. from django.contrib.admin.views.main import ChangeList
  753. return ChangeList
  754. def get_changelist_instance(self, request):
  755. """
  756. Return a `ChangeList` instance based on `request`. May raise
  757. `IncorrectLookupParameters`.
  758. """
  759. list_display = self.get_list_display(request)
  760. list_display_links = self.get_list_display_links(request, list_display)
  761. # Add the action checkboxes if any actions are available.
  762. if self.get_actions(request):
  763. list_display = ["action_checkbox", *list_display]
  764. sortable_by = self.get_sortable_by(request)
  765. ChangeList = self.get_changelist(request)
  766. return ChangeList(
  767. request,
  768. self.model,
  769. list_display,
  770. list_display_links,
  771. self.get_list_filter(request),
  772. self.date_hierarchy,
  773. self.get_search_fields(request),
  774. self.get_list_select_related(request),
  775. self.list_per_page,
  776. self.list_max_show_all,
  777. self.list_editable,
  778. self,
  779. sortable_by,
  780. self.search_help_text,
  781. )
  782. def get_object(self, request, object_id, from_field=None):
  783. """
  784. Return an instance matching the field and value provided, the primary
  785. key is used if no field is provided. Return ``None`` if no match is
  786. found or the object_id fails validation.
  787. """
  788. queryset = self.get_queryset(request)
  789. model = queryset.model
  790. field = (
  791. model._meta.pk if from_field is None else model._meta.get_field(from_field)
  792. )
  793. try:
  794. object_id = field.to_python(object_id)
  795. return queryset.get(**{field.name: object_id})
  796. except (model.DoesNotExist, ValidationError, ValueError):
  797. return None
  798. def get_changelist_form(self, request, **kwargs):
  799. """
  800. Return a Form class for use in the Formset on the changelist page.
  801. """
  802. defaults = {
  803. "formfield_callback": partial(self.formfield_for_dbfield, request=request),
  804. **kwargs,
  805. }
  806. if defaults.get("fields") is None and not modelform_defines_fields(
  807. defaults.get("form")
  808. ):
  809. defaults["fields"] = forms.ALL_FIELDS
  810. return modelform_factory(self.model, **defaults)
  811. def get_changelist_formset(self, request, **kwargs):
  812. """
  813. Return a FormSet class for use on the changelist page if list_editable
  814. is used.
  815. """
  816. defaults = {
  817. "formfield_callback": partial(self.formfield_for_dbfield, request=request),
  818. **kwargs,
  819. }
  820. return modelformset_factory(
  821. self.model,
  822. self.get_changelist_form(request),
  823. extra=0,
  824. fields=self.list_editable,
  825. **defaults,
  826. )
  827. def get_formsets_with_inlines(self, request, obj=None):
  828. """
  829. Yield formsets and the corresponding inlines.
  830. """
  831. for inline in self.get_inline_instances(request, obj):
  832. yield inline.get_formset(request, obj), inline
  833. def get_paginator(
  834. self, request, queryset, per_page, orphans=0, allow_empty_first_page=True
  835. ):
  836. return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
  837. def log_addition(self, request, obj, message):
  838. """
  839. Log that an object has been successfully added.
  840. The default implementation creates an admin LogEntry object.
  841. """
  842. from django.contrib.admin.models import ADDITION, LogEntry
  843. return LogEntry.objects.log_actions(
  844. user_id=request.user.pk,
  845. queryset=[obj],
  846. action_flag=ADDITION,
  847. change_message=message,
  848. single_object=True,
  849. )
  850. def log_change(self, request, obj, message):
  851. """
  852. Log that an object has been successfully changed.
  853. The default implementation creates an admin LogEntry object.
  854. """
  855. from django.contrib.admin.models import CHANGE, LogEntry
  856. return LogEntry.objects.log_actions(
  857. user_id=request.user.pk,
  858. queryset=[obj],
  859. action_flag=CHANGE,
  860. change_message=message,
  861. single_object=True,
  862. )
  863. def log_deletion(self, request, obj, object_repr):
  864. """
  865. Log that an object will be deleted. Note that this method must be
  866. called before the deletion.
  867. The default implementation creates an admin LogEntry object.
  868. """
  869. warnings.warn(
  870. "ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead.",
  871. RemovedInDjango60Warning,
  872. stacklevel=2,
  873. )
  874. from django.contrib.admin.models import DELETION, LogEntry
  875. return LogEntry.objects.log_action(
  876. user_id=request.user.pk,
  877. content_type_id=get_content_type_for_model(obj).pk,
  878. object_id=obj.pk,
  879. object_repr=object_repr,
  880. action_flag=DELETION,
  881. )
  882. def log_deletions(self, request, queryset):
  883. """
  884. Log that objects will be deleted. Note that this method must be called
  885. before the deletion.
  886. The default implementation creates admin LogEntry objects.
  887. """
  888. from django.contrib.admin.models import DELETION, LogEntry
  889. # RemovedInDjango60Warning.
  890. if type(self).log_deletion != ModelAdmin.log_deletion:
  891. warnings.warn(
  892. "The usage of log_deletion() is deprecated. Implement log_deletions() "
  893. "instead.",
  894. RemovedInDjango60Warning,
  895. stacklevel=2,
  896. )
  897. return [self.log_deletion(request, obj, str(obj)) for obj in queryset]
  898. return LogEntry.objects.log_actions(
  899. user_id=request.user.pk,
  900. queryset=queryset,
  901. action_flag=DELETION,
  902. )
  903. def action_checkbox(self, obj):
  904. """
  905. A list_display column containing a checkbox widget.
  906. """
  907. attrs = {
  908. "class": "action-select",
  909. "aria-label": format_html(
  910. _("Select this object for an action - {}"), str(obj)
  911. ),
  912. }
  913. checkbox = forms.CheckboxInput(attrs, lambda value: False)
  914. return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
  915. @staticmethod
  916. def _get_action_description(func, name):
  917. try:
  918. return func.short_description
  919. except AttributeError:
  920. return capfirst(name.replace("_", " "))
  921. def _get_base_actions(self):
  922. """Return the list of actions, prior to any request-based filtering."""
  923. actions = []
  924. base_actions = (self.get_action(action) for action in self.actions or [])
  925. # get_action might have returned None, so filter any of those out.
  926. base_actions = [action for action in base_actions if action]
  927. base_action_names = {name for _, name, _ in base_actions}
  928. # Gather actions from the admin site first
  929. for name, func in self.admin_site.actions:
  930. if name in base_action_names:
  931. continue
  932. description = self._get_action_description(func, name)
  933. actions.append((func, name, description))
  934. # Add actions from this ModelAdmin.
  935. actions.extend(base_actions)
  936. return actions
  937. def _filter_actions_by_permissions(self, request, actions):
  938. """Filter out any actions that the user doesn't have access to."""
  939. filtered_actions = []
  940. for action in actions:
  941. callable = action[0]
  942. if not hasattr(callable, "allowed_permissions"):
  943. filtered_actions.append(action)
  944. continue
  945. permission_checks = (
  946. getattr(self, "has_%s_permission" % permission)
  947. for permission in callable.allowed_permissions
  948. )
  949. if any(has_permission(request) for has_permission in permission_checks):
  950. filtered_actions.append(action)
  951. return filtered_actions
  952. def get_actions(self, request):
  953. """
  954. Return a dictionary mapping the names of all actions for this
  955. ModelAdmin to a tuple of (callable, name, description) for each action.
  956. """
  957. # If self.actions is set to None that means actions are disabled on
  958. # this page.
  959. if self.actions is None or IS_POPUP_VAR in request.GET:
  960. return {}
  961. actions = self._filter_actions_by_permissions(request, self._get_base_actions())
  962. return {name: (func, name, desc) for func, name, desc in actions}
  963. def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH):
  964. """
  965. Return a list of choices for use in a form object. Each choice is a
  966. tuple (name, description).
  967. """
  968. choices = [] + default_choices
  969. for func, name, description in self.get_actions(request).values():
  970. choice = (name, description % model_format_dict(self.opts))
  971. choices.append(choice)
  972. return choices
  973. def get_action(self, action):
  974. """
  975. Return a given action from a parameter, which can either be a callable,
  976. or the name of a method on the ModelAdmin. Return is a tuple of
  977. (callable, name, description).
  978. """
  979. # If the action is a callable, just use it.
  980. if callable(action):
  981. func = action
  982. action = action.__name__
  983. # Next, look for a method. Grab it off self.__class__ to get an unbound
  984. # method instead of a bound one; this ensures that the calling
  985. # conventions are the same for functions and methods.
  986. elif hasattr(self.__class__, action):
  987. func = getattr(self.__class__, action)
  988. # Finally, look for a named method on the admin site
  989. else:
  990. try:
  991. func = self.admin_site.get_action(action)
  992. except KeyError:
  993. return None
  994. description = self._get_action_description(func, action)
  995. return func, action, description
  996. def get_list_display(self, request):
  997. """
  998. Return a sequence containing the fields to be displayed on the
  999. changelist.
  1000. """
  1001. return self.list_display
  1002. def get_list_display_links(self, request, list_display):
  1003. """
  1004. Return a sequence containing the fields to be displayed as links
  1005. on the changelist. The list_display parameter is the list of fields
  1006. returned by get_list_display().
  1007. """
  1008. if (
  1009. self.list_display_links
  1010. or self.list_display_links is None
  1011. or not list_display
  1012. ):
  1013. return self.list_display_links
  1014. else:
  1015. # Use only the first item in list_display as link
  1016. return list(list_display)[:1]
  1017. def get_list_filter(self, request):
  1018. """
  1019. Return a sequence containing the fields to be displayed as filters in
  1020. the right sidebar of the changelist page.
  1021. """
  1022. return self.list_filter
  1023. def get_list_select_related(self, request):
  1024. """
  1025. Return a list of fields to add to the select_related() part of the
  1026. changelist items query.
  1027. """
  1028. return self.list_select_related
  1029. def get_search_fields(self, request):
  1030. """
  1031. Return a sequence containing the fields to be searched whenever
  1032. somebody submits a search query.
  1033. """
  1034. return self.search_fields
  1035. def get_search_results(self, request, queryset, search_term):
  1036. """
  1037. Return a tuple containing a queryset to implement the search
  1038. and a boolean indicating if the results may contain duplicates.
  1039. """
  1040. # Apply keyword searches.
  1041. def construct_search(field_name):
  1042. if field_name.startswith("^"):
  1043. return "%s__istartswith" % field_name.removeprefix("^")
  1044. elif field_name.startswith("="):
  1045. return "%s__iexact" % field_name.removeprefix("=")
  1046. elif field_name.startswith("@"):
  1047. return "%s__search" % field_name.removeprefix("@")
  1048. # Use field_name if it includes a lookup.
  1049. opts = queryset.model._meta
  1050. lookup_fields = field_name.split(LOOKUP_SEP)
  1051. # Go through the fields, following all relations.
  1052. prev_field = None
  1053. for path_part in lookup_fields:
  1054. if path_part == "pk":
  1055. path_part = opts.pk.name
  1056. try:
  1057. field = opts.get_field(path_part)
  1058. except FieldDoesNotExist:
  1059. # Use valid query lookups.
  1060. if prev_field and prev_field.get_lookup(path_part):
  1061. return field_name
  1062. else:
  1063. prev_field = field
  1064. if hasattr(field, "path_infos"):
  1065. # Update opts to follow the relation.
  1066. opts = field.path_infos[-1].to_opts
  1067. # Otherwise, use the field with icontains.
  1068. return "%s__icontains" % field_name
  1069. may_have_duplicates = False
  1070. search_fields = self.get_search_fields(request)
  1071. if search_fields and search_term:
  1072. orm_lookups = [
  1073. construct_search(str(search_field)) for search_field in search_fields
  1074. ]
  1075. term_queries = []
  1076. for bit in smart_split(search_term):
  1077. if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
  1078. bit = unescape_string_literal(bit)
  1079. or_queries = models.Q.create(
  1080. [(orm_lookup, bit) for orm_lookup in orm_lookups],
  1081. connector=models.Q.OR,
  1082. )
  1083. term_queries.append(or_queries)
  1084. queryset = queryset.filter(models.Q.create(term_queries))
  1085. may_have_duplicates |= any(
  1086. lookup_spawns_duplicates(self.opts, search_spec)
  1087. for search_spec in orm_lookups
  1088. )
  1089. return queryset, may_have_duplicates
  1090. def get_preserved_filters(self, request):
  1091. """
  1092. Return the preserved filters querystring.
  1093. """
  1094. match = request.resolver_match
  1095. if self.preserve_filters and match:
  1096. current_url = "%s:%s" % (match.app_name, match.url_name)
  1097. changelist_url = "admin:%s_%s_changelist" % (
  1098. self.opts.app_label,
  1099. self.opts.model_name,
  1100. )
  1101. if current_url == changelist_url:
  1102. preserved_filters = request.GET.urlencode()
  1103. else:
  1104. preserved_filters = request.GET.get("_changelist_filters")
  1105. if preserved_filters:
  1106. return urlencode({"_changelist_filters": preserved_filters})
  1107. return ""
  1108. def construct_change_message(self, request, form, formsets, add=False):
  1109. """
  1110. Construct a JSON structure describing changes from a changed object.
  1111. """
  1112. return construct_change_message(form, formsets, add)
  1113. def message_user(
  1114. self, request, message, level=messages.INFO, extra_tags="", fail_silently=False
  1115. ):
  1116. """
  1117. Send a message to the user. The default implementation
  1118. posts a message using the django.contrib.messages backend.
  1119. Exposes almost the same API as messages.add_message(), but accepts the
  1120. positional arguments in a different order to maintain backwards
  1121. compatibility. For convenience, it accepts the `level` argument as
  1122. a string rather than the usual level number.
  1123. """
  1124. if not isinstance(level, int):
  1125. # attempt to get the level if passed a string
  1126. try:
  1127. level = getattr(messages.constants, level.upper())
  1128. except AttributeError:
  1129. levels = messages.constants.DEFAULT_TAGS.values()
  1130. levels_repr = ", ".join("`%s`" % level for level in levels)
  1131. raise ValueError(
  1132. "Bad message level string: `%s`. Possible values are: %s"
  1133. % (level, levels_repr)
  1134. )
  1135. messages.add_message(
  1136. request, level, message, extra_tags=extra_tags, fail_silently=fail_silently
  1137. )
  1138. def save_form(self, request, form, change):
  1139. """
  1140. Given a ModelForm return an unsaved instance. ``change`` is True if
  1141. the object is being changed, and False if it's being added.
  1142. """
  1143. return form.save(commit=False)
  1144. def save_model(self, request, obj, form, change):
  1145. """
  1146. Given a model instance save it to the database.
  1147. """
  1148. obj.save()
  1149. def delete_model(self, request, obj):
  1150. """
  1151. Given a model instance delete it from the database.
  1152. """
  1153. obj.delete()
  1154. def delete_queryset(self, request, queryset):
  1155. """Given a queryset, delete it from the database."""
  1156. queryset.delete()
  1157. def save_formset(self, request, form, formset, change):
  1158. """
  1159. Given an inline formset save it to the database.
  1160. """
  1161. formset.save()
  1162. def save_related(self, request, form, formsets, change):
  1163. """
  1164. Given the ``HttpRequest``, the parent ``ModelForm`` instance, the
  1165. list of inline formsets and a boolean value based on whether the
  1166. parent is being added or changed, save the related objects to the
  1167. database. Note that at this point save_form() and save_model() have
  1168. already been called.
  1169. """
  1170. form.save_m2m()
  1171. for formset in formsets:
  1172. self.save_formset(request, form, formset, change=change)
  1173. def render_change_form(
  1174. self, request, context, add=False, change=False, form_url="", obj=None
  1175. ):
  1176. app_label = self.opts.app_label
  1177. preserved_filters = self.get_preserved_filters(request)
  1178. form_url = add_preserved_filters(
  1179. {"preserved_filters": preserved_filters, "opts": self.opts}, form_url
  1180. )
  1181. view_on_site_url = self.get_view_on_site_url(obj)
  1182. has_editable_inline_admin_formsets = False
  1183. for inline in context["inline_admin_formsets"]:
  1184. if (
  1185. inline.has_add_permission
  1186. or inline.has_change_permission
  1187. or inline.has_delete_permission
  1188. ):
  1189. has_editable_inline_admin_formsets = True
  1190. break
  1191. context.update(
  1192. {
  1193. "add": add,
  1194. "change": change,
  1195. "has_view_permission": self.has_view_permission(request, obj),
  1196. "has_add_permission": self.has_add_permission(request),
  1197. "has_change_permission": self.has_change_permission(request, obj),
  1198. "has_delete_permission": self.has_delete_permission(request, obj),
  1199. "has_editable_inline_admin_formsets": (
  1200. has_editable_inline_admin_formsets
  1201. ),
  1202. "has_file_field": context["adminform"].form.is_multipart()
  1203. or any(
  1204. admin_formset.formset.is_multipart()
  1205. for admin_formset in context["inline_admin_formsets"]
  1206. ),
  1207. "has_absolute_url": view_on_site_url is not None,
  1208. "absolute_url": view_on_site_url,
  1209. "form_url": form_url,
  1210. "opts": self.opts,
  1211. "content_type_id": get_content_type_for_model(self.model).pk,
  1212. "save_as": self.save_as,
  1213. "save_on_top": self.save_on_top,
  1214. "to_field_var": TO_FIELD_VAR,
  1215. "is_popup_var": IS_POPUP_VAR,
  1216. "app_label": app_label,
  1217. }
  1218. )
  1219. if add and self.add_form_template is not None:
  1220. form_template = self.add_form_template
  1221. else:
  1222. form_template = self.change_form_template
  1223. request.current_app = self.admin_site.name
  1224. return TemplateResponse(
  1225. request,
  1226. form_template
  1227. or [
  1228. "admin/%s/%s/change_form.html" % (app_label, self.opts.model_name),
  1229. "admin/%s/change_form.html" % app_label,
  1230. "admin/change_form.html",
  1231. ],
  1232. context,
  1233. )
  1234. def _get_preserved_qsl(self, request, preserved_filters):
  1235. query_string = urlparse(request.build_absolute_uri()).query
  1236. return parse_qsl(query_string.replace(preserved_filters, ""))
  1237. def response_add(self, request, obj, post_url_continue=None):
  1238. """
  1239. Determine the HttpResponse for the add_view stage.
  1240. """
  1241. opts = obj._meta
  1242. preserved_filters = self.get_preserved_filters(request)
  1243. preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
  1244. obj_url = reverse(
  1245. "admin:%s_%s_change" % (opts.app_label, opts.model_name),
  1246. args=(quote(obj.pk),),
  1247. current_app=self.admin_site.name,
  1248. )
  1249. # Add a link to the object's change form if the user can edit the obj.
  1250. if self.has_change_permission(request, obj):
  1251. obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
  1252. else:
  1253. obj_repr = str(obj)
  1254. msg_dict = {
  1255. "name": opts.verbose_name,
  1256. "obj": obj_repr,
  1257. }
  1258. # Here, we distinguish between different save types by checking for
  1259. # the presence of keys in request.POST.
  1260. if IS_POPUP_VAR in request.POST:
  1261. to_field = request.POST.get(TO_FIELD_VAR)
  1262. if to_field:
  1263. attr = str(to_field)
  1264. else:
  1265. attr = obj._meta.pk.attname
  1266. value = obj.serializable_value(attr)
  1267. popup_response_data = json.dumps(
  1268. {
  1269. "value": str(value),
  1270. "obj": str(obj),
  1271. }
  1272. )
  1273. return TemplateResponse(
  1274. request,
  1275. self.popup_response_template
  1276. or [
  1277. "admin/%s/%s/popup_response.html"
  1278. % (opts.app_label, opts.model_name),
  1279. "admin/%s/popup_response.html" % opts.app_label,
  1280. "admin/popup_response.html",
  1281. ],
  1282. {
  1283. "popup_response_data": popup_response_data,
  1284. },
  1285. )
  1286. elif "_continue" in request.POST or (
  1287. # Redirecting after "Save as new".
  1288. "_saveasnew" in request.POST
  1289. and self.save_as_continue
  1290. and self.has_change_permission(request, obj)
  1291. ):
  1292. msg = _("The {name} “{obj}” was added successfully.")
  1293. if self.has_change_permission(request, obj):
  1294. msg += " " + _("You may edit it again below.")
  1295. self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
  1296. if post_url_continue is None:
  1297. post_url_continue = obj_url
  1298. post_url_continue = add_preserved_filters(
  1299. {
  1300. "preserved_filters": preserved_filters,
  1301. "preserved_qsl": preserved_qsl,
  1302. "opts": opts,
  1303. },
  1304. post_url_continue,
  1305. )
  1306. return HttpResponseRedirect(post_url_continue)
  1307. elif "_addanother" in request.POST:
  1308. msg = format_html(
  1309. _(
  1310. "The {name} “{obj}” was added successfully. You may add another "
  1311. "{name} below."
  1312. ),
  1313. **msg_dict,
  1314. )
  1315. self.message_user(request, msg, messages.SUCCESS)
  1316. redirect_url = request.path
  1317. redirect_url = add_preserved_filters(
  1318. {
  1319. "preserved_filters": preserved_filters,
  1320. "preserved_qsl": preserved_qsl,
  1321. "opts": opts,
  1322. },
  1323. redirect_url,
  1324. )
  1325. return HttpResponseRedirect(redirect_url)
  1326. else:
  1327. msg = format_html(
  1328. _("The {name} “{obj}” was added successfully."), **msg_dict
  1329. )
  1330. self.message_user(request, msg, messages.SUCCESS)
  1331. return self.response_post_save_add(request, obj)
  1332. def response_change(self, request, obj):
  1333. """
  1334. Determine the HttpResponse for the change_view stage.
  1335. """
  1336. if IS_POPUP_VAR in request.POST:
  1337. opts = obj._meta
  1338. to_field = request.POST.get(TO_FIELD_VAR)
  1339. attr = str(to_field) if to_field else opts.pk.attname
  1340. value = request.resolver_match.kwargs["object_id"]
  1341. new_value = obj.serializable_value(attr)
  1342. popup_response_data = json.dumps(
  1343. {
  1344. "action": "change",
  1345. "value": str(value),
  1346. "obj": str(obj),
  1347. "new_value": str(new_value),
  1348. }
  1349. )
  1350. return TemplateResponse(
  1351. request,
  1352. self.popup_response_template
  1353. or [
  1354. "admin/%s/%s/popup_response.html"
  1355. % (opts.app_label, opts.model_name),
  1356. "admin/%s/popup_response.html" % opts.app_label,
  1357. "admin/popup_response.html",
  1358. ],
  1359. {
  1360. "popup_response_data": popup_response_data,
  1361. },
  1362. )
  1363. opts = self.opts
  1364. preserved_filters = self.get_preserved_filters(request)
  1365. preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
  1366. msg_dict = {
  1367. "name": opts.verbose_name,
  1368. "obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
  1369. }
  1370. if "_continue" in request.POST:
  1371. msg = format_html(
  1372. _(
  1373. "The {name} “{obj}” was changed successfully. You may edit it "
  1374. "again below."
  1375. ),
  1376. **msg_dict,
  1377. )
  1378. self.message_user(request, msg, messages.SUCCESS)
  1379. redirect_url = request.path
  1380. redirect_url = add_preserved_filters(
  1381. {
  1382. "preserved_filters": preserved_filters,
  1383. "preserved_qsl": preserved_qsl,
  1384. "opts": opts,
  1385. },
  1386. redirect_url,
  1387. )
  1388. return HttpResponseRedirect(redirect_url)
  1389. elif "_addanother" in request.POST:
  1390. msg = format_html(
  1391. _(
  1392. "The {name} “{obj}” was changed successfully. You may add another "
  1393. "{name} below."
  1394. ),
  1395. **msg_dict,
  1396. )
  1397. self.message_user(request, msg, messages.SUCCESS)
  1398. redirect_url = reverse(
  1399. "admin:%s_%s_add" % (opts.app_label, opts.model_name),
  1400. current_app=self.admin_site.name,
  1401. )
  1402. redirect_url = add_preserved_filters(
  1403. {
  1404. "preserved_filters": preserved_filters,
  1405. "preserved_qsl": preserved_qsl,
  1406. "opts": opts,
  1407. },
  1408. redirect_url,
  1409. )
  1410. return HttpResponseRedirect(redirect_url)
  1411. else:
  1412. msg = format_html(
  1413. _("The {name} “{obj}” was changed successfully."), **msg_dict
  1414. )
  1415. self.message_user(request, msg, messages.SUCCESS)
  1416. return self.response_post_save_change(request, obj)
  1417. def _response_post_save(self, request, obj):
  1418. if self.has_view_or_change_permission(request):
  1419. post_url = reverse(
  1420. "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name),
  1421. current_app=self.admin_site.name,
  1422. )
  1423. preserved_filters = self.get_preserved_filters(request)
  1424. post_url = add_preserved_filters(
  1425. {"preserved_filters": preserved_filters, "opts": self.opts}, post_url
  1426. )
  1427. else:
  1428. post_url = reverse("admin:index", current_app=self.admin_site.name)
  1429. return HttpResponseRedirect(post_url)
  1430. def response_post_save_add(self, request, obj):
  1431. """
  1432. Figure out where to redirect after the 'Save' button has been pressed
  1433. when adding a new object.
  1434. """
  1435. return self._response_post_save(request, obj)
  1436. def response_post_save_change(self, request, obj):
  1437. """
  1438. Figure out where to redirect after the 'Save' button has been pressed
  1439. when editing an existing object.
  1440. """
  1441. return self._response_post_save(request, obj)
  1442. def response_action(self, request, queryset):
  1443. """
  1444. Handle an admin action. This is called if a request is POSTed to the
  1445. changelist; it returns an HttpResponse if the action was handled, and
  1446. None otherwise.
  1447. """
  1448. # There can be multiple action forms on the page (at the top
  1449. # and bottom of the change list, for example). Get the action
  1450. # whose button was pushed.
  1451. try:
  1452. action_index = int(request.POST.get("index", 0))
  1453. except ValueError:
  1454. action_index = 0
  1455. # Construct the action form.
  1456. data = request.POST.copy()
  1457. data.pop(helpers.ACTION_CHECKBOX_NAME, None)
  1458. data.pop("index", None)
  1459. # Use the action whose button was pushed
  1460. try:
  1461. data.update({"action": data.getlist("action")[action_index]})
  1462. except IndexError:
  1463. # If we didn't get an action from the chosen form that's invalid
  1464. # POST data, so by deleting action it'll fail the validation check
  1465. # below. So no need to do anything here
  1466. pass
  1467. action_form = self.action_form(data, auto_id=None)
  1468. action_form.fields["action"].choices = self.get_action_choices(request)
  1469. # If the form's valid we can handle the action.
  1470. if action_form.is_valid():
  1471. action = action_form.cleaned_data["action"]
  1472. select_across = action_form.cleaned_data["select_across"]
  1473. func = self.get_actions(request)[action][0]
  1474. # Get the list of selected PKs. If nothing's selected, we can't
  1475. # perform an action on it, so bail. Except we want to perform
  1476. # the action explicitly on all objects.
  1477. selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
  1478. if not selected and not select_across:
  1479. # Reminder that something needs to be selected or nothing will happen
  1480. msg = _(
  1481. "Items must be selected in order to perform "
  1482. "actions on them. No items have been changed."
  1483. )
  1484. self.message_user(request, msg, messages.WARNING)
  1485. return None
  1486. if not select_across:
  1487. # Perform the action only on the selected objects
  1488. queryset = queryset.filter(pk__in=selected)
  1489. response = func(self, request, queryset)
  1490. # Actions may return an HttpResponse-like object, which will be
  1491. # used as the response from the POST. If not, we'll be a good
  1492. # little HTTP citizen and redirect back to the changelist page.
  1493. if isinstance(response, HttpResponseBase):
  1494. return response
  1495. else:
  1496. return HttpResponseRedirect(request.get_full_path())
  1497. else:
  1498. msg = _("No action selected.")
  1499. self.message_user(request, msg, messages.WARNING)
  1500. return None
  1501. def response_delete(self, request, obj_display, obj_id):
  1502. """
  1503. Determine the HttpResponse for the delete_view stage.
  1504. """
  1505. if IS_POPUP_VAR in request.POST:
  1506. popup_response_data = json.dumps(
  1507. {
  1508. "action": "delete",
  1509. "value": str(obj_id),
  1510. }
  1511. )
  1512. return TemplateResponse(
  1513. request,
  1514. self.popup_response_template
  1515. or [
  1516. "admin/%s/%s/popup_response.html"
  1517. % (self.opts.app_label, self.opts.model_name),
  1518. "admin/%s/popup_response.html" % self.opts.app_label,
  1519. "admin/popup_response.html",
  1520. ],
  1521. {
  1522. "popup_response_data": popup_response_data,
  1523. },
  1524. )
  1525. self.message_user(
  1526. request,
  1527. _("The %(name)s “%(obj)s” was deleted successfully.")
  1528. % {
  1529. "name": self.opts.verbose_name,
  1530. "obj": obj_display,
  1531. },
  1532. messages.SUCCESS,
  1533. )
  1534. if self.has_change_permission(request, None):
  1535. post_url = reverse(
  1536. "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name),
  1537. current_app=self.admin_site.name,
  1538. )
  1539. preserved_filters = self.get_preserved_filters(request)
  1540. post_url = add_preserved_filters(
  1541. {"preserved_filters": preserved_filters, "opts": self.opts}, post_url
  1542. )
  1543. else:
  1544. post_url = reverse("admin:index", current_app=self.admin_site.name)
  1545. return HttpResponseRedirect(post_url)
  1546. def render_delete_form(self, request, context):
  1547. app_label = self.opts.app_label
  1548. request.current_app = self.admin_site.name
  1549. context.update(
  1550. to_field_var=TO_FIELD_VAR,
  1551. is_popup_var=IS_POPUP_VAR,
  1552. media=self.media,
  1553. )
  1554. return TemplateResponse(
  1555. request,
  1556. self.delete_confirmation_template
  1557. or [
  1558. "admin/{}/{}/delete_confirmation.html".format(
  1559. app_label, self.opts.model_name
  1560. ),
  1561. "admin/{}/delete_confirmation.html".format(app_label),
  1562. "admin/delete_confirmation.html",
  1563. ],
  1564. context,
  1565. )
  1566. def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
  1567. # Edit permissions on parent model are required for editable inlines.
  1568. can_edit_parent = (
  1569. self.has_change_permission(request, obj)
  1570. if obj
  1571. else self.has_add_permission(request)
  1572. )
  1573. inline_admin_formsets = []
  1574. for inline, formset in zip(inline_instances, formsets):
  1575. fieldsets = list(inline.get_fieldsets(request, obj))
  1576. readonly = list(inline.get_readonly_fields(request, obj))
  1577. if can_edit_parent:
  1578. has_add_permission = inline.has_add_permission(request, obj)
  1579. has_change_permission = inline.has_change_permission(request, obj)
  1580. has_delete_permission = inline.has_delete_permission(request, obj)
  1581. else:
  1582. # Disable all edit-permissions, and override formset settings.
  1583. has_add_permission = has_change_permission = has_delete_permission = (
  1584. False
  1585. )
  1586. formset.extra = formset.max_num = 0
  1587. has_view_permission = inline.has_view_permission(request, obj)
  1588. prepopulated = dict(inline.get_prepopulated_fields(request, obj))
  1589. inline_admin_formset = helpers.InlineAdminFormSet(
  1590. inline,
  1591. formset,
  1592. fieldsets,
  1593. prepopulated,
  1594. readonly,
  1595. model_admin=self,
  1596. has_add_permission=has_add_permission,
  1597. has_change_permission=has_change_permission,
  1598. has_delete_permission=has_delete_permission,
  1599. has_view_permission=has_view_permission,
  1600. )
  1601. inline_admin_formsets.append(inline_admin_formset)
  1602. return inline_admin_formsets
  1603. def get_changeform_initial_data(self, request):
  1604. """
  1605. Get the initial form data from the request's GET params.
  1606. """
  1607. initial = dict(request.GET.items())
  1608. for k in initial:
  1609. try:
  1610. f = self.opts.get_field(k)
  1611. except FieldDoesNotExist:
  1612. continue
  1613. # We have to special-case M2Ms as a list of comma-separated PKs.
  1614. if isinstance(f, models.ManyToManyField):
  1615. initial[k] = initial[k].split(",")
  1616. return initial
  1617. def _get_obj_does_not_exist_redirect(self, request, opts, object_id):
  1618. """
  1619. Create a message informing the user that the object doesn't exist
  1620. and return a redirect to the admin index page.
  1621. """
  1622. msg = _("%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?") % {
  1623. "name": opts.verbose_name,
  1624. "key": unquote(object_id),
  1625. }
  1626. self.message_user(request, msg, messages.WARNING)
  1627. url = reverse("admin:index", current_app=self.admin_site.name)
  1628. return HttpResponseRedirect(url)
  1629. @csrf_protect_m
  1630. def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
  1631. with transaction.atomic(using=router.db_for_write(self.model)):
  1632. return self._changeform_view(request, object_id, form_url, extra_context)
  1633. def _changeform_view(self, request, object_id, form_url, extra_context):
  1634. to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
  1635. if to_field and not self.to_field_allowed(request, to_field):
  1636. raise DisallowedModelAdminToField(
  1637. "The field %s cannot be referenced." % to_field
  1638. )
  1639. if request.method == "POST" and "_saveasnew" in request.POST:
  1640. object_id = None
  1641. add = object_id is None
  1642. if add:
  1643. if not self.has_add_permission(request):
  1644. raise PermissionDenied
  1645. obj = None
  1646. else:
  1647. obj = self.get_object(request, unquote(object_id), to_field)
  1648. if request.method == "POST":
  1649. if not self.has_change_permission(request, obj):
  1650. raise PermissionDenied
  1651. else:
  1652. if not self.has_view_or_change_permission(request, obj):
  1653. raise PermissionDenied
  1654. if obj is None:
  1655. return self._get_obj_does_not_exist_redirect(
  1656. request, self.opts, object_id
  1657. )
  1658. fieldsets = self.get_fieldsets(request, obj)
  1659. ModelForm = self.get_form(
  1660. request, obj, change=not add, fields=flatten_fieldsets(fieldsets)
  1661. )
  1662. if request.method == "POST":
  1663. form = ModelForm(request.POST, request.FILES, instance=obj)
  1664. formsets, inline_instances = self._create_formsets(
  1665. request,
  1666. form.instance,
  1667. change=not add,
  1668. )
  1669. form_validated = form.is_valid()
  1670. if form_validated:
  1671. new_object = self.save_form(request, form, change=not add)
  1672. else:
  1673. new_object = form.instance
  1674. if all_valid(formsets) and form_validated:
  1675. self.save_model(request, new_object, form, not add)
  1676. self.save_related(request, form, formsets, not add)
  1677. change_message = self.construct_change_message(
  1678. request, form, formsets, add
  1679. )
  1680. if add:
  1681. self.log_addition(request, new_object, change_message)
  1682. return self.response_add(request, new_object)
  1683. else:
  1684. self.log_change(request, new_object, change_message)
  1685. return self.response_change(request, new_object)
  1686. else:
  1687. form_validated = False
  1688. else:
  1689. if add:
  1690. initial = self.get_changeform_initial_data(request)
  1691. form = ModelForm(initial=initial)
  1692. formsets, inline_instances = self._create_formsets(
  1693. request, form.instance, change=False
  1694. )
  1695. else:
  1696. form = ModelForm(instance=obj)
  1697. formsets, inline_instances = self._create_formsets(
  1698. request, obj, change=True
  1699. )
  1700. if not add and not self.has_change_permission(request, obj):
  1701. readonly_fields = flatten_fieldsets(fieldsets)
  1702. else:
  1703. readonly_fields = self.get_readonly_fields(request, obj)
  1704. admin_form = helpers.AdminForm(
  1705. form,
  1706. list(fieldsets),
  1707. # Clear prepopulated fields on a view-only form to avoid a crash.
  1708. (
  1709. self.get_prepopulated_fields(request, obj)
  1710. if add or self.has_change_permission(request, obj)
  1711. else {}
  1712. ),
  1713. readonly_fields,
  1714. model_admin=self,
  1715. )
  1716. media = self.media + admin_form.media
  1717. inline_formsets = self.get_inline_formsets(
  1718. request, formsets, inline_instances, obj
  1719. )
  1720. for inline_formset in inline_formsets:
  1721. media += inline_formset.media
  1722. if add:
  1723. title = _("Add %s")
  1724. elif self.has_change_permission(request, obj):
  1725. title = _("Change %s")
  1726. else:
  1727. title = _("View %s")
  1728. context = {
  1729. **self.admin_site.each_context(request),
  1730. "title": title % self.opts.verbose_name,
  1731. "subtitle": str(obj) if obj else None,
  1732. "adminform": admin_form,
  1733. "object_id": object_id,
  1734. "original": obj,
  1735. "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
  1736. "to_field": to_field,
  1737. "media": media,
  1738. "inline_admin_formsets": inline_formsets,
  1739. "errors": helpers.AdminErrorList(form, formsets),
  1740. "preserved_filters": self.get_preserved_filters(request),
  1741. }
  1742. # Hide the "Save" and "Save and continue" buttons if "Save as New" was
  1743. # previously chosen to prevent the interface from getting confusing.
  1744. if (
  1745. request.method == "POST"
  1746. and not form_validated
  1747. and "_saveasnew" in request.POST
  1748. ):
  1749. context["show_save"] = False
  1750. context["show_save_and_continue"] = False
  1751. # Use the change template instead of the add template.
  1752. add = False
  1753. context.update(extra_context or {})
  1754. return self.render_change_form(
  1755. request, context, add=add, change=not add, obj=obj, form_url=form_url
  1756. )
  1757. def add_view(self, request, form_url="", extra_context=None):
  1758. return self.changeform_view(request, None, form_url, extra_context)
  1759. def change_view(self, request, object_id, form_url="", extra_context=None):
  1760. return self.changeform_view(request, object_id, form_url, extra_context)
  1761. def _get_edited_object_pks(self, request, prefix):
  1762. """Return POST data values of list_editable primary keys."""
  1763. pk_pattern = re.compile(
  1764. r"{}-\d+-{}$".format(re.escape(prefix), self.opts.pk.name)
  1765. )
  1766. return [value for key, value in request.POST.items() if pk_pattern.match(key)]
  1767. def _get_list_editable_queryset(self, request, prefix):
  1768. """
  1769. Based on POST data, return a queryset of the objects that were edited
  1770. via list_editable.
  1771. """
  1772. object_pks = self._get_edited_object_pks(request, prefix)
  1773. queryset = self.get_queryset(request)
  1774. validate = queryset.model._meta.pk.to_python
  1775. try:
  1776. for pk in object_pks:
  1777. validate(pk)
  1778. except ValidationError:
  1779. # Disable the optimization if the POST data was tampered with.
  1780. return queryset
  1781. return queryset.filter(pk__in=object_pks)
  1782. @csrf_protect_m
  1783. def changelist_view(self, request, extra_context=None):
  1784. """
  1785. The 'change list' admin view for this model.
  1786. """
  1787. from django.contrib.admin.views.main import ERROR_FLAG
  1788. app_label = self.opts.app_label
  1789. if not self.has_view_or_change_permission(request):
  1790. raise PermissionDenied
  1791. try:
  1792. cl = self.get_changelist_instance(request)
  1793. except IncorrectLookupParameters:
  1794. # Wacky lookup parameters were given, so redirect to the main
  1795. # changelist page, without parameters, and pass an 'invalid=1'
  1796. # parameter via the query string. If wacky parameters were given
  1797. # and the 'invalid=1' parameter was already in the query string,
  1798. # something is screwed up with the database, so display an error
  1799. # page.
  1800. if ERROR_FLAG in request.GET:
  1801. return SimpleTemplateResponse(
  1802. "admin/invalid_setup.html",
  1803. {
  1804. "title": _("Database error"),
  1805. },
  1806. )
  1807. return HttpResponseRedirect(request.path + "?" + ERROR_FLAG + "=1")
  1808. # If the request was POSTed, this might be a bulk action or a bulk
  1809. # edit. Try to look up an action or confirmation first, but if this
  1810. # isn't an action the POST will fall through to the bulk edit check,
  1811. # below.
  1812. action_failed = False
  1813. selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
  1814. actions = self.get_actions(request)
  1815. # Actions with no confirmation
  1816. if (
  1817. actions
  1818. and request.method == "POST"
  1819. and "index" in request.POST
  1820. and "_save" not in request.POST
  1821. ):
  1822. if selected:
  1823. response = self.response_action(
  1824. request, queryset=cl.get_queryset(request)
  1825. )
  1826. if response:
  1827. return response
  1828. else:
  1829. action_failed = True
  1830. else:
  1831. msg = _(
  1832. "Items must be selected in order to perform "
  1833. "actions on them. No items have been changed."
  1834. )
  1835. self.message_user(request, msg, messages.WARNING)
  1836. action_failed = True
  1837. # Actions with confirmation
  1838. if (
  1839. actions
  1840. and request.method == "POST"
  1841. and helpers.ACTION_CHECKBOX_NAME in request.POST
  1842. and "index" not in request.POST
  1843. and "_save" not in request.POST
  1844. ):
  1845. if selected:
  1846. response = self.response_action(
  1847. request, queryset=cl.get_queryset(request)
  1848. )
  1849. if response:
  1850. return response
  1851. else:
  1852. action_failed = True
  1853. if action_failed:
  1854. # Redirect back to the changelist page to avoid resubmitting the
  1855. # form if the user refreshes the browser or uses the "No, take
  1856. # me back" button on the action confirmation page.
  1857. return HttpResponseRedirect(request.get_full_path())
  1858. # If we're allowing changelist editing, we need to construct a formset
  1859. # for the changelist given all the fields to be edited. Then we'll
  1860. # use the formset to validate/process POSTed data.
  1861. formset = cl.formset = None
  1862. # Handle POSTed bulk-edit data.
  1863. if request.method == "POST" and cl.list_editable and "_save" in request.POST:
  1864. if not self.has_change_permission(request):
  1865. raise PermissionDenied
  1866. FormSet = self.get_changelist_formset(request)
  1867. modified_objects = self._get_list_editable_queryset(
  1868. request, FormSet.get_default_prefix()
  1869. )
  1870. formset = cl.formset = FormSet(
  1871. request.POST, request.FILES, queryset=modified_objects
  1872. )
  1873. if formset.is_valid():
  1874. changecount = 0
  1875. with transaction.atomic(using=router.db_for_write(self.model)):
  1876. for form in formset.forms:
  1877. if form.has_changed():
  1878. obj = self.save_form(request, form, change=True)
  1879. self.save_model(request, obj, form, change=True)
  1880. self.save_related(request, form, formsets=[], change=True)
  1881. change_msg = self.construct_change_message(
  1882. request, form, None
  1883. )
  1884. self.log_change(request, obj, change_msg)
  1885. changecount += 1
  1886. if changecount:
  1887. msg = ngettext(
  1888. "%(count)s %(name)s was changed successfully.",
  1889. "%(count)s %(name)s were changed successfully.",
  1890. changecount,
  1891. ) % {
  1892. "count": changecount,
  1893. "name": model_ngettext(self.opts, changecount),
  1894. }
  1895. self.message_user(request, msg, messages.SUCCESS)
  1896. return HttpResponseRedirect(request.get_full_path())
  1897. # Handle GET -- construct a formset for display.
  1898. elif cl.list_editable and self.has_change_permission(request):
  1899. FormSet = self.get_changelist_formset(request)
  1900. formset = cl.formset = FormSet(queryset=cl.result_list)
  1901. # Build the list of media to be used by the formset.
  1902. if formset:
  1903. media = self.media + formset.media
  1904. else:
  1905. media = self.media
  1906. # Build the action form and populate it with available actions.
  1907. if actions:
  1908. action_form = self.action_form(auto_id=None)
  1909. action_form.fields["action"].choices = self.get_action_choices(request)
  1910. media += action_form.media
  1911. else:
  1912. action_form = None
  1913. selection_note_all = ngettext(
  1914. "%(total_count)s selected", "All %(total_count)s selected", cl.result_count
  1915. )
  1916. context = {
  1917. **self.admin_site.each_context(request),
  1918. "module_name": str(self.opts.verbose_name_plural),
  1919. "selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)},
  1920. "selection_note_all": selection_note_all % {"total_count": cl.result_count},
  1921. "title": cl.title,
  1922. "subtitle": None,
  1923. "is_popup": cl.is_popup,
  1924. "to_field": cl.to_field,
  1925. "cl": cl,
  1926. "media": media,
  1927. "has_add_permission": self.has_add_permission(request),
  1928. "opts": cl.opts,
  1929. "action_form": action_form,
  1930. "actions_on_top": self.actions_on_top,
  1931. "actions_on_bottom": self.actions_on_bottom,
  1932. "actions_selection_counter": self.actions_selection_counter,
  1933. "preserved_filters": self.get_preserved_filters(request),
  1934. **(extra_context or {}),
  1935. }
  1936. request.current_app = self.admin_site.name
  1937. return TemplateResponse(
  1938. request,
  1939. self.change_list_template
  1940. or [
  1941. "admin/%s/%s/change_list.html" % (app_label, self.opts.model_name),
  1942. "admin/%s/change_list.html" % app_label,
  1943. "admin/change_list.html",
  1944. ],
  1945. context,
  1946. )
  1947. def get_deleted_objects(self, objs, request):
  1948. """
  1949. Hook for customizing the delete process for the delete view and the
  1950. "delete selected" action.
  1951. """
  1952. return get_deleted_objects(objs, request, self.admin_site)
  1953. @csrf_protect_m
  1954. def delete_view(self, request, object_id, extra_context=None):
  1955. with transaction.atomic(using=router.db_for_write(self.model)):
  1956. return self._delete_view(request, object_id, extra_context)
  1957. def _delete_view(self, request, object_id, extra_context):
  1958. "The 'delete' admin view for this model."
  1959. app_label = self.opts.app_label
  1960. to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
  1961. if to_field and not self.to_field_allowed(request, to_field):
  1962. raise DisallowedModelAdminToField(
  1963. "The field %s cannot be referenced." % to_field
  1964. )
  1965. obj = self.get_object(request, unquote(object_id), to_field)
  1966. if not self.has_delete_permission(request, obj):
  1967. raise PermissionDenied
  1968. if obj is None:
  1969. return self._get_obj_does_not_exist_redirect(request, self.opts, object_id)
  1970. # Populate deleted_objects, a data structure of all related objects that
  1971. # will also be deleted.
  1972. (
  1973. deleted_objects,
  1974. model_count,
  1975. perms_needed,
  1976. protected,
  1977. ) = self.get_deleted_objects([obj], request)
  1978. if request.POST and not protected: # The user has confirmed the deletion.
  1979. if perms_needed:
  1980. raise PermissionDenied
  1981. obj_display = str(obj)
  1982. attr = str(to_field) if to_field else self.opts.pk.attname
  1983. obj_id = obj.serializable_value(attr)
  1984. self.log_deletions(request, [obj])
  1985. self.delete_model(request, obj)
  1986. return self.response_delete(request, obj_display, obj_id)
  1987. object_name = str(self.opts.verbose_name)
  1988. if perms_needed or protected:
  1989. title = _("Cannot delete %(name)s") % {"name": object_name}
  1990. else:
  1991. title = _("Are you sure?")
  1992. context = {
  1993. **self.admin_site.each_context(request),
  1994. "title": title,
  1995. "subtitle": None,
  1996. "object_name": object_name,
  1997. "object": obj,
  1998. "deleted_objects": deleted_objects,
  1999. "model_count": dict(model_count).items(),
  2000. "perms_lacking": perms_needed,
  2001. "protected": protected,
  2002. "opts": self.opts,
  2003. "app_label": app_label,
  2004. "preserved_filters": self.get_preserved_filters(request),
  2005. "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
  2006. "to_field": to_field,
  2007. **(extra_context or {}),
  2008. }
  2009. return self.render_delete_form(request, context)
  2010. def history_view(self, request, object_id, extra_context=None):
  2011. "The 'history' admin view for this model."
  2012. from django.contrib.admin.models import LogEntry
  2013. from django.contrib.admin.views.main import PAGE_VAR
  2014. # First check if the user can see this history.
  2015. model = self.model
  2016. obj = self.get_object(request, unquote(object_id))
  2017. if obj is None:
  2018. return self._get_obj_does_not_exist_redirect(
  2019. request, model._meta, object_id
  2020. )
  2021. if not self.has_view_or_change_permission(request, obj):
  2022. raise PermissionDenied
  2023. # Then get the history for this object.
  2024. app_label = self.opts.app_label
  2025. action_list = (
  2026. LogEntry.objects.filter(
  2027. object_id=unquote(object_id),
  2028. content_type=get_content_type_for_model(model),
  2029. )
  2030. .select_related()
  2031. .order_by("action_time")
  2032. )
  2033. paginator = self.get_paginator(request, action_list, 100)
  2034. page_number = request.GET.get(PAGE_VAR, 1)
  2035. page_obj = paginator.get_page(page_number)
  2036. page_range = paginator.get_elided_page_range(page_obj.number)
  2037. context = {
  2038. **self.admin_site.each_context(request),
  2039. "title": _("Change history: %s") % obj,
  2040. "subtitle": None,
  2041. "action_list": page_obj,
  2042. "page_range": page_range,
  2043. "page_var": PAGE_VAR,
  2044. "pagination_required": paginator.count > 100,
  2045. "module_name": str(capfirst(self.opts.verbose_name_plural)),
  2046. "object": obj,
  2047. "opts": self.opts,
  2048. "preserved_filters": self.get_preserved_filters(request),
  2049. **(extra_context or {}),
  2050. }
  2051. request.current_app = self.admin_site.name
  2052. return TemplateResponse(
  2053. request,
  2054. self.object_history_template
  2055. or [
  2056. "admin/%s/%s/object_history.html" % (app_label, self.opts.model_name),
  2057. "admin/%s/object_history.html" % app_label,
  2058. "admin/object_history.html",
  2059. ],
  2060. context,
  2061. )
  2062. def get_formset_kwargs(self, request, obj, inline, prefix):
  2063. formset_params = {
  2064. "instance": obj,
  2065. "prefix": prefix,
  2066. "queryset": inline.get_queryset(request),
  2067. }
  2068. if request.method == "POST":
  2069. formset_params.update(
  2070. {
  2071. "data": request.POST.copy(),
  2072. "files": request.FILES,
  2073. "save_as_new": "_saveasnew" in request.POST,
  2074. }
  2075. )
  2076. return formset_params
  2077. def _create_formsets(self, request, obj, change):
  2078. "Helper function to generate formsets for add/change_view."
  2079. formsets = []
  2080. inline_instances = []
  2081. prefixes = {}
  2082. get_formsets_args = [request]
  2083. if change:
  2084. get_formsets_args.append(obj)
  2085. for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
  2086. prefix = FormSet.get_default_prefix()
  2087. prefixes[prefix] = prefixes.get(prefix, 0) + 1
  2088. if prefixes[prefix] != 1 or not prefix:
  2089. prefix = "%s-%s" % (prefix, prefixes[prefix])
  2090. formset_params = self.get_formset_kwargs(request, obj, inline, prefix)
  2091. formset = FormSet(**formset_params)
  2092. def user_deleted_form(request, obj, formset, index, inline):
  2093. """Return whether or not the user deleted the form."""
  2094. return (
  2095. inline.has_delete_permission(request, obj)
  2096. and "{}-{}-DELETE".format(formset.prefix, index) in request.POST
  2097. )
  2098. # Bypass validation of each view-only inline form (since the form's
  2099. # data won't be in request.POST), unless the form was deleted.
  2100. if not inline.has_change_permission(request, obj if change else None):
  2101. for index, form in enumerate(formset.initial_forms):
  2102. if user_deleted_form(request, obj, formset, index, inline):
  2103. continue
  2104. form._errors = {}
  2105. form.cleaned_data = form.initial
  2106. formsets.append(formset)
  2107. inline_instances.append(inline)
  2108. return formsets, inline_instances
  2109. class InlineModelAdmin(BaseModelAdmin):
  2110. """
  2111. Options for inline editing of ``model`` instances.
  2112. Provide ``fk_name`` to specify the attribute name of the ``ForeignKey``
  2113. from ``model`` to its parent. This is required if ``model`` has more than
  2114. one ``ForeignKey`` to its parent.
  2115. """
  2116. model = None
  2117. fk_name = None
  2118. formset = BaseInlineFormSet
  2119. extra = 3
  2120. min_num = None
  2121. max_num = None
  2122. template = None
  2123. verbose_name = None
  2124. verbose_name_plural = None
  2125. can_delete = True
  2126. show_change_link = False
  2127. checks_class = InlineModelAdminChecks
  2128. classes = None
  2129. def __init__(self, parent_model, admin_site):
  2130. self.admin_site = admin_site
  2131. self.parent_model = parent_model
  2132. self.opts = self.model._meta
  2133. self.has_registered_model = admin_site.is_registered(self.model)
  2134. super().__init__()
  2135. if self.verbose_name_plural is None:
  2136. if self.verbose_name is None:
  2137. self.verbose_name_plural = self.opts.verbose_name_plural
  2138. else:
  2139. self.verbose_name_plural = format_lazy("{}s", self.verbose_name)
  2140. if self.verbose_name is None:
  2141. self.verbose_name = self.opts.verbose_name
  2142. @property
  2143. def media(self):
  2144. extra = "" if settings.DEBUG else ".min"
  2145. js = ["vendor/jquery/jquery%s.js" % extra, "jquery.init.js", "inlines.js"]
  2146. if self.filter_vertical or self.filter_horizontal:
  2147. js.extend(["SelectBox.js", "SelectFilter2.js"])
  2148. return forms.Media(js=["admin/js/%s" % url for url in js])
  2149. def get_extra(self, request, obj=None, **kwargs):
  2150. """Hook for customizing the number of extra inline forms."""
  2151. return self.extra
  2152. def get_min_num(self, request, obj=None, **kwargs):
  2153. """Hook for customizing the min number of inline forms."""
  2154. return self.min_num
  2155. def get_max_num(self, request, obj=None, **kwargs):
  2156. """Hook for customizing the max number of extra inline forms."""
  2157. return self.max_num
  2158. def get_formset(self, request, obj=None, **kwargs):
  2159. """Return a BaseInlineFormSet class for use in admin add/change views."""
  2160. if "fields" in kwargs:
  2161. fields = kwargs.pop("fields")
  2162. else:
  2163. fields = flatten_fieldsets(self.get_fieldsets(request, obj))
  2164. excluded = self.get_exclude(request, obj)
  2165. exclude = [] if excluded is None else list(excluded)
  2166. exclude.extend(self.get_readonly_fields(request, obj))
  2167. if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
  2168. # Take the custom ModelForm's Meta.exclude into account only if the
  2169. # InlineModelAdmin doesn't define its own.
  2170. exclude.extend(self.form._meta.exclude)
  2171. # If exclude is an empty list we use None, since that's the actual
  2172. # default.
  2173. exclude = exclude or None
  2174. can_delete = self.can_delete and self.has_delete_permission(request, obj)
  2175. defaults = {
  2176. "form": self.form,
  2177. "formset": self.formset,
  2178. "fk_name": self.fk_name,
  2179. "fields": fields,
  2180. "exclude": exclude,
  2181. "formfield_callback": partial(self.formfield_for_dbfield, request=request),
  2182. "extra": self.get_extra(request, obj, **kwargs),
  2183. "min_num": self.get_min_num(request, obj, **kwargs),
  2184. "max_num": self.get_max_num(request, obj, **kwargs),
  2185. "can_delete": can_delete,
  2186. **kwargs,
  2187. }
  2188. base_model_form = defaults["form"]
  2189. can_change = self.has_change_permission(request, obj) if request else True
  2190. can_add = self.has_add_permission(request, obj) if request else True
  2191. class DeleteProtectedModelForm(base_model_form):
  2192. def hand_clean_DELETE(self):
  2193. """
  2194. We don't validate the 'DELETE' field itself because on
  2195. templates it's not rendered using the field information, but
  2196. just using a generic "deletion_field" of the InlineModelAdmin.
  2197. """
  2198. if self.cleaned_data.get(DELETION_FIELD_NAME, False):
  2199. using = router.db_for_write(self._meta.model)
  2200. collector = NestedObjects(using=using)
  2201. if self.instance._state.adding:
  2202. return
  2203. collector.collect([self.instance])
  2204. if collector.protected:
  2205. objs = []
  2206. for p in collector.protected:
  2207. objs.append(
  2208. # Translators: Model verbose name and instance
  2209. # representation, suitable to be an item in a
  2210. # list.
  2211. _("%(class_name)s %(instance)s")
  2212. % {"class_name": p._meta.verbose_name, "instance": p}
  2213. )
  2214. params = {
  2215. "class_name": self._meta.model._meta.verbose_name,
  2216. "instance": self.instance,
  2217. "related_objects": get_text_list(objs, _("and")),
  2218. }
  2219. msg = _(
  2220. "Deleting %(class_name)s %(instance)s would require "
  2221. "deleting the following protected related objects: "
  2222. "%(related_objects)s"
  2223. )
  2224. raise ValidationError(
  2225. msg, code="deleting_protected", params=params
  2226. )
  2227. def is_valid(self):
  2228. result = super().is_valid()
  2229. self.hand_clean_DELETE()
  2230. return result
  2231. def has_changed(self):
  2232. # Protect against unauthorized edits.
  2233. if not can_change and not self.instance._state.adding:
  2234. return False
  2235. if not can_add and self.instance._state.adding:
  2236. return False
  2237. return super().has_changed()
  2238. defaults["form"] = DeleteProtectedModelForm
  2239. if defaults["fields"] is None and not modelform_defines_fields(
  2240. defaults["form"]
  2241. ):
  2242. defaults["fields"] = forms.ALL_FIELDS
  2243. return inlineformset_factory(self.parent_model, self.model, **defaults)
  2244. def _get_form_for_get_fields(self, request, obj=None):
  2245. return self.get_formset(request, obj, fields=None).form
  2246. def get_queryset(self, request):
  2247. queryset = super().get_queryset(request)
  2248. if not self.has_view_or_change_permission(request):
  2249. queryset = queryset.none()
  2250. return queryset
  2251. def _has_any_perms_for_target_model(self, request, perms):
  2252. """
  2253. This method is called only when the ModelAdmin's model is for an
  2254. ManyToManyField's implicit through model (if self.opts.auto_created).
  2255. Return True if the user has any of the given permissions ('add',
  2256. 'change', etc.) for the model that points to the through model.
  2257. """
  2258. opts = self.opts
  2259. # Find the target model of an auto-created many-to-many relationship.
  2260. for field in opts.fields:
  2261. if field.remote_field and field.remote_field.model != self.parent_model:
  2262. opts = field.remote_field.model._meta
  2263. break
  2264. return any(
  2265. request.user.has_perm(
  2266. "%s.%s" % (opts.app_label, get_permission_codename(perm, opts))
  2267. )
  2268. for perm in perms
  2269. )
  2270. def has_add_permission(self, request, obj):
  2271. if self.opts.auto_created:
  2272. # Auto-created intermediate models don't have their own
  2273. # permissions. The user needs to have the change permission for the
  2274. # related model in order to be able to do anything with the
  2275. # intermediate model.
  2276. return self._has_any_perms_for_target_model(request, ["change"])
  2277. return super().has_add_permission(request)
  2278. def has_change_permission(self, request, obj=None):
  2279. if self.opts.auto_created:
  2280. # Same comment as has_add_permission().
  2281. return self._has_any_perms_for_target_model(request, ["change"])
  2282. return super().has_change_permission(request)
  2283. def has_delete_permission(self, request, obj=None):
  2284. if self.opts.auto_created:
  2285. # Same comment as has_add_permission().
  2286. return self._has_any_perms_for_target_model(request, ["change"])
  2287. return super().has_delete_permission(request, obj)
  2288. def has_view_permission(self, request, obj=None):
  2289. if self.opts.auto_created:
  2290. # Same comment as has_add_permission(). The 'change' permission
  2291. # also implies the 'view' permission.
  2292. return self._has_any_perms_for_target_model(request, ["view", "change"])
  2293. return super().has_view_permission(request)
  2294. class StackedInline(InlineModelAdmin):
  2295. template = "admin/edit_inline/stacked.html"
  2296. class TabularInline(InlineModelAdmin):
  2297. template = "admin/edit_inline/tabular.html"