since this module gets imported in the application s root package it cannot import models from other applications at the module level defaults for formfieldoverrides modeladmin subclasses can change this by adding to modeladmin formfieldoverrides functionality common to both modeladmin and inlineadmin autocompletefields rawidfields fields none exclude none fieldsets none form forms modelform filtervertical filterhorizontal radiofields prepopulatedfields formfieldoverrides readonlyfields ordering none sortableby none viewonsite true showfullresultcount true checksclass basemodeladminchecks def checkself kwargs return self checksclass checkself kwargs def initself merge formfieldfordbfielddefaults with the formfieldoverrides rather than simply overwriting overrides copy deepcopyformfieldfordbfielddefaults for k v in self formfieldoverrides items overrides setdefaultk updatev self formfieldoverrides overrides def formfieldfordbfieldself dbfield request kwargs if the field specifies choices we don t need to look for special admin widgets we just need to use a select widget of some kind if dbfield choices return self formfieldforchoicefielddbfield request kwargs foreignkey or manytomanyfields if isinstancedbfield models foreignkey models manytomanyfield combine the field kwargs with any options for formfieldoverrides make sure the passed in kwargs override anything in formfieldoverrides because kwargs is more specific and should always win if dbfield class in self formfieldoverrides kwargs self formfieldoverridesdbfield class kwargs get the correct formfield if isinstancedbfield models foreignkey formfield self formfieldforforeignkeydbfield request kwargs elif isinstancedbfield models manytomanyfield formfield self formfieldformanytomanydbfield request kwargs for nonrawid fields wrap the widget with a wrapper that adds extra html the add other interface to the end of the rendered output formfield can be none if it came from a onetoonefield with parentlinktrue or a m2m intermediary if formfield and dbfield name not in self rawidfields try relatedmodeladmin self adminsite getmodeladmin dbfield remotefield model except notregistered wrapperkwargs else wrapperkwargs canaddrelated relatedmodeladmin hasaddpermission request canchangerelated relatedmodeladmin haschangepermission request candeleterelated relatedmodeladmin hasdeletepermission request canviewrelated relatedmodeladmin hasviewpermission request formfield widget widgets relatedfieldwidgetwrapper formfield widget dbfield remotefield self adminsite wrapperkwargs return formfield if we ve got overrides for the formfield defined use em kwargs passed to formfieldfordbfield override the defaults for klass in dbfield class mro if klass in self formfieldoverrides kwargs copy deepcopyself formfieldoverridesklass kwargs return dbfield formfieldkwargs for any other type of field just call its formfield method return dbfield formfieldkwargs def formfieldforchoicefieldself dbfield request kwargs if the field is named as a radiofield use a radioselect if dbfield name in self radiofields avoid stomping on custom widgetchoices arguments if widget not in kwargs kwargswidget widgets adminradioselect attrs class getulclassself radiofieldsdbfield name if choices not in kwargs kwargschoices dbfield getchoices includeblankdbfield blank blankchoice none return dbfield formfieldkwargs def getfieldquerysetself db dbfield request try relatedadmin self adminsite getmodeladmindbfield remotefield model except notregistered return none else ordering relatedadmin getorderingrequest if ordering is not none and ordering return dbfield remotefield model defaultmanager usingdb orderby ordering return none def formfieldforforeignkeyself dbfield request kwargs db kwargs getusing if widget not in kwargs if dbfield name in self getautocompletefieldsrequest kwargswidget autocompleteselect dbfield self adminsite usingdb elif dbfield name in self rawidfields kwargswidget widgets foreignkeyrawidwidget dbfield remotefield self adminsite usingdb elif dbfield name in self radiofields kwargswidget widgets adminradioselect attrs class getulclassself radiofieldsdbfield name kwargsemptylabel kwargs getemptylabel none if dbfield blank else none if queryset not in kwargs queryset self getfieldquerysetdb dbfield request if queryset is not none kwargsqueryset queryset return dbfield formfieldkwargs def formfieldformanytomanyself dbfield request kwargs if it uses an intermediary model that isn t auto created don t show a field in admin if not dbfield remotefield through meta autocreated return none db kwargs getusing if widget not in kwargs autocompletefields self getautocompletefieldsrequest if dbfield name in autocompletefields kwargswidget autocompleteselectmultiple dbfield self adminsite usingdb elif dbfield name in self rawidfields kwargswidget widgets manytomanyrawidwidget dbfield remotefield self adminsite usingdb elif dbfield name in self filtervertical self filterhorizontal kwargswidget widgets filteredselectmultiple dbfield verbosename dbfield name in self filtervertical if queryset not in kwargs queryset self getfieldquerysetdb dbfield request if queryset is not none kwargsqueryset queryset formfield dbfield formfieldkwargs if isinstanceformfield widget selectmultiple and formfield widget allowmultipleselected and not isinstance formfield widget checkboxselectmultiple autocompleteselectmultiple msg hold down control or command on a mac to select more than one helptext formfield helptext formfield helptext formatlazy helptext msg if helptext else msg return formfield def getautocompletefieldsself request return self autocompletefields def getviewonsiteurlself objnone if obj is none or not self viewonsite return none if callableself viewonsite return self viewonsiteobj elif hasattrobj getabsoluteurl use the contenttype lookup if viewonsite is true return reverse admin viewonsite kwargs contenttypeid getcontenttypeformodelobj pk objectid obj pk currentappself adminsite name def getemptyvaluedisplayself try return marksafeself emptyvaluedisplay except attributeerror return marksafeself adminsite emptyvaluedisplay def getexcludeself request objnone return self exclude def getfieldsself request objnone if self fields return self fields getformforgetfields is implemented in subclasses form self getformforgetfieldsrequest obj return form basefields self getreadonlyfieldsrequest obj def getfieldsetsself request objnone if self fieldsets return self fieldsets return none fields self getfieldsrequest obj def getinlinesself request obj hook for specifying field ordering hook for specifying custom readonly fields hook for specifying custom prepopulated fields return a queryset of all model instances that can be edited by the admin site this is used by changelistview todo this should be handled by some parameter to the changelist hook for specifying which fields can be sorted in the changelist return self sortableby if self sortableby is not none else self getlistdisplayrequest removedindjango60warning when the deprecation ends replace with def lookupallowedself lookup value request def lookupallowedself lookup value requestnone from django contrib admin filters import simplelistfilter model self model check fkey lookups that are allowed so that popups produced by foreignkeyrawidwidget on the basis of foreignkey limitchoicesto are allowed to work for fklookup in model meta relatedfkeylookups as limitchoicesto can be a callable invoke it here if callablefklookup fklookup fklookup if lookup value in widgets urlparamsfromlookupdict fklookup items return true relationparts prevfield none for part in lookup splitlookupsep try field model meta getfieldpart except fielddoesnotexist lookups on nonexistent fields are ok since they re ignored later break if not prevfield or prevfield isrelation and field not in model meta parents values and field is not model meta autofield and model meta autofield is none or part not in getattrprevfield tofields relationparts appendpart if not getattrfield pathinfos none this is not a relational field so further parts must be transforms break prevfield field model field pathinfos1 toopts model if lenrelationparts 1 either a local field filter or no fields at all return true validlookups self datehierarchy removedindjango60warning when the deprecation ends replace with for filteritem in self getlistfilterrequest listfilter self getlistfilterrequest if request is not none else self listfilter for filteritem in listfilter if isinstancefilteritem type and issubclass filteritem simplelistfilter validlookups addfilteritem parametername elif isinstancefilteritem list tuple validlookups addfilteritem0 else validlookups addfilteritem is it a valid relational lookup return not lookupsep joinrelationparts lookupsep joinrelationparts part isdisjointvalidlookups def tofieldallowedself request tofield try field self opts getfieldtofield except fielddoesnotexist return false always allow referencing the primary key since it s already possible to get this information from the change view url if field primarykey return true allow reverse relationships to models defining m2m fields if they target the specified field for manytomany in self opts manytomany if manytomany m2mtargetfieldname tofield return true make sure at least one of the models registered for this site references this field through a fk or a m2m relationship registeredmodels set for model admin in self adminsite registry items registeredmodels addmodel for inline in admin inlines registeredmodels addinline model relatedobjects f for f in self opts getfieldsincludehiddentrue if f autocreated and not f concrete for relatedobject in relatedobjects relatedmodel relatedobject relatedmodel remotefield relatedobject field remotefield if anyissubclassmodel relatedmodel for model in registeredmodels and hasattrremotefield getrelatedfield and remotefield getrelatedfield field return true return false def hasaddpermissionself request opts self opts codename getpermissioncodenameadd opts return request user hasperms s opts applabel codename def haschangepermissionself request objnone opts self opts codename getpermissioncodenamechange opts return request user hasperms s opts applabel codename def hasdeletepermissionself request objnone opts self opts codename getpermissioncodenamedelete opts return request user hasperms s opts applabel codename def hasviewpermissionself request objnone opts self opts codenameview getpermissioncodenameview opts codenamechange getpermissioncodenamechange opts return request user hasperm s s opts applabel codenameview or request user hasperms s opts applabel codenamechange def hasvieworchangepermissionself request objnone return self hasviewpermissionrequest obj or self haschangepermission request obj def hasmodulepermissionself request return request user hasmodulepermsself opts applabel class modeladminbasemodeladmin custom templates designed to be overridden in subclasses actions for backwards compatibility was the change url before 1 9 return a dict of all perms for this model this dict has the keys add change delete and view mapping to the truefalse for each of those actions return a form class for use in the admin add view this is used by addview and changeview exclude all fields if it s a change form and the user doesn t have the change permission take the custom modelform s meta exclude into account only if the modeladmin doesn t define its own if exclude is an empty list we pass none to be consistent with the default on modelformfactory remove declared form fields which are in readonlyfields return the changelist class for use on the changelist page return a changelist instance based on request may raise incorrectlookupparameters add the action checkboxes if any actions are available return an instance matching the field and value provided the primary key is used if no field is provided return none if no match is found or the objectid fails validation return a form class for use in the formset on the changelist page return a formset class for use on the changelist page if listeditable is used yield formsets and the corresponding inlines log that an object has been successfully added the default implementation creates an admin logentry object log that an object has been successfully changed the default implementation creates an admin logentry object log that an object will be deleted note that this method must be called before the deletion the default implementation creates an admin logentry object log that objects will be deleted note that this method must be called before the deletion the default implementation creates admin logentry objects removedindjango60warning a listdisplay column containing a checkbox widget return the list of actions prior to any requestbased filtering actions baseactions self getactionaction for action in self actions or getaction might have returned none so filter any of those out baseactions action for action in baseactions if action baseactionnames name for name in baseactions gather actions from the admin site first for name func in self adminsite actions if name in baseactionnames continue description self getactiondescriptionfunc name actions appendfunc name description add actions from this modeladmin actions extendbaseactions return actions def filteractionsbypermissionsself request actions return a dictionary mapping the names of all actions for this modeladmin to a tuple of callable name description for each action if self actions is set to none that means actions are disabled on this page return a list of choices for use in a form object each choice is a tuple name description return a given action from a parameter which can either be a callable or the name of a method on the modeladmin return is a tuple of callable name description if the action is a callable just use it next look for a method grab it off self class to get an unbound method instead of a bound one this ensures that the calling conventions are the same for functions and methods finally look for a named method on the admin site return a sequence containing the fields to be displayed on the changelist return a sequence containing the fields to be displayed as links on the changelist the listdisplay parameter is the list of fields returned by getlistdisplay use only the first item in listdisplay as link return a sequence containing the fields to be displayed as filters in the right sidebar of the changelist page return a list of fields to add to the selectrelated part of the changelist items query return a sequence containing the fields to be searched whenever somebody submits a search query return a tuple containing a queryset to implement the search and a boolean indicating if the results may contain duplicates apply keyword searches use fieldname if it includes a lookup go through the fields following all relations use valid query lookups update opts to follow the relation otherwise use the field with icontains return the preserved filters querystring construct a json structure describing changes from a changed object send a message to the user the default implementation posts a message using the django contrib messages backend exposes almost the same api as messages addmessage but accepts the positional arguments in a different order to maintain backwards compatibility for convenience it accepts the level argument as a string rather than the usual level number attempt to get the level if passed a string given a modelform return an unsaved instance change is true if the object is being changed and false if it s being added given a model instance save it to the database given a model instance delete it from the database given a queryset delete it from the database queryset delete def saveformsetself request form formset change formset save def saverelatedself request form formsets change form savem2m for formset in formsets self saveformsetrequest form formset changechange def renderchangeform self request context addfalse changefalse formurl objnone applabel self opts applabel preservedfilters self getpreservedfiltersrequest formurl addpreservedfilters preservedfilters preservedfilters opts self opts formurl viewonsiteurl self getviewonsiteurlobj haseditableinlineadminformsets false for inline in contextinlineadminformsets if inline hasaddpermission or inline haschangepermission or inline hasdeletepermission haseditableinlineadminformsets true break context update add add change change hasviewpermission self hasviewpermissionrequest obj hasaddpermission self hasaddpermissionrequest haschangepermission self haschangepermissionrequest obj hasdeletepermission self hasdeletepermissionrequest obj haseditableinlineadminformsets haseditableinlineadminformsets hasfilefield contextadminform form ismultipart or any adminformset formset ismultipart for adminformset in contextinlineadminformsets hasabsoluteurl viewonsiteurl is not none absoluteurl viewonsiteurl formurl formurl opts self opts contenttypeid getcontenttypeformodelself model pk saveas self saveas saveontop self saveontop tofieldvar tofieldvar ispopupvar ispopupvar applabel applabel if add and self addformtemplate is not none formtemplate self addformtemplate else formtemplate self changeformtemplate request currentapp self adminsite name return templateresponse request formtemplate or adminsschangeform html applabel self opts modelname adminschangeform html applabel adminchangeform html context def getpreservedqslself request preservedfilters querystring urlparserequest buildabsoluteuri query return parseqslquerystring replacepreservedfilters def responseaddself request obj posturlcontinuenone opts obj meta preservedfilters self getpreservedfiltersrequest preservedqsl self getpreservedqslrequest preservedfilters objurl reverse admin sschange opts applabel opts modelname argsquoteobj pk currentappself adminsite name add a link to the object s change form if the user can edit the obj if self haschangepermissionrequest obj objrepr formathtml a hrefa urlquoteobjurl obj else objrepr strobj msgdict name opts verbosename obj objrepr here we distinguish between different save types by checking for the presence of keys in request post if ispopupvar in request post tofield request post gettofieldvar if tofield attr strtofield else attr obj meta pk attname value obj serializablevalueattr popupresponsedata json dumps value strvalue obj strobj return templateresponse request self popupresponsetemplate or adminsspopupresponse html opts applabel opts modelname adminspopupresponse html opts applabel adminpopupresponse html popupresponsedata popupresponsedata elif continue in request post or redirecting after save as new saveasnew in request post and self saveascontinue and self haschangepermissionrequest obj msg the name obj was added successfully if self haschangepermissionrequest obj msg you may edit it again below self messageuserrequest formathtmlmsg msgdict messages success if posturlcontinue is none posturlcontinue objurl posturlcontinue addpreservedfilters preservedfilters preservedfilters preservedqsl preservedqsl opts opts posturlcontinue return httpresponseredirectposturlcontinue elif addanother in request post msg formathtml the name obj was added successfully you may add another name below msgdict self messageuserrequest msg messages success redirecturl request path redirecturl addpreservedfilters preservedfilters preservedfilters preservedqsl preservedqsl opts opts redirecturl return httpresponseredirectredirecturl else msg formathtml the name obj was added successfully msgdict self messageuserrequest msg messages success return self responsepostsaveaddrequest obj def responsechangeself request obj if ispopupvar in request post opts obj meta tofield request post gettofieldvar attr strtofield if tofield else opts pk attname value request resolvermatch kwargsobjectid newvalue obj serializablevalueattr popupresponsedata json dumps action change value strvalue obj strobj newvalue strnewvalue return templateresponse request self popupresponsetemplate or adminsspopupresponse html opts applabel opts modelname adminspopupresponse html opts applabel adminpopupresponse html popupresponsedata popupresponsedata opts self opts preservedfilters self getpreservedfiltersrequest preservedqsl self getpreservedqslrequest preservedfilters msgdict name opts verbosename obj formathtml a hrefa urlquoterequest path obj if continue in request post msg formathtml the name obj was changed successfully you may edit it again below msgdict self messageuserrequest msg messages success redirecturl request path redirecturl addpreservedfilters preservedfilters preservedfilters preservedqsl preservedqsl opts opts redirecturl return httpresponseredirectredirecturl elif addanother in request post msg formathtml the name obj was changed successfully you may add another name below msgdict self messageuserrequest msg messages success redirecturl reverse admin ssadd opts applabel opts modelname currentappself adminsite name redirecturl addpreservedfilters preservedfilters preservedfilters preservedqsl preservedqsl opts opts redirecturl return httpresponseredirectredirecturl else msg formathtml the name obj was changed successfully msgdict self messageuserrequest msg messages success return self responsepostsavechangerequest obj def responsepostsaveself request obj if self hasvieworchangepermissionrequest posturl reverse admin sschangelist self opts applabel self opts modelname currentappself adminsite name preservedfilters self getpreservedfiltersrequest posturl addpreservedfilters preservedfilters preservedfilters opts self opts posturl else posturl reverseadmin index currentappself adminsite name return httpresponseredirectposturl def responsepostsaveaddself request obj return self responsepostsaverequest obj def responsepostsavechangeself request obj return self responsepostsaverequest obj def responseactionself request queryset there can be multiple action forms on the page at the top and bottom of the change list for example get the action whose button was pushed try actionindex intrequest post getindex 0 except valueerror actionindex 0 construct the action form data request post copy data pophelpers actioncheckboxname none data popindex none use the action whose button was pushed try data updateaction data getlistactionactionindex except indexerror if we didn t get an action from the chosen form that s invalid post data so by deleting action it ll fail the validation check below so no need to do anything here pass actionform self actionformdata autoidnone actionform fieldsaction choices self getactionchoicesrequest if the form s valid we can handle the action if actionform isvalid action actionform cleaneddataaction selectacross actionform cleaneddataselectacross func self getactionsrequestaction0 get the list of selected pks if nothing s selected we can t perform an action on it so bail except we want to perform the action explicitly on all objects selected request post getlisthelpers actioncheckboxname if not selected and not selectacross reminder that something needs to be selected or nothing will happen msg items must be selected in order to perform actions on them no items have been changed self messageuserrequest msg messages warning return none if not selectacross perform the action only on the selected objects queryset queryset filterpkinselected response funcself request queryset actions may return an httpresponselike object which will be used as the response from the post if not we ll be a good little http citizen and redirect back to the changelist page if isinstanceresponse httpresponsebase return response else return httpresponseredirectrequest getfullpath else msg no action selected self messageuserrequest msg messages warning return none def responsedeleteself request objdisplay objid if ispopupvar in request post popupresponsedata json dumps action delete value strobjid return templateresponse request self popupresponsetemplate or adminsspopupresponse html self opts applabel self opts modelname adminspopupresponse html self opts applabel adminpopupresponse html popupresponsedata popupresponsedata self messageuser request the names objs was deleted successfully name self opts verbosename obj objdisplay messages success if self haschangepermissionrequest none posturl reverse admin sschangelist self opts applabel self opts modelname currentappself adminsite name preservedfilters self getpreservedfiltersrequest posturl addpreservedfilters preservedfilters preservedfilters opts self opts posturl else posturl reverseadmin index currentappself adminsite name return httpresponseredirectposturl def renderdeleteformself request context applabel self opts applabel request currentapp self adminsite name context update tofieldvartofieldvar ispopupvarispopupvar mediaself media return templateresponse request self deleteconfirmationtemplate or admindeleteconfirmation html format applabel self opts modelname admindeleteconfirmation html formatapplabel admindeleteconfirmation html context def getinlineformsetsself request formsets inlineinstances objnone edit permissions on parent model are required for editable inlines caneditparent self haschangepermissionrequest obj if obj else self hasaddpermissionrequest inlineadminformsets for inline formset in zipinlineinstances formsets fieldsets listinline getfieldsetsrequest obj readonly listinline getreadonlyfieldsrequest obj if caneditparent hasaddpermission inline hasaddpermissionrequest obj haschangepermission inline haschangepermissionrequest obj hasdeletepermission inline hasdeletepermissionrequest obj else disable all editpermissions and override formset settings hasaddpermission haschangepermission hasdeletepermission false formset extra formset maxnum 0 hasviewpermission inline hasviewpermissionrequest obj prepopulated dictinline getprepopulatedfieldsrequest obj inlineadminformset helpers inlineadminformset inline formset fieldsets prepopulated readonly modeladminself hasaddpermissionhasaddpermission haschangepermissionhaschangepermission hasdeletepermissionhasdeletepermission hasviewpermissionhasviewpermission inlineadminformsets appendinlineadminformset return inlineadminformsets def getchangeforminitialdataself request initial dictrequest get items for k in initial try f self opts getfieldk except fielddoesnotexist continue we have to specialcase m2ms as a list of commaseparated pks if isinstancef models manytomanyfield initialk initialk split return initial def getobjdoesnotexistredirectself request opts objectid msg names with id keys doesnt exist perhaps it was deleted name opts verbosename key unquoteobjectid self messageuserrequest msg messages warning url reverseadmin index currentappself adminsite name return httpresponseredirecturl csrfprotectm def changeformviewself request objectidnone formurl extracontextnone with transaction atomicusingrouter dbforwriteself model return self changeformviewrequest objectid formurl extracontext def changeformviewself request objectid formurl extracontext tofield request post gettofieldvar request get gettofieldvar if tofield and not self tofieldallowedrequest tofield raise disallowedmodeladmintofield the field s cannot be referenced tofield if request method post and saveasnew in request post objectid none add objectid is none if add if not self hasaddpermissionrequest raise permissiondenied obj none else obj self getobjectrequest unquoteobjectid tofield if request method post if not self haschangepermissionrequest obj raise permissiondenied else if not self hasvieworchangepermissionrequest obj raise permissiondenied if obj is none return self getobjdoesnotexistredirect request self opts objectid fieldsets self getfieldsetsrequest obj modelform self getform request obj changenot add fieldsflattenfieldsetsfieldsets if request method post form modelformrequest post request files instanceobj formsets inlineinstances self createformsets request form instance changenot add formvalidated form isvalid if formvalidated newobject self saveformrequest form changenot add else newobject form instance if allvalidformsets and formvalidated self savemodelrequest newobject form not add self saverelatedrequest form formsets not add changemessage self constructchangemessage request form formsets add if add self logadditionrequest newobject changemessage return self responseaddrequest newobject else self logchangerequest newobject changemessage return self responsechangerequest newobject else formvalidated false else if add initial self getchangeforminitialdatarequest form modelforminitialinitial formsets inlineinstances self createformsets request form instance changefalse else form modelforminstanceobj formsets inlineinstances self createformsets request obj changetrue if not add and not self haschangepermissionrequest obj readonlyfields flattenfieldsetsfieldsets else readonlyfields self getreadonlyfieldsrequest obj adminform helpers adminform form listfieldsets clear prepopulated fields on a viewonly form to avoid a crash self getprepopulatedfieldsrequest obj if add or self haschangepermissionrequest obj else readonlyfields modeladminself media self media adminform media inlineformsets self getinlineformsets request formsets inlineinstances obj for inlineformset in inlineformsets media inlineformset media if add title add s elif self haschangepermissionrequest obj title change s else title view s context self adminsite eachcontextrequest title title self opts verbosename subtitle strobj if obj else none adminform adminform objectid objectid original obj ispopup ispopupvar in request post or ispopupvar in request get tofield tofield media media inlineadminformsets inlineformsets errors helpers adminerrorlistform formsets preservedfilters self getpreservedfiltersrequest hide the save and save and continue buttons if save as new was previously chosen to prevent the interface from getting confusing if request method post and not formvalidated and saveasnew in request post contextshowsave false contextshowsaveandcontinue false use the change template instead of the add template add false context updateextracontext or return self renderchangeform request context addadd changenot add objobj formurlformurl def addviewself request formurl extracontextnone return self changeformviewrequest none formurl extracontext def changeviewself request objectid formurl extracontextnone return self changeformviewrequest objectid formurl extracontext def geteditedobjectpksself request prefix based on post data return a queryset of the objects that were edited via listeditable disable the optimization if the post data was tampered with the change list admin view for this model wacky lookup parameters were given so redirect to the main changelist page without parameters and pass an invalid1 parameter via the query string if wacky parameters were given and the invalid1 parameter was already in the query string something is screwed up with the database so display an error page if the request was posted this might be a bulk action or a bulk edit try to look up an action or confirmation first but if this isn t an action the post will fall through to the bulk edit check below actions with no confirmation actions with confirmation redirect back to the changelist page to avoid resubmitting the form if the user refreshes the browser or uses the no take me back button on the action confirmation page if we re allowing changelist editing we need to construct a formset for the changelist given all the fields to be edited then we ll use the formset to validateprocess posted data handle posted bulkedit data handle get construct a formset for display build the list of media to be used by the formset build the action form and populate it with available actions hook for customizing the delete process for the delete view and the delete selected action populate deletedobjects a data structure of all related objects that will also be deleted first check if the user can see this history then get the history for this object return whether or not the user deleted the form return inline hasdeletepermissionrequest obj and delete formatformset prefix index in request post bypass validation of each viewonly inline form since the form s data won t be in request post unless the form was deleted if not inline haschangepermissionrequest obj if change else none for index form in enumerateformset initialforms if userdeletedformrequest obj formset index inline continue form errors form cleaneddata form initial formsets appendformset inlineinstances appendinline return formsets inlineinstances class inlinemodeladminbasemodeladmin model none fkname none formset baseinlineformset extra 3 minnum none maxnum none template none verbosename none verbosenameplural none candelete true showchangelink false checksclass inlinemodeladminchecks classes none def initself parentmodel adminsite self adminsite adminsite self parentmodel parentmodel self opts self model meta self hasregisteredmodel adminsite isregisteredself model super init if self verbosenameplural is none if self verbosename is none self verbosenameplural self opts verbosenameplural else self verbosenameplural formatlazys self verbosename if self verbosename is none self verbosename self opts verbosename property def mediaself extra if settings debug else min js vendorjqueryjquerys js extra jquery init js inlines js if self filtervertical or self filterhorizontal js extendselectbox js selectfilter2 js if self classes and collapse in self classes js appendcollapse js return forms mediajsadminjss url for url in js def getextraself request objnone kwargs hook for customizing the min number of inline forms return self minnum def getmaxnumself request objnone kwargs return a baseinlineformset class for use in admin addchange views if fields in kwargs fields kwargs popfields else fields flattenfieldsetsself getfieldsetsrequest obj excluded self getexcluderequest obj exclude if excluded is none else listexcluded exclude extendself getreadonlyfieldsrequest obj if excluded is none and hasattrself form meta and self form meta exclude take the custom modelform s meta exclude into account only if the inlinemodeladmin doesn t define its own exclude extendself form meta exclude if exclude is an empty list we use none since that s the actual default exclude exclude or none candelete self candelete and self hasdeletepermissionrequest obj defaults form self form formset self formset fkname self fkname fields fields exclude exclude formfieldcallback partialself formfieldfordbfield requestrequest extra self getextrarequest obj kwargs minnum self getminnumrequest obj kwargs maxnum self getmaxnumrequest obj kwargs candelete candelete kwargs basemodelform defaultsform canchange self haschangepermissionrequest obj if request else true canadd self hasaddpermissionrequest obj if request else true class deleteprotectedmodelformbasemodelform def handcleandeleteself if self cleaneddata getdeletionfieldname false using router dbforwriteself meta model collector nestedobjectsusingusing if self instance state adding return collector collectself instance if collector protected objs for p in collector protected objs append translators model verbose name and instance representation suitable to be an item in a list classnames instances classname p meta verbosename instance p params classname self meta model meta verbosename instance self instance relatedobjects gettextlistobjs and msg deleting classnames instances would require deleting the following protected related objects relatedobjectss raise validationerror msg codedeletingprotected paramsparams def isvalidself result super isvalid self handcleandelete return result def haschangedself protect against unized edits if not canchange and not self instance state adding return false if not canadd and self instance state adding return false return super haschanged defaultsform deleteprotectedmodelform if defaultsfields is none and not modelformdefinesfields defaultsform defaultsfields forms allfields return inlineformsetfactoryself parentmodel self model defaults def getformforgetfieldsself request objnone return self getformsetrequest obj fieldsnone form def getquerysetself request queryset super getquerysetrequest if not self hasvieworchangepermissionrequest queryset queryset none return queryset def hasanypermsfortargetmodelself request perms opts self opts find the target model of an autocreated manytomany relationship for field in opts fields if field remotefield and field remotefield model self parentmodel opts field remotefield model meta break return any request user hasperm s s opts applabel getpermissioncodenameperm opts for perm in perms def hasaddpermissionself request obj if self opts autocreated autocreated intermediate models don t have their own permissions the user needs to have the change permission for the related model in order to be able to do anything with the intermediate model return self hasanypermsfortargetmodelrequest change return super hasaddpermissionrequest def haschangepermissionself request objnone if self opts autocreated same comment as hasaddpermission return self hasanypermsfortargetmodelrequest change return super haschangepermissionrequest def hasdeletepermissionself request objnone if self opts autocreated same comment as hasaddpermission return self hasanypermsfortargetmodelrequest change return super hasdeletepermissionrequest obj def hasviewpermissionself request objnone if self opts autocreated same comment as hasaddpermission the change permission also implies the view permission return self hasanypermsfortargetmodelrequest view change return super hasviewpermissionrequest class stackedinlineinlinemodeladmin template admineditinlinestacked html class tabularinlineinlinemodeladmin template admineditinlinetabular html since this module gets imported in the application s root package it cannot import models from other applications at the module level defaults for formfield_overrides modeladmin subclasses can change this by adding to modeladmin formfield_overrides functionality common to both modeladmin and inlineadmin merge formfield_for_dbfield_defaults with the formfield_overrides rather than simply overwriting hook for specifying the form field instance for a given database field instance if kwargs are given they re passed to the form field s constructor if the field specifies choices we don t need to look for special admin widgets we just need to use a select widget of some kind foreignkey or manytomanyfields combine the field kwargs with any options for formfield_overrides make sure the passed in kwargs override anything in formfield_overrides because kwargs is more specific and should always win get the correct formfield for non raw_id fields wrap the widget with a wrapper that adds extra html the add other interface to the end of the rendered output formfield can be none if it came from a onetoonefield with parent_link true or a m2m intermediary if we ve got overrides for the formfield defined use em kwargs passed to formfield_for_dbfield override the defaults for any other type of field just call its formfield method get a form field for a database field that has declared choices if the field is named as a radio_field use a radioselect avoid stomping on custom widget choices arguments if the modeladmin specifies ordering the queryset should respect that ordering otherwise don t specify the queryset let the field decide return none in that case get a form field for a foreignkey get a form field for a manytomanyfield if it uses an intermediary model that isn t auto created don t show a field in admin return a list of foreignkey and or manytomany fields which should use an autocomplete widget use the contenttype lookup if view_on_site is true return the empty_value_display set on modeladmin or adminsite hook for specifying exclude hook for specifying fields _get_form_for_get_fields is implemented in subclasses hook for specifying fieldsets hook for specifying custom inlines hook for specifying field ordering otherwise we might try to none which is bad hook for specifying custom readonly fields hook for specifying custom prepopulated fields return a queryset of all model instances that can be edited by the admin site this is used by changelist_view todo this should be handled by some parameter to the changelist hook for specifying which fields can be sorted in the changelist removedindjango60warning when the deprecation ends replace with def lookup_allowed self lookup value request check fkey lookups that are allowed so that popups produced by foreignkeyrawidwidget on the basis of foreignkey limit_choices_to are allowed to work as limit_choices_to can be a callable invoke it here lookups on nonexistent fields are ok since they re ignored later this is not a relational field so further parts must be transforms either a local field filter or no fields at all removedindjango60warning when the deprecation ends replace with for filter_item in self get_list_filter request is it a valid relational lookup return true if the model associated with this admin should be allowed to be referenced by the specified field always allow referencing the primary key since it s already possible to get this information from the change view url allow reverse relationships to models defining m2m fields if they target the specified field make sure at least one of the models registered for this site references this field through a fk or a m2m relationship return true if the given request has permission to add an object can be overridden by the user in subclasses return true if the given request has permission to change the given django model instance the default implementation doesn t examine the obj parameter can be overridden by the user in subclasses in such case it should return true if the given request has permission to change the obj model instance if obj is none this should return true if the given request has permission to change any object of the given type return true if the given request has permission to delete the given django model instance the default implementation doesn t examine the obj parameter can be overridden by the user in subclasses in such case it should return true if the given request has permission to delete the obj model instance if obj is none this should return true if the given request has permission to delete any object of the given type return true if the given request has permission to view the given django model instance the default implementation doesn t examine the obj parameter if overridden by the user in subclasses it should return true if the given request has permission to view the obj model instance if obj is none it should return true if the request has permission to view any object of the given type return true if the given request has any permission in the given app label can be overridden by the user in subclasses in such case it should return true if the given request has permission to view the module on the admin index page and access the module s index page overriding it does not restrict access to the add change or delete views use modeladmin has_ add change delete _permission for that encapsulate all admin options and functionality for a given model custom templates designed to be over ridden in subclasses actions for backwards compatibility was the change url before 1 9 return a dict of all perms for this model this dict has the keys add change delete and view mapping to the true false for each of those actions return a form class for use in the admin add view this is used by add_view and change_view exclude all fields if it s a change form and the user doesn t have the change permission take the custom modelform s meta exclude into account only if the modeladmin doesn t define its own if exclude is an empty list we pass none to be consistent with the default on modelform_factory remove declared form fields which are in readonly_fields return the changelist class for use on the changelist page return a changelist instance based on request may raise incorrectlookupparameters add the action checkboxes if any actions are available return an instance matching the field and value provided the primary key is used if no field is provided return none if no match is found or the object_id fails validation return a form class for use in the formset on the changelist page return a formset class for use on the changelist page if list_editable is used yield formsets and the corresponding inlines log that an object has been successfully added the default implementation creates an admin logentry object log that an object has been successfully changed the default implementation creates an admin logentry object log that an object will be deleted note that this method must be called before the deletion the default implementation creates an admin logentry object log that objects will be deleted note that this method must be called before the deletion the default implementation creates admin logentry objects removedindjango60warning a list_display column containing a checkbox widget return the list of actions prior to any request based filtering get_action might have returned none so filter any of those out gather actions from the admin site first add actions from this modeladmin filter out any actions that the user doesn t have access to return a dictionary mapping the names of all actions for this modeladmin to a tuple of callable name description for each action if self actions is set to none that means actions are disabled on this page return a list of choices for use in a form object each choice is a tuple name description return a given action from a parameter which can either be a callable or the name of a method on the modeladmin return is a tuple of callable name description if the action is a callable just use it next look for a method grab it off self __class__ to get an unbound method instead of a bound one this ensures that the calling conventions are the same for functions and methods finally look for a named method on the admin site return a sequence containing the fields to be displayed on the changelist return a sequence containing the fields to be displayed as links on the changelist the list_display parameter is the list of fields returned by get_list_display use only the first item in list_display as link return a sequence containing the fields to be displayed as filters in the right sidebar of the changelist page return a list of fields to add to the select_related part of the changelist items query return a sequence containing the fields to be searched whenever somebody submits a search query return a tuple containing a queryset to implement the search and a boolean indicating if the results may contain duplicates apply keyword searches use field_name if it includes a lookup go through the fields following all relations use valid query lookups update opts to follow the relation otherwise use the field with icontains return the preserved filters querystring construct a json structure describing changes from a changed object send a message to the user the default implementation posts a message using the django contrib messages backend exposes almost the same api as messages add_message but accepts the positional arguments in a different order to maintain backwards compatibility for convenience it accepts the level argument as a string rather than the usual level number attempt to get the level if passed a string given a modelform return an unsaved instance change is true if the object is being changed and false if it s being added given a model instance save it to the database given a model instance delete it from the database given a queryset delete it from the database given an inline formset save it to the database given the httprequest the parent modelform instance the list of inline formsets and a boolean value based on whether the parent is being added or changed save the related objects to the database note that at this point save_form and save_model have already been called determine the httpresponse for the add_view stage add a link to the object s change form if the user can edit the obj here we distinguish between different save types by checking for the presence of keys in request post redirecting after save as new determine the httpresponse for the change_view stage figure out where to redirect after the save button has been pressed when adding a new object figure out where to redirect after the save button has been pressed when editing an existing object handle an admin action this is called if a request is posted to the changelist it returns an httpresponse if the action was handled and none otherwise there can be multiple action forms on the page at the top and bottom of the change list for example get the action whose button was pushed construct the action form use the action whose button was pushed if we didn t get an action from the chosen form that s invalid post data so by deleting action it ll fail the validation check below so no need to do anything here if the form s valid we can handle the action get the list of selected pks if nothing s selected we can t perform an action on it so bail except we want to perform the action explicitly on all objects reminder that something needs to be selected or nothing will happen perform the action only on the selected objects actions may return an httpresponse like object which will be used as the response from the post if not we ll be a good little http citizen and redirect back to the changelist page determine the httpresponse for the delete_view stage edit permissions on parent model are required for editable inlines disable all edit permissions and override formset settings get the initial form data from the request s get params we have to special case m2ms as a list of comma separated pks create a message informing the user that the object doesn t exist and return a redirect to the admin index page clear prepopulated fields on a view only form to avoid a crash hide the save and save and continue buttons if save as new was previously chosen to prevent the interface from getting confusing use the change template instead of the add template return post data values of list_editable primary keys based on post data return a queryset of the objects that were edited via list_editable disable the optimization if the post data was tampered with the change list admin view for this model wacky lookup parameters were given so redirect to the main changelist page without parameters and pass an invalid 1 parameter via the query string if wacky parameters were given and the invalid 1 parameter was already in the query string something is screwed up with the database so display an error page if the request was posted this might be a bulk action or a bulk edit try to look up an action or confirmation first but if this isn t an action the post will fall through to the bulk edit check below actions with no confirmation actions with confirmation redirect back to the changelist page to avoid resubmitting the form if the user refreshes the browser or uses the no take me back button on the action confirmation page if we re allowing changelist editing we need to construct a formset for the changelist given all the fields to be edited then we ll use the formset to validate process posted data handle posted bulk edit data handle get construct a formset for display build the list of media to be used by the formset build the action form and populate it with available actions hook for customizing the delete process for the delete view and the delete selected action populate deleted_objects a data structure of all related objects that will also be deleted the user has confirmed the deletion first check if the user can see this history then get the history for this object return whether or not the user deleted the form bypass validation of each view only inline form since the form s data won t be in request post unless the form was deleted options for inline editing of model instances provide fk_name to specify the attribute name of the foreignkey from model to its parent this is required if model has more than one foreignkey to its parent hook for customizing the number of extra inline forms hook for customizing the min number of inline forms hook for customizing the max number of extra inline forms return a baseinlineformset class for use in admin add change views take the custom modelform s meta exclude into account only if the inlinemodeladmin doesn t define its own if exclude is an empty list we use none since that s the actual default we don t validate the delete field itself because on templates it s not rendered using the field information but just using a generic deletion_field of the inlinemodeladmin translators model verbose name and instance representation suitable to be an item in a list protect against unized edits this method is called only when the modeladmin s model is for an manytomanyfield s implicit through model if self opts auto_created return true if the user has any of the given permissions add change etc for the model that points to the through model find the target model of an auto created many to many relationship auto created intermediate models don t have their own permissions the user needs to have the change permission for the related model in order to be able to do anything with the intermediate model same comment as has_add_permission same comment as has_add_permission same comment as has_add_permission the change permission also implies the view permission
import copy
import enum
import json
import re
import warnings
from functools import partial, update_wrapper
from urllib.parse import parse_qsl
from urllib.parse import quote as urlquote
from urllib.parse import urlparse

from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.admin import helpers, widgets
from django.contrib.admin.checks import (
    BaseModelAdminChecks,
    InlineModelAdminChecks,
    ModelAdminChecks,
)
from django.contrib.admin.exceptions import DisallowedModelAdminToField, NotRegistered
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.utils import (
    NestedObjects,
    construct_change_message,
    flatten_fieldsets,
    get_deleted_objects,
    lookup_spawns_duplicates,
    model_format_dict,
    model_ngettext,
    quote,
    unquote,
)
from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple
from django.contrib.auth import get_permission_codename
from django.core.exceptions import (
    FieldDoesNotExist,
    FieldError,
    PermissionDenied,
    ValidationError,
)
from django.core.paginator import Paginator
from django.db import models, router, transaction
from django.db.models.constants import LOOKUP_SEP
from django.forms.formsets import DELETION_FIELD_NAME, all_valid
from django.forms.models import (
    BaseInlineFormSet,
    inlineformset_factory,
    modelform_defines_fields,
    modelform_factory,
    modelformset_factory,
)
from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple
from django.http import HttpResponseRedirect
from django.http.response import HttpResponseBase
from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.deprecation import RemovedInDjango60Warning
from django.utils.html import format_html
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.text import (
    capfirst,
    format_lazy,
    get_text_list,
    smart_split,
    unescape_string_literal,
)
from django.utils.translation import gettext as _
from django.utils.translation import ngettext
from django.views.decorators.csrf import csrf_protect
from django.views.generic import RedirectView

IS_POPUP_VAR = "_popup"
TO_FIELD_VAR = "_to_field"
IS_FACETS_VAR = "_facets"


class ShowFacets(enum.Enum):
    NEVER = "NEVER"
    ALLOW = "ALLOW"
    ALWAYS = "ALWAYS"


HORIZONTAL, VERTICAL = 1, 2


def get_content_type_for_model(obj):
    
    
    from django.contrib.contenttypes.models import ContentType

    return ContentType.objects.get_for_model(obj, for_concrete_model=False)


def get_ul_class(radio_style):
    return "radiolist" if radio_style == VERTICAL else "radiolist inline"


class IncorrectLookupParameters(Exception):
    pass





FORMFIELD_FOR_DBFIELD_DEFAULTS = {
    models.DateTimeField: {
        "form_class": forms.SplitDateTimeField,
        "widget": widgets.AdminSplitDateTime,
    },
    models.DateField: {"widget": widgets.AdminDateWidget},
    models.TimeField: {"widget": widgets.AdminTimeWidget},
    models.TextField: {"widget": widgets.AdminTextareaWidget},
    models.URLField: {"widget": widgets.AdminURLFieldWidget},
    models.IntegerField: {"widget": widgets.AdminIntegerFieldWidget},
    models.BigIntegerField: {"widget": widgets.AdminBigIntegerFieldWidget},
    models.CharField: {"widget": widgets.AdminTextInputWidget},
    models.ImageField: {"widget": widgets.AdminFileWidget},
    models.FileField: {"widget": widgets.AdminFileWidget},
    models.EmailField: {"widget": widgets.AdminEmailInputWidget},
    models.UUIDField: {"widget": widgets.AdminUUIDInputWidget},
}

csrf_protect_m = method_decorator(csrf_protect)


class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
    

    autocomplete_fields = ()
    raw_id_fields = ()
    fields = None
    exclude = None
    fieldsets = None
    form = forms.ModelForm
    filter_vertical = ()
    filter_horizontal = ()
    radio_fields = {}
    prepopulated_fields = {}
    formfield_overrides = {}
    readonly_fields = ()
    ordering = None
    sortable_by = None
    view_on_site = True
    show_full_result_count = True
    checks_class = BaseModelAdminChecks

    def check(self, **kwargs):
        return self.checks_class().check(self, **kwargs)

    def __init__(self):
        
        
        overrides = copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS)
        for k, v in self.formfield_overrides.items():
            overrides.setdefault(k, {}).update(v)
        self.formfield_overrides = overrides

    def formfield_for_dbfield(self, db_field, request, **kwargs):
        
        
        
        if db_field.choices:
            return self.formfield_for_choice_field(db_field, request, **kwargs)

        
        if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
            
            
            
            
            if db_field.__class__ in self.formfield_overrides:
                kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs}

            
            if isinstance(db_field, models.ForeignKey):
                formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
            elif isinstance(db_field, models.ManyToManyField):
                formfield = self.formfield_for_manytomany(db_field, request, **kwargs)

            
            
            
            
            if formfield and db_field.name not in self.raw_id_fields:
                try:
                    related_modeladmin = self.admin_site.get_model_admin(
                        db_field.remote_field.model
                    )
                except NotRegistered:
                    wrapper_kwargs = {}
                else:
                    wrapper_kwargs = {
                        "can_add_related": related_modeladmin.has_add_permission(
                            request
                        ),
                        "can_change_related": related_modeladmin.has_change_permission(
                            request
                        ),
                        "can_delete_related": related_modeladmin.has_delete_permission(
                            request
                        ),
                        "can_view_related": related_modeladmin.has_view_permission(
                            request
                        ),
                    }
                formfield.widget = widgets.RelatedFieldWidgetWrapper(
                    formfield.widget,
                    db_field.remote_field,
                    self.admin_site,
                    **wrapper_kwargs,
                )

            return formfield

        
        
        for klass in db_field.__class__.mro():
            if klass in self.formfield_overrides:
                kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs}
                return db_field.formfield(**kwargs)

        
        return db_field.formfield(**kwargs)

    def formfield_for_choice_field(self, db_field, request, **kwargs):
        
        
        if db_field.name in self.radio_fields:
            
            if "widget" not in kwargs:
                kwargs["widget"] = widgets.AdminRadioSelect(
                    attrs={
                        "class": get_ul_class(self.radio_fields[db_field.name]),
                    }
                )
            if "choices" not in kwargs:
                kwargs["choices"] = db_field.get_choices(
                    include_blank=db_field.blank, blank_choice=[("", _("None"))]
                )
        return db_field.formfield(**kwargs)

    def get_field_queryset(self, db, db_field, request):
        
        try:
            related_admin = self.admin_site.get_model_admin(db_field.remote_field.model)
        except NotRegistered:
            return None
        else:
            ordering = related_admin.get_ordering(request)
            if ordering is not None and ordering != ():
                return db_field.remote_field.model._default_manager.using(db).order_by(
                    *ordering
                )
        return None

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        
        db = kwargs.get("using")

        if "widget" not in kwargs:
            if db_field.name in self.get_autocomplete_fields(request):
                kwargs["widget"] = AutocompleteSelect(
                    db_field, self.admin_site, using=db
                )
            elif db_field.name in self.raw_id_fields:
                kwargs["widget"] = widgets.ForeignKeyRawIdWidget(
                    db_field.remote_field, self.admin_site, using=db
                )
            elif db_field.name in self.radio_fields:
                kwargs["widget"] = widgets.AdminRadioSelect(
                    attrs={
                        "class": get_ul_class(self.radio_fields[db_field.name]),
                    }
                )
                kwargs["empty_label"] = (
                    kwargs.get("empty_label", _("None")) if db_field.blank else None
                )

        if "queryset" not in kwargs:
            queryset = self.get_field_queryset(db, db_field, request)
            if queryset is not None:
                kwargs["queryset"] = queryset

        return db_field.formfield(**kwargs)

    def formfield_for_manytomany(self, db_field, request, **kwargs):
        
        
        
        if not db_field.remote_field.through._meta.auto_created:
            return None
        db = kwargs.get("using")

        if "widget" not in kwargs:
            autocomplete_fields = self.get_autocomplete_fields(request)
            if db_field.name in autocomplete_fields:
                kwargs["widget"] = AutocompleteSelectMultiple(
                    db_field,
                    self.admin_site,
                    using=db,
                )
            elif db_field.name in self.raw_id_fields:
                kwargs["widget"] = widgets.ManyToManyRawIdWidget(
                    db_field.remote_field,
                    self.admin_site,
                    using=db,
                )
            elif db_field.name in [*self.filter_vertical, *self.filter_horizontal]:
                kwargs["widget"] = widgets.FilteredSelectMultiple(
                    db_field.verbose_name, db_field.name in self.filter_vertical
                )
        if "queryset" not in kwargs:
            queryset = self.get_field_queryset(db, db_field, request)
            if queryset is not None:
                kwargs["queryset"] = queryset

        form_field = db_field.formfield(**kwargs)
        if (
            isinstance(form_field.widget, SelectMultiple)
            and form_field.widget.allow_multiple_selected
            and not isinstance(
                form_field.widget, (CheckboxSelectMultiple, AutocompleteSelectMultiple)
            )
        ):
            msg = _(
                "Hold down “Control”, or “Command” on a Mac, to select more than one."
            )
            help_text = form_field.help_text
            form_field.help_text = (
                format_lazy("{} {}", help_text, msg) if help_text else msg
            )
        return form_field

    def get_autocomplete_fields(self, request):
        
        return self.autocomplete_fields

    def get_view_on_site_url(self, obj=None):
        if obj is None or not self.view_on_site:
            return None

        if callable(self.view_on_site):
            return self.view_on_site(obj)
        elif hasattr(obj, "get_absolute_url"):
            
            return reverse(
                "admin:view_on_site",
                kwargs={
                    "content_type_id": get_content_type_for_model(obj).pk,
                    "object_id": obj.pk,
                },
                current_app=self.admin_site.name,
            )

    def get_empty_value_display(self):
        
        try:
            return mark_safe(self.empty_value_display)
        except AttributeError:
            return mark_safe(self.admin_site.empty_value_display)

    def get_exclude(self, request, obj=None):
        
        return self.exclude

    def get_fields(self, request, obj=None):
        
        if self.fields:
            return self.fields
        
        form = self._get_form_for_get_fields(request, obj)
        return [*form.base_fields, *self.get_readonly_fields(request, obj)]

    def get_fieldsets(self, request, obj=None):
        
        if self.fieldsets:
            return self.fieldsets
        return [(None, {"fields": self.get_fields(request, obj)})]

    def get_inlines(self, request, obj):
        
        return self.inlines

    def get_ordering(self, request):
        
        return self.ordering or ()  

    def get_readonly_fields(self, request, obj=None):
        
        return self.readonly_fields

    def get_prepopulated_fields(self, request, obj=None):
        
        return self.prepopulated_fields

    def get_queryset(self, request):
        
        qs = self.model._default_manager.get_queryset()
        
        ordering = self.get_ordering(request)
        if ordering:
            qs = qs.order_by(*ordering)
        return qs

    def get_sortable_by(self, request):
        
        return (
            self.sortable_by
            if self.sortable_by is not None
            else self.get_list_display(request)
        )

    
    
    def lookup_allowed(self, lookup, value, request=None):
        from django.contrib.admin.filters import SimpleListFilter

        model = self.model
        
        
        
        for fk_lookup in model._meta.related_fkey_lookups:
            
            if callable(fk_lookup):
                fk_lookup = fk_lookup()
            if (lookup, value) in widgets.url_params_from_lookup_dict(
                fk_lookup
            ).items():
                return True

        relation_parts = []
        prev_field = None
        for part in lookup.split(LOOKUP_SEP):
            try:
                field = model._meta.get_field(part)
            except FieldDoesNotExist:
                
                
                break
            if not prev_field or (
                prev_field.is_relation
                and field not in model._meta.parents.values()
                and field is not model._meta.auto_field
                and (
                    model._meta.auto_field is None
                    or part not in getattr(prev_field, "to_fields", [])
                )
            ):
                relation_parts.append(part)
            if not getattr(field, "path_infos", None):
                
                
                break
            prev_field = field
            model = field.path_infos[-1].to_opts.model

        if len(relation_parts) <= 1:
            
            return True
        valid_lookups = {self.date_hierarchy}
        
        
        list_filter = (
            self.get_list_filter(request) if request is not None else self.list_filter
        )
        for filter_item in list_filter:
            if isinstance(filter_item, type) and issubclass(
                filter_item, SimpleListFilter
            ):
                valid_lookups.add(filter_item.parameter_name)
            elif isinstance(filter_item, (list, tuple)):
                valid_lookups.add(filter_item[0])
            else:
                valid_lookups.add(filter_item)

        
        return not {
            LOOKUP_SEP.join(relation_parts),
            LOOKUP_SEP.join(relation_parts + [part]),
        }.isdisjoint(valid_lookups)

    def to_field_allowed(self, request, to_field):
        
        try:
            field = self.opts.get_field(to_field)
        except FieldDoesNotExist:
            return False

        
        
        if field.primary_key:
            return True

        
        
        for many_to_many in self.opts.many_to_many:
            if many_to_many.m2m_target_field_name() == to_field:
                return True

        
        
        registered_models = set()
        for model, admin in self.admin_site._registry.items():
            registered_models.add(model)
            for inline in admin.inlines:
                registered_models.add(inline.model)

        related_objects = (
            f
            for f in self.opts.get_fields(include_hidden=True)
            if (f.auto_created and not f.concrete)
        )
        for related_object in related_objects:
            related_model = related_object.related_model
            remote_field = related_object.field.remote_field
            if (
                any(issubclass(model, related_model) for model in registered_models)
                and hasattr(remote_field, "get_related_field")
                and remote_field.get_related_field() == field
            ):
                return True

        return False

    def has_add_permission(self, request):
        
        opts = self.opts
        codename = get_permission_codename("add", opts)
        return request.user.has_perm("%s.%s" % (opts.app_label, codename))

    def has_change_permission(self, request, obj=None):
        
        opts = self.opts
        codename = get_permission_codename("change", opts)
        return request.user.has_perm("%s.%s" % (opts.app_label, codename))

    def has_delete_permission(self, request, obj=None):
        
        opts = self.opts
        codename = get_permission_codename("delete", opts)
        return request.user.has_perm("%s.%s" % (opts.app_label, codename))

    def has_view_permission(self, request, obj=None):
        
        opts = self.opts
        codename_view = get_permission_codename("view", opts)
        codename_change = get_permission_codename("change", opts)
        return request.user.has_perm(
            "%s.%s" % (opts.app_label, codename_view)
        ) or request.user.has_perm("%s.%s" % (opts.app_label, codename_change))

    def has_view_or_change_permission(self, request, obj=None):
        return self.has_view_permission(request, obj) or self.has_change_permission(
            request, obj
        )

    def has_module_permission(self, request):
        
        return request.user.has_module_perms(self.opts.app_label)


class ModelAdmin(BaseModelAdmin):
    

    list_display = ("__str__",)
    list_display_links = ()
    list_filter = ()
    list_select_related = False
    list_per_page = 100
    list_max_show_all = 200
    list_editable = ()
    search_fields = ()
    search_help_text = None
    date_hierarchy = None
    save_as = False
    save_as_continue = True
    save_on_top = False
    paginator = Paginator
    preserve_filters = True
    show_facets = ShowFacets.ALLOW
    inlines = ()

    
    add_form_template = None
    change_form_template = None
    change_list_template = None
    delete_confirmation_template = None
    delete_selected_confirmation_template = None
    object_history_template = None
    popup_response_template = None

    
    actions = ()
    action_form = helpers.ActionForm
    actions_on_top = True
    actions_on_bottom = False
    actions_selection_counter = True
    checks_class = ModelAdminChecks

    def __init__(self, model, admin_site):
        self.model = model
        self.opts = model._meta
        self.admin_site = admin_site
        super().__init__()

    def __str__(self):
        return "%s.%s" % (self.opts.app_label, self.__class__.__name__)

    def __repr__(self):
        return (
            f"<{self.__class__.__qualname__}: model={self.model.__qualname__} "
            f"site={self.admin_site!r}>"
        )

    def get_inline_instances(self, request, obj=None):
        inline_instances = []
        for inline_class in self.get_inlines(request, obj):
            inline = inline_class(self.model, self.admin_site)
            if request:
                if not (
                    inline.has_view_or_change_permission(request, obj)
                    or inline.has_add_permission(request, obj)
                    or inline.has_delete_permission(request, obj)
                ):
                    continue
                if not inline.has_add_permission(request, obj):
                    inline.max_num = 0
            inline_instances.append(inline)

        return inline_instances

    def get_urls(self):
        from django.urls import path

        def wrap(view):
            def wrapper(*args, **kwargs):
                return self.admin_site.admin_view(view)(*args, **kwargs)

            wrapper.model_admin = self
            return update_wrapper(wrapper, view)

        info = self.opts.app_label, self.opts.model_name

        return [
            path("", wrap(self.changelist_view), name="%s_%s_changelist" % info),
            path("add/", wrap(self.add_view), name="%s_%s_add" % info),
            path(
                "<path:object_id>/history/",
                wrap(self.history_view),
                name="%s_%s_history" % info,
            ),
            path(
                "<path:object_id>/delete/",
                wrap(self.delete_view),
                name="%s_%s_delete" % info,
            ),
            path(
                "<path:object_id>/change/",
                wrap(self.change_view),
                name="%s_%s_change" % info,
            ),
            
            path(
                "<path:object_id>/",
                wrap(
                    RedirectView.as_view(
                        pattern_name="%s:%s_%s_change"
                        % ((self.admin_site.name,) + info)
                    )
                ),
            ),
        ]

    @property
    def urls(self):
        return self.get_urls()

    @property
    def media(self):
        extra = "" if settings.DEBUG else ".min"
        js = [
            "vendor/jquery/jquery%s.js" % extra,
            "jquery.init.js",
            "core.js",
            "admin/RelatedObjectLookups.js",
            "actions.js",
            "urlify.js",
            "prepopulate.js",
            "vendor/xregexp/xregexp%s.js" % extra,
        ]
        return forms.Media(js=["admin/js/%s" % url for url in js])

    def get_model_perms(self, request):
        
        return {
            "add": self.has_add_permission(request),
            "change": self.has_change_permission(request),
            "delete": self.has_delete_permission(request),
            "view": self.has_view_permission(request),
        }

    def _get_form_for_get_fields(self, request, obj):
        return self.get_form(request, obj, fields=None)

    def get_form(self, request, obj=None, change=False, **kwargs):
        
        if "fields" in kwargs:
            fields = kwargs.pop("fields")
        else:
            fields = flatten_fieldsets(self.get_fieldsets(request, obj))
        excluded = self.get_exclude(request, obj)
        exclude = [] if excluded is None else list(excluded)
        readonly_fields = self.get_readonly_fields(request, obj)
        exclude.extend(readonly_fields)
        
        
        if (
            change
            and hasattr(request, "user")
            and not self.has_change_permission(request, obj)
        ):
            exclude.extend(fields)
        if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
            
            
            exclude.extend(self.form._meta.exclude)
        
        
        exclude = exclude or None

        
        new_attrs = dict.fromkeys(
            f for f in readonly_fields if f in self.form.declared_fields
        )
        form = type(self.form.__name__, (self.form,), new_attrs)

        defaults = {
            "form": form,
            "fields": fields,
            "exclude": exclude,
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
            **kwargs,
        }

        if defaults["fields"] is None and not modelform_defines_fields(
            defaults["form"]
        ):
            defaults["fields"] = forms.ALL_FIELDS

        try:
            return modelform_factory(self.model, **defaults)
        except FieldError as e:
            raise FieldError(
                "%s. Check fields/fieldsets/exclude attributes of class %s."
                % (e, self.__class__.__name__)
            )

    def get_changelist(self, request, **kwargs):
        
        from django.contrib.admin.views.main import ChangeList

        return ChangeList

    def get_changelist_instance(self, request):
        
        list_display = self.get_list_display(request)
        list_display_links = self.get_list_display_links(request, list_display)
        
        if self.get_actions(request):
            list_display = ["action_checkbox", *list_display]
        sortable_by = self.get_sortable_by(request)
        ChangeList = self.get_changelist(request)
        return ChangeList(
            request,
            self.model,
            list_display,
            list_display_links,
            self.get_list_filter(request),
            self.date_hierarchy,
            self.get_search_fields(request),
            self.get_list_select_related(request),
            self.list_per_page,
            self.list_max_show_all,
            self.list_editable,
            self,
            sortable_by,
            self.search_help_text,
        )

    def get_object(self, request, object_id, from_field=None):
        
        queryset = self.get_queryset(request)
        model = queryset.model
        field = (
            model._meta.pk if from_field is None else model._meta.get_field(from_field)
        )
        try:
            object_id = field.to_python(object_id)
            return queryset.get(**{field.name: object_id})
        except (model.DoesNotExist, ValidationError, ValueError):
            return None

    def get_changelist_form(self, request, **kwargs):
        
        defaults = {
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
            **kwargs,
        }
        if defaults.get("fields") is None and not modelform_defines_fields(
            defaults.get("form")
        ):
            defaults["fields"] = forms.ALL_FIELDS

        return modelform_factory(self.model, **defaults)

    def get_changelist_formset(self, request, **kwargs):
        
        defaults = {
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
            **kwargs,
        }
        return modelformset_factory(
            self.model,
            self.get_changelist_form(request),
            extra=0,
            fields=self.list_editable,
            **defaults,
        )

    def get_formsets_with_inlines(self, request, obj=None):
        
        for inline in self.get_inline_instances(request, obj):
            yield inline.get_formset(request, obj), inline

    def get_paginator(
        self, request, queryset, per_page, orphans=0, allow_empty_first_page=True
    ):
        return self.paginator(queryset, per_page, orphans, allow_empty_first_page)

    def log_addition(self, request, obj, message):
        
        from django.contrib.admin.models import ADDITION, LogEntry

        return LogEntry.objects.log_actions(
            user_id=request.user.pk,
            queryset=[obj],
            action_flag=ADDITION,
            change_message=message,
            single_object=True,
        )

    def log_change(self, request, obj, message):
        
        from django.contrib.admin.models import CHANGE, LogEntry

        return LogEntry.objects.log_actions(
            user_id=request.user.pk,
            queryset=[obj],
            action_flag=CHANGE,
            change_message=message,
            single_object=True,
        )

    def log_deletion(self, request, obj, object_repr):
        
        warnings.warn(
            "ModelAdmin.log_deletion() is deprecated. Use log_deletions() instead.",
            RemovedInDjango60Warning,
            stacklevel=2,
        )
        from django.contrib.admin.models import DELETION, LogEntry

        return LogEntry.objects.log_action(
            user_id=request.user.pk,
            content_type_id=get_content_type_for_model(obj).pk,
            object_id=obj.pk,
            object_repr=object_repr,
            action_flag=DELETION,
        )

    def log_deletions(self, request, queryset):
        
        from django.contrib.admin.models import DELETION, LogEntry

        
        if type(self).log_deletion != ModelAdmin.log_deletion:
            warnings.warn(
                "The usage of log_deletion() is deprecated. Implement log_deletions() "
                "instead.",
                RemovedInDjango60Warning,
                stacklevel=2,
            )
            return [self.log_deletion(request, obj, str(obj)) for obj in queryset]

        return LogEntry.objects.log_actions(
            user_id=request.user.pk,
            queryset=queryset,
            action_flag=DELETION,
        )

    def action_checkbox(self, obj):
        
        attrs = {
            "class": "action-select",
            "aria-label": format_html(_("Select this object for an action - {}"), obj),
        }
        checkbox = forms.CheckboxInput(attrs, lambda value: False)
        return checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))

    @staticmethod
    def _get_action_description(func, name):
        return getattr(func, "short_description", capfirst(name.replace("_", " ")))

    def _get_base_actions(self):
        
        actions = []
        base_actions = (self.get_action(action) for action in self.actions or [])
        
        base_actions = [action for action in base_actions if action]
        base_action_names = {name for _, name, _ in base_actions}

        
        for name, func in self.admin_site.actions:
            if name in base_action_names:
                continue
            description = self._get_action_description(func, name)
            actions.append((func, name, description))
        
        actions.extend(base_actions)
        return actions

    def _filter_actions_by_permissions(self, request, actions):
        
        filtered_actions = []
        for action in actions:
            callable = action[0]
            if not hasattr(callable, "allowed_permissions"):
                filtered_actions.append(action)
                continue
            permission_checks = (
                getattr(self, "has_%s_permission" % permission)
                for permission in callable.allowed_permissions
            )
            if any(has_permission(request) for has_permission in permission_checks):
                filtered_actions.append(action)
        return filtered_actions

    def get_actions(self, request):
        
        
        
        if self.actions is None or IS_POPUP_VAR in request.GET:
            return {}
        actions = self._filter_actions_by_permissions(request, self._get_base_actions())
        return {name: (func, name, desc) for func, name, desc in actions}

    def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH):
        
        choices = [] + default_choices
        for func, name, description in self.get_actions(request).values():
            choice = (name, description % model_format_dict(self.opts))
            choices.append(choice)
        return choices

    def get_action(self, action):
        
        
        if callable(action):
            func = action
            action = action.__name__

        
        
        
        elif hasattr(self.__class__, action):
            func = getattr(self.__class__, action)

        
        else:
            try:
                func = self.admin_site.get_action(action)
            except KeyError:
                return None

        description = self._get_action_description(func, action)
        return func, action, description

    def get_list_display(self, request):
        
        return self.list_display

    def get_list_display_links(self, request, list_display):
        
        if (
            self.list_display_links
            or self.list_display_links is None
            or not list_display
        ):
            return self.list_display_links
        else:
            
            return list(list_display)[:1]

    def get_list_filter(self, request):
        
        return self.list_filter

    def get_list_select_related(self, request):
        
        return self.list_select_related

    def get_search_fields(self, request):
        
        return self.search_fields

    def get_search_results(self, request, queryset, search_term):
        

        
        def construct_search(field_name):
            if field_name.startswith("^"):
                return "%s__istartswith" % field_name.removeprefix("^")
            elif field_name.startswith("="):
                return "%s__iexact" % field_name.removeprefix("=")
            elif field_name.startswith("@"):
                return "%s__search" % field_name.removeprefix("@")
            
            opts = queryset.model._meta
            lookup_fields = field_name.split(LOOKUP_SEP)
            
            prev_field = None
            for path_part in lookup_fields:
                if path_part == "pk":
                    path_part = opts.pk.name
                try:
                    field = opts.get_field(path_part)
                except FieldDoesNotExist:
                    
                    if prev_field and prev_field.get_lookup(path_part):
                        return field_name
                else:
                    prev_field = field
                    if hasattr(field, "path_infos"):
                        
                        opts = field.path_infos[-1].to_opts
            
            return "%s__icontains" % field_name

        may_have_duplicates = False
        search_fields = self.get_search_fields(request)
        if search_fields and search_term:
            orm_lookups = [
                construct_search(str(search_field)) for search_field in search_fields
            ]
            term_queries = []
            for bit in smart_split(search_term):
                if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
                    bit = unescape_string_literal(bit)
                or_queries = models.Q.create(
                    [(orm_lookup, bit) for orm_lookup in orm_lookups],
                    connector=models.Q.OR,
                )
                term_queries.append(or_queries)
            queryset = queryset.filter(models.Q.create(term_queries))
            may_have_duplicates |= any(
                lookup_spawns_duplicates(self.opts, search_spec)
                for search_spec in orm_lookups
            )
        return queryset, may_have_duplicates

    def get_preserved_filters(self, request):
        
        match = request.resolver_match
        if self.preserve_filters and match:
            current_url = "%s:%s" % (match.app_name, match.url_name)
            changelist_url = "admin:%s_%s_changelist" % (
                self.opts.app_label,
                self.opts.model_name,
            )
            if current_url == changelist_url:
                preserved_filters = request.GET.urlencode()
            else:
                preserved_filters = request.GET.get("_changelist_filters")

            if preserved_filters:
                return urlencode({"_changelist_filters": preserved_filters})
        return ""

    def construct_change_message(self, request, form, formsets, add=False):
        
        return construct_change_message(form, formsets, add)

    def message_user(
        self, request, message, level=messages.INFO, extra_tags="", fail_silently=False
    ):
        
        if not isinstance(level, int):
            
            try:
                level = getattr(messages.constants, level.upper())
            except AttributeError:
                levels = messages.constants.DEFAULT_TAGS.values()
                levels_repr = ", ".join("`%s`" % level for level in levels)
                raise ValueError(
                    "Bad message level string: `%s`. Possible values are: %s"
                    % (level, levels_repr)
                )

        messages.add_message(
            request, level, message, extra_tags=extra_tags, fail_silently=fail_silently
        )

    def save_form(self, request, form, change):
        
        return form.save(commit=False)

    def save_model(self, request, obj, form, change):
        
        obj.save()

    def delete_model(self, request, obj):
        
        obj.delete()

    def delete_queryset(self, request, queryset):
        
        queryset.delete()

    def save_formset(self, request, form, formset, change):
        
        formset.save()

    def save_related(self, request, form, formsets, change):
        
        form.save_m2m()
        for formset in formsets:
            self.save_formset(request, form, formset, change=change)

    def render_change_form(
        self, request, context, add=False, change=False, form_url="", obj=None
    ):
        app_label = self.opts.app_label
        preserved_filters = self.get_preserved_filters(request)
        form_url = add_preserved_filters(
            {"preserved_filters": preserved_filters, "opts": self.opts}, form_url
        )
        view_on_site_url = self.get_view_on_site_url(obj)
        has_editable_inline_admin_formsets = False
        for inline in context["inline_admin_formsets"]:
            if (
                inline.has_add_permission
                or inline.has_change_permission
                or inline.has_delete_permission
            ):
                has_editable_inline_admin_formsets = True
                break
        context.update(
            {
                "add": add,
                "change": change,
                "has_view_permission": self.has_view_permission(request, obj),
                "has_add_permission": self.has_add_permission(request),
                "has_change_permission": self.has_change_permission(request, obj),
                "has_delete_permission": self.has_delete_permission(request, obj),
                "has_editable_inline_admin_formsets": (
                    has_editable_inline_admin_formsets
                ),
                "has_file_field": context["adminform"].form.is_multipart()
                or any(
                    admin_formset.formset.is_multipart()
                    for admin_formset in context["inline_admin_formsets"]
                ),
                "has_absolute_url": view_on_site_url is not None,
                "absolute_url": view_on_site_url,
                "form_url": form_url,
                "opts": self.opts,
                "content_type_id": get_content_type_for_model(self.model).pk,
                "save_as": self.save_as,
                "save_on_top": self.save_on_top,
                "to_field_var": TO_FIELD_VAR,
                "is_popup_var": IS_POPUP_VAR,
                "app_label": app_label,
            }
        )
        if add and self.add_form_template is not None:
            form_template = self.add_form_template
        else:
            form_template = self.change_form_template

        request.current_app = self.admin_site.name

        return TemplateResponse(
            request,
            form_template
            or [
                "admin/%s/%s/change_form.html" % (app_label, self.opts.model_name),
                "admin/%s/change_form.html" % app_label,
                "admin/change_form.html",
            ],
            context,
        )

    def _get_preserved_qsl(self, request, preserved_filters):
        query_string = urlparse(request.build_absolute_uri()).query
        return parse_qsl(query_string.replace(preserved_filters, ""))

    def response_add(self, request, obj, post_url_continue=None):
        
        opts = obj._meta
        preserved_filters = self.get_preserved_filters(request)
        preserved_qsl = self._get_preserved_qsl(request, preserved_filters)
        obj_url = reverse(
            "admin:%s_%s_change" % (opts.app_label, opts.model_name),
            args=(quote(obj.pk),),
            current_app=self.admin_site.name,
        )
        
        if self.has_change_permission(request, obj):
            obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
        else:
            obj_repr = str(obj)
        msg_dict = {
            "name": opts.verbose_name,
            "obj": obj_repr,
        }
        
        

        if IS_POPUP_VAR in request.POST:
            to_field = request.POST.get(TO_FIELD_VAR)
            if to_field:
                attr = str(to_field)
            else:
                attr = obj._meta.pk.attname
            value = obj.serializable_value(attr)
            popup_response_data = json.dumps(
                {
                    "value": str(value),
                    "obj": str(obj),
                }
            )
            return TemplateResponse(
                request,
                self.popup_response_template
                or [
                    "admin/%s/%s/popup_response.html"
                    % (opts.app_label, opts.model_name),
                    "admin/%s/popup_response.html" % opts.app_label,
                    "admin/popup_response.html",
                ],
                {
                    "popup_response_data": popup_response_data,
                },
            )

        elif "_continue" in request.POST or (
            
            "_saveasnew" in request.POST
            and self.save_as_continue
            and self.has_change_permission(request, obj)
        ):
            msg = _("The {name} “{obj}” was added successfully.")
            if self.has_change_permission(request, obj):
                msg += " " + _("You may edit it again below.")
            self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS)
            if post_url_continue is None:
                post_url_continue = obj_url
            post_url_continue = add_preserved_filters(
                {
                    "preserved_filters": preserved_filters,
                    "preserved_qsl": preserved_qsl,
                    "opts": opts,
                },
                post_url_continue,
            )
            return HttpResponseRedirect(post_url_continue)

        elif "_addanother" in request.POST:
            msg = format_html(
                _(
                    "The {name} “{obj}” was added successfully. You may add another "
                    "{name} below."
                ),
                **msg_dict,
            )
            self.message_user(request, msg, messages.SUCCESS)
            redirect_url = request.path
            redirect_url = add_preserved_filters(
                {
                    "preserved_filters": preserved_filters,
                    "preserved_qsl": preserved_qsl,
                    "opts": opts,
                },
                redirect_url,
            )
            return HttpResponseRedirect(redirect_url)

        else:
            msg = format_html(
                _("The {name} “{obj}” was added successfully."), **msg_dict
            )
            self.message_user(request, msg, messages.SUCCESS)
            return self.response_post_save_add(request, obj)

    def response_change(self, request, obj):
        

        if IS_POPUP_VAR in request.POST:
            opts = obj._meta
            to_field = request.POST.get(TO_FIELD_VAR)
            attr = str(to_field) if to_field else opts.pk.attname
            value = request.resolver_match.kwargs["object_id"]
            new_value = obj.serializable_value(attr)
            popup_response_data = json.dumps(
                {
                    "action": "change",
                    "value": str(value),
                    "obj": str(obj),
                    "new_value": str(new_value),
                }
            )
            return TemplateResponse(
                request,
                self.popup_response_template
                or [
                    "admin/%s/%s/popup_response.html"
                    % (opts.app_label, opts.model_name),
                    "admin/%s/popup_response.html" % opts.app_label,
                    "admin/popup_response.html",
                ],
                {
                    "popup_response_data": popup_response_data,
                },
            )

        opts = self.opts
        preserved_filters = self.get_preserved_filters(request)
        preserved_qsl = self._get_preserved_qsl(request, preserved_filters)

        msg_dict = {
            "name": opts.verbose_name,
            "obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj),
        }
        if "_continue" in request.POST:
            msg = format_html(
                _(
                    "The {name} “{obj}” was changed successfully. You may edit it "
                    "again below."
                ),
                **msg_dict,
            )
            self.message_user(request, msg, messages.SUCCESS)
            redirect_url = request.path
            redirect_url = add_preserved_filters(
                {
                    "preserved_filters": preserved_filters,
                    "preserved_qsl": preserved_qsl,
                    "opts": opts,
                },
                redirect_url,
            )
            return HttpResponseRedirect(redirect_url)

        elif "_addanother" in request.POST:
            msg = format_html(
                _(
                    "The {name} “{obj}” was changed successfully. You may add another "
                    "{name} below."
                ),
                **msg_dict,
            )
            self.message_user(request, msg, messages.SUCCESS)
            redirect_url = reverse(
                "admin:%s_%s_add" % (opts.app_label, opts.model_name),
                current_app=self.admin_site.name,
            )
            redirect_url = add_preserved_filters(
                {
                    "preserved_filters": preserved_filters,
                    "preserved_qsl": preserved_qsl,
                    "opts": opts,
                },
                redirect_url,
            )
            return HttpResponseRedirect(redirect_url)

        else:
            msg = format_html(
                _("The {name} “{obj}” was changed successfully."), **msg_dict
            )
            self.message_user(request, msg, messages.SUCCESS)
            return self.response_post_save_change(request, obj)

    def _response_post_save(self, request, obj):
        if self.has_view_or_change_permission(request):
            post_url = reverse(
                "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name),
                current_app=self.admin_site.name,
            )
            preserved_filters = self.get_preserved_filters(request)
            post_url = add_preserved_filters(
                {"preserved_filters": preserved_filters, "opts": self.opts}, post_url
            )
        else:
            post_url = reverse("admin:index", current_app=self.admin_site.name)
        return HttpResponseRedirect(post_url)

    def response_post_save_add(self, request, obj):
        
        return self._response_post_save(request, obj)

    def response_post_save_change(self, request, obj):
        
        return self._response_post_save(request, obj)

    def response_action(self, request, queryset):
        

        
        
        
        try:
            action_index = int(request.POST.get("index", 0))
        except ValueError:
            action_index = 0

        
        data = request.POST.copy()
        data.pop(helpers.ACTION_CHECKBOX_NAME, None)
        data.pop("index", None)

        
        try:
            data.update({"action": data.getlist("action")[action_index]})
        except IndexError:
            
            
            
            pass

        action_form = self.action_form(data, auto_id=None)
        action_form.fields["action"].choices = self.get_action_choices(request)

        
        if action_form.is_valid():
            action = action_form.cleaned_data["action"]
            select_across = action_form.cleaned_data["select_across"]
            func = self.get_actions(request)[action][0]

            
            
            
            selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
            if not selected and not select_across:
                
                msg = _(
                    "Items must be selected in order to perform "
                    "actions on them. No items have been changed."
                )
                self.message_user(request, msg, messages.WARNING)
                return None

            if not select_across:
                
                queryset = queryset.filter(pk__in=selected)

            response = func(self, request, queryset)

            
            
            
            if isinstance(response, HttpResponseBase):
                return response
            else:
                return HttpResponseRedirect(request.get_full_path())
        else:
            msg = _("No action selected.")
            self.message_user(request, msg, messages.WARNING)
            return None

    def response_delete(self, request, obj_display, obj_id):
        
        if IS_POPUP_VAR in request.POST:
            popup_response_data = json.dumps(
                {
                    "action": "delete",
                    "value": str(obj_id),
                }
            )
            return TemplateResponse(
                request,
                self.popup_response_template
                or [
                    "admin/%s/%s/popup_response.html"
                    % (self.opts.app_label, self.opts.model_name),
                    "admin/%s/popup_response.html" % self.opts.app_label,
                    "admin/popup_response.html",
                ],
                {
                    "popup_response_data": popup_response_data,
                },
            )

        self.message_user(
            request,
            _("The %(name)s “%(obj)s” was deleted successfully.")
            % {
                "name": self.opts.verbose_name,
                "obj": obj_display,
            },
            messages.SUCCESS,
        )

        if self.has_change_permission(request, None):
            post_url = reverse(
                "admin:%s_%s_changelist" % (self.opts.app_label, self.opts.model_name),
                current_app=self.admin_site.name,
            )
            preserved_filters = self.get_preserved_filters(request)
            post_url = add_preserved_filters(
                {"preserved_filters": preserved_filters, "opts": self.opts}, post_url
            )
        else:
            post_url = reverse("admin:index", current_app=self.admin_site.name)
        return HttpResponseRedirect(post_url)

    def render_delete_form(self, request, context):
        app_label = self.opts.app_label

        request.current_app = self.admin_site.name
        context.update(
            to_field_var=TO_FIELD_VAR,
            is_popup_var=IS_POPUP_VAR,
            media=self.media,
        )

        return TemplateResponse(
            request,
            self.delete_confirmation_template
            or [
                "admin/{}/{}/delete_confirmation.html".format(
                    app_label, self.opts.model_name
                ),
                "admin/{}/delete_confirmation.html".format(app_label),
                "admin/delete_confirmation.html",
            ],
            context,
        )

    def get_inline_formsets(self, request, formsets, inline_instances, obj=None):
        
        can_edit_parent = (
            self.has_change_permission(request, obj)
            if obj
            else self.has_add_permission(request)
        )
        inline_admin_formsets = []
        for inline, formset in zip(inline_instances, formsets):
            fieldsets = list(inline.get_fieldsets(request, obj))
            readonly = list(inline.get_readonly_fields(request, obj))
            if can_edit_parent:
                has_add_permission = inline.has_add_permission(request, obj)
                has_change_permission = inline.has_change_permission(request, obj)
                has_delete_permission = inline.has_delete_permission(request, obj)
            else:
                
                has_add_permission = (
                    has_change_permission
                ) = has_delete_permission = False
                formset.extra = formset.max_num = 0
            has_view_permission = inline.has_view_permission(request, obj)
            prepopulated = dict(inline.get_prepopulated_fields(request, obj))
            inline_admin_formset = helpers.InlineAdminFormSet(
                inline,
                formset,
                fieldsets,
                prepopulated,
                readonly,
                model_admin=self,
                has_add_permission=has_add_permission,
                has_change_permission=has_change_permission,
                has_delete_permission=has_delete_permission,
                has_view_permission=has_view_permission,
            )
            inline_admin_formsets.append(inline_admin_formset)
        return inline_admin_formsets

    def get_changeform_initial_data(self, request):
        
        initial = dict(request.GET.items())
        for k in initial:
            try:
                f = self.opts.get_field(k)
            except FieldDoesNotExist:
                continue
            
            if isinstance(f, models.ManyToManyField):
                initial[k] = initial[k].split(",")
        return initial

    def _get_obj_does_not_exist_redirect(self, request, opts, object_id):
        
        msg = _("%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?") % {
            "name": opts.verbose_name,
            "key": unquote(object_id),
        }
        self.message_user(request, msg, messages.WARNING)
        url = reverse("admin:index", current_app=self.admin_site.name)
        return HttpResponseRedirect(url)

    @csrf_protect_m
    def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
        with transaction.atomic(using=router.db_for_write(self.model)):
            return self._changeform_view(request, object_id, form_url, extra_context)

    def _changeform_view(self, request, object_id, form_url, extra_context):
        to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
        if to_field and not self.to_field_allowed(request, to_field):
            raise DisallowedModelAdminToField(
                "The field %s cannot be referenced." % to_field
            )

        if request.method == "POST" and "_saveasnew" in request.POST:
            object_id = None

        add = object_id is None

        if add:
            if not self.has_add_permission(request):
                raise PermissionDenied
            obj = None

        else:
            obj = self.get_object(request, unquote(object_id), to_field)

            if request.method == "POST":
                if not self.has_change_permission(request, obj):
                    raise PermissionDenied
            else:
                if not self.has_view_or_change_permission(request, obj):
                    raise PermissionDenied

            if obj is None:
                return self._get_obj_does_not_exist_redirect(
                    request, self.opts, object_id
                )

        fieldsets = self.get_fieldsets(request, obj)
        ModelForm = self.get_form(
            request, obj, change=not add, fields=flatten_fieldsets(fieldsets)
        )
        if request.method == "POST":
            form = ModelForm(request.POST, request.FILES, instance=obj)
            formsets, inline_instances = self._create_formsets(
                request,
                form.instance,
                change=not add,
            )
            form_validated = form.is_valid()
            if form_validated:
                new_object = self.save_form(request, form, change=not add)
            else:
                new_object = form.instance
            if all_valid(formsets) and form_validated:
                self.save_model(request, new_object, form, not add)
                self.save_related(request, form, formsets, not add)
                change_message = self.construct_change_message(
                    request, form, formsets, add
                )
                if add:
                    self.log_addition(request, new_object, change_message)
                    return self.response_add(request, new_object)
                else:
                    self.log_change(request, new_object, change_message)
                    return self.response_change(request, new_object)
            else:
                form_validated = False
        else:
            if add:
                initial = self.get_changeform_initial_data(request)
                form = ModelForm(initial=initial)
                formsets, inline_instances = self._create_formsets(
                    request, form.instance, change=False
                )
            else:
                form = ModelForm(instance=obj)
                formsets, inline_instances = self._create_formsets(
                    request, obj, change=True
                )

        if not add and not self.has_change_permission(request, obj):
            readonly_fields = flatten_fieldsets(fieldsets)
        else:
            readonly_fields = self.get_readonly_fields(request, obj)
        admin_form = helpers.AdminForm(
            form,
            list(fieldsets),
            
            self.get_prepopulated_fields(request, obj)
            if add or self.has_change_permission(request, obj)
            else {},
            readonly_fields,
            model_admin=self,
        )
        media = self.media + admin_form.media

        inline_formsets = self.get_inline_formsets(
            request, formsets, inline_instances, obj
        )
        for inline_formset in inline_formsets:
            media += inline_formset.media

        if add:
            title = _("Add %s")
        elif self.has_change_permission(request, obj):
            title = _("Change %s")
        else:
            title = _("View %s")
        context = {
            **self.admin_site.each_context(request),
            "title": title % self.opts.verbose_name,
            "subtitle": str(obj) if obj else None,
            "adminform": admin_form,
            "object_id": object_id,
            "original": obj,
            "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
            "to_field": to_field,
            "media": media,
            "inline_admin_formsets": inline_formsets,
            "errors": helpers.AdminErrorList(form, formsets),
            "preserved_filters": self.get_preserved_filters(request),
        }

        
        
        if (
            request.method == "POST"
            and not form_validated
            and "_saveasnew" in request.POST
        ):
            context["show_save"] = False
            context["show_save_and_continue"] = False
            
            add = False

        context.update(extra_context or {})

        return self.render_change_form(
            request, context, add=add, change=not add, obj=obj, form_url=form_url
        )

    def add_view(self, request, form_url="", extra_context=None):
        return self.changeform_view(request, None, form_url, extra_context)

    def change_view(self, request, object_id, form_url="", extra_context=None):
        return self.changeform_view(request, object_id, form_url, extra_context)

    def _get_edited_object_pks(self, request, prefix):
        
        pk_pattern = re.compile(
            r"{}-\d+-{}$".format(re.escape(prefix), self.opts.pk.name)
        )
        return [value for key, value in request.POST.items() if pk_pattern.match(key)]

    def _get_list_editable_queryset(self, request, prefix):
        
        object_pks = self._get_edited_object_pks(request, prefix)
        queryset = self.get_queryset(request)
        validate = queryset.model._meta.pk.to_python
        try:
            for pk in object_pks:
                validate(pk)
        except ValidationError:
            
            return queryset
        return queryset.filter(pk__in=object_pks)

    @csrf_protect_m
    def changelist_view(self, request, extra_context=None):
        
        from django.contrib.admin.views.main import ERROR_FLAG

        app_label = self.opts.app_label
        if not self.has_view_or_change_permission(request):
            raise PermissionDenied

        try:
            cl = self.get_changelist_instance(request)
        except IncorrectLookupParameters:
            
            
            
            
            
            
            if ERROR_FLAG in request.GET:
                return SimpleTemplateResponse(
                    "admin/invalid_setup.html",
                    {
                        "title": _("Database error"),
                    },
                )
            return HttpResponseRedirect(request.path + "?" + ERROR_FLAG + "=1")

        
        
        
        
        action_failed = False
        selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)

        actions = self.get_actions(request)
        
        if (
            actions
            and request.method == "POST"
            and "index" in request.POST
            and "_save" not in request.POST
        ):
            if selected:
                response = self.response_action(
                    request, queryset=cl.get_queryset(request)
                )
                if response:
                    return response
                else:
                    action_failed = True
            else:
                msg = _(
                    "Items must be selected in order to perform "
                    "actions on them. No items have been changed."
                )
                self.message_user(request, msg, messages.WARNING)
                action_failed = True

        
        if (
            actions
            and request.method == "POST"
            and helpers.ACTION_CHECKBOX_NAME in request.POST
            and "index" not in request.POST
            and "_save" not in request.POST
        ):
            if selected:
                response = self.response_action(
                    request, queryset=cl.get_queryset(request)
                )
                if response:
                    return response
                else:
                    action_failed = True

        if action_failed:
            
            
            
            return HttpResponseRedirect(request.get_full_path())

        
        
        
        formset = cl.formset = None

        
        if request.method == "POST" and cl.list_editable and "_save" in request.POST:
            if not self.has_change_permission(request):
                raise PermissionDenied
            FormSet = self.get_changelist_formset(request)
            modified_objects = self._get_list_editable_queryset(
                request, FormSet.get_default_prefix()
            )
            formset = cl.formset = FormSet(
                request.POST, request.FILES, queryset=modified_objects
            )
            if formset.is_valid():
                changecount = 0
                with transaction.atomic(using=router.db_for_write(self.model)):
                    for form in formset.forms:
                        if form.has_changed():
                            obj = self.save_form(request, form, change=True)
                            self.save_model(request, obj, form, change=True)
                            self.save_related(request, form, formsets=[], change=True)
                            change_msg = self.construct_change_message(
                                request, form, None
                            )
                            self.log_change(request, obj, change_msg)
                            changecount += 1
                if changecount:
                    msg = ngettext(
                        "%(count)s %(name)s was changed successfully.",
                        "%(count)s %(name)s were changed successfully.",
                        changecount,
                    ) % {
                        "count": changecount,
                        "name": model_ngettext(self.opts, changecount),
                    }
                    self.message_user(request, msg, messages.SUCCESS)

                return HttpResponseRedirect(request.get_full_path())

        
        elif cl.list_editable and self.has_change_permission(request):
            FormSet = self.get_changelist_formset(request)
            formset = cl.formset = FormSet(queryset=cl.result_list)

        
        if formset:
            media = self.media + formset.media
        else:
            media = self.media

        
        if actions:
            action_form = self.action_form(auto_id=None)
            action_form.fields["action"].choices = self.get_action_choices(request)
            media += action_form.media
        else:
            action_form = None

        selection_note_all = ngettext(
            "%(total_count)s selected", "All %(total_count)s selected", cl.result_count
        )

        context = {
            **self.admin_site.each_context(request),
            "module_name": str(self.opts.verbose_name_plural),
            "selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)},
            "selection_note_all": selection_note_all % {"total_count": cl.result_count},
            "title": cl.title,
            "subtitle": None,
            "is_popup": cl.is_popup,
            "to_field": cl.to_field,
            "cl": cl,
            "media": media,
            "has_add_permission": self.has_add_permission(request),
            "opts": cl.opts,
            "action_form": action_form,
            "actions_on_top": self.actions_on_top,
            "actions_on_bottom": self.actions_on_bottom,
            "actions_selection_counter": self.actions_selection_counter,
            "preserved_filters": self.get_preserved_filters(request),
            **(extra_context or {}),
        }

        request.current_app = self.admin_site.name

        return TemplateResponse(
            request,
            self.change_list_template
            or [
                "admin/%s/%s/change_list.html" % (app_label, self.opts.model_name),
                "admin/%s/change_list.html" % app_label,
                "admin/change_list.html",
            ],
            context,
        )

    def get_deleted_objects(self, objs, request):
        
        return get_deleted_objects(objs, request, self.admin_site)

    @csrf_protect_m
    def delete_view(self, request, object_id, extra_context=None):
        with transaction.atomic(using=router.db_for_write(self.model)):
            return self._delete_view(request, object_id, extra_context)

    def _delete_view(self, request, object_id, extra_context):
        "The 'delete' admin view for this model."
        app_label = self.opts.app_label

        to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR))
        if to_field and not self.to_field_allowed(request, to_field):
            raise DisallowedModelAdminToField(
                "The field %s cannot be referenced." % to_field
            )

        obj = self.get_object(request, unquote(object_id), to_field)

        if not self.has_delete_permission(request, obj):
            raise PermissionDenied

        if obj is None:
            return self._get_obj_does_not_exist_redirect(request, self.opts, object_id)

        
        
        (
            deleted_objects,
            model_count,
            perms_needed,
            protected,
        ) = self.get_deleted_objects([obj], request)

        if request.POST and not protected:  
            if perms_needed:
                raise PermissionDenied
            obj_display = str(obj)
            attr = str(to_field) if to_field else self.opts.pk.attname
            obj_id = obj.serializable_value(attr)
            self.log_deletions(request, [obj])
            self.delete_model(request, obj)

            return self.response_delete(request, obj_display, obj_id)

        object_name = str(self.opts.verbose_name)

        if perms_needed or protected:
            title = _("Cannot delete %(name)s") % {"name": object_name}
        else:
            title = _("Are you sure?")

        context = {
            **self.admin_site.each_context(request),
            "title": title,
            "subtitle": None,
            "object_name": object_name,
            "object": obj,
            "deleted_objects": deleted_objects,
            "model_count": dict(model_count).items(),
            "perms_lacking": perms_needed,
            "protected": protected,
            "opts": self.opts,
            "app_label": app_label,
            "preserved_filters": self.get_preserved_filters(request),
            "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET,
            "to_field": to_field,
            **(extra_context or {}),
        }

        return self.render_delete_form(request, context)

    def history_view(self, request, object_id, extra_context=None):
        "The 'history' admin view for this model."
        from django.contrib.admin.models import LogEntry
        from django.contrib.admin.views.main import PAGE_VAR

        
        model = self.model
        obj = self.get_object(request, unquote(object_id))
        if obj is None:
            return self._get_obj_does_not_exist_redirect(
                request, model._meta, object_id
            )

        if not self.has_view_or_change_permission(request, obj):
            raise PermissionDenied

        
        app_label = self.opts.app_label
        action_list = (
            LogEntry.objects.filter(
                object_id=unquote(object_id),
                content_type=get_content_type_for_model(model),
            )
            .select_related()
            .order_by("action_time")
        )

        paginator = self.get_paginator(request, action_list, 100)
        page_number = request.GET.get(PAGE_VAR, 1)
        page_obj = paginator.get_page(page_number)
        page_range = paginator.get_elided_page_range(page_obj.number)

        context = {
            **self.admin_site.each_context(request),
            "title": _("Change history: %s") % obj,
            "subtitle": None,
            "action_list": page_obj,
            "page_range": page_range,
            "page_var": PAGE_VAR,
            "pagination_required": paginator.count > 100,
            "module_name": str(capfirst(self.opts.verbose_name_plural)),
            "object": obj,
            "opts": self.opts,
            "preserved_filters": self.get_preserved_filters(request),
            **(extra_context or {}),
        }

        request.current_app = self.admin_site.name

        return TemplateResponse(
            request,
            self.object_history_template
            or [
                "admin/%s/%s/object_history.html" % (app_label, self.opts.model_name),
                "admin/%s/object_history.html" % app_label,
                "admin/object_history.html",
            ],
            context,
        )

    def get_formset_kwargs(self, request, obj, inline, prefix):
        formset_params = {
            "instance": obj,
            "prefix": prefix,
            "queryset": inline.get_queryset(request),
        }
        if request.method == "POST":
            formset_params.update(
                {
                    "data": request.POST.copy(),
                    "files": request.FILES,
                    "save_as_new": "_saveasnew" in request.POST,
                }
            )
        return formset_params

    def _create_formsets(self, request, obj, change):
        "Helper function to generate formsets for add/change_view."
        formsets = []
        inline_instances = []
        prefixes = {}
        get_formsets_args = [request]
        if change:
            get_formsets_args.append(obj)
        for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args):
            prefix = FormSet.get_default_prefix()
            prefixes[prefix] = prefixes.get(prefix, 0) + 1
            if prefixes[prefix] != 1 or not prefix:
                prefix = "%s-%s" % (prefix, prefixes[prefix])
            formset_params = self.get_formset_kwargs(request, obj, inline, prefix)
            formset = FormSet(**formset_params)

            def user_deleted_form(request, obj, formset, index, inline):
                
                return (
                    inline.has_delete_permission(request, obj)
                    and "{}-{}-DELETE".format(formset.prefix, index) in request.POST
                )

            
            
            if not inline.has_change_permission(request, obj if change else None):
                for index, form in enumerate(formset.initial_forms):
                    if user_deleted_form(request, obj, formset, index, inline):
                        continue
                    form._errors = {}
                    form.cleaned_data = form.initial
            formsets.append(formset)
            inline_instances.append(inline)
        return formsets, inline_instances


class InlineModelAdmin(BaseModelAdmin):
    

    model = None
    fk_name = None
    formset = BaseInlineFormSet
    extra = 3
    min_num = None
    max_num = None
    template = None
    verbose_name = None
    verbose_name_plural = None
    can_delete = True
    show_change_link = False
    checks_class = InlineModelAdminChecks
    classes = None

    def __init__(self, parent_model, admin_site):
        self.admin_site = admin_site
        self.parent_model = parent_model
        self.opts = self.model._meta
        self.has_registered_model = admin_site.is_registered(self.model)
        super().__init__()
        if self.verbose_name_plural is None:
            if self.verbose_name is None:
                self.verbose_name_plural = self.opts.verbose_name_plural
            else:
                self.verbose_name_plural = format_lazy("{}s", self.verbose_name)
        if self.verbose_name is None:
            self.verbose_name = self.opts.verbose_name

    @property
    def media(self):
        extra = "" if settings.DEBUG else ".min"
        js = ["vendor/jquery/jquery%s.js" % extra, "jquery.init.js", "inlines.js"]
        if self.filter_vertical or self.filter_horizontal:
            js.extend(["SelectBox.js", "SelectFilter2.js"])
        if self.classes and "collapse" in self.classes:
            js.append("collapse.js")
        return forms.Media(js=["admin/js/%s" % url for url in js])

    def get_extra(self, request, obj=None, **kwargs):
        
        return self.extra

    def get_min_num(self, request, obj=None, **kwargs):
        
        return self.min_num

    def get_max_num(self, request, obj=None, **kwargs):
        
        return self.max_num

    def get_formset(self, request, obj=None, **kwargs):
        
        if "fields" in kwargs:
            fields = kwargs.pop("fields")
        else:
            fields = flatten_fieldsets(self.get_fieldsets(request, obj))
        excluded = self.get_exclude(request, obj)
        exclude = [] if excluded is None else list(excluded)
        exclude.extend(self.get_readonly_fields(request, obj))
        if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude:
            
            
            exclude.extend(self.form._meta.exclude)
        
        
        exclude = exclude or None
        can_delete = self.can_delete and self.has_delete_permission(request, obj)
        defaults = {
            "form": self.form,
            "formset": self.formset,
            "fk_name": self.fk_name,
            "fields": fields,
            "exclude": exclude,
            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
            "extra": self.get_extra(request, obj, **kwargs),
            "min_num": self.get_min_num(request, obj, **kwargs),
            "max_num": self.get_max_num(request, obj, **kwargs),
            "can_delete": can_delete,
            **kwargs,
        }

        base_model_form = defaults["form"]
        can_change = self.has_change_permission(request, obj) if request else True
        can_add = self.has_add_permission(request, obj) if request else True

        class DeleteProtectedModelForm(base_model_form):
            def hand_clean_DELETE(self):
                
                if self.cleaned_data.get(DELETION_FIELD_NAME, False):
                    using = router.db_for_write(self._meta.model)
                    collector = NestedObjects(using=using)
                    if self.instance._state.adding:
                        return
                    collector.collect([self.instance])
                    if collector.protected:
                        objs = []
                        for p in collector.protected:
                            objs.append(
                                
                                
                                
                                _("%(class_name)s %(instance)s")
                                % {"class_name": p._meta.verbose_name, "instance": p}
                            )
                        params = {
                            "class_name": self._meta.model._meta.verbose_name,
                            "instance": self.instance,
                            "related_objects": get_text_list(objs, _("and")),
                        }
                        msg = _(
                            "Deleting %(class_name)s %(instance)s would require "
                            "deleting the following protected related objects: "
                            "%(related_objects)s"
                        )
                        raise ValidationError(
                            msg, code="deleting_protected", params=params
                        )

            def is_valid(self):
                result = super().is_valid()
                self.hand_clean_DELETE()
                return result

            def has_changed(self):
                
                if not can_change and not self.instance._state.adding:
                    return False
                if not can_add and self.instance._state.adding:
                    return False
                return super().has_changed()

        defaults["form"] = DeleteProtectedModelForm

        if defaults["fields"] is None and not modelform_defines_fields(
            defaults["form"]
        ):
            defaults["fields"] = forms.ALL_FIELDS

        return inlineformset_factory(self.parent_model, self.model, **defaults)

    def _get_form_for_get_fields(self, request, obj=None):
        return self.get_formset(request, obj, fields=None).form

    def get_queryset(self, request):
        queryset = super().get_queryset(request)
        if not self.has_view_or_change_permission(request):
            queryset = queryset.none()
        return queryset

    def _has_any_perms_for_target_model(self, request, perms):
        
        opts = self.opts
        
        for field in opts.fields:
            if field.remote_field and field.remote_field.model != self.parent_model:
                opts = field.remote_field.model._meta
                break
        return any(
            request.user.has_perm(
                "%s.%s" % (opts.app_label, get_permission_codename(perm, opts))
            )
            for perm in perms
        )

    def has_add_permission(self, request, obj):
        if self.opts.auto_created:
            
            
            
            
            return self._has_any_perms_for_target_model(request, ["change"])
        return super().has_add_permission(request)

    def has_change_permission(self, request, obj=None):
        if self.opts.auto_created:
            
            return self._has_any_perms_for_target_model(request, ["change"])
        return super().has_change_permission(request)

    def has_delete_permission(self, request, obj=None):
        if self.opts.auto_created:
            
            return self._has_any_perms_for_target_model(request, ["change"])
        return super().has_delete_permission(request, obj)

    def has_view_permission(self, request, obj=None):
        if self.opts.auto_created:
            
            
            return self._has_any_perms_for_target_model(request, ["view", "change"])
        return super().has_view_permission(request)


class StackedInline(InlineModelAdmin):
    template = "admin/edit_inline/stacked.html"


class TabularInline(InlineModelAdmin):
    template = "admin/edit_inline/tabular.html"