PageRenderTime 58ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/django/contrib/admin/options.py

https://code.google.com/p/mango-py/
Python | 1363 lines | 1131 code | 86 blank | 146 comment | 141 complexity | 83266c79b4cba8e524074ac86a97f947 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. from django import forms, template
  2. from django.forms.formsets import all_valid
  3. from django.forms.models import (modelform_factory, modelformset_factory,
  4. inlineformset_factory, BaseInlineFormSet)
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.contrib.admin import widgets, helpers
  7. from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
  8. from django.contrib import messages
  9. from django.views.decorators.csrf import csrf_protect
  10. from django.core.exceptions import PermissionDenied, ValidationError
  11. from django.core.paginator import Paginator
  12. from django.db import models, transaction, router
  13. from django.db.models.related import RelatedObject
  14. from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
  15. from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS
  16. from django.http import Http404, HttpResponse, HttpResponseRedirect
  17. from django.shortcuts import get_object_or_404, render_to_response
  18. from django.utils.decorators import method_decorator
  19. from django.utils.datastructures import SortedDict
  20. from django.utils.functional import update_wrapper
  21. from django.utils.html import escape, escapejs
  22. from django.utils.safestring import mark_safe
  23. from django.utils.functional import curry
  24. from django.utils.text import capfirst, get_text_list
  25. from django.utils.translation import ugettext as _
  26. from django.utils.translation import ungettext
  27. from django.utils.encoding import force_unicode
  28. HORIZONTAL, VERTICAL = 1, 2
  29. # returns the <ul> class for a given radio_admin field
  30. get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
  31. class IncorrectLookupParameters(Exception):
  32. pass
  33. # Defaults for formfield_overrides. ModelAdmin subclasses can change this
  34. # by adding to ModelAdmin.formfield_overrides.
  35. FORMFIELD_FOR_DBFIELD_DEFAULTS = {
  36. models.DateTimeField: {
  37. 'form_class': forms.SplitDateTimeField,
  38. 'widget': widgets.AdminSplitDateTime
  39. },
  40. models.DateField: {'widget': widgets.AdminDateWidget},
  41. models.TimeField: {'widget': widgets.AdminTimeWidget},
  42. models.TextField: {'widget': widgets.AdminTextareaWidget},
  43. models.URLField: {'widget': widgets.AdminURLFieldWidget},
  44. models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
  45. models.BigIntegerField: {'widget': widgets.AdminIntegerFieldWidget},
  46. models.CharField: {'widget': widgets.AdminTextInputWidget},
  47. models.ImageField: {'widget': widgets.AdminFileWidget},
  48. models.FileField: {'widget': widgets.AdminFileWidget},
  49. }
  50. csrf_protect_m = method_decorator(csrf_protect)
  51. class InlineAdminField(object):
  52. def __init__(self, name):
  53. self.name = name
  54. self.is_inline = True
  55. self.empty_widget = True
  56. self.widget = None
  57. #self.required = True
  58. self.help_text = ''
  59. class BaseModelAdmin(object):
  60. """Functionality common to both ModelAdmin and InlineAdmin."""
  61. __metaclass__ = forms.MediaDefiningClass
  62. raw_id_fields = ()
  63. fields = None
  64. exclude = None
  65. fieldsets = None
  66. form = forms.ModelForm
  67. filter_vertical = ()
  68. filter_horizontal = ()
  69. radio_fields = {}
  70. prepopulated_fields = {}
  71. formfield_overrides = {}
  72. readonly_fields = ()
  73. ordering = None
  74. def __init__(self):
  75. overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
  76. overrides.update(self.formfield_overrides)
  77. self.formfield_overrides = overrides
  78. def formfield_for_dbfield(self, db_field, **kwargs):
  79. """
  80. Hook for specifying the form Field instance for a given database Field
  81. instance.
  82. If kwargs are given, they're passed to the form Field's constructor.
  83. """
  84. request = kwargs.pop("request", None)
  85. # If the field specifies choices, we don't need to look for special
  86. # admin widgets - we just need to use a select widget of some kind.
  87. if db_field.choices:
  88. return self.formfield_for_choice_field(db_field, request, **kwargs)
  89. # ForeignKey or ManyToManyFields
  90. if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
  91. # Combine the field kwargs with any options for formfield_overrides.
  92. # Make sure the passed in **kwargs override anything in
  93. # formfield_overrides because **kwargs is more specific, and should
  94. # always win.
  95. if db_field.__class__ in self.formfield_overrides:
  96. kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
  97. # Get the correct formfield.
  98. if isinstance(db_field, models.ForeignKey):
  99. formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
  100. elif isinstance(db_field, models.ManyToManyField):
  101. formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
  102. # For non-raw_id fields, wrap the widget with a wrapper that adds
  103. # extra HTML -- the "add other" interface -- to the end of the
  104. # rendered output. formfield can be None if it came from a
  105. # OneToOneField with parent_link=True or a M2M intermediary.
  106. if formfield and db_field.name not in self.raw_id_fields:
  107. related_modeladmin = self.admin_site._registry.get(
  108. db_field.rel.to)
  109. can_add_related = bool(related_modeladmin and
  110. related_modeladmin.has_add_permission(request))
  111. formfield.widget = widgets.RelatedFieldWidgetWrapper(
  112. formfield.widget, db_field.rel, self.admin_site,
  113. can_add_related=can_add_related)
  114. return formfield
  115. # If we've got overrides for the formfield defined, use 'em. **kwargs
  116. # passed to formfield_for_dbfield override the defaults.
  117. for klass in db_field.__class__.mro():
  118. if klass in self.formfield_overrides:
  119. kwargs = dict(self.formfield_overrides[klass], **kwargs)
  120. return db_field.formfield(**kwargs)
  121. # For any other type of field, just call its formfield() method.
  122. return db_field.formfield(**kwargs)
  123. def formfield_for_choice_field(self, db_field, request=None, **kwargs):
  124. """
  125. Get a form Field for a database Field that has declared choices.
  126. """
  127. # If the field is named as a radio_field, use a RadioSelect
  128. if db_field.name in self.radio_fields:
  129. # Avoid stomping on custom widget/choices arguments.
  130. if 'widget' not in kwargs:
  131. kwargs['widget'] = widgets.AdminRadioSelect(attrs={
  132. 'class': get_ul_class(self.radio_fields[db_field.name]),
  133. })
  134. if 'choices' not in kwargs:
  135. kwargs['choices'] = db_field.get_choices(
  136. include_blank = db_field.blank,
  137. blank_choice=[('', _('None'))]
  138. )
  139. return db_field.formfield(**kwargs)
  140. def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
  141. """
  142. Get a form Field for a ForeignKey.
  143. """
  144. db = kwargs.get('using')
  145. if db_field.name in self.raw_id_fields:
  146. kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel, using=db)
  147. elif db_field.name in self.radio_fields:
  148. kwargs['widget'] = widgets.AdminRadioSelect(attrs={
  149. 'class': get_ul_class(self.radio_fields[db_field.name]),
  150. })
  151. kwargs['empty_label'] = db_field.blank and _('None') or None
  152. return db_field.formfield(**kwargs)
  153. def formfield_for_manytomany(self, db_field, request=None, **kwargs):
  154. """
  155. Get a form Field for a ManyToManyField.
  156. """
  157. # If it uses an intermediary model that isn't auto created, don't show
  158. # a field in admin.
  159. if not db_field.rel.through._meta.auto_created:
  160. return None
  161. db = kwargs.get('using')
  162. if db_field.name in self.raw_id_fields:
  163. kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
  164. kwargs['help_text'] = ''
  165. elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
  166. kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
  167. return db_field.formfield(**kwargs)
  168. def _declared_fieldsets(self):
  169. if self.fieldsets:
  170. return self.fieldsets
  171. elif self.fields:
  172. return [(None, {'fields': self.fields})]
  173. return None
  174. declared_fieldsets = property(_declared_fieldsets)
  175. def get_readonly_fields(self, request, obj=None):
  176. return self.readonly_fields
  177. def queryset(self, request):
  178. """
  179. Returns a QuerySet of all model instances that can be edited by the
  180. admin site. This is used by changelist_view.
  181. """
  182. qs = self.model._default_manager.get_query_set()
  183. # TODO: this should be handled by some parameter to the ChangeList.
  184. ordering = self.ordering or () # otherwise we might try to *None, which is bad ;)
  185. if ordering:
  186. qs = qs.order_by(*ordering)
  187. return qs
  188. def lookup_allowed(self, lookup, value):
  189. model = self.model
  190. # Check FKey lookups that are allowed, so that popups produced by
  191. # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to,
  192. # are allowed to work.
  193. for l in model._meta.related_fkey_lookups:
  194. for k, v in widgets.url_params_from_lookup_dict(l).items():
  195. if k == lookup and v == value:
  196. return True
  197. parts = lookup.split(LOOKUP_SEP)
  198. # Last term in lookup is a query term (__exact, __startswith etc)
  199. # This term can be ignored.
  200. if len(parts) > 1 and parts[-1] in QUERY_TERMS:
  201. parts.pop()
  202. # Special case -- foo__id__exact and foo__id queries are implied
  203. # if foo has been specificially included in the lookup list; so
  204. # drop __id if it is the last part. However, first we need to find
  205. # the pk attribute name.
  206. pk_attr_name = None
  207. for part in parts[:-1]:
  208. field, _, _, _ = model._meta.get_field_by_name(part)
  209. if hasattr(field, 'rel'):
  210. model = field.rel.to
  211. pk_attr_name = model._meta.pk.name
  212. elif isinstance(field, RelatedObject):
  213. model = field.model
  214. pk_attr_name = model._meta.pk.name
  215. else:
  216. pk_attr_name = None
  217. if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name:
  218. parts.pop()
  219. try:
  220. self.model._meta.get_field_by_name(parts[0])
  221. except FieldDoesNotExist:
  222. # Lookups on non-existants fields are ok, since they're ignored
  223. # later.
  224. return True
  225. else:
  226. if len(parts) == 1:
  227. return True
  228. clean_lookup = LOOKUP_SEP.join(parts)
  229. return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
  230. class ModelAdmin(BaseModelAdmin):
  231. "Encapsulates all admin options and functionality for a given model."
  232. list_display = ('__str__',)
  233. list_display_links = ()
  234. list_filter = ()
  235. list_select_related = False
  236. list_per_page = 100
  237. list_editable = ()
  238. search_fields = ()
  239. date_hierarchy = None
  240. save_as = False
  241. save_on_top = False
  242. paginator = Paginator
  243. inlines = []
  244. # Custom templates (designed to be over-ridden in subclasses)
  245. add_form_template = None
  246. change_form_template = None
  247. change_list_template = None
  248. delete_confirmation_template = None
  249. delete_selected_confirmation_template = None
  250. object_history_template = None
  251. # Actions
  252. actions = []
  253. action_form = helpers.ActionForm
  254. actions_on_top = True
  255. actions_on_bottom = False
  256. actions_selection_counter = True
  257. def __init__(self, model, admin_site):
  258. self.model = model
  259. self.opts = model._meta
  260. self.admin_site = admin_site
  261. self.inline_instances = []
  262. for inline_class in self.inlines:
  263. inline_instance = inline_class(self.model, self.admin_site)
  264. self.inline_instances.append(inline_instance)
  265. if 'action_checkbox' not in self.list_display and self.actions is not None:
  266. self.list_display = ['action_checkbox'] + list(self.list_display)
  267. if not self.list_display_links:
  268. for name in self.list_display:
  269. if name != 'action_checkbox':
  270. self.list_display_links = [name]
  271. break
  272. super(ModelAdmin, self).__init__()
  273. def get_urls(self):
  274. from django.conf.urls.defaults import patterns, url
  275. def wrap(view):
  276. def wrapper(*args, **kwargs):
  277. return self.admin_site.admin_view(view)(*args, **kwargs)
  278. return update_wrapper(wrapper, view)
  279. info = self.model._meta.app_label, self.model._meta.module_name
  280. urlpatterns = patterns('',
  281. url(r'^$',
  282. wrap(self.changelist_view),
  283. name='%s_%s_changelist' % info),
  284. url(r'^add/$',
  285. wrap(self.add_view),
  286. name='%s_%s_add' % info),
  287. url(r'^(.+)/history/$',
  288. wrap(self.history_view),
  289. name='%s_%s_history' % info),
  290. url(r'^(.+)/delete/$',
  291. wrap(self.delete_view),
  292. name='%s_%s_delete' % info),
  293. url(r'^(.+)/$',
  294. wrap(self.change_view),
  295. name='%s_%s_change' % info),
  296. )
  297. return urlpatterns
  298. def urls(self):
  299. return self.get_urls()
  300. urls = property(urls)
  301. def _media(self):
  302. from django.conf import settings
  303. js = ['js/core.js', 'js/admin/RelatedObjectLookups.js',
  304. 'js/jquery.min.js', 'js/jquery.init.js']
  305. if self.actions is not None:
  306. js.extend(['js/actions.min.js'])
  307. if self.prepopulated_fields:
  308. js.append('js/urlify.js')
  309. js.append('js/prepopulate.min.js')
  310. if self.opts.get_ordered_objects():
  311. js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js'])
  312. return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
  313. media = property(_media)
  314. def has_add_permission(self, request):
  315. """
  316. Returns True if the given request has permission to add an object.
  317. Can be overriden by the user in subclasses.
  318. """
  319. opts = self.opts
  320. return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
  321. def has_change_permission(self, request, obj=None):
  322. """
  323. Returns True if the given request has permission to change the given
  324. Django model instance, the default implementation doesn't examine the
  325. `obj` parameter.
  326. Can be overriden by the user in subclasses. In such case it should
  327. return True if the given request has permission to change the `obj`
  328. model instance. If `obj` is None, this should return True if the given
  329. request has permission to change *any* object of the given type.
  330. """
  331. opts = self.opts
  332. return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
  333. def has_delete_permission(self, request, obj=None):
  334. """
  335. Returns True if the given request has permission to change the given
  336. Django model instance, the default implementation doesn't examine the
  337. `obj` parameter.
  338. Can be overriden by the user in subclasses. In such case it should
  339. return True if the given request has permission to delete the `obj`
  340. model instance. If `obj` is None, this should return True if the given
  341. request has permission to delete *any* object of the given type.
  342. """
  343. opts = self.opts
  344. return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
  345. def get_model_perms(self, request):
  346. """
  347. Returns a dict of all perms for this model. This dict has the keys
  348. ``add``, ``change``, and ``delete`` mapping to the True/False for each
  349. of those actions.
  350. """
  351. return {
  352. 'add': self.has_add_permission(request),
  353. 'change': self.has_change_permission(request),
  354. 'delete': self.has_delete_permission(request),
  355. }
  356. def get_fieldsets(self, request, obj=None):
  357. "Hook for specifying fieldsets for the add form."
  358. if self.declared_fieldsets:
  359. return self.declared_fieldsets
  360. form = self.get_form(request, obj)
  361. fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
  362. return [(None, {'fields': fields})]
  363. def get_form(self, request, obj=None, **kwargs):
  364. """
  365. Returns a Form class for use in the admin add view. This is used by
  366. add_view and change_view.
  367. """
  368. if self.declared_fieldsets:
  369. fields = flatten_fieldsets(self.declared_fieldsets)
  370. else:
  371. fields = None
  372. if self.exclude is None:
  373. exclude = []
  374. else:
  375. exclude = list(self.exclude)
  376. exclude.extend(kwargs.get("exclude", []))
  377. exclude.extend(self.get_readonly_fields(request, obj))
  378. # if exclude is an empty list we pass None to be consistant with the
  379. # default on modelform_factory
  380. exclude = exclude or None
  381. defaults = {
  382. "form": self.form,
  383. "fields": fields,
  384. "exclude": exclude,
  385. "formfield_callback": curry(self.formfield_for_dbfield, request=request),
  386. }
  387. defaults.update(kwargs)
  388. form = modelform_factory(self.model, **defaults)
  389. possible_inlines = [name for name in form.base_fields if form.base_fields[name] is None]
  390. inlines = [name for name in possible_inlines if name in [inl.__name__ for inl in self.inlines]]
  391. for inl in inlines:
  392. form.base_fields[inl] = InlineAdminField(inl)
  393. return form
  394. def get_changelist(self, request, **kwargs):
  395. """
  396. Returns the ChangeList class for use on the changelist page.
  397. """
  398. from django.contrib.admin.views.main import ChangeList
  399. return ChangeList
  400. def get_object(self, request, object_id):
  401. """
  402. Returns an instance matching the primary key provided. ``None`` is
  403. returned if no match is found (or the object_id failed validation
  404. against the primary key field).
  405. """
  406. queryset = self.queryset(request)
  407. model = queryset.model
  408. try:
  409. object_id = model._meta.pk.to_python(object_id)
  410. return queryset.get(pk=object_id)
  411. except (model.DoesNotExist, ValidationError):
  412. return None
  413. def get_changelist_form(self, request, **kwargs):
  414. """
  415. Returns a Form class for use in the Formset on the changelist page.
  416. """
  417. defaults = {
  418. "formfield_callback": curry(self.formfield_for_dbfield, request=request),
  419. }
  420. defaults.update(kwargs)
  421. return modelform_factory(self.model, **defaults)
  422. def get_changelist_formset(self, request, **kwargs):
  423. """
  424. Returns a FormSet class for use on the changelist page if list_editable
  425. is used.
  426. """
  427. defaults = {
  428. "formfield_callback": curry(self.formfield_for_dbfield, request=request),
  429. }
  430. defaults.update(kwargs)
  431. return modelformset_factory(self.model,
  432. self.get_changelist_form(request), extra=0,
  433. fields=self.list_editable, **defaults)
  434. def get_formsets(self, request, obj=None):
  435. for inline in self.inline_instances:
  436. yield inline.get_formset(request, obj)
  437. def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
  438. return self.paginator(queryset, per_page, orphans, allow_empty_first_page)
  439. def log_addition(self, request, object):
  440. """
  441. Log that an object has been successfully added.
  442. The default implementation creates an admin LogEntry object.
  443. """
  444. from django.contrib.admin.models import LogEntry, ADDITION
  445. LogEntry.objects.log_action(
  446. user_id = request.user.pk,
  447. content_type_id = ContentType.objects.get_for_model(object).pk,
  448. object_id = object.pk,
  449. object_repr = force_unicode(object),
  450. action_flag = ADDITION
  451. )
  452. def log_change(self, request, object, message):
  453. """
  454. Log that an object has been successfully changed.
  455. The default implementation creates an admin LogEntry object.
  456. """
  457. from django.contrib.admin.models import LogEntry, CHANGE
  458. LogEntry.objects.log_action(
  459. user_id = request.user.pk,
  460. content_type_id = ContentType.objects.get_for_model(object).pk,
  461. object_id = object.pk,
  462. object_repr = force_unicode(object),
  463. action_flag = CHANGE,
  464. change_message = message
  465. )
  466. def log_deletion(self, request, object, object_repr):
  467. """
  468. Log that an object will be deleted. Note that this method is called
  469. before the deletion.
  470. The default implementation creates an admin LogEntry object.
  471. """
  472. from django.contrib.admin.models import LogEntry, DELETION
  473. LogEntry.objects.log_action(
  474. user_id = request.user.id,
  475. content_type_id = ContentType.objects.get_for_model(self.model).pk,
  476. object_id = object.pk,
  477. object_repr = object_repr,
  478. action_flag = DELETION
  479. )
  480. def action_checkbox(self, obj):
  481. """
  482. A list_display column containing a checkbox widget.
  483. """
  484. return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, force_unicode(obj.pk))
  485. action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle" />')
  486. action_checkbox.allow_tags = True
  487. def get_actions(self, request):
  488. """
  489. Return a dictionary mapping the names of all actions for this
  490. ModelAdmin to a tuple of (callable, name, description) for each action.
  491. """
  492. # If self.actions is explicitally set to None that means that we don't
  493. # want *any* actions enabled on this page.
  494. from django.contrib.admin.views.main import IS_POPUP_VAR
  495. if self.actions is None or IS_POPUP_VAR in request.GET:
  496. return SortedDict()
  497. actions = []
  498. # Gather actions from the admin site first
  499. for (name, func) in self.admin_site.actions:
  500. description = getattr(func, 'short_description', name.replace('_', ' '))
  501. actions.append((func, name, description))
  502. # Then gather them from the model admin and all parent classes,
  503. # starting with self and working back up.
  504. for klass in self.__class__.mro()[::-1]:
  505. class_actions = getattr(klass, 'actions', [])
  506. # Avoid trying to iterate over None
  507. if not class_actions:
  508. continue
  509. actions.extend([self.get_action(action) for action in class_actions])
  510. # get_action might have returned None, so filter any of those out.
  511. actions = filter(None, actions)
  512. # Convert the actions into a SortedDict keyed by name
  513. # and sorted by description.
  514. actions.sort(key=lambda k: k[2].lower())
  515. actions = SortedDict([
  516. (name, (func, name, desc))
  517. for func, name, desc in actions
  518. ])
  519. return actions
  520. def get_action_choices(self, request, default_choices=BLANK_CHOICE_DASH):
  521. """
  522. Return a list of choices for use in a form object. Each choice is a
  523. tuple (name, description).
  524. """
  525. choices = [] + default_choices
  526. for func, name, description in self.get_actions(request).itervalues():
  527. choice = (name, description % model_format_dict(self.opts))
  528. choices.append(choice)
  529. return choices
  530. def get_action(self, action):
  531. """
  532. Return a given action from a parameter, which can either be a callable,
  533. or the name of a method on the ModelAdmin. Return is a tuple of
  534. (callable, name, description).
  535. """
  536. # If the action is a callable, just use it.
  537. if callable(action):
  538. func = action
  539. action = action.__name__
  540. # Next, look for a method. Grab it off self.__class__ to get an unbound
  541. # method instead of a bound one; this ensures that the calling
  542. # conventions are the same for functions and methods.
  543. elif hasattr(self.__class__, action):
  544. func = getattr(self.__class__, action)
  545. # Finally, look for a named method on the admin site
  546. else:
  547. try:
  548. func = self.admin_site.get_action(action)
  549. except KeyError:
  550. return None
  551. if hasattr(func, 'short_description'):
  552. description = func.short_description
  553. else:
  554. description = capfirst(action.replace('_', ' '))
  555. return func, action, description
  556. def construct_change_message(self, request, form, formsets):
  557. """
  558. Construct a change message from a changed object.
  559. """
  560. change_message = []
  561. if form.changed_data:
  562. change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and')))
  563. if formsets:
  564. for formset in formsets:
  565. for added_object in formset.new_objects:
  566. change_message.append(_('Added %(name)s "%(object)s".')
  567. % {'name': force_unicode(added_object._meta.verbose_name),
  568. 'object': force_unicode(added_object)})
  569. for changed_object, changed_fields in formset.changed_objects:
  570. change_message.append(_('Changed %(list)s for %(name)s "%(object)s".')
  571. % {'list': get_text_list(changed_fields, _('and')),
  572. 'name': force_unicode(changed_object._meta.verbose_name),
  573. 'object': force_unicode(changed_object)})
  574. for deleted_object in formset.deleted_objects:
  575. change_message.append(_('Deleted %(name)s "%(object)s".')
  576. % {'name': force_unicode(deleted_object._meta.verbose_name),
  577. 'object': force_unicode(deleted_object)})
  578. change_message = ' '.join(change_message)
  579. return change_message or _('No fields changed.')
  580. def message_user(self, request, message):
  581. """
  582. Send a message to the user. The default implementation
  583. posts a message using the django.contrib.messages backend.
  584. """
  585. messages.info(request, message)
  586. def save_form(self, request, form, change):
  587. """
  588. Given a ModelForm return an unsaved instance. ``change`` is True if
  589. the object is being changed, and False if it's being added.
  590. """
  591. return form.save(commit=False)
  592. def save_model(self, request, obj, form, change):
  593. """
  594. Given a model instance save it to the database.
  595. """
  596. obj.save()
  597. def delete_model(self, request, obj):
  598. """
  599. Given a model instance delete it from the database.
  600. """
  601. obj.delete()
  602. def save_formset(self, request, form, formset, change):
  603. """
  604. Given an inline formset save it to the database.
  605. """
  606. formset.save()
  607. def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
  608. opts = self.model._meta
  609. app_label = opts.app_label
  610. ordered_objects = opts.get_ordered_objects()
  611. context.update({
  612. 'add': add,
  613. 'change': change,
  614. 'has_add_permission': self.has_add_permission(request),
  615. 'has_change_permission': self.has_change_permission(request, obj),
  616. 'has_delete_permission': self.has_delete_permission(request, obj),
  617. 'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
  618. 'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
  619. 'ordered_objects': ordered_objects,
  620. 'form_url': mark_safe(form_url),
  621. 'opts': opts,
  622. 'content_type_id': ContentType.objects.get_for_model(self.model).id,
  623. 'save_as': self.save_as,
  624. 'save_on_top': self.save_on_top,
  625. 'root_path': self.admin_site.root_path,
  626. })
  627. if add and self.add_form_template is not None:
  628. form_template = self.add_form_template
  629. else:
  630. form_template = self.change_form_template
  631. context_instance = template.RequestContext(request, current_app=self.admin_site.name)
  632. return render_to_response(form_template or [
  633. "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
  634. "admin/%s/change_form.html" % app_label,
  635. "admin/change_form.html"
  636. ], context, context_instance=context_instance)
  637. def response_add(self, request, obj, post_url_continue='../%s/'):
  638. """
  639. Determines the HttpResponse for the add_view stage.
  640. """
  641. opts = obj._meta
  642. pk_value = obj._get_pk_val()
  643. msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)}
  644. # Here, we distinguish between different save types by checking for
  645. # the presence of keys in request.POST.
  646. if "_continue" in request.POST:
  647. self.message_user(request, msg + ' ' + _("You may edit it again below."))
  648. if "_popup" in request.POST:
  649. post_url_continue += "?_popup=1"
  650. return HttpResponseRedirect(post_url_continue % pk_value)
  651. if "_popup" in request.POST:
  652. return HttpResponse('<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script>' % \
  653. # escape() calls force_unicode.
  654. (escape(pk_value), escapejs(obj)))
  655. elif "_addanother" in request.POST:
  656. self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name)))
  657. return HttpResponseRedirect(request.path)
  658. else:
  659. self.message_user(request, msg)
  660. # Figure out where to redirect. If the user has change permission,
  661. # redirect to the change-list page for this object. Otherwise,
  662. # redirect to the admin index.
  663. if self.has_change_permission(request, None):
  664. post_url = '../'
  665. else:
  666. post_url = '../../../'
  667. return HttpResponseRedirect(post_url)
  668. def response_change(self, request, obj):
  669. """
  670. Determines the HttpResponse for the change_view stage.
  671. """
  672. opts = obj._meta
  673. # Handle proxy models automatically created by .only() or .defer()
  674. verbose_name = opts.verbose_name
  675. if obj._deferred:
  676. opts_ = opts.proxy_for_model._meta
  677. verbose_name = opts_.verbose_name
  678. pk_value = obj._get_pk_val()
  679. msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(verbose_name), 'obj': force_unicode(obj)}
  680. if "_continue" in request.POST:
  681. self.message_user(request, msg + ' ' + _("You may edit it again below."))
  682. if "_popup" in request.REQUEST:
  683. return HttpResponseRedirect(request.path + "?_popup=1")
  684. else:
  685. return HttpResponseRedirect(request.path)
  686. elif "_saveasnew" in request.POST:
  687. msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj}
  688. self.message_user(request, msg)
  689. return HttpResponseRedirect("../%s/" % pk_value)
  690. elif "_addanother" in request.POST:
  691. self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name)))
  692. return HttpResponseRedirect("../add/")
  693. else:
  694. self.message_user(request, msg)
  695. # Figure out where to redirect. If the user has change permission,
  696. # redirect to the change-list page for this object. Otherwise,
  697. # redirect to the admin index.
  698. if self.has_change_permission(request, None):
  699. return HttpResponseRedirect('../')
  700. else:
  701. return HttpResponseRedirect('../../../')
  702. def response_action(self, request, queryset):
  703. """
  704. Handle an admin action. This is called if a request is POSTed to the
  705. changelist; it returns an HttpResponse if the action was handled, and
  706. None otherwise.
  707. """
  708. # There can be multiple action forms on the page (at the top
  709. # and bottom of the change list, for example). Get the action
  710. # whose button was pushed.
  711. try:
  712. action_index = int(request.POST.get('index', 0))
  713. except ValueError:
  714. action_index = 0
  715. # Construct the action form.
  716. data = request.POST.copy()
  717. data.pop(helpers.ACTION_CHECKBOX_NAME, None)
  718. data.pop("index", None)
  719. # Use the action whose button was pushed
  720. try:
  721. data.update({'action': data.getlist('action')[action_index]})
  722. except IndexError:
  723. # If we didn't get an action from the chosen form that's invalid
  724. # POST data, so by deleting action it'll fail the validation check
  725. # below. So no need to do anything here
  726. pass
  727. action_form = self.action_form(data, auto_id=None)
  728. action_form.fields['action'].choices = self.get_action_choices(request)
  729. # If the form's valid we can handle the action.
  730. if action_form.is_valid():
  731. action = action_form.cleaned_data['action']
  732. select_across = action_form.cleaned_data['select_across']
  733. func, name, description = self.get_actions(request)[action]
  734. # Get the list of selected PKs. If nothing's selected, we can't
  735. # perform an action on it, so bail. Except we want to perform
  736. # the action explicitly on all objects.
  737. selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
  738. if not selected and not select_across:
  739. # Reminder that something needs to be selected or nothing will happen
  740. msg = _("Items must be selected in order to perform "
  741. "actions on them. No items have been changed.")
  742. self.message_user(request, msg)
  743. return None
  744. if not select_across:
  745. # Perform the action only on the selected objects
  746. queryset = queryset.filter(pk__in=selected)
  747. response = func(self, request, queryset)
  748. # Actions may return an HttpResponse, which will be used as the
  749. # response from the POST. If not, we'll be a good little HTTP
  750. # citizen and redirect back to the changelist page.
  751. if isinstance(response, HttpResponse):
  752. return response
  753. else:
  754. return HttpResponseRedirect(request.get_full_path())
  755. else:
  756. msg = _("No action selected.")
  757. self.message_user(request, msg)
  758. return None
  759. @csrf_protect_m
  760. @transaction.commit_on_success
  761. def add_view(self, request, form_url='', extra_context=None):
  762. "The 'add' admin view for this model."
  763. model = self.model
  764. opts = model._meta
  765. if not self.has_add_permission(request):
  766. raise PermissionDenied
  767. ModelForm = self.get_form(request)
  768. formsets = []
  769. if request.method == 'POST':
  770. form = ModelForm(request.POST, request.FILES)
  771. if form.is_valid():
  772. new_object = self.save_form(request, form, change=False)
  773. form_validated = True
  774. else:
  775. form_validated = False
  776. new_object = self.model()
  777. prefixes = {}
  778. for FormSet, inline in zip(self.get_formsets(request), self.inline_instances):
  779. prefix = FormSet.get_default_prefix()
  780. prefixes[prefix] = prefixes.get(prefix, 0) + 1
  781. if prefixes[prefix] != 1:
  782. prefix = "%s-%s" % (prefix, prefixes[prefix])
  783. formset = FormSet(data=request.POST, files=request.FILES,
  784. instance=new_object,
  785. save_as_new="_saveasnew" in request.POST,
  786. prefix=prefix, queryset=inline.queryset(request))
  787. formsets.append(formset)
  788. if all_valid(formsets) and form_validated:
  789. self.save_model(request, new_object, form, change=False)
  790. form.save_m2m()
  791. for formset in formsets:
  792. self.save_formset(request, form, formset, change=False)
  793. self.log_addition(request, new_object)
  794. return self.response_add(request, new_object)
  795. else:
  796. # Prepare the dict of initial data from the request.
  797. # We have to special-case M2Ms as a list of comma-separated PKs.
  798. initial = dict(request.GET.items())
  799. for k in initial:
  800. try:
  801. f = opts.get_field(k)
  802. except models.FieldDoesNotExist:
  803. continue
  804. if isinstance(f, models.ManyToManyField):
  805. initial[k] = initial[k].split(",")
  806. form = ModelForm(initial=initial)
  807. prefixes = {}
  808. for FormSet, inline in zip(self.get_formsets(request),
  809. self.inline_instances):
  810. prefix = FormSet.get_default_prefix()
  811. prefixes[prefix] = prefixes.get(prefix, 0) + 1
  812. if prefixes[prefix] != 1:
  813. prefix = "%s-%s" % (prefix, prefixes[prefix])
  814. formset = FormSet(instance=self.model(), prefix=prefix,
  815. queryset=inline.queryset(request))
  816. formsets.append(formset)
  817. adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
  818. self.prepopulated_fields, self.get_readonly_fields(request),
  819. model_admin=self)
  820. media = self.media + adminForm.media
  821. inline_admin_formsets = []
  822. for inline, formset in zip(self.inline_instances, formsets):
  823. fieldsets = list(inline.get_fieldsets(request))
  824. readonly = list(inline.get_readonly_fields(request))
  825. inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
  826. fieldsets, readonly, model_admin=self)
  827. inline_admin_formsets.append(inline_admin_formset)
  828. media = media + inline_admin_formset.media
  829. context = {
  830. 'title': _('Add %s') % force_unicode(opts.verbose_name),
  831. 'adminform': adminForm,
  832. 'is_popup': "_popup" in request.REQUEST,
  833. 'show_delete': False,
  834. 'media': mark_safe(media),
  835. 'inline_admin_formsets': inline_admin_formsets,
  836. 'errors': helpers.AdminErrorList(form, formsets),
  837. 'root_path': self.admin_site.root_path,
  838. 'app_label': opts.app_label,
  839. }
  840. context.update(extra_context or {})
  841. return self.render_change_form(request, context, form_url=form_url, add=True)
  842. @csrf_protect_m
  843. @transaction.commit_on_success
  844. def change_view(self, request, object_id, extra_context=None):
  845. "The 'change' admin view for this model."
  846. model = self.model
  847. opts = model._meta
  848. obj = self.get_object(request, unquote(object_id))
  849. if not self.has_change_permission(request, obj):
  850. raise PermissionDenied
  851. if obj is None:
  852. raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
  853. if request.method == 'POST' and "_saveasnew" in request.POST:
  854. return self.add_view(request, form_url='../add/')
  855. ModelForm = self.get_form(request, obj)
  856. formsets = []
  857. if request.method == 'POST':
  858. form = ModelForm(request.POST, request.FILES, instance=obj)
  859. if form.is_valid():
  860. form_validated = True
  861. new_object = self.save_form(request, form, change=True)
  862. else:
  863. form_validated = False
  864. new_object = obj
  865. prefixes = {}
  866. for FormSet, inline in zip(self.get_formsets(request, new_object),
  867. self.inline_instances):
  868. prefix = FormSet.get_default_prefix()
  869. prefixes[prefix] = prefixes.get(prefix, 0) + 1
  870. if prefixes[prefix] != 1:
  871. prefix = "%s-%s" % (prefix, prefixes[prefix])
  872. formset = FormSet(request.POST, request.FILES,
  873. instance=new_object, prefix=prefix,
  874. queryset=inline.queryset(request))
  875. formsets.append(formset)
  876. if all_valid(formsets) and form_validated:
  877. self.save_model(request, new_object, form, change=True)
  878. form.save_m2m()
  879. for formset in formsets:
  880. self.save_formset(request, form, formset, change=True)
  881. change_message = self.construct_change_message(request, form, formsets)
  882. self.log_change(request, new_object, change_message)
  883. return self.response_change(request, new_object)
  884. else:
  885. form = ModelForm(instance=obj)
  886. prefixes = {}
  887. for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
  888. prefix = FormSet.get_default_prefix()
  889. prefixes[prefix] = prefixes.get(prefix, 0) + 1
  890. if prefixes[prefix] != 1:
  891. prefix = "%s-%s" % (prefix, prefixes[prefix])
  892. formset = FormSet(instance=obj, prefix=prefix,
  893. queryset=inline.queryset(request))
  894. formsets.append(formset)
  895. adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
  896. self.prepopulated_fields, self.get_readonly_fields(request, obj),
  897. model_admin=self)
  898. media = self.media + adminForm.media
  899. inline_admin_formsets = []
  900. for inline, formset in zip(self.inline_instances, formsets):
  901. fieldsets = list(inline.get_fieldsets(request, obj))
  902. readonly = list(inline.get_readonly_fields(request, obj))
  903. inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
  904. fieldsets, readonly, model_admin=self)
  905. inline_admin_formsets.append(inline_admin_formset)
  906. media = media + inline_admin_formset.media
  907. context = {
  908. 'title': _('Change %s') % force_unicode(opts.verbose_name),
  909. 'adminform': adminForm,
  910. 'object_id': object_id,
  911. 'original': obj,
  912. 'is_popup': "_popup" in request.REQUEST,
  913. 'media': mark_safe(media),
  914. 'inline_admin_formsets': inline_admin_formsets,
  915. 'errors': helpers.AdminErrorList(form, formsets),
  916. 'root_path': self.admin_site.root_path,
  917. 'app_label': opts.app_label,
  918. }
  919. context.update(extra_context or {})
  920. return self.render_change_form(request, context, change=True, obj=obj)
  921. @csrf_protect_m
  922. def changelist_view(self, request, extra_context=None):
  923. "The 'change list' admin view for this model."
  924. from django.contrib.admin.views.main import ERROR_FLAG
  925. opts = self.model._meta
  926. app_label = opts.app_label
  927. if not self.has_change_permission(request, None):
  928. raise PermissionDenied
  929. # Check actions to see if any are available on this changelist
  930. actions = self.get_actions(request)
  931. # Remove action checkboxes if there aren't any actions available.
  932. list_display = list(self.list_display)
  933. if not actions:
  934. try:
  935. list_display.remove('action_checkbox')
  936. except ValueError:
  937. pass
  938. ChangeList = self.get_changelist(request)
  939. try:
  940. cl = ChangeList(request, self.model, list_display, self.list_display_links,
  941. self.list_filter, self.date_hierarchy, self.search_fields,
  942. self.list_select_related, self.list_per_page, self.list_editable, self)
  943. except IncorrectLookupParameters:
  944. # Wacky lookup parameters were given, so redirect to the main
  945. # changelist page, without parameters, and pass an 'invalid=1'
  946. # parameter via the query string. If wacky parameters were given
  947. # and the 'invalid=1' parameter was already in the query string,
  948. # something is screwed up with the database, so display an error
  949. # page.
  950. if ERROR_FLAG in request.GET.keys():
  951. return render_to_response('admin/invalid_setup.html', {'title': _('Database error')})
  952. return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')
  953. # If the request was POSTed, this might be a bulk action or a bulk
  954. # edit. Try to look up an action or confirmation first, but if this
  955. # isn't an action the POST will fall through to the bulk edit check,
  956. # below.
  957. action_failed = False
  958. selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)
  959. # Actions with no confirmation
  960. if (actions and request.method == 'POST' and
  961. 'index' in request.POST and '_save' not in request.POST):
  962. if selected:
  963. response = self.response_action(request, queryset=cl.get_query_set())
  964. if response:
  965. return response
  966. else:
  967. action_failed = True
  968. else:
  969. msg = _("Items must be selected in order to perform "
  970. "actions on them. No items have been changed.")
  971. self.message_user(request, msg)
  972. action_failed = True
  973. # Actions with confirmation
  974. if (actions and request.method == 'POST' and
  975. helpers.ACTION_CHECKBOX_NAME in request.POST and
  976. 'index' not in request.POST and '_save' not in request.POST):
  977. if selected:
  978. response = self.response_action(request, queryset=cl.get_query_set())
  979. if response:
  980. return response
  981. else:
  982. action_failed = True
  983. # If we're allowing changelist editing, we need to construct a formset
  984. # for the changelist given all the fields to be edited. Then we'll
  985. # use the formset to validate/process POSTed data.
  986. formset = cl.formset = None
  987. # Handle POSTed bulk-edit data.
  988. if (request.method == "POST" and cl.list_editable and
  989. '_save' in request.POST and not action_failed):
  990. FormSet = self.get_changelist_formset(request)
  991. formset = cl.formset = FormSet(request.POST, request.FILES, queryset=cl.result_list)
  992. if formset.is_valid():
  993. changecount = 0
  994. for form in formset.forms:
  995. if form.has_changed():
  996. obj = self.save_form(request, form, change=True)
  997. self.save_model(request, obj, form, change=True)
  998. form.save_m2m()
  999. change_msg = self.construct_change_message(request, form, None)
  1000. self.log_change(request, obj, change_msg)
  1001. changecount += 1
  1002. if changecount:
  1003. if changecount == 1:
  1004. name = force_unicode(opts.verbose_name)
  1005. else:
  1006. name = force_unicode(opts.verbose_name_plural)
  1007. msg = ungettext("%(count)s %(name)s was changed successfully.",
  1008. "%(count)s %(name)s were changed successfully.",
  1009. changecount) % {'count': changecount,
  1010. 'name': name,
  1011. 'obj': force_unicode(obj)}
  1012. self.message_user(request, msg)
  1013. return HttpResponseRedirect(request.get_full_path())
  1014. # Handle GET -- construct a formset for display.
  1015. elif cl.list_editable:
  1016. FormSet = self.get_changelist_formset(request)
  1017. formset = cl.formset = FormSet(queryset=cl.result_list)
  1018. # Build the list of media to be used by the formset.
  1019. if formset:
  1020. media = self.media + formset.media
  1021. else:
  1022. media = self.media
  1023. # Build the action form and populate it with available actions.
  1024. if actions:
  1025. action_form = self.action_form(auto_id=None)
  1026. action_form.fields['action'].choices = self.get_action_choices(request)
  1027. else:
  1028. action_form = None
  1029. selection_note_all = ungettext('%(total_count)s selected',
  1030. 'All %(total_count)s selected', cl.result_count)
  1031. context = {
  1032. 'module_name': force_unicode(opts.verbose_name_plural),
  1033. 'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(cl.result_list)},
  1034. 'selection_note_all': selection_note_all % {'total_count': cl.result_count},
  1035. 'title': cl.title,
  1036. 'is_popup': cl.is_popup,
  1037. 'cl': cl,
  1038. 'media': media,
  1039. 'has_add_permission': self.has_add_permission(request),
  1040. 'root_path': self.admin_site.root_path,
  1041. 'app_label': app_label,
  1042. 'action_form': action_form,
  1043. 'actions_on_top': self.actions_on_top,
  1044. 'actions_on_bottom': self.actions_on_bottom,
  1045. 'actions_selection_counter': self.actions_selection_counter,
  1046. }
  1047. context.update(extra_context or {})
  1048. context_instance = template.RequestContext(request, current_app=self.admin_site.name)
  1049. return render_to_response(self.change_list_template or [
  1050. 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
  1051. 'admin/%s/change_list.html' % app_label,
  1052. 'admin/change_list.html'
  1053. ], context, context_instance=context_instance)
  1054. @csrf_protect_m
  1055. @transaction.commit_on_success
  1056. def delete_view(self, request, object_id, extra_context=None):
  1057. "The 'delete' admin view for this model."
  1058. opts = self.model._meta
  1059. app_label = opts.app_label
  1060. obj = self.get_object(request, unquote(object_id))
  1061. if not self.has_delete_permission(request, obj):
  1062. raise PermissionDenied
  1063. if obj is None:
  1064. raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)})
  1065. using = router.db_for_write(self.model)
  1066. # Populate deleted_objects, a data structure of all related objects that
  1067. # will also be deleted.
  1068. (deleted_objects, perms_needed, protected) = get_deleted_objects(
  1069. [obj], opts, request.user, self.admin_site, using)
  1070. if request.POST: # The user has already confirmed the deletion.
  1071. if perms_needed:
  1072. raise PermissionDenied
  1073. obj_display = force_unicode(obj)
  1074. self.log_deletion(request, obj, obj_display)
  1075. self.delete_model(request, obj)
  1076. self.message_user(request, _('The %(name)s "%(obj)s" was deleted successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj_display)})
  1077. if not self.has_change_permission(request, None):
  1078. return HttpResponseRedirect("../../../../")
  1079. return HttpResponseRedirect("../../")
  1080. object_name = force_unicode(opts.verbose_name)
  1081. if perms_needed or protected:
  1082. title = _("Cannot delete %(name)s") % {"name": object_name}
  1083. else:
  1084. title = _("Are you sure?")
  1085. context = {
  1086. "title": title,
  1087. "object_name": object_name,
  1088. "object": obj,
  1089. "deleted_objects": deleted_objects,
  1090. "perms_lacking": perms_needed,
  1091. "protected": protected,
  1092. "opts": opts,
  1093. "root_path": self.admin_site.root_path,
  1094. "app_label": app_label,
  1095. }
  1096. context.update(extra_context or {})
  1097. context_instance = template.RequestContext(request, current_app=self.admin_site.name)
  1098. return render_to_response(self.delete_confirmation_template or [
  1099. "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
  1100. "admin/%s/delete_confirmation.html" % app_label,
  1101. "admin/delete_confirmation.html"
  1102. ], context, context_instance=context_instance)
  1103. def history_view(self, request, object_id, extra_context=None):
  1104. "The 'history' admin view for this model."
  1105. from django.contrib.admin.models import LogEntry
  1106. model = self.model
  1107. opts = model._meta
  1108. app_label = opts.app_label
  1109. action_list = LogEntry.objects.filter(
  1110. object_id = object_id,
  1111. content_type__id__exact = ContentType.objects.get_for_model(model).id
  1112. ).select_related().order_by('action_time')
  1113. # If no history was found, see whether this object even exists.
  1114. obj = get_object_or_404(model, pk=unquote(object_id))
  1115. context = {
  1116. 'title': _('Change history: %s') % force_unicode(obj),
  1117. 'action_list': action_list,
  1118. 'module_name': capfirst(force_unicode(opts.verbose_name_plural)),
  1119. 'object': obj,
  1120. 'root_path': self.admin_site.root_path,
  1121. 'app_label': app_label,
  1122. }
  1123. context.update(extra_context or {})
  1124. context_instance = template.RequestContext(request, current_app=self.admin_site.name)
  1125. return render_to_response(self.object_history_template or [
  1126. "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
  1127. "admin/%s/object_history.html" % app_label,
  1128. "admin/object_history.html"
  1129. ], context, context_instance=context_instance)
  1130. class InlineModelAdmin(BaseModelAdmin):
  1131. """
  1132. Options for inline editing of ``model`` instances.
  1133. Provide ``name`` to specify the attribute name of the ``ForeignKey`` from
  1134. ``model`` to its parent. This is required if ``model`` has more than one
  1135. ``ForeignKey`` to its parent.
  1136. """
  1137. model = None
  1138. fk_name = None
  1139. formset = BaseInlineFormSet
  1140. extra = 3
  1141. max_num = None
  1142. template = None
  1143. verbose_name = None
  1144. verbose_name_plural = None
  1145. can_delete = True
  1146. def __init__(self, parent_model, admin_site):
  1147. self.admin_site = admin_site
  1148. self.parent_model = parent_model
  1149. self.opts = self.model._meta
  1150. super(InlineModelAdmin, self).__init__()
  1151. if self.verbose_name is None:
  1152. self.verbose_name = self.model._meta.verbose_name
  1153. if self.verbose_name_plural is None:
  1154. self.verbose_name_plural = self.model._meta.verbose_name_plural
  1155. def _media(self):
  1156. from django.conf import settings
  1157. js = ['js/jquery.min.js', 'js/jquery.init.js', 'js/inlines.min.js']
  1158. if self.prepopulated_fields:
  1159. js.append('js/urlify.js')
  1160. js.append('js/prepopulate.min.js')
  1161. if self.filter_vertical or self.filter_horizontal:
  1162. js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
  1163. return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
  1164. media = property(_media)
  1165. def get_formset(self, request, obj=None, **kwargs):
  1166. """Returns a BaseInlineFormSet class for use in admin add/change views."""
  1167. if self.declared_fieldsets:
  1168. fields = flatten_fieldsets(self.declared_fieldsets)
  1169. else:
  1170. fields = None
  1171. if self.exclude is None:
  1172. exclude = []
  1173. else:
  1174. exclude = list(self.exclude)
  1175. exclude.extend(kwargs.get("exclude", []))
  1176. exclude.extend(self.get_readonly_fields(request, obj))
  1177. # if exclude is an empty list we use None, since that's the actual
  1178. # default
  1179. exclude = exclude or None
  1180. defaults = {
  1181. "form": self.form,
  1182. "formset": self.formset,
  1183. "fk_name": self.fk_name,
  1184. "fields": fields,
  1185. "exclude": exclude,
  1186. "formfield_callback": curry(self.formfield_for_dbfield, request=request),
  1187. "extra": self.extra,
  1188. "max_num": self.max_num,
  1189. "can_delete": self.can_delete,
  1190. }
  1191. defaults.update(kwargs)
  1192. return inlineformset_factory(self.parent_model, self.model, **defaults)
  1193. def get_fieldsets(self, request, obj=None):
  1194. if self.declared_fieldsets:
  1195. return self.declared_fieldsets
  1196. form = self.get_formset(request).form
  1197. fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
  1198. return [(None, {'fields': fields})]
  1199. class StackedInline(InlineModelAdmin):
  1200. template = 'admin/edit_inline/stacked.html'
  1201. class TabularInline(InlineModelAdmin):
  1202. template = 'admin/edit_inline/tabular.html'