options.py 78 KB

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