Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge remote-tracking branch 'core/master' into schema-alteration

Conflicts:
	django/core/management/commands/flush.py
	django/core/management/commands/syncdb.py
	django/db/models/loading.py
	docs/internals/deprecation.txt
	docs/ref/django-admin.txt
	docs/releases/1.7.txt
  • Loading branch information...
commit de64c4d6e97c980fb4c0ace045fc4070b3f763d9 2 parents fddc595 + b575d69
@andrewgodwin andrewgodwin authored
Showing with 2,807 additions and 1,216 deletions.
  1. +3 −0  AUTHORS
  2. +4 −1 django/conf/__init__.py
  3. +2 −1  django/conf/urls/__init__.py
  4. +1 −1  django/contrib/admin/__init__.py
  5. +1 −1  django/contrib/admin/filters.py
  6. +5 −3 django/contrib/admin/helpers.py
  7. +5 −2 django/contrib/admin/models.py
  8. +126 −91 django/contrib/admin/options.py
  9. +10 −22 django/contrib/admin/static/admin/css/base.css
  10. +4 −4 django/contrib/admin/static/admin/css/widgets.css
  11. BIN  django/contrib/admin/static/admin/img/chooser-bg.gif
  12. BIN  django/contrib/admin/static/admin/img/chooser_stacked-bg.gif
  13. BIN  django/contrib/admin/static/admin/img/tool-left.gif
  14. BIN  django/contrib/admin/static/admin/img/tool-left_over.gif
  15. BIN  django/contrib/admin/static/admin/img/tool-right.gif
  16. BIN  django/contrib/admin/static/admin/img/tool-right_over.gif
  17. BIN  django/contrib/admin/static/admin/img/tooltag-add.gif
  18. BIN  django/contrib/admin/static/admin/img/tooltag-add.png
  19. BIN  django/contrib/admin/static/admin/img/tooltag-add_over.gif
  20. BIN  django/contrib/admin/static/admin/img/tooltag-arrowright.gif
  21. BIN  django/contrib/admin/static/admin/img/tooltag-arrowright.png
  22. BIN  django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif
  23. +2 −2 django/contrib/admin/static/admin/js/collapse.min.js
  24. +9 −9 django/contrib/admin/static/admin/js/inlines.min.js
  25. +23 −18 django/contrib/admin/static/admin/js/prepopulate.js
  26. +1 −1  django/contrib/admin/static/admin/js/prepopulate.min.js
  27. +2 −6 django/contrib/admin/templates/registration/password_change_done.html
  28. +2 −2 django/contrib/admin/templates/registration/password_change_form.html
  29. +2 −3 django/contrib/admin/templates/registration/password_reset_complete.html
  30. +2 −6 django/contrib/admin/templates/registration/password_reset_confirm.html
  31. +2 −4 django/contrib/admin/templates/registration/password_reset_done.html
  32. +2 −4 django/contrib/admin/templates/registration/password_reset_form.html
  33. +5 −4 django/contrib/admin/templatetags/admin_list.py
  34. +1 −1  django/contrib/admin/templatetags/admin_modify.py
  35. +3 −3 django/contrib/admin/views/main.py
  36. +10 −1 django/contrib/admin/widgets.py
  37. +1 −1  django/contrib/admindocs/tests/test_fields.py
  38. +2 −2 django/contrib/admindocs/views.py
  39. +5 −2 django/contrib/auth/admin.py
  40. +3 −1 django/contrib/auth/backends.py
  41. +38 −9 django/contrib/auth/forms.py
  42. +10 −10 django/contrib/auth/hashers.py
  43. +1 −0  django/contrib/auth/tests/templates/registration/html_password_reset_email.html
  44. +28 −1 django/contrib/auth/tests/test_auth_backends.py
  45. +1 −1  django/contrib/auth/tests/test_context_processors.py
  46. +91 −1 django/contrib/auth/tests/test_forms.py
  47. +59 −0 django/contrib/auth/tests/test_templates.py
  48. +85 −8 django/contrib/auth/tests/test_views.py
  49. +1 −0  django/contrib/auth/tests/urls.py
  50. +17 −6 django/contrib/auth/views.py
  51. +1 −1  django/contrib/comments/__init__.py
  52. +0 −2  django/contrib/comments/views/comments.py
  53. +0 −2  django/contrib/comments/views/moderation.py
  54. +3 −3 django/contrib/contenttypes/generic.py
  55. +1 −0  django/contrib/flatpages/tests/test_csrf.py
  56. +1 −1  django/contrib/formtools/preview.py
  57. +0 −2  django/contrib/formtools/tests/urls.py
  58. +1 −1  django/contrib/formtools/tests/wizard/storage.py
  59. +2 −1  django/contrib/formtools/tests/wizard/test_forms.py
  60. +1 −2  django/contrib/formtools/tests/wizard/wizardtests/forms.py
  61. +12 −10 django/contrib/formtools/wizard/views.py
  62. +1 −1  django/contrib/gis/admin/options.py
  63. +3 −3 django/contrib/gis/db/backends/spatialite/creation.py
  64. +1 −0  django/contrib/gis/db/models/fields.py
  65. +2 −2 django/contrib/gis/gdal/geometries.py
  66. +3 −3 django/contrib/gis/gdal/tests/test_envelope.py
  67. +9 −12 django/contrib/gis/gdal/tests/test_geom.py
  68. +2 −2 django/contrib/gis/gdal/tests/test_srs.py
  69. +0 −2  django/contrib/gis/geoip/__init__.py
  70. +2 −1  django/contrib/gis/geoip/base.py
  71. +1 −1  django/contrib/gis/geoip/prototypes.py
  72. +3 −2 django/contrib/gis/geometry/backend/__init__.py
  73. +0 −1  django/contrib/gis/geos/geometry.py
  74. +5 −13 django/contrib/gis/geos/tests/test_geos.py
  75. +1 −1  django/contrib/gis/tests/distapp/tests.py
  76. +1 −1  django/contrib/gis/tests/geo3d/tests.py
  77. +1 −1  django/contrib/gis/tests/geoadmin/tests.py
  78. +1 −1  django/contrib/gis/tests/geoapp/feeds.py
  79. +0 −2  django/contrib/gis/tests/geoapp/sitemaps.py
  80. +1 −1  django/contrib/gis/tests/geoapp/test_feeds.py
  81. +1 −1  django/contrib/gis/tests/geoapp/test_regress.py
  82. +1 −1  django/contrib/gis/tests/geoapp/test_sitemaps.py
  83. +1 −1  django/contrib/gis/tests/geoapp/tests.py
  84. +1 −1  django/contrib/gis/tests/geoapp/urls.py
  85. +1 −1  django/contrib/gis/tests/geogapp/tests.py
  86. +1 −1  django/contrib/gis/tests/inspectapp/tests.py
  87. +1 −1  django/contrib/gis/tests/layermap/tests.py
  88. +1 −1  django/contrib/gis/tests/relatedapp/tests.py
  89. +1 −1  django/contrib/gis/tests/test_spatialrefsys.py
  90. +0 −2  django/contrib/messages/__init__.py
  91. +2 −1  django/contrib/sessions/management/commands/clearsessions.py
  92. +1 −1  django/contrib/sessions/middleware.py
  93. +11 −1 django/contrib/sitemaps/__init__.py
  94. +15 −0 django/contrib/sitemaps/tests/test_http.py
  95. +28 −0 django/contrib/sitemaps/tests/urls/http.py
  96. +10 −2 django/contrib/sitemaps/views.py
  97. +5 −4 django/contrib/staticfiles/finders.py
  98. +2 −2 django/contrib/staticfiles/management/commands/collectstatic.py
  99. +4 −4 django/contrib/staticfiles/storage.py
  100. +1 −4 django/contrib/staticfiles/views.py
  101. +1 −1  django/core/cache/__init__.py
  102. +1 −1  django/core/cache/utils.py
  103. +1 −1  django/core/handlers/wsgi.py
  104. +7 −3 django/core/mail/__init__.py
  105. +4 −4 django/core/management/__init__.py
  106. +8 −2 django/core/management/base.py
  107. +1 −1  django/core/management/color.py
  108. +6 −5 django/core/management/commands/dumpdata.py
  109. +1 −0  django/core/management/commands/flush.py
  110. +3 −3 django/core/management/commands/inspectdb.py
  111. +8 −6 django/core/management/commands/loaddata.py
  112. +2 −2 django/core/management/commands/migrate.py
  113. +7 −1 django/core/management/commands/runfcgi.py
  114. +26 −14 django/core/management/commands/shell.py
  115. +2 −1  django/core/management/commands/startapp.py
  116. +2 −1  django/core/management/commands/startproject.py
  117. +17 −3 django/core/management/sql.py
  118. +1 −1  django/core/management/utils.py
  119. +2 −1  django/core/serializers/__init__.py
  120. +1 −0  django/core/serializers/json.py
  121. +1 −1  django/core/servers/fastcgi.py
  122. +1 −1  django/core/urlresolvers.py
  123. +1 −1  django/db/backends/__init__.py
  124. +1 −1  django/db/backends/creation.py
  125. +1 −1  django/db/backends/oracle/base.py
  126. +1 −1  django/db/backends/oracle/compiler.py
  127. +1 −1  django/db/backends/sqlite3/base.py
  128. +2 −2 django/db/models/__init__.py
  129. +2 −2 django/db/models/deletion.py
  130. +24 −3 django/db/models/fields/__init__.py
  131. +1 −0  django/db/models/fields/files.py
  132. +13 −7 django/db/models/fields/related.py
  133. +34 −17 django/db/models/loading.py
  134. +52 −122 django/db/models/manager.py
  135. +12 −11 django/db/models/options.py
  136. +93 −26 django/db/models/query.py
  137. +0 −2  django/db/models/sql/__init__.py
  138. +32 −36 django/db/models/sql/query.py
  139. +0 −12 django/db/models/sql/subqueries.py
  140. +0 −2  django/db/models/sql/where.py
  141. +2 −2 django/db/utils.py
  142. +0 −2  django/forms/__init__.py
  143. +0 −2  django/forms/extras/__init__.py
  144. +28 −9 django/forms/fields.py
  145. +13 −7 django/forms/forms.py
  146. +1 −1  django/forms/formsets.py
  147. +7 −6 django/forms/models.py
  148. +8 −1 django/forms/widgets.py
  149. +1 −1  django/http/cookie.py
  150. +8 −3 django/http/request.py
  151. +3 −3 django/http/response.py
  152. +3 −2 django/middleware/locale.py
  153. +2 −2 django/template/base.py
  154. +1 −1  django/template/loaders/app_directories.py
  155. +27 −12 django/template/loaders/cached.py
  156. +1 −1  django/test/client.py
  157. +1 −1  django/test/simple.py
  158. +5 −1 django/utils/datastructures.py
  159. +2 −2 django/utils/formats.py
  160. +6 −9 django/utils/html.py
  161. +5 −0 django/utils/importlib.py
  162. +1 −1  django/utils/module_loading.py
  163. +5 −5 django/utils/translation/trans_real.py
  164. +6 −0 django/views/decorators/debug.py
  165. +7 −4 django/views/defaults.py
  166. +12 −8 django/views/generic/detail.py
  167. +14 −6 django/views/generic/list.py
  168. +1 −1  django/views/i18n.py
  169. +5 −5 docs/Makefile
  170. +6 −0 docs/_ext/djangodocs.py
  171. +3 −3 docs/howto/custom-management-commands.txt
  172. +15 −0 docs/howto/deployment/checklist.txt
  173. +3 −0  docs/howto/deployment/fastcgi.txt
  174. +7 −1 docs/howto/deployment/index.txt
  175. +4 −4 docs/howto/error-reporting.txt
  176. +1 −1  docs/howto/initial-data.txt
  177. +1 −1  docs/index.txt
  178. +13 −3 docs/internals/contributing/writing-code/unit-tests.txt
  179. +14 −0 docs/internals/deprecation.txt
  180. +0 −6 docs/intro/tutorial02.txt
  181. +0 −45 docs/intro/tutorial03.txt
  182. +1 −1  docs/intro/tutorial05.txt
  183. +5 −0 docs/intro/whatsnext.txt
  184. +2 −0  docs/ref/class-based-views/base.txt
  185. +1 −1  docs/ref/class-based-views/mixins-date-based.txt
  186. +8 −0 docs/ref/class-based-views/mixins-multiple-object.txt
  187. +8 −0 docs/ref/class-based-views/mixins-single-object.txt
  188. +1 −1  docs/ref/contrib/admin/actions.txt
  189. +30 −1 docs/ref/contrib/admin/index.txt
  190. +8 −6 docs/ref/contrib/auth.txt
  191. +1 −1  docs/ref/contrib/comments/moderation.txt
  192. +9 −0 docs/ref/contrib/sitemaps.txt
  193. +7 −1 docs/ref/contrib/staticfiles.txt
  194. +12 −14 docs/ref/contrib/syndication.txt
  195. +21 −5 docs/ref/django-admin.txt
  196. +31 −17 docs/ref/exceptions.txt
  197. +14 −1 docs/ref/forms/api.txt
  198. +40 −1 docs/ref/forms/fields.txt
  199. +5 −11 docs/ref/forms/validation.txt
  200. +3 −4 docs/ref/forms/widgets.txt
  201. +1 −1  docs/ref/middleware.txt
  202. +17 −5 docs/ref/models/fields.txt
  203. +14 −10 docs/ref/models/instances.txt
  204. +3 −4 docs/ref/models/options.txt
  205. +103 −42 docs/ref/models/querysets.txt
  206. +20 −0 docs/ref/request-response.txt
  207. +2 −2 docs/ref/settings.txt
  208. +1 −1  docs/ref/templates/api.txt
  209. +62 −0 docs/ref/utils.txt
  210. +1 −1  docs/releases/1.2.1.txt
  211. +2 −2 docs/releases/1.3.txt
  212. +1 −1  docs/releases/1.5-beta-1.txt
  213. +22 −7 docs/releases/1.6.txt
  214. +104 −4 docs/releases/1.7.txt
  215. +134 −9 docs/topics/auth/default.txt
  216. +4 −0 docs/topics/cache.txt
  217. +3 −3 docs/topics/class-based-views/mixins.txt
  218. +121 −2 docs/topics/db/managers.txt
  219. +1 −1  docs/topics/db/queries.txt
  220. +1 −1  docs/topics/db/tablespaces.txt
  221. +1 −1  docs/topics/db/transactions.txt
  222. +9 −1 docs/topics/email.txt
  223. +26 −8 docs/topics/forms/formsets.txt
  224. +9 −2 docs/topics/forms/modelforms.txt
  225. +2 −1  docs/topics/http/middleware.txt
  226. +1 −1  docs/topics/http/sessions.txt
  227. +2 −2 docs/topics/http/urls.txt
  228. +7 −7 docs/topics/http/views.txt
  229. +4 −5 docs/topics/install.txt
  230. +18 −62 docs/topics/localflavor.txt
  231. +3 −1 docs/topics/templates.txt
  232. +1 −1  docs/topics/testing/advanced.txt
  233. +3 −3 docs/topics/testing/overview.txt
  234. +7 −2 tests/admin_changelist/admin.py
  235. +13 −5 tests/admin_changelist/tests.py
  236. +1 −1  tests/admin_custom_urls/tests.py
  237. +0 −3  tests/admin_docs/urls.py
  238. +1 −1  tests/admin_filters/tests.py
  239. +0 −2  tests/admin_inlines/admin.py
  240. +1 −1  tests/admin_inlines/tests.py
  241. +0 −2  tests/admin_inlines/urls.py
  242. +1 −1  tests/admin_ordering/tests.py
  243. +1 −1  tests/admin_registration/tests.py
  244. +0 −2  tests/admin_scripts/complex_app/admin/foo.py
  245. +0 −2  tests/admin_scripts/complex_app/models/bar.py
  246. +9 −0 tests/admin_scripts/management/commands/color_command.py
  247. +0 −2  tests/admin_scripts/simple_app/models.py
  248. +35 −20 tests/admin_scripts/tests.py
  249. +3 −3 tests/admin_util/tests.py
  250. +6 −3 tests/admin_validation/tests.py
  251. +1 −1  tests/admin_views/admin.py
  252. +2 −1  tests/admin_views/customadmin.py
  253. +2 −2 tests/admin_views/models.py
  254. +83 −6 tests/admin_views/tests.py
  255. +0 −2  tests/admin_views/urls.py
  256. +3 −1 tests/admin_widgets/models.py
  257. +120 −13 tests/admin_widgets/tests.py
  258. +0 −2  tests/admin_widgets/urls.py
  259. +2 −7 tests/admin_widgets/widgetadmin.py
  260. +33 −1 tests/aggregation/tests.py
  261. +8 −1 tests/aggregation_regress/tests.py
  262. +27 −5 tests/app_loading/tests.py
  263. +7 −0 tests/backends/models.py
  264. +30 −11 tests/backends/tests.py
  265. +58 −1 tests/basic/tests.py
  266. +0 −2  tests/bug639/tests.py
  267. +0 −2  tests/bug8245/admin.py
  268. +1 −1  tests/bulk_create/tests.py
  269. +1 −1  tests/cache/tests.py
  270. +0 −2  tests/choices/tests.py
  271. +0 −2  tests/comment_tests/tests/__init__.py
  272. +0 −2  tests/comment_tests/tests/test_app_api.py
  273. +0 −2  tests/comment_tests/tests/test_comment_form.py
  274. +0 −2  tests/comment_tests/tests/test_comment_utils_moderators.py
  275. +1 −1  tests/comment_tests/tests/test_comment_view.py
  276. +0 −2  tests/comment_tests/tests/test_feeds.py
  277. +0 −2  tests/comment_tests/tests/test_models.py
  278. +1 −1  tests/comment_tests/tests/test_moderation_views.py
  279. +0 −2  tests/comment_tests/tests/test_templatetags.py
  280. +0 −2  tests/comment_tests/urls.py
  281. +0 −3  tests/conditional_processing/views.py
  282. +1 −1  tests/contenttypes_tests/models.py
  283. +1 −1  tests/contenttypes_tests/tests.py
  284. +1 −1  tests/contenttypes_tests/urls.py
  285. +0 −2  tests/context_processors/urls.py
  286. +1 −1  tests/custom_columns/tests.py
  287. +1 −1  tests/custom_columns_regress/tests.py
  288. +46 −6 tests/custom_managers/models.py
  289. +43 −1 tests/custom_managers/tests.py
  290. +1 −1  tests/custom_managers_regress/tests.py
  291. +1 −1  tests/custom_methods/tests.py
  292. +1 −1  tests/custom_pk/models.py
  293. +1 −1  tests/custom_pk/tests.py
  294. +1 −1  tests/datatypes/tests.py
  295. +1 −1  tests/dates/tests.py
  296. +1 −1  tests/datetimes/tests.py
  297. +4 −3 tests/defaultfilters/tests.py
  298. +1 −1  tests/defer/tests.py
  299. +1 −1  tests/defer_regress/tests.py
  300. +1 −1  tests/delete/tests.py
Sorry, we could not display the entire diff because too many files (490) changed.
View
3  AUTHORS
@@ -161,6 +161,7 @@ answer newbie questions, and generally made Django that much better:
Paul Collier <paul@paul-collier.com>
Paul Collins <paul.collins.iii@gmail.com>
Robert Coup
+ Alex Couper <http://alexcouper.com/>
Deric Crago <deric.crago@gmail.com>
Brian Fabian Crain <http://www.bfc.do/>
David Cramer <dcramer@gmail.com>
@@ -416,6 +417,7 @@ answer newbie questions, and generally made Django that much better:
Zain Memon
Christian Metts
michal@plovarna.cz
+ Justin Michalicek <jmichalicek@gmail.com>
Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com>
@@ -542,6 +544,7 @@ answer newbie questions, and generally made Django that much better:
smurf@smurf.noris.de
Vsevolod Solovyov
George Song <george@damacy.net>
+ Jimmy Song <jaejoon@gmail.com>
sopel
Leo Soto <leo.soto@gmail.com>
Thomas Sorrel
View
5 django/conf/__init__.py
@@ -6,6 +6,7 @@
a list of all possible variables.
"""
+import importlib
import logging
import os
import sys
@@ -15,7 +16,6 @@
from django.conf import global_settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import LazyObject, empty
-from django.utils import importlib
from django.utils.module_loading import import_by_path
from django.utils import six
@@ -107,6 +107,9 @@ def __setattr__(self, name, value):
elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types):
raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set "
"to a tuple, not a string.")
+ elif name == "INSTALLED_APPS" and len(value) != len(set(value)):
+ raise ImproperlyConfigured("The INSTALLED_APPS setting must contain unique values.")
+
object.__setattr__(self, name, value)
View
3  django/conf/urls/__init__.py
@@ -1,7 +1,8 @@
+from importlib import import_module
+
from django.core.urlresolvers import (RegexURLPattern,
RegexURLResolver, LocaleRegexURLResolver)
from django.core.exceptions import ImproperlyConfigured
-from django.utils.importlib import import_module
from django.utils import six
View
2  django/contrib/admin/__init__.py
@@ -17,8 +17,8 @@ def autodiscover():
"""
import copy
+ from importlib import import_module
from django.conf import settings
- from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
for app in settings.INSTALLED_APPS:
View
2  django/contrib/admin/filters.py
@@ -87,7 +87,7 @@ def value(self):
def lookups(self, request, model_admin):
"""
- Must be overriden to return a list of tuples (value, verbose value)
+ Must be overridden to return a list of tuples (value, verbose value)
"""
raise NotImplementedError
View
8 django/contrib/admin/helpers.py
@@ -125,14 +125,16 @@ def label_tag(self):
contents = conditional_escape(force_text(self.field.label))
if self.is_checkbox:
classes.append('vCheckboxLabel')
- else:
- contents += ':'
+
if self.field.field.required:
classes.append('required')
if not self.is_first:
classes.append('inline')
attrs = {'class': ' '.join(classes)} if classes else {}
- return self.field.label_tag(contents=mark_safe(contents), attrs=attrs)
+ # checkboxes should not have a label suffix as the checkbox appears
+ # to the left of the label.
+ return self.field.label_tag(contents=mark_safe(contents), attrs=attrs,
+ label_suffix='' if self.is_checkbox else None)
def errors(self):
return mark_safe(self.field.errors.as_ul())
View
7 django/contrib/admin/models.py
@@ -4,7 +4,7 @@
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin.util import quote
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, NoReverseMatch
from django.utils.translation import ugettext, ugettext_lazy as _
from django.utils.encoding import smart_text
from django.utils.encoding import python_2_unicode_compatible
@@ -74,5 +74,8 @@ def get_admin_url(self):
"""
if self.content_type and self.object_id:
url_name = 'admin:%s_%s_change' % (self.content_type.app_label, self.content_type.model)
- return reverse(url_name, args=(quote(self.object_id),))
+ try:
+ return reverse(url_name, args=(quote(self.object_id),))
+ except NoReverseMatch:
+ pass
return None
View
217 django/contrib/admin/options.py
@@ -1,6 +1,8 @@
+from collections import OrderedDict
import copy
import operator
from functools import partial, reduce, update_wrapper
+import warnings
from django import forms
from django.conf import settings
@@ -29,7 +31,6 @@
from django.shortcuts import get_object_or_404
from django.template.response import SimpleTemplateResponse, TemplateResponse
from django.utils.decorators import method_decorator
-from django.utils.datastructures import SortedDict
from django.utils.html import escape, escapejs
from django.utils.safestring import mark_safe
from django.utils import six
@@ -69,6 +70,7 @@ class IncorrectLookupParameters(Exception):
models.CharField: {'widget': widgets.AdminTextInputWidget},
models.ImageField: {'widget': widgets.AdminFileWidget},
models.FileField: {'widget': widgets.AdminFileWidget},
+ models.EmailField: {'widget': widgets.AdminEmailInputWidget},
}
csrf_protect_m = method_decorator(csrf_protect)
@@ -237,13 +239,49 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs):
return db_field.formfield(**kwargs)
- def _declared_fieldsets(self):
+ @property
+ def declared_fieldsets(self):
+ warnings.warn(
+ "ModelAdmin.declared_fieldsets is deprecated and "
+ "will be removed in Django 1.9.",
+ PendingDeprecationWarning, stacklevel=2
+ )
+
if self.fieldsets:
return self.fieldsets
elif self.fields:
return [(None, {'fields': self.fields})]
return None
- declared_fieldsets = property(_declared_fieldsets)
+
+ def get_fields(self, request, obj=None):
+ """
+ Hook for specifying fields.
+ """
+ return self.fields
+
+ def get_fieldsets(self, request, obj=None):
+ """
+ Hook for specifying fieldsets.
+ """
+ # We access the property and check if it triggers a warning.
+ # If it does, then it's ours and we can safely ignore it, but if
+ # it doesn't then it has been overriden so we must warn about the
+ # deprecation.
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ declared_fieldsets = self.declared_fieldsets
+ if len(w) != 1 or not issubclass(w[0].category, PendingDeprecationWarning):
+ warnings.warn(
+ "ModelAdmin.declared_fieldsets is deprecated and "
+ "will be removed in Django 1.9.",
+ PendingDeprecationWarning
+ )
+ if declared_fieldsets:
+ return declared_fieldsets
+
+ if self.fieldsets:
+ return self.fieldsets
+ return [(None, {'fields': self.get_fields(request, obj)})]
def get_ordering(self, request):
"""
@@ -263,34 +301,6 @@ def get_prepopulated_fields(self, request, obj=None):
"""
return self.prepopulated_fields
- def get_search_results(self, request, queryset, search_term):
- # Apply keyword searches.
- def construct_search(field_name):
- if field_name.startswith('^'):
- return "%s__istartswith" % field_name[1:]
- elif field_name.startswith('='):
- return "%s__iexact" % field_name[1:]
- elif field_name.startswith('@'):
- return "%s__search" % field_name[1:]
- else:
- return "%s__icontains" % field_name
-
- use_distinct = False
- if self.search_fields and search_term:
- orm_lookups = [construct_search(str(search_field))
- for search_field in self.search_fields]
- for bit in search_term.split():
- or_queries = [models.Q(**{orm_lookup: bit})
- for orm_lookup in orm_lookups]
- queryset = queryset.filter(reduce(operator.or_, or_queries))
- if not use_distinct:
- for search_spec in orm_lookups:
- if lookup_needs_distinct(self.opts, search_spec):
- use_distinct = True
- break
-
- return queryset, use_distinct
-
def get_queryset(self, request):
"""
Returns a QuerySet of all model instances that can be edited by the
@@ -505,13 +515,11 @@ def get_model_perms(self, request):
'delete': self.has_delete_permission(request),
}
- def get_fieldsets(self, request, obj=None):
- "Hook for specifying fieldsets for the add form."
- if self.declared_fieldsets:
- return self.declared_fieldsets
+ def get_fields(self, request, obj=None):
+ if self.fields:
+ return self.fields
form = self.get_form(request, obj, fields=None)
- fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
- return [(None, {'fields': fields})]
+ return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
def get_form(self, request, obj=None, **kwargs):
"""
@@ -670,7 +678,7 @@ def get_actions(self, request):
# want *any* actions enabled on this page.
from django.contrib.admin.views.main import _is_changelist_popup
if self.actions is None or _is_changelist_popup(request):
- return SortedDict()
+ return OrderedDict()
actions = []
@@ -691,8 +699,8 @@ def get_actions(self, request):
# get_action might have returned None, so filter any of those out.
actions = filter(None, actions)
- # Convert the actions into a SortedDict keyed by name.
- actions = SortedDict([
+ # Convert the actions into an OrderedDict keyed by name.
+ actions = OrderedDict([
(name, (func, name, desc))
for func, name, desc in actions
])
@@ -766,11 +774,50 @@ def get_list_filter(self, request):
"""
return self.list_filter
+ def get_search_fields(self, request):
+ """
+ Returns a sequence containing the fields to be searched whenever
+ somebody submits a search query.
+ """
+ return self.search_fields
+
+ def get_search_results(self, request, queryset, search_term):
+ """
+ Returns a tuple containing a queryset to implement the search,
+ and a boolean indicating if the results may contain duplicates.
+ """
+ # Apply keyword searches.
+ def construct_search(field_name):
+ if field_name.startswith('^'):
+ return "%s__istartswith" % field_name[1:]
+ elif field_name.startswith('='):
+ return "%s__iexact" % field_name[1:]
+ elif field_name.startswith('@'):
+ return "%s__search" % field_name[1:]
+ else:
+ return "%s__icontains" % field_name
+
+ use_distinct = False
+ search_fields = self.get_search_fields(request)
+ if search_fields and search_term:
+ orm_lookups = [construct_search(str(search_field))
+ for search_field in search_fields]
+ for bit in search_term.split():
+ or_queries = [models.Q(**{orm_lookup: bit})
+ for orm_lookup in orm_lookups]
+ queryset = queryset.filter(reduce(operator.or_, or_queries))
+ if not use_distinct:
+ for search_spec in orm_lookups:
+ if lookup_needs_distinct(self.opts, search_spec):
+ use_distinct = True
+ break
+
+ return queryset, use_distinct
+
def get_preserved_filters(self, request):
"""
Returns the preserved filters querystring.
"""
-
match = request.resolver_match
if self.preserve_filters and match:
opts = self.model._meta
@@ -1106,17 +1153,7 @@ def add_view(self, request, form_url='', extra_context=None):
else:
form_validated = False
new_object = self.model()
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(data=request.POST, files=request.FILES,
- instance=new_object,
- save_as_new="_saveasnew" in request.POST,
- prefix=prefix, queryset=inline.get_queryset(request))
- formsets.append(formset)
+ formsets = self._create_formsets(request, new_object, inline_instances)
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, False)
self.save_related(request, form, formsets, False)
@@ -1134,15 +1171,7 @@ def add_view(self, request, form_url='', extra_context=None):
if isinstance(f, models.ManyToManyField):
initial[k] = initial[k].split(",")
form = ModelForm(initial=initial)
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(instance=self.model(), prefix=prefix,
- queryset=inline.get_queryset(request))
- formsets.append(formset)
+ formsets = self._create_formsets(request, self.model(), inline_instances)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
self.get_prepopulated_fields(request),
@@ -1194,7 +1223,6 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
current_app=self.admin_site.name))
ModelForm = self.get_form(request, obj)
- formsets = []
inline_instances = self.get_inline_instances(request, obj)
if request.method == 'POST':
form = ModelForm(request.POST, request.FILES, instance=obj)
@@ -1204,18 +1232,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
else:
form_validated = False
new_object = obj
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(request.POST, request.FILES,
- instance=new_object, prefix=prefix,
- queryset=inline.get_queryset(request))
-
- formsets.append(formset)
-
+ formsets = self._create_formsets(request, new_object, inline_instances)
if all_valid(formsets) and form_validated:
self.save_model(request, new_object, form, True)
self.save_related(request, form, formsets, True)
@@ -1225,15 +1242,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
else:
form = ModelForm(instance=obj)
- prefixes = {}
- for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
- prefix = FormSet.get_default_prefix()
- prefixes[prefix] = prefixes.get(prefix, 0) + 1
- if prefixes[prefix] != 1 or not prefix:
- prefix = "%s-%s" % (prefix, prefixes[prefix])
- formset = FormSet(instance=obj, prefix=prefix,
- queryset=inline.get_queryset(request))
- formsets.append(formset)
+ formsets = self._create_formsets(request, obj, inline_instances)
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
self.get_prepopulated_fields(request, obj),
@@ -1280,6 +1289,7 @@ def changelist_view(self, request, extra_context=None):
list_display = self.get_list_display(request)
list_display_links = self.get_list_display_links(request, list_display)
list_filter = self.get_list_filter(request)
+ search_fields = self.get_search_fields(request)
# Check actions to see if any are available on this changelist
actions = self.get_actions(request)
@@ -1291,9 +1301,9 @@ def changelist_view(self, request, extra_context=None):
try:
cl = ChangeList(request, self.model, list_display,
list_display_links, list_filter, self.date_hierarchy,
- self.search_fields, self.list_select_related,
- self.list_per_page, self.list_max_show_all, self.list_editable,
- self)
+ search_fields, self.list_select_related, self.list_per_page,
+ self.list_max_show_all, self.list_editable, self)
+
except IncorrectLookupParameters:
# Wacky lookup parameters were given, so redirect to the main
# changelist page, without parameters, and pass an 'invalid=1'
@@ -1532,6 +1542,32 @@ def history_view(self, request, object_id, extra_context=None):
"admin/object_history.html"
], context, current_app=self.admin_site.name)
+ def _create_formsets(self, request, obj, inline_instances):
+ "Helper function to generate formsets for add/change_view."
+ formsets = []
+ prefixes = {}
+ get_formsets_args = [request]
+ if obj.pk:
+ get_formsets_args.append(obj)
+ for FormSet, inline in zip(self.get_formsets(*get_formsets_args), inline_instances):
+ prefix = FormSet.get_default_prefix()
+ prefixes[prefix] = prefixes.get(prefix, 0) + 1
+ if prefixes[prefix] != 1 or not prefix:
+ prefix = "%s-%s" % (prefix, prefixes[prefix])
+ formset_params = {
+ 'instance': obj,
+ 'prefix': prefix,
+ 'queryset': inline.get_queryset(request),
+ }
+ if request.method == 'POST':
+ formset_params.update({
+ 'data': request.POST,
+ 'files': request.FILES,
+ 'save_as_new': '_saveasnew' in request.POST
+ })
+ formsets.append(FormSet(**formset_params))
+ return formsets
+
class InlineModelAdmin(BaseModelAdmin):
"""
@@ -1656,12 +1692,11 @@ def is_valid(self):
return inlineformset_factory(self.parent_model, self.model, **defaults)
- def get_fieldsets(self, request, obj=None):
- if self.declared_fieldsets:
- return self.declared_fieldsets
+ def get_fields(self, request, obj=None):
+ if self.fields:
+ return self.fields
form = self.get_formset(request, obj, fields=None).form
- fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
- return [(None, {'fields': fields})]
+ return list(form.base_fields) + list(self.get_readonly_fields(request, obj))
def get_queryset(self, request):
queryset = super(InlineModelAdmin, self).get_queryset(request)
View
32 django/contrib/admin/static/admin/css/base.css
@@ -661,45 +661,34 @@ a.deletelink:hover {
.object-tools li {
display: block;
float: left;
- background: url(../img/tool-left.gif) 0 0 no-repeat;
- padding: 0 0 0 8px;
- margin-left: 2px;
+ margin-left: 5px;
height: 16px;
}
-.object-tools li:hover {
- background: url(../img/tool-left_over.gif) 0 0 no-repeat;
+.object-tools a {
+ border-radius: 15px;
}
.object-tools a:link, .object-tools a:visited {
display: block;
float: left;
color: white;
- padding: .1em 14px .1em 8px;
- height: 14px;
- background: #999 url(../img/tool-right.gif) 100% 0 no-repeat;
+ padding: .2em 10px;
+ background: #999;
}
.object-tools a:hover, .object-tools li:hover a {
- background: #5b80b2 url(../img/tool-right_over.gif) 100% 0 no-repeat;
+ background-color: #5b80b2;
}
.object-tools a.viewsitelink, .object-tools a.golink {
- background: #999 url(../img/tooltag-arrowright.gif) top right no-repeat;
- padding-right: 28px;
-}
-
-.object-tools a.viewsitelink:hover, .object-tools a.golink:hover {
- background: #5b80b2 url(../img/tooltag-arrowright_over.gif) top right no-repeat;
+ background: #999 url(../img/tooltag-arrowright.png) 95% center no-repeat;
+ padding-right: 26px;
}
.object-tools a.addlink {
- background: #999 url(../img/tooltag-add.gif) top right no-repeat;
- padding-right: 28px;
-}
-
-.object-tools a.addlink:hover {
- background: #5b80b2 url(../img/tooltag-add_over.gif) top right no-repeat;
+ background: #999 url(../img/tooltag-add.png) 95% center no-repeat;
+ padding-right: 26px;
}
/* OBJECT HISTORY */
@@ -837,4 +826,3 @@ table#change-history tbody th {
background: #eee url(../img/nav-bg.gif) bottom left repeat-x;
color: #666;
}
-
View
8 django/contrib/admin/static/admin/css/widgets.css
@@ -54,8 +54,8 @@
.selector ul.selector-chooser {
float: left;
width: 22px;
- height: 50px;
- background: url(../img/chooser-bg.gif) top center no-repeat;
+ background-color: #eee;
+ border-radius: 10px;
margin: 10em 5px 0 5px;
padding: 0;
}
@@ -169,7 +169,8 @@ a.active.selector-clearall {
height: 22px;
width: 50px;
margin: 0 0 3px 40%;
- background: url(../img/chooser_stacked-bg.gif) top center no-repeat;
+ background-color: #eee;
+ border-radius: 10px;
}
.stacked .selector-chooser li {
@@ -575,4 +576,3 @@ ul.orderer li.deleted:hover, ul.orderer li.deleted a.selector:hover {
font-size: 11px;
border-top: 1px solid #ddd;
}
-
View
BIN  django/contrib/admin/static/admin/img/chooser-bg.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/chooser_stacked-bg.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tool-left.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tool-left_over.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tool-right.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tool-right_over.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tooltag-add.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tooltag-add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  django/contrib/admin/static/admin/img/tooltag-add_over.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tooltag-arrowright.gif
Deleted file not rendered
View
BIN  django/contrib/admin/static/admin/img/tooltag-arrowright.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  django/contrib/admin/static/admin/img/tooltag-arrowright_over.gif
Deleted file not rendered
View
4 django/contrib/admin/static/admin/js/collapse.min.js
@@ -1,2 +1,2 @@
-(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){0==a(b).find("div.errors").length&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset",
-[a(this).attr("id")]);return!1})})})(django.jQuery);
+(function(a){a(document).ready(function(){a("fieldset.collapse").each(function(c,b){a(b).find("div.errors").length==0&&a(b).addClass("collapsed").find("h2").first().append(' (<a id="fieldsetcollapser'+c+'" class="collapse-toggle" href="#">'+gettext("Show")+"</a>)")});a("fieldset.collapse a.collapse-toggle").click(function(){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset",
+[a(this).attr("id")]);return false})})})(django.jQuery);
View
18 django/contrib/admin/static/admin/js/inlines.min.js
@@ -1,9 +1,9 @@
-(function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),c=b(this),d=c.parent(),i=function(a,e,g){var d=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+g;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(d,e));a.id&&(a.id=a.id.replace(d,e));a.name&&(a.name=a.name.replace(d,e))},f=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),g=parseInt(f.val(),10),e=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),f=""===e.val()||0<e.val()-f.val();c.each(function(){b(this).not("."+
-a.emptyCssClass).addClass(a.formCssClass)});if(c.length&&f){var h;"TR"==c.prop("tagName")?(c=this.eq(-1).children().length,d.append('<tr class="'+a.addCssClass+'"><td colspan="'+c+'"><a href="javascript:void(0)">'+a.addText+"</a></tr>"),h=d.find("tr:last a")):(c.filter(":last").after('<div class="'+a.addCssClass+'"><a href="javascript:void(0)">'+a.addText+"</a></div>"),h=c.filter(":last").next().find("a"));h.click(function(d){d.preventDefault();var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),d=b("#"+a.prefix+
-"-empty"),c=d.clone(true);c.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);c.is("tr")?c.children(":last").append('<div><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></div>"):c.is("ul")||c.is("ol")?c.append('<li><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></li>"):c.children(":first").append('<span><a class="'+a.deleteCssClass+'" href="javascript:void(0)">'+a.deleteText+"</a></span>");c.find("*").each(function(){i(this,
-a.prefix,f.val())});c.insertBefore(b(d));b(f).val(parseInt(f.val(),10)+1);g=g+1;e.val()!==""&&e.val()-f.val()<=0&&h.parent().hide();c.find("a."+a.deleteCssClass).click(function(d){d.preventDefault();d=b(this).parents("."+a.formCssClass);d.remove();g=g-1;a.removed&&a.removed(d);d=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(d.length);(e.val()===""||e.val()-d.length>0)&&h.parent().show();for(var c=0,f=d.length;c<f;c++){i(b(d).get(c),a.prefix,c);b(d.get(c)).find("*").each(function(){i(this,
-a.prefix,c)})}});a.added&&a.added(c)})}return this};b.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};b.fn.tabularFormset=function(d){var a=b(this),c=function(){b(a.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")};a.formset({prefix:d.prefix,addText:d.addText,formCssClass:"dynamic-"+
-d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),DateTimeShortcuts.init());"undefined"!=
-typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a};b.fn.stackedFormset=function(d){var a=b(this),c=function(){b(a.selector).find(".inline_label").each(function(a){a+=1;b(this).html(b(this).html().replace(/(#\d+)/g,"#"+a))})};a.formset({prefix:d.prefix,
-addText:d.addText,formCssClass:"dynamic-"+d.prefix,deleteCssClass:"inline-deletelink",deleteText:d.deleteText,emptyCssClass:"empty-form",removed:c,added:function(a){a.find(".prepopulated_field").each(function(){var d=b(this).find("input, select, textarea"),c=d.data("dependency_list")||[],e=[];b.each(c,function(d,b){e.push("#"+a.find(".form-row .field-"+b).find("input, select, textarea").attr("id"))});e.length&&d.prepopulate(e,d.attr("maxlength"))});"undefined"!=typeof DateTimeShortcuts&&(b(".datetimeshortcuts").remove(),
-DateTimeShortcuts.init());"undefined"!=typeof SelectFilter&&(b(".selectfilter").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],false,d.adminStaticPrefix)}),b(".selectfilterstacked").each(function(a,b){var c=b.name.split("-");SelectFilter.init(b.id,c[c.length-1],true,d.adminStaticPrefix)}));c(a)}});return a}})(django.jQuery);
+(function(a){a.fn.formset=function(g){var b=a.extend({},a.fn.formset.defaults,g),i=a(this);g=i.parent();var m=function(e,k,h){var j=RegExp("("+k+"-(\\d+|__prefix__))");k=k+"-"+h;a(e).prop("for")&&a(e).prop("for",a(e).prop("for").replace(j,k));if(e.id)e.id=e.id.replace(j,k);if(e.name)e.name=e.name.replace(j,k)},l=a("#id_"+b.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),d=parseInt(l.val(),10),c=a("#id_"+b.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off");l=c.val()===""||c.val()-l.val()>0;i.each(function(){a(this).not("."+
+b.emptyCssClass).addClass(b.formCssClass)});if(i.length&&l){var f;if(i.prop("tagName")=="TR"){i=this.eq(-1).children().length;g.append('<tr class="'+b.addCssClass+'"><td colspan="'+i+'"><a href="javascript:void(0)">'+b.addText+"</a></tr>");f=g.find("tr:last a")}else{i.filter(":last").after('<div class="'+b.addCssClass+'"><a href="javascript:void(0)">'+b.addText+"</a></div>");f=i.filter(":last").next().find("a")}f.click(function(e){e.preventDefault();var k=a("#id_"+b.prefix+"-TOTAL_FORMS");e=a("#"+
+b.prefix+"-empty");var h=e.clone(true);h.removeClass(b.emptyCssClass).addClass(b.formCssClass).attr("id",b.prefix+"-"+d);if(h.is("tr"))h.children(":last").append('<div><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></div>");else h.is("ul")||h.is("ol")?h.append('<li><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></li>"):h.children(":first").append('<span><a class="'+b.deleteCssClass+'" href="javascript:void(0)">'+b.deleteText+"</a></span>");
+h.find("*").each(function(){m(this,b.prefix,k.val())});h.insertBefore(a(e));a(k).val(parseInt(k.val(),10)+1);d+=1;c.val()!==""&&c.val()-k.val()<=0&&f.parent().hide();h.find("a."+b.deleteCssClass).click(function(j){j.preventDefault();j=a(this).parents("."+b.formCssClass);j.remove();d-=1;b.removed&&b.removed(j);j=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(j.length);if(c.val()===""||c.val()-j.length>0)f.parent().show();for(var n=0,o=j.length;n<o;n++){m(a(j).get(n),b.prefix,n);a(j.get(n)).find("*").each(function(){m(this,
+b.prefix,n)})}});b.added&&b.added(h)})}return this};a.fn.formset.defaults={prefix:"form",addText:"add another",deleteText:"remove",addCssClass:"add-row",deleteCssClass:"delete-row",emptyCssClass:"empty-row",formCssClass:"dynamic-form",added:null,removed:null};a.fn.tabularFormset=function(g){var b=a(this),i=function(){a(b.selector).not(".add-row").removeClass("row1 row2").filter(":even").addClass("row1").end().filter(":odd").addClass("row2")},m=function(){if(typeof SelectFilter!="undefined"){a(".selectfilter").each(function(d,
+c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".field-"+h).find("input, select, textarea").attr("id"))});e.length&&c.prepopulate(e,c.attr("maxlength"))})};
+b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b};a.fn.stackedFormset=function(g){var b=a(this),i=function(){a(b.selector).find(".inline_label").each(function(d){d=d+1;a(this).html(a(this).html().replace(/(#\d+)/g,"#"+d))})},m=function(){if(typeof SelectFilter!=
+"undefined"){a(".selectfilter").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],false,g.adminStaticPrefix)});a(".selectfilterstacked").each(function(d,c){var f=c.name.split("-");SelectFilter.init(c.id,f[f.length-1],true,g.adminStaticPrefix)})}},l=function(d){d.find(".prepopulated_field").each(function(){var c=a(this).find("input, select, textarea"),f=c.data("dependency_list")||[],e=[];a.each(f,function(k,h){e.push("#"+d.find(".form-row .field-"+h).find("input, select, textarea").attr("id"))});
+e.length&&c.prepopulate(e,c.attr("maxlength"))})};b.formset({prefix:g.prefix,addText:g.addText,formCssClass:"dynamic-"+g.prefix,deleteCssClass:"inline-deletelink",deleteText:g.deleteText,emptyCssClass:"empty-form",removed:i,added:function(d){l(d);if(typeof DateTimeShortcuts!="undefined"){a(".datetimeshortcuts").remove();DateTimeShortcuts.init()}m();i(d)}});return b}})(django.jQuery);
View
41 django/contrib/admin/static/admin/js/prepopulate.js
@@ -3,32 +3,37 @@
/*
Depends on urlify.js
Populates a selected field with the values of the dependent fields,
- URLifies and shortens the string.
- dependencies - array of dependent fields id's
- maxLength - maximum length of the URLify'd string
+ URLifies and shortens the string.
+ dependencies - array of dependent fields ids
+ maxLength - maximum length of the URLify'd string
*/
return this.each(function() {
- var field = $(this);
-
- field.data('_changed', false);
- field.change(function() {
- field.data('_changed', true);
- });
+ var prepopulatedField = $(this);
var populate = function () {
- // Bail if the fields value has changed
- if (field.data('_changed') == true) return;
-
+ // Bail if the field's value has been changed by the user
+ if (prepopulatedField.data('_changed')) {
+ return;
+ }
+
var values = [];
$.each(dependencies, function(i, field) {
- if ($(field).val().length > 0) {
- values.push($(field).val());
- }
- })
- field.val(URLify(values.join(' '), maxLength));
+ field = $(field);
+ if (field.val().length > 0) {
+ values.push(field.val());
+ }
+ });
+ prepopulatedField.val(URLify(values.join(' '), maxLength));
};
- $(dependencies.join(',')).keyup(populate).change(populate).focus(populate);
+ prepopulatedField.data('_changed', false);
+ prepopulatedField.change(function() {
+ prepopulatedField.data('_changed', true);
+ });
+
+ if (!prepopulatedField.val()) {
+ $(dependencies.join(',')).keyup(populate).change(populate).focus(populate);
+ }
});
};
})(django.jQuery);
View
2  django/contrib/admin/static/admin/js/prepopulate.min.js
@@ -1 +1 @@
-(function(a){a.fn.prepopulate=function(d,g){return this.each(function(){var b=a(this);b.data("_changed",false);b.change(function(){b.data("_changed",true)});var c=function(){if(b.data("_changed")!=true){var e=[];a.each(d,function(h,f){a(f).val().length>0&&e.push(a(f).val())});b.val(URLify(e.join(" "),g))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery);
+(function(b){b.fn.prepopulate=function(e,g){return this.each(function(){var a=b(this),d=function(){if(!a.data("_changed")){var f=[];b.each(e,function(h,c){c=b(c);c.val().length>0&&f.push(c.val())});a.val(URLify(f.join(" "),g))}};a.data("_changed",false);a.change(function(){a.data("_changed",true)});a.val()||b(e.join(",")).keyup(d).change(d).focus(d)})}})(django.jQuery);
View
8 django/contrib/admin/templates/registration/password_change_done.html
@@ -8,12 +8,8 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password change successful' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-
-<h1>{% trans 'Password change successful' %}</h1>
-
<p>{% trans 'Your password was changed.' %}</p>
-
{% endblock %}
View
4 django/contrib/admin/templates/registration/password_change_form.html
@@ -9,7 +9,8 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password change' %}{% endblock %}
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}<div id="content-main">
@@ -21,7 +22,6 @@
</p>
{% endif %}
-<h1>{% trans 'Password change' %}</h1>
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
View
5 django/contrib/admin/templates/registration/password_reset_complete.html
@@ -8,12 +8,11 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset complete' %}{% endblock %}
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans 'Password reset complete' %}</h1>
-
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p>
View
8 django/contrib/admin/templates/registration/password_reset_confirm.html
@@ -8,14 +8,12 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
{% if validlink %}
-<h1>{% trans 'Enter new password' %}</h1>
-
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form action="" method="post">{% csrf_token %}
@@ -28,8 +26,6 @@
{% else %}
-<h1>{% trans 'Password reset unsuccessful' %}</h1>
-
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
View
6 django/contrib/admin/templates/registration/password_reset_done.html
@@ -8,12 +8,10 @@
</div>
{% endblock %}
-{% block title %}{% trans 'Password reset successful' %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans 'Password reset successful' %}</h1>
-
<p>{% trans "We've emailed you instructions for setting your password. You should be receiving them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
View
6 django/contrib/admin/templates/registration/password_reset_form.html
@@ -8,12 +8,10 @@
</div>
{% endblock %}
-{% block title %}{% trans "Password reset" %}{% endblock %}
-
+{% block title %}{{ title }}{% endblock %}
+{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
-<h1>{% trans "Password reset" %}</h1>
-
<p>{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}</p>
<form action="" method="post">{% csrf_token %}
View
9 django/contrib/admin/templatetags/admin_list.py
@@ -180,7 +180,7 @@ def items_for_result(cl, result, form):
first = True
pk = cl.lookup_opts.pk.attname
for field_name in cl.list_display:
- row_class = ''
+ row_classes = ['field-%s' % field_name]
try:
f, attr, value = lookup_field(field_name, result, cl.model_admin)
except ObjectDoesNotExist:
@@ -188,7 +188,7 @@ def items_for_result(cl, result, form):
else:
if f is None:
if field_name == 'action_checkbox':
- row_class = mark_safe(' class="action-checkbox"')
+ row_classes = ['action-checkbox']
allow_tags = getattr(attr, 'allow_tags', False)
boolean = getattr(attr, 'boolean', False)
if boolean:
@@ -199,7 +199,7 @@ def items_for_result(cl, result, form):
if allow_tags:
result_repr = mark_safe(result_repr)
if isinstance(value, (datetime.date, datetime.time)):
- row_class = mark_safe(' class="nowrap"')
+ row_classes.append('nowrap')
else:
if isinstance(f.rel, models.ManyToOneRel):
field_val = getattr(result, f.name)
@@ -210,9 +210,10 @@ def items_for_result(cl, result, form):
else:
result_repr = display_for_field(value, f)
if isinstance(f, (models.DateField, models.TimeField, models.ForeignKey)):
- row_class = mark_safe(' class="nowrap"')
+ row_classes.append('nowrap')
if force_text(result_repr) == '':
result_repr = mark_safe('&nbsp;')
+ row_class = mark_safe(' class="%s"' % ' '.join(row_classes))
# If list_display_links not defined, add the link tag to the first field
if (first and not cl.list_display_links) or field_name in cl.list_display_links:
table_tag = {True:'th', False:'td'}[first]
View
2  django/contrib/admin/templatetags/admin_modify.py
@@ -9,7 +9,7 @@ def prepopulated_fields_js(context):
the prepopulated fields for both the admin form and inlines.
"""
prepopulated_fields = []
- if context['add'] and 'adminform' in context:
+ if 'adminform' in context:
prepopulated_fields.extend(context['adminform'].prepopulated_fields)
if 'inline_admin_formsets' in context:
for inline_admin_formset in context['inline_admin_formsets']:
View
6 django/contrib/admin/views/main.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
import sys
import warnings
@@ -7,7 +8,6 @@
from django.db import models
from django.db.models.fields import FieldDoesNotExist
from django.utils import six
-from django.utils.datastructures import SortedDict
from django.utils.deprecation import RenameMethodsBase
from django.utils.encoding import force_str, force_text
from django.utils.translation import ugettext, ugettext_lazy
@@ -319,13 +319,13 @@ def get_ordering(self, request, queryset):
def get_ordering_field_columns(self):
"""
- Returns a SortedDict of ordering field column numbers and asc/desc
+ Returns an OrderedDict of ordering field column numbers and asc/desc
"""
# We must cope with more than one column having the same underlying sort
# field, so we base things on column numbers.
ordering = self._get_default_ordering()
- ordering_fields = SortedDict()
+ ordering_fields = OrderedDict()
if ORDER_VAR not in self.params:
# for ordering specified on ModelAdmin or model Meta, we don't know
# the right column numbers absolutely, because there might be more
View
11 django/contrib/admin/widgets.py
@@ -116,6 +116,8 @@ def url_params_from_lookup_dict(lookups):
if lookups and hasattr(lookups, 'items'):
items = []
for k, v in lookups.items():
+ if callable(v):
+ v = v()
if isinstance(v, (tuple, list)):
v = ','.join([str(x) for x in v])
elif isinstance(v, bool):
@@ -285,7 +287,14 @@ def __init__(self, attrs=None):
final_attrs.update(attrs)
super(AdminTextInputWidget, self).__init__(attrs=final_attrs)
-class AdminURLFieldWidget(forms.TextInput):
+class AdminEmailInputWidget(forms.EmailInput):
+ def __init__(self, attrs=None):
+ final_attrs = {'class': 'vTextField'}
+ if attrs is not None:
+ final_attrs.update(attrs)
+ super(AdminEmailInputWidget, self).__init__(attrs=final_attrs)
+
+class AdminURLFieldWidget(forms.URLInput):
def __init__(self, attrs=None):
final_attrs = {'class': 'vURLField'}
if attrs is not None:
View
2  django/contrib/admindocs/tests/test_fields.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import, unicode_literals
+from __future__ import unicode_literals
import unittest
View
4 django/contrib/admindocs/views.py
@@ -1,3 +1,4 @@
+from importlib import import_module
import inspect
import os
import re
@@ -13,7 +14,6 @@
from django.core import urlresolvers
from django.contrib.admindocs import utils
from django.contrib.sites.models import Site
-from django.utils.importlib import import_module
from django.utils._os import upath
from django.utils import six
from django.utils.translation import ugettext as _
@@ -319,7 +319,7 @@ def load_all_installed_template_libraries():
libraries = []
for library_name in libraries:
try:
- lib = template.get_library(library_name)
+ template.get_library(library_name)
except template.InvalidTemplateLibrary:
pass
View
7 django/contrib/auth/admin.py
@@ -17,6 +17,7 @@
from django.views.decorators.debug import sensitive_post_parameters
csrf_protect_m = method_decorator(csrf_protect)
+sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
class GroupAdmin(admin.ModelAdmin):
@@ -87,7 +88,7 @@ def lookup_allowed(self, lookup, value):
return False
return super(UserAdmin, self).lookup_allowed(lookup, value)
- @sensitive_post_parameters()
+ @sensitive_post_parameters_m
@csrf_protect_m
@transaction.atomic
def add_view(self, request, form_url='', extra_context=None):
@@ -118,7 +119,7 @@ def add_view(self, request, form_url='', extra_context=None):
return super(UserAdmin, self).add_view(request, form_url,
extra_context)
- @sensitive_post_parameters()
+ @sensitive_post_parameters_m
def user_change_password(self, request, id, form_url=''):
if not self.has_change_permission(request):
raise PermissionDenied
@@ -127,6 +128,8 @@ def user_change_password(self, request, id, form_url=''):
form = self.change_password_form(user, request.POST)
if form.is_valid():
form.save()
+ change_message = self.construct_change_message(request, form, None)
+ self.log_change(request, request.user, change_message)
msg = ugettext('Password changed successfully.')
messages.success(request, msg)
return HttpResponseRedirect('..')
View
4 django/contrib/auth/backends.py
@@ -17,7 +17,9 @@ def authenticate(self, username=None, password=None, **kwargs):
if user.check_password(password):
return user
except UserModel.DoesNotExist:
- return None
+ # Run the default password hasher once to reduce the timing
+ # difference between an existing and a non-existing user (#20760).
+ UserModel().set_password(password)
def get_group_permissions(self, user_obj, obj=None):
"""
View
47 django/contrib/auth/forms.py
@@ -1,9 +1,10 @@
from __future__ import unicode_literals
+from collections import OrderedDict
+
from django import forms
from django.forms.util import flatatt
from django.template import loader
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes
from django.utils.html import format_html, format_html_join
from django.utils.http import urlsafe_base64_encode
@@ -191,13 +192,28 @@ def clean(self):
code='invalid_login',
params={'username': self.username_field.verbose_name},
)
- elif not self.user_cache.is_active:
- raise forms.ValidationError(
- self.error_messages['inactive'],
- code='inactive',
- )
+ else:
+ self.confirm_login_allowed(self.user_cache)
+
return self.cleaned_data
+ def confirm_login_allowed(self, user):
+ """
+ Controls whether the given User may log in. This is a policy setting,
+ independent of end-user authentication. This default behavior is to
+ allow login by active users, and reject login by inactive users.
+
+ If the given user cannot log in, this method should raise a
+ ``forms.ValidationError``.
+
+ If the given user may log in, this method should return None.
+ """
+ if not user.is_active:
+ raise forms.ValidationError(
+ self.error_messages['inactive'],
+ code='inactive',
+ )
+
def get_user_id(self):
if self.user_cache:
return self.user_cache.id
@@ -214,7 +230,7 @@ def save(self, domain_override=None,
subject_template_name='registration/password_reset_subject.txt',
email_template_name='registration/password_reset_email.html',
use_https=False, token_generator=default_token_generator,
- from_email=None, request=None):
+ from_email=None, request=None, html_email_template_name=None):
"""
Generates a one-use only link for resetting password and sends to the
user.
@@ -247,7 +263,12 @@ def save(self, domain_override=None,
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
email = loader.render_to_string(email_template_name, c)
- send_mail(subject, email, from_email, [user.email])
+
+ if html_email_template_name:
+ html_email = loader.render_to_string(html_email_template_name, c)
+ else:
+ html_email = None
+ send_mail(subject, email, from_email, [user.email], html_message=html_email)
class SetPasswordForm(forms.Form):
@@ -309,7 +330,7 @@ def clean_old_password(self):
)
return old_password
-PasswordChangeForm.base_fields = SortedDict([
+PasswordChangeForm.base_fields = OrderedDict([
(k, PasswordChangeForm.base_fields[k])
for k in ['old_password', 'new_password1', 'new_password2']
])
@@ -350,3 +371,11 @@ def save(self, commit=True):
if commit:
self.user.save()
return self.user
+
+ def _get_changed_data(self):
+ data = super(AdminPasswordChangeForm, self).changed_data
+ for name in self.fields.keys():
+ if name not in data:
+ return []
+ return ['password']
+ changed_data = property(_get_changed_data)
View
20 django/contrib/auth/hashers.py
@@ -2,13 +2,13 @@
import base64
import binascii
+from collections import OrderedDict
import hashlib
+import importlib
from django.dispatch import receiver
from django.conf import settings
from django.test.signals import setting_changed
-from django.utils import importlib
-from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes, force_str, force_text
from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import (
@@ -172,7 +172,7 @@ def _load_library(self):
if isinstance(self.library, (tuple, list)):
name, mod_path = self.library
else:
- name = mod_path = self.library
+ mod_path = self.library
try:
module = importlib.import_module(mod_path)
except ImportError as e:
@@ -243,7 +243,7 @@ def verify(self, password, encoded):
def safe_summary(self, encoded):
algorithm, iterations, salt, hash = encoded.split('$', 3)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('iterations'), iterations),
(_('salt'), mask_hash(salt)),
@@ -320,7 +320,7 @@ def safe_summary(self, encoded):
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
assert algorithm == self.algorithm
salt, checksum = data[:22], data[22:]
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('work factor'), work_factor),
(_('salt'), mask_hash(salt)),
@@ -368,7 +368,7 @@ def verify(self, password, encoded):
def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
@@ -396,7 +396,7 @@ def verify(self, password, encoded):
def safe_summary(self, encoded):
algorithm, salt, hash = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
@@ -429,7 +429,7 @@ def verify(self, password, encoded):
def safe_summary(self, encoded):
assert encoded.startswith('sha1$$')
hash = encoded[6:]
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(hash)),
])
@@ -462,7 +462,7 @@ def verify(self, password, encoded):
return constant_time_compare(encoded, encoded_2)
def safe_summary(self, encoded):
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), self.algorithm),
(_('hash'), mask_hash(encoded, show=3)),
])
@@ -496,7 +496,7 @@ def verify(self, password, encoded):
def safe_summary(self, encoded):
algorithm, salt, data = encoded.split('$', 2)
assert algorithm == self.algorithm
- return SortedDict([
+ return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), salt),
(_('hash'), mask_hash(data, show=3)),
View
1  django/contrib/auth/tests/templates/registration/html_password_reset_email.html
@@ -0,0 +1 @@
+<html><a href="{{ protocol }}://{{ domain }}/reset/{{ uid }}/{{ token }}/">Link</a></html>
View
29 django/contrib/auth/tests/test_auth_backends.py
@@ -12,6 +12,17 @@
from django.http import HttpRequest
from django.test import TestCase
from django.test.utils import override_settings
+from django.contrib.auth.hashers import MD5PasswordHasher
+
+
+class CountingMD5PasswordHasher(MD5PasswordHasher):
+ """Hasher that counts how many times it computes a hash."""
+
+ calls = 0
+
+ def encode(self, *args, **kwargs):
+ type(self).calls += 1
+ return super(CountingMD5PasswordHasher, self).encode(*args, **kwargs)
class BaseModelBackendTest(object):
@@ -107,10 +118,26 @@ def test_has_no_object_perm(self):
self.assertEqual(user.get_all_permissions(), set(['auth.test']))
def test_get_all_superuser_permissions(self):
- "A superuser has all permissions. Refs #14795"
+ """A superuser has all permissions. Refs #14795."""
user = self.UserModel._default_manager.get(pk=self.superuser.pk)
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
+ @override_settings(PASSWORD_HASHERS=('django.contrib.auth.tests.test_auth_backends.CountingMD5PasswordHasher',))
+ def test_authentication_timing(self):
+ """Hasher is run once regardless of whether the user exists. Refs #20760."""
+ # Re-set the password, because this tests overrides PASSWORD_HASHERS
+ self.user.set_password('test')
+ self.user.save()
+
+ CountingMD5PasswordHasher.calls = 0
+ username = getattr(self.user, self.UserModel.USERNAME_FIELD)
+ authenticate(username=username, password='test')
+ self.assertEqual(CountingMD5PasswordHasher.calls, 1)
+
+ CountingMD5PasswordHasher.calls = 0
+ authenticate(username='no_such_user', password='test')
+ self.assertEqual(CountingMD5PasswordHasher.calls, 1)
+
@skipIfCustomUser
class ModelBackendTest(BaseModelBackendTest, TestCase):
View
2  django/contrib/auth/tests/test_context_processors.py
@@ -161,7 +161,7 @@ def test_user_attrs(self):
# Exception RuntimeError: 'maximum recursion depth exceeded while
# calling a Python object' in <type 'exceptions.AttributeError'>
# ignored"
- query = Q(user=response.context['user']) & Q(someflag=True)
+ Q(user=response.context['user']) & Q(someflag=True)
# Tests for user equality. This is hard because User defines
# equality in a non-duck-typing way
View
92 django/contrib/auth/tests/test_forms.py
@@ -1,7 +1,9 @@
from __future__ import unicode_literals
import os
+import re
+from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
@@ -131,6 +133,40 @@ def test_inactive_user_i18n(self):
self.assertEqual(form.non_field_errors(),
[force_text(form.error_messages['inactive'])])
+ def test_custom_login_allowed_policy(self):
+ # The user is inactive, but our custom form policy allows him to log in.
+ data = {
+ 'username': 'inactive',
+ 'password': 'password',
+ }
+
+ class AuthenticationFormWithInactiveUsersOkay(AuthenticationForm):
+ def confirm_login_allowed(self, user):
+ pass
+
+ form = AuthenticationFormWithInactiveUsersOkay(None, data)
+ self.assertTrue(form.is_valid())
+
+ # If we want to disallow some logins according to custom logic,
+ # we should raise a django.forms.ValidationError in the form.
+ class PickyAuthenticationForm(AuthenticationForm):
+ def confirm_login_allowed(self, user):
+ if user.username == "inactive":
+ raise forms.ValidationError(_("This user is disallowed."))
+ raise forms.ValidationError(_("Sorry, nobody's allowed in."))
+
+ form = PickyAuthenticationForm(None, data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.non_field_errors(), ['This user is disallowed.'])
+
+ data = {
+ 'username': 'testclient',
+ 'password': 'password',
+ }
+ form = PickyAuthenticationForm(None, data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(form.non_field_errors(), ["Sorry, nobody's allowed in."])
+
def test_success(self):
# The success case
data = {
@@ -272,7 +308,7 @@ class Meta(UserChangeForm.Meta):
fields = ('groups',)
# Just check we can create it
- form = MyUserForm({})
+ MyUserForm({})
def test_unsuable_password(self):
user = User.objects.get(username='empty_password')
@@ -417,6 +453,60 @@ def test_unusable_password(self):
form.save()
self.assertEqual(len(mail.outbox), 0)
+ @override_settings(
+ TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
+ TEMPLATE_DIRS=(
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
+ ),
+ )
+ def test_save_plaintext_email(self):
+ """
+ Test the PasswordResetForm.save() method with no html_email_template_name
+ parameter passed in.
+ Test to ensure original behavior is unchanged after the parameter was added.
+ """
+ (user, username, email) = self.create_dummy_user()
+ form = PasswordResetForm({"email": email})
+ self.assertTrue(form.is_valid())
+ form.save()
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0].message()
+ self.assertFalse(message.is_multipart())
+ self.assertEqual(message.get_content_type(), 'text/plain')
+ self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
+ self.assertEqual(len(mail.outbox[0].alternatives), 0)
+ self.assertEqual(message.get_all('to'), [email])
+ self.assertTrue(re.match(r'^http://example.com/reset/[\w+/-]', message.get_payload()))
+
+ @override_settings(
+ TEMPLATE_LOADERS=('django.template.loaders.filesystem.Loader',),
+ TEMPLATE_DIRS=(
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
+ ),
+ )
+ def test_save_html_email_template_name(self):
+ """
+ Test the PasswordResetFOrm.save() method with html_email_template_name
+ parameter specified.
+ Test to ensure that a multipart email is sent with both text/plain
+ and text/html parts.
+ """
+ (user, username, email) = self.create_dummy_user()
+ form = PasswordResetForm({"email": email})
+ self.assertTrue(form.is_valid())
+ form.save(html_email_template_name='registration/html_password_reset_email.html')
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(len(mail.outbox[0].alternatives), 1)
+ message = mail.outbox[0].message()
+ self.assertEqual(message.get('subject'), 'Custom password reset on example.com')
+ self.assertEqual(len(message.get_payload()), 2)
+ self.assertTrue(message.is_multipart())
+ self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
+ self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
+ self.assertEqual(message.get_all('to'), [email])
+ self.assertTrue(re.match(r'^http://example.com/reset/[\w/-]+', message.get_payload(0).get_payload()))
+ self.assertTrue(re.match(r'^<html><a href="http://example.com/reset/[\w/-]+/">Link</a></html>$', message.get_payload(1).get_payload()))
+
class ReadOnlyPasswordHashTest(TestCase):
View
59 django/contrib/auth/tests/test_templates.py
@@ -0,0 +1,59 @@
+from django.contrib.auth import authenticate
+from django.contrib.auth.models import User
+from django.contrib.auth.tests.utils import skipIfCustomUser
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+from django.contrib.auth.views import (
+ password_reset, password_reset_done, password_reset_confirm,
+ password_reset_complete, password_change, password_change_done,
+)
+from django.test import RequestFactory, TestCase
+from django.test.utils import override_settings
+from django.utils.encoding import force_bytes, force_text
+from django.utils.http import urlsafe_base64_encode
+
+
+@skipIfCustomUser
+@override_settings(
+ PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
+)
+class AuthTemplateTests(TestCase):
+
+ def test_titles(self):
+ rf = RequestFactory()
+ user = User.objects.create_user('jsmith', 'jsmith@example.com', 'pass')
+ user = authenticate(username=user.username, password='pass')
+ request = rf.get('/somepath/')
+ request.user = user
+
+ response = password_reset(request, post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Password reset</title>')
+ self.assertContains(response, '<h1>Password reset</h1>')
+
+ response = password_reset_done(request)
+ self.assertContains(response, '<title>Password reset successful</title>')
+ self.assertContains(response, '<h1>Password reset successful</h1>')
+
+ # password_reset_confirm invalid token
+ response = password_reset_confirm(request, uidb64='Bad', token='Bad', post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Password reset unsuccessful</title>')
+ self.assertContains(response, '<h1>Password reset unsuccessful</h1>')
+
+ # password_reset_confirm valid token
+ default_token_generator = PasswordResetTokenGenerator()
+ token = default_token_generator.make_token(user)
+ uidb64 = force_text(urlsafe_base64_encode(force_bytes(user.pk)))
+ response = password_reset_confirm(request, uidb64, token, post_reset_redirect='dummy/')
+ self.assertContains(response, '<title>Enter new password</title>')
+ self.assertContains(response, '<h1>Enter new password</h1>')
+
+ response = password_reset_complete(request)
+ self.assertContains(response, '<title>Password reset complete</title>')
+ self.assertContains(response, '<h1>Password reset complete</h1>')
+
+ response = password_change(request, post_change_redirect='dummy/')
+ self.assertContains(response, '<title>Password change</title>')
+ self.assertContains(response, '<h1>Password change</h1>')
+
+ response = password_change_done(request)
+ self.assertContains(response, '<title>Password change successful</title>')
+ self.assertContains(response, '<h1>Password change successful</h1>')
View
93 django/contrib/auth/tests/test_views.py
@@ -8,6 +8,7 @@
from django.conf import global_settings, settings
from django.contrib.sites.models import Site, RequestSite
+from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from django.core import mail
from django.core.urlresolvers import reverse, NoReverseMatch
@@ -54,6 +55,11 @@ def login(self, password='password'):
self.assertTrue(SESSION_KEY in self.client.session)
return response
+ def logout(self):
+ response = self.client.get('/admin/logout/')
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(SESSION_KEY not in self.client.session)
+
def assertFormError(self, response, error):
"""Assert that error is found in response.context['form'] errors"""
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
@@ -122,6 +128,25 @@ def test_email_found(self):
self.assertEqual(len(mail.outbox), 1)
self.assertTrue("http://" in mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
+ # optional multipart text/html email has been added. Make sure original,
+ # default functionality is 100% the same
+ self.assertFalse(mail.outbox[0].message().is_multipart())
+
+ def test_html_mail_template(self):
+ """
+ A multipart email with text/plain and text/html is sent
+ if the html_email_template parameter is passed to the view
+ """
+ response = self.client.post('/password_reset/html_email_template/', {'email': 'staffmember@example.com'})
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(len(mail.outbox), 1)
+ message = mail.outbox[0].message()
+ self.assertEqual(len(message.get_payload()), 2)
+ self.assertTrue(message.is_multipart())
+ self.assertEqual(message.get_payload(0).get_content_type(), 'text/plain')
+ self.assertEqual(message.get_payload(1).get_content_type(), 'text/html')
+ self.assertTrue('<html>' not in message.get_payload(0).get_payload())
+ self.assertTrue('<html>' in message.get_payload(1).get_payload())
def test_email_found_custom_from(self):