Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

options.py 89KB

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