Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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

Conflicts:
	django/db/models/loading.py
  • Loading branch information...
commit 9daf81b94e7945d7adfdf62206cf9cb436166f0d 2 parents 315ab41 + d9a4354
@andrewgodwin andrewgodwin authored
Showing with 3,130 additions and 1,222 deletions.
  1. +1 −0  .gitignore
  2. +2 −1  AUTHORS
  3. +3 −6 django/contrib/admin/forms.py
  4. +84 −33 django/contrib/admin/options.py
  5. +5 −3 django/contrib/admin/templates/admin/change_form.html
  6. +4 −4 django/contrib/admin/templates/admin/change_list.html
  7. +1 −2  django/contrib/admin/templates/admin/delete_confirmation.html
  8. +1 −2  django/contrib/admin/templates/admin/delete_selected_confirmation.html
  9. +1 −2  django/contrib/admin/templates/admin/object_history.html
  10. +4 −1 django/contrib/admin/templates/admin/submit_line.html
  11. +2 −0  django/contrib/admin/templatetags/admin_list.py
  12. +2 −1  django/contrib/admin/templatetags/admin_modify.py
  13. +42 −0 django/contrib/admin/templatetags/admin_urls.py
  14. +9 −0 django/contrib/admin/tests.py
  15. +1 −0  django/contrib/admin/views/main.py
  16. +27 −12 django/contrib/auth/forms.py
  17. +20 −17 django/contrib/auth/hashers.py
  18. +12 −3 django/contrib/auth/management/__init__.py
  19. +1 −1  django/contrib/auth/models.py
  20. +12 −0 django/contrib/auth/tests/test_custom_user.py
  21. +8 −0 django/contrib/auth/tests/test_handlers.py
  22. +78 −6 django/contrib/auth/tests/test_hashers.py
  23. +7 −0 django/contrib/auth/tests/test_management.py
  24. +1 −1  django/contrib/auth/tests/test_models.py
  25. +6 −1 django/contrib/contenttypes/management.py
  26. +12 −4 django/contrib/flatpages/forms.py
  27. +4 −1 django/contrib/formtools/wizard/views.py
  28. +3 −3 django/contrib/gis/forms/fields.py
  29. +3 −1 django/contrib/sites/models.py
  30. 0  {tests/shared_models → django/core/compat_checks}/__init__.py
  31. +39 −0 django/core/compat_checks/base.py
  32. +37 −0 django/core/compat_checks/django_1_6_0.py
  33. +43 −21 django/core/exceptions.py
  34. +14 −0 django/core/management/commands/checksetup.py
  35. +21 −12 django/core/management/commands/flush.py
  36. +5 −0 django/core/management/commands/runserver.py
  37. +2 −2 django/core/management/sql.py
  38. +4 −0 django/core/management/validation.py
  39. +4 −1 django/core/urlresolvers.py
  40. +2 −6 django/core/validators.py
  41. +6 −2 django/db/backends/__init__.py
  42. +5 −2 django/db/backends/mysql/base.py
  43. +6 −6 django/db/backends/oracle/base.py
  44. +14 −5 django/db/backends/postgresql_psycopg2/operations.py
  45. +6 −6 django/db/backends/sqlite3/base.py
  46. +1 −1  django/db/models/__init__.py
  47. +28 −15 django/db/models/base.py
  48. +94 −54 django/db/models/fields/__init__.py
  49. +5 −2 django/db/models/fields/related.py
  50. +57 −18 django/db/models/loading.py
  51. +37 −27 django/db/models/sql/compiler.py
  52. +15 −14 django/db/models/sql/query.py
  53. +66 −45 django/forms/fields.py
  54. +3 −5 django/forms/forms.py
  55. +14 −2 django/forms/formsets.py
  56. +78 −24 django/forms/models.py
  57. +9 −4 django/forms/util.py
  58. +4 −0 django/http/response.py
  59. +0 −2  django/test/client.py
  60. +50 −23 django/test/testcases.py
  61. +3 −2 django/utils/ipv6.py
  62. +14 −6 django/views/generic/base.py
  63. +2 −2 django/views/generic/edit.py
  64. +2 −1  docs/Makefile
  65. +3 −0  docs/conf.py
  66. +8 −0 docs/howto/deployment/wsgi/index.txt
  67. +9 −0 docs/howto/deployment/wsgi/uwsgi.txt
  68. +11 −0 docs/internals/committers.txt
  69. +2 −2 docs/internals/contributing/writing-code/submitting-patches.txt
  70. +7 −0 docs/intro/tutorial03.txt
  71. +11 −4 docs/ref/class-based-views/base.txt
  72. +12 −12 docs/ref/class-based-views/generic-date-based.txt
  73. +26 −3 docs/ref/class-based-views/generic-display.txt
  74. +43 −6 docs/ref/class-based-views/generic-editing.txt
  75. +11 −3 docs/ref/contrib/admin/index.txt
  76. +16 −0 docs/ref/contrib/auth.txt
  77. +9 −3 docs/ref/contrib/flatpages.txt
  78. +1 −1  docs/ref/contrib/formtools/form-wizard.txt
  79. +13 −0 docs/ref/django-admin.txt
  80. +31 −1 docs/ref/forms/api.txt
  81. +20 −9 docs/ref/forms/models.txt
  82. +101 −9 docs/ref/forms/validation.txt
  83. +4 −1 docs/ref/forms/widgets.txt
  84. +10 −4 docs/ref/models/instances.txt
  85. +4 −0 docs/ref/settings.txt
  86. +11 −8 docs/ref/signals.txt
  87. +2 −2 docs/ref/templates/builtins.txt
  88. +2 −0  docs/ref/utils.txt
  89. +9 −1 docs/releases/1.5.txt
  90. +94 −3 docs/releases/1.6.txt
  91. +17 −1 docs/topics/auth/customizing.txt
  92. +6 −0 docs/topics/auth/passwords.txt
  93. +3 −3 docs/topics/class-based-views/mixins.txt
  94. +3 −3 docs/topics/files.txt
  95. +19 −3 docs/topics/forms/formsets.txt
  96. +11 −5 docs/topics/forms/index.txt
  97. +1 −1  docs/topics/forms/media.txt
  98. +45 −16 docs/topics/forms/modelforms.txt
  99. +5 −0 docs/topics/settings.txt
  100. +74 −0 docs/topics/testing/advanced.txt
  101. +16 −36 docs/topics/testing/overview.txt
  102. +2 −0  setup.py
  103. +3 −1 tests/admin_inlines/tests.py
  104. +22 −0 tests/admin_scripts/tests.py
  105. +2 −2 tests/admin_util/tests.py
  106. +161 −6 tests/admin_views/tests.py
  107. +6 −2 tests/admin_widgets/tests.py
  108. +7 −0 tests/backends/tests.py
  109. +3 −0  tests/basic/tests.py
  110. +2 −0  tests/cache/tests.py
  111. 0  tests/compat_checks/__init__.py
  112. +1 −0  tests/compat_checks/models.py
  113. +107 −0 tests/compat_checks/tests.py
  114. +6 −0 tests/delete_regress/tests.py
  115. +13 −7 tests/file_storage/tests.py
  116. +7 −0 tests/file_storage/urls.py
  117. +7 −0 tests/fixtures/tests.py
  118. +3 −0  tests/fixtures_model_package/tests.py
  119. +6 −0 tests/fixtures_regress/tests.py
  120. +25 −18 tests/forms_tests/tests/test_forms.py
  121. +78 −76 tests/forms_tests/tests/test_formsets.py
  122. +2 −2 tests/forms_tests/tests/test_regressions.py
  123. +2 −0  tests/forms_tests/tests/test_widgets.py
  124. +4 −2 tests/forms_tests/tests/tests.py
  125. +19 −1 tests/generic_views/test_base.py
  126. +2 −0  tests/get_or_create/tests.py
  127. +2 −0  tests/handlers/tests.py
  128. +16 −5 tests/lookup/models.py
  129. +398 −397 tests/lookup/tests.py
  130. +1 −0  tests/m2m_through_regress/tests.py
  131. +3 −0  tests/middleware/tests.py
  132. +32 −5 tests/model_forms/models.py
  133. +122 −57 tests/model_forms/tests.py
  134. +3 −1 tests/model_forms_regress/tests.py
  135. +51 −1 tests/model_formsets/tests.py
  136. +6 −0 tests/multiple_database/tests.py
  137. +7 −1 tests/proxy_model_inheritance/tests.py
  138. +85 −21 tests/queries/tests.py
  139. +10 −0 tests/queryset_pickle/models.py
  140. +42 −1 tests/queryset_pickle/tests.py
  141. +2 −0  tests/requests/tests.py
  142. +13 −2 tests/runtests.py
  143. +2 −0  tests/select_for_update/tests.py
  144. +3 −0  tests/serializers/tests.py
  145. +8 −1 tests/servers/tests.py
  146. +2 −0  tests/settings_tests/tests.py
  147. +0 −37 tests/shared_models/models.py
  148. +18 −0 tests/signals_regress/models.py
  149. +3 −3 tests/signals_regress/tests.py
  150. +7 −0 tests/swappable_models/tests.py
  151. +6 −1 tests/syncdb_signals/tests.py
  152. +14 −8 tests/test_runner/tests.py
  153. +3 −2 tests/test_suite_override/tests.py
  154. +3 −3 tests/test_utils/tests.py
  155. +16 −0 tests/transactions/tests.py
  156. +21 −0 tests/transactions_regress/tests.py
  157. +14 −0 tests/urlpatterns_reverse/tests.py
  158. +1 −0  tests/validation/models.py
  159. +5 −1 tests/validation/tests.py
  160. +3 −3 tests/validators/tests.py
  161. +2 −0  tests/view_tests/tests/test_i18n.py
View
1  .gitignore
@@ -4,5 +4,6 @@
MANIFEST
dist/
docs/_build/
+docs/locale/
tests/coverage_html/
tests/.coverage
View
3  AUTHORS
@@ -45,6 +45,7 @@ The PRIMARY AUTHORS are (and/or have been):
* Donald Stufft
* Daniel Lindsley
* Marc Tamlyn
+ * Baptiste Mispelon
More information on the main contributors to Django can be found in
docs/internals/committers.txt.
@@ -413,7 +414,6 @@ answer newbie questions, and generally made Django that much better:
Slawek Mikula <slawek dot mikula at gmail dot com>
Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com>
- Baptiste Mispelon <bmispelon@gmail.com>
mitakummaa@gmail.com
Taylor Mitchell <taylor.mitchell@gmail.com>
mmarshall
@@ -550,6 +550,7 @@ answer newbie questions, and generally made Django that much better:
Thomas Steinacher <http://www.eggdrop.ch/>
Emil Stenström <em@kth.se>
Johan C. Stöver <johan@nilling.nl>
+ Chris Streeter <chris@chrisstreeter.com>
Nowell Strite <http://nowell.strite.org/>
Thomas Stromberg <tstromberg@google.com>
Hannes Struß <x@hannesstruss.de>
View
9 django/contrib/admin/forms.py
@@ -22,15 +22,12 @@ def clean(self):
username = self.cleaned_data.get('username')
password = self.cleaned_data.get('password')
message = ERROR_MESSAGE
+ params = {'username': self.username_field.verbose_name}
if username and password:
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
elif not self.user_cache.is_active or not self.user_cache.is_staff:
- raise forms.ValidationError(message % {
- 'username': self.username_field.verbose_name
- })
+ raise forms.ValidationError(message, code='invalid', params=params)
return self.cleaned_data
View
117 django/contrib/admin/options.py
@@ -13,6 +13,7 @@
model_format_dict, NestedObjects, lookup_needs_distinct)
from django.contrib.admin import validation
from django.contrib.admin.templatetags.admin_static import static
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied, ValidationError, FieldError
@@ -33,6 +34,7 @@
from django.utils.safestring import mark_safe
from django.utils import six
from django.utils.deprecation import RenameMethodsBase
+from django.utils.http import urlencode
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
@@ -393,6 +395,7 @@ class ModelAdmin(BaseModelAdmin):
save_as = False
save_on_top = False
paginator = Paginator
+ preserve_filters = True
inlines = []
# Custom templates (designed to be over-ridden in subclasses)
@@ -755,6 +758,27 @@ def get_list_filter(self, request):
"""
return self.list_filter
+ def get_preserved_filters(self, request):
+ """
+ Returns the preserved filters querystring.
+ """
+
+ # FIXME: We can remove that getattr as soon as #20619 is fixed.
+ match = getattr(request, 'resolver_match', None)
+
+ if self.preserve_filters and match:
+ opts = self.model._meta
+ current_url = '%s:%s' % (match.namespace, match.url_name)
+ changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
+ if current_url == changelist_url:
+ preserved_filters = request.GET.urlencode()
+ else:
+ preserved_filters = request.GET.get('_changelist_filters')
+
+ if preserved_filters:
+ return urlencode({'_changelist_filters': preserved_filters})
+ return ''
+
def construct_change_message(self, request, form, formsets):
"""
Construct a change message from a changed object.
@@ -846,6 +870,8 @@ def save_related(self, request, form, formsets, change):
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta
app_label = opts.app_label
+ preserved_filters = self.get_preserved_filters(request)
+ form_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, form_url)
context.update({
'add': add,
'change': change,
@@ -877,11 +903,19 @@ def response_add(self, request, obj, post_url_continue=None):
"""
opts = obj._meta
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
# Here, we distinguish between different save types by checking for
# the presence of keys in request.POST.
- if "_continue" in request.POST:
+ if "_popup" in request.POST:
+ return HttpResponse(
+ '<!DOCTYPE html><html><head><title></title></head><body>'
+ '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
+ # escape() calls force_text.
+ (escape(pk_value), escapejs(obj)))
+
+ elif "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
if post_url_continue is None:
@@ -889,20 +923,16 @@ def response_add(self, request, obj, post_url_continue=None):
(opts.app_label, opts.model_name),
args=(pk_value,),
current_app=self.admin_site.name)
- if "_popup" in request.POST:
- post_url_continue += "?_popup=1"
+ post_url_continue = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue)
return HttpResponseRedirect(post_url_continue)
- if "_popup" in request.POST:
- return HttpResponse(
- '<!DOCTYPE html><html><head><title></title></head><body>'
- '<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % \
- # escape() calls force_text.
- (escape(pk_value), escapejs(obj)))
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -913,30 +943,36 @@ def response_change(self, request, obj):
Determines the HttpResponse for the change_view stage.
"""
opts = self.model._meta
-
pk_value = obj._get_pk_val()
+ preserved_filters = self.get_preserved_filters(request)
msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
if "_continue" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- if "_popup" in request.REQUEST:
- return HttpResponseRedirect(request.path + "?_popup=1")
- else:
- return HttpResponseRedirect(request.path)
+ redirect_url = request.path
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_saveasnew" in request.POST:
msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_change' %
- (opts.app_label, opts.model_name),
- args=(pk_value,),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_change' %
+ (opts.app_label, opts.model_name),
+ args=(pk_value,),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
elif "_addanother" in request.POST:
msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
- return HttpResponseRedirect(reverse('admin:%s_%s_add' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ redirect_url = reverse('admin:%s_%s_add' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
else:
msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
self.message_user(request, msg, messages.SUCCESS)
@@ -952,6 +988,8 @@ def response_post_save_add(self, request, obj):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -963,10 +1001,13 @@ def response_post_save_change(self, request, obj):
when editing an existing object.
"""
opts = self.model._meta
+
if self.has_change_permission(request, None):
post_url = reverse('admin:%s_%s_changelist' %
(opts.app_label, opts.model_name),
current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
else:
post_url = reverse('admin:index',
current_app=self.admin_site.name)
@@ -1122,6 +1163,7 @@ def add_view(self, request, form_url='', extra_context=None):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True)
@@ -1214,6 +1256,7 @@ def change_view(self, request, object_id, form_url='', extra_context=None):
'inline_admin_formsets': inline_admin_formsets,
'errors': helpers.AdminErrorList(form, formsets),
'app_label': opts.app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url)
@@ -1357,11 +1400,13 @@ def changelist_view(self, request, extra_context=None):
'cl': cl,
'media': media,
'has_add_permission': self.has_add_permission(request),
+ 'opts': cl.opts,
'app_label': app_label,
'action_form': action_form,
'actions_on_top': self.actions_on_top,
'actions_on_bottom': self.actions_on_bottom,
'actions_selection_counter': self.actions_selection_counter,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1406,12 +1451,16 @@ def delete_view(self, request, object_id, extra_context=None):
'obj': force_text(obj_display)},
messages.SUCCESS)
- if not self.has_change_permission(request, None):
- return HttpResponseRedirect(reverse('admin:index',
- current_app=self.admin_site.name))
- return HttpResponseRedirect(reverse('admin:%s_%s_changelist' %
- (opts.app_label, opts.model_name),
- current_app=self.admin_site.name))
+ if self.has_change_permission(request, None):
+ post_url = reverse('admin:%s_%s_changelist' %
+ (opts.app_label, opts.model_name),
+ current_app=self.admin_site.name)
+ preserved_filters = self.get_preserved_filters(request)
+ post_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, post_url)
+ else:
+ post_url = reverse('admin:index',
+ current_app=self.admin_site.name)
+ return HttpResponseRedirect(post_url)
object_name = force_text(opts.verbose_name)
@@ -1429,6 +1478,7 @@ def delete_view(self, request, object_id, extra_context=None):
"protected": protected,
"opts": opts,
"app_label": app_label,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
@@ -1463,6 +1513,7 @@ def history_view(self, request, object_id, extra_context=None):
'object': obj,
'app_label': app_label,
'opts': opts,
+ 'preserved_filters': self.get_preserved_filters(request),
}
context.update(extra_context or {})
return TemplateResponse(request, self.object_history_template or [
@@ -1574,13 +1625,13 @@ def hand_clean_DELETE(self):
'class_name': p._meta.verbose_name,
'instance': p}
)
- msg_dict = {'class_name': self._meta.model._meta.verbose_name,
- 'instance': self.instance,
- 'related_objects': get_text_list(objs, _('and'))}
+ params = {'class_name': self._meta.model._meta.verbose_name,
+ 'instance': self.instance,
+ 'related_objects': get_text_list(objs, _('and'))}
msg = _("Deleting %(class_name)s %(instance)s would require "
"deleting the following protected related objects: "
- "%(related_objects)s") % msg_dict
- raise ValidationError(msg)
+ "%(related_objects)s")
+ raise ValidationError(msg, code='deleting_protected', params=params)
def is_valid(self):
result = super(DeleteProtectedModelForm, self).is_valid()
View
8 django/contrib/admin/templates/admin/change_form.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_modify %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_modify %}
{% block extrahead %}{{ block.super }}
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script>
@@ -29,7 +28,10 @@
{% if change %}{% if not is_popup %}
<ul class="object-tools">
{% block object-tools-items %}
- <li><a href="{% url opts|admin_urlname:'history' original.pk|admin_urlquote %}" class="historylink">{% trans "History" %}</a></li>
+ <li>
+ {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
+ <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
+ </li>
{% if has_absolute_url %}<li><a href="{% url 'admin:view_on_site' content_type_id original.pk %}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif%}
{% endblock %}
</ul>
View
8 django/contrib/admin/templates/admin/change_list.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_list %}
-{% load admin_urls %}
+{% load i18n admin_urls admin_static admin_list %}
{% block extrastyle %}
{{ block.super }}
@@ -54,7 +53,8 @@
<ul class="object-tools">
{% block object-tools-items %}
<li>
- <a href="{% url cl.opts|admin_urlname:'add' %}{% if is_popup %}?_popup=1{% endif %}" class="addlink">
+ {% url cl.opts|admin_urlname:'add' as add_url %}
+ <a href="{% add_preserved_filters add_url is_popup %}" class="addlink">
{% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
</a>
</li>
@@ -64,7 +64,7 @@
{% endblock %}
{% if cl.formset.errors %}
<p class="errornote">
- {% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
+ {% if cl.formset.total_error_count == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{{ cl.formset.non_form_errors }}
{% endif %}
View
3  django/contrib/admin/templates/admin/delete_confirmation.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n %}
-{% load admin_urls %}
+{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
View
3  django/contrib/admin/templates/admin/delete_selected_confirmation.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n l10n %}
-{% load admin_urls %}
+{% load i18n l10n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
View
3  django/contrib/admin/templates/admin/object_history.html
@@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n %}
-{% load admin_urls %}
+{% load i18n admin_urls %}
{% block breadcrumbs %}
<div class="breadcrumbs">
View
5 django/contrib/admin/templates/admin/submit_line.html
@@ -1,7 +1,10 @@
{% load i18n admin_urls %}
<div class="submit-row">
{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
-{% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
+{% if show_delete_link %}
+ {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
+ <p class="deletelink-box"><a href="{% add_preserved_filters delete_url %}" class="deletelink">{% trans "Delete" %}</a></p>
+{% endif %}
{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
View
2  django/contrib/admin/templatetags/admin_list.py
@@ -2,6 +2,7 @@
import datetime
+from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.util import (lookup_field, display_for_field,
display_for_value, label_for_field)
from django.contrib.admin.views.main import (ALL_VAR, EMPTY_CHANGELIST_VALUE,
@@ -217,6 +218,7 @@ def items_for_result(cl, result, form):
table_tag = {True:'th', False:'td'}[first]
first = False
url = cl.url_for_result(result)
+ url = add_preserved_filters({'preserved_filters': cl.preserved_filters, 'opts': cl.opts}, url)
# Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings.
if cl.to_field:
View
3  django/contrib/admin/templatetags/admin_modify.py
@@ -37,7 +37,8 @@ def submit_row(context):
not is_popup and (not save_as or context['add']),
'show_save_and_continue': not is_popup and context['has_change_permission'],
'is_popup': is_popup,
- 'show_save': True
+ 'show_save': True,
+ 'preserved_filters': context.get('preserved_filters'),
}
if context.get('original') is not None:
ctx['original'] = context['original']
View
42 django/contrib/admin/templatetags/admin_urls.py
@@ -1,8 +1,17 @@
+from django.utils.http import urlencode
+
+try:
+ from urllib.parse import parse_qsl, urlparse, urlunparse
+except ImportError:
+ from urlparse import parse_qsl, urlparse, urlunparse
+
from django import template
from django.contrib.admin.util import quote
+from django.core.urlresolvers import resolve, Resolver404
register = template.Library()
+
@register.filter
def admin_urlname(value, arg):
return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg)
@@ -11,3 +20,36 @@ def admin_urlname(value, arg):
@register.filter
def admin_urlquote(value):
return quote(value)
+
+
+@register.simple_tag(takes_context=True)
+def add_preserved_filters(context, url, popup=False):
+ opts = context.get('opts')
+ preserved_filters = context.get('preserved_filters')
+
+ parsed_url = list(urlparse(url))
+ parsed_qs = dict(parse_qsl(parsed_url[4]))
+ merged_qs = dict()
+
+ if opts and preserved_filters:
+ preserved_filters = dict(parse_qsl(preserved_filters))
+
+ try:
+ match = resolve(url)
+ except Resolver404:
+ pass
+ else:
+ current_url = '%s:%s' % (match.namespace, match.url_name)
+ changelist_url = 'admin:%s_%s_changelist' % (opts.app_label, opts.model_name)
+ if changelist_url == current_url and '_changelist_filters' in preserved_filters:
+ preserved_filters = dict(parse_qsl(preserved_filters['_changelist_filters']))
+
+ merged_qs.update(preserved_filters)
+
+ if popup:
+ merged_qs['_popup'] = 1
+
+ merged_qs.update(parsed_qs)
+
+ parsed_url[4] = urlencode(merged_qs)
+ return urlunparse(parsed_url)
View
9 django/contrib/admin/tests.py
@@ -5,7 +5,16 @@
from django.utils.unittest import SkipTest
from django.utils.translation import ugettext as _
+
class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
+
+ available_apps = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ ]
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
@classmethod
View
1  django/contrib/admin/views/main.py
@@ -59,6 +59,7 @@ def __init__(self, request, model, list_display, list_display_links,
self.list_per_page = list_per_page
self.list_max_show_all = list_max_show_all
self.model_admin = model_admin
+ self.preserved_filters = model_admin.get_preserved_filters(request)
# Get search parameters from the query string.
try:
View
39 django/contrib/auth/forms.py
@@ -14,7 +14,7 @@
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.models import User
-from django.contrib.auth.hashers import UNUSABLE_PASSWORD, identify_hasher
+from django.contrib.auth.hashers import UNUSABLE_PASSWORD_PREFIX, identify_hasher
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.models import get_current_site
@@ -29,7 +29,7 @@ def render(self, name, value, attrs):
encoded = value
final_attrs = self.build_attrs(attrs)
- if not encoded or encoded == UNUSABLE_PASSWORD:
+ if not encoded or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
else:
try:
@@ -97,14 +97,19 @@ def clean_username(self):
User._default_manager.get(username=username)
except User.DoesNotExist:
return username
- raise forms.ValidationError(self.error_messages['duplicate_username'])
+ raise forms.ValidationError(
+ self.error_messages['duplicate_username'],
+ code='duplicate_username',
+ )
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -183,11 +188,15 @@ def clean(self):
password=password)
if self.user_cache is None:
raise forms.ValidationError(
- self.error_messages['invalid_login'] % {
- 'username': self.username_field.verbose_name
- })
+ self.error_messages['invalid_login'],
+ code='invalid_login',
+ params={'username': self.username_field.verbose_name},
+ )
elif not self.user_cache.is_active:
- raise forms.ValidationError(self.error_messages['inactive'])
+ raise forms.ValidationError(
+ self.error_messages['inactive'],
+ code='inactive',
+ )
return self.cleaned_data
def check_for_test_cookie(self):
@@ -222,7 +231,7 @@ def save(self, domain_override=None,
for user in users:
# Make sure that no email is sent to a user that actually has
# a password marked as unusable
- if user.password == UNUSABLE_PASSWORD:
+ if not user.has_usable_password():
continue
if not domain_override:
current_site = get_current_site(request)
@@ -269,7 +278,9 @@ def clean_new_password2(self):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
@@ -298,7 +309,9 @@ def clean_old_password(self):
old_password = self.cleaned_data["old_password"]
if not self.user.check_password(old_password):
raise forms.ValidationError(
- self.error_messages['password_incorrect'])
+ self.error_messages['password_incorrect'],
+ code='password_incorrect',
+ )
return old_password
PasswordChangeForm.base_fields = SortedDict([
@@ -329,7 +342,9 @@ def clean_password2(self):
if password1 and password2:
if password1 != password2:
raise forms.ValidationError(
- self.error_messages['password_mismatch'])
+ self.error_messages['password_mismatch'],
+ code='password_mismatch',
+ )
return password2
def save(self, commit=True):
View
37 django/contrib/auth/hashers.py
@@ -17,7 +17,8 @@
from django.utils.translation import ugettext_noop as _
-UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
+UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
+UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
HASHERS = None # lazily loaded from PASSWORD_HASHERS
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
@@ -30,7 +31,7 @@ def reset_hashers(**kwargs):
def is_password_usable(encoded):
- if encoded is None or encoded == UNUSABLE_PASSWORD:
+ if encoded is None or encoded.startswith(UNUSABLE_PASSWORD_PREFIX):
return False
try:
hasher = identify_hasher(encoded)
@@ -47,7 +48,7 @@ def check_password(password, encoded, setter=None, preferred='default'):
If setter is specified, it'll be called when you need to
regenerate the password.
"""
- if not password or not is_password_usable(encoded):
+ if not is_password_usable(encoded):
return False
preferred = get_hasher(preferred)
@@ -64,13 +65,15 @@ def make_password(password, salt=None, hasher='default'):
"""
Turn a plain-text password into a hash for database storage
- Same as encode() but generates a new random salt. If
- password is None or blank then UNUSABLE_PASSWORD will be
- returned which disallows logins.
+ Same as encode() but generates a new random salt.
+ If password is None then a concatenation of
+ UNUSABLE_PASSWORD_PREFIX and a random string will be returned
+ which disallows logins. Additional random string reduces chances
+ of gaining access to staff or superuser accounts.
+ See ticket #20079 for more info.
"""
- if not password:
- return UNUSABLE_PASSWORD
-
+ if password is None:
+ return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
hasher = get_hasher(hasher)
if not salt:
@@ -171,12 +174,12 @@ def _load_library(self):
name = mod_path = self.library
try:
module = importlib.import_module(mod_path)
- except ImportError:
- raise ValueError("Couldn't load %s password algorithm "
- "library" % name)
+ except ImportError as e:
+ raise ValueError("Couldn't load %r algorithm library: %s" %
+ (self.__class__.__name__, e))
return module
- raise ValueError("Hasher '%s' doesn't specify a library attribute" %
- self.__class__)
+ raise ValueError("Hasher %r doesn't specify a library attribute" %
+ self.__class__.__name__)
def salt(self):
"""
@@ -222,7 +225,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
digest = hashlib.sha256
def encode(self, password, salt, iterations=None):
- assert password
+ assert password is not None
assert salt and '$' not in salt
if not iterations:
iterations = self.iterations
@@ -350,7 +353,7 @@ class SHA1PasswordHasher(BasePasswordHasher):
algorithm = "sha1"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.sha1(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
@@ -378,7 +381,7 @@ class MD5PasswordHasher(BasePasswordHasher):
algorithm = "md5"
def encode(self, password, salt):
- assert password
+ assert password is not None
assert salt and '$' not in salt
hash = hashlib.md5(force_bytes(salt + password)).hexdigest()
return "%s$%s$%s" % (self.algorithm, salt, hash)
View
15 django/contrib/auth/management/__init__.py
@@ -11,7 +11,7 @@
from django.core import exceptions
from django.core.management.base import CommandError
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_models, signals
+from django.db.models import get_model, get_models, signals, UnavailableApp
from django.utils.encoding import DEFAULT_LOCALE_ENCODING
from django.utils import six
from django.utils.six.moves import input
@@ -60,6 +60,11 @@ def _check_permission_clashing(custom, builtin, ctype):
pool.add(codename)
def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
+ try:
+ get_model('auth', 'Permission')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, auth_app.Permission):
return
@@ -101,9 +106,13 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
def create_superuser(app, created_models, verbosity, db, **kwargs):
- from django.core.management import call_command
+ try:
+ get_model('auth', 'Permission')
+ UserModel = get_user_model()
+ except UnavailableApp:
+ return
- UserModel = get_user_model()
+ from django.core.management import call_command
if UserModel in created_models and kwargs.get('interactive', True):
msg = ("\nYou just installed Django's auth system, which means you "
View
2  django/contrib/auth/models.py
@@ -16,7 +16,7 @@
from django.contrib import auth
# UNUSABLE_PASSWORD is still imported here for backwards compatibility
from django.contrib.auth.hashers import (
- check_password, make_password, is_password_usable, UNUSABLE_PASSWORD)
+ check_password, make_password, is_password_usable)
from django.contrib.auth.signals import user_logged_in
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import python_2_unicode_compatible
View
12 django/contrib/auth/tests/test_custom_user.py
@@ -156,6 +156,18 @@ class Meta:
app_label = 'auth'
+class CustomUserNonListRequiredFields(AbstractBaseUser):
+ "A user with a non-list REQUIRED_FIELDS"
+ username = models.CharField(max_length=30, unique=True)
+ date_of_birth = models.DateField()
+
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = 'date_of_birth'
+
+ class Meta:
+ app_label = 'auth'
+
+
class CustomUserBadRequiredFields(AbstractBaseUser):
"A user with a non-unique username"
username = models.CharField(max_length=30, unique=True)
View
8 django/contrib/auth/tests/test_handlers.py
@@ -8,10 +8,18 @@
from django.test.utils import override_settings
+# This must be a TransactionTestCase because the WSGI auth handler performs
+# its own transaction management.
class ModWsgiHandlerTestCase(TransactionTestCase):
"""
Tests for the mod_wsgi authentication handler
"""
+
+ available_apps = [
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ ]
+
@skipIfCustomUser
def test_check_password(self):
"""
View
84 django/contrib/auth/tests/test_hashers.py
@@ -2,9 +2,10 @@
from __future__ import unicode_literals
from django.conf.global_settings import PASSWORD_HASHERS as default_hashers
-from django.contrib.auth.hashers import (is_password_usable,
- check_password, make_password, PBKDF2PasswordHasher, load_hashers,
- PBKDF2SHA1PasswordHasher, get_hasher, identify_hasher, UNUSABLE_PASSWORD)
+from django.contrib.auth.hashers import (is_password_usable, BasePasswordHasher,
+ check_password, make_password, PBKDF2PasswordHasher, load_hashers, PBKDF2SHA1PasswordHasher,
+ get_hasher, identify_hasher, UNUSABLE_PASSWORD_PREFIX, UNUSABLE_PASSWORD_SUFFIX_LENGTH)
+from django.utils import six
from django.utils import unittest
from django.utils.unittest import skipUnless
@@ -31,6 +32,12 @@ def test_simple(self):
self.assertTrue(is_password_usable(encoded))
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
+ # Blank passwords
+ blank_encoded = make_password('')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_pkbdf2(self):
encoded = make_password('lètmein', 'seasalt', 'pbkdf2_sha256')
@@ -40,6 +47,12 @@ def test_pkbdf2(self):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "pbkdf2_sha256")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'pbkdf2_sha256')
+ self.assertTrue(blank_encoded.startswith('pbkdf2_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_sha1(self):
encoded = make_password('lètmein', 'seasalt', 'sha1')
@@ -49,6 +62,12 @@ def test_sha1(self):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "sha1")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_md5(self):
encoded = make_password('lètmein', 'seasalt', 'md5')
@@ -58,6 +77,12 @@ def test_md5(self):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "md5")
+ # Blank passwords
+ blank_encoded = make_password('', 'seasalt', 'md5')
+ self.assertTrue(blank_encoded.startswith('md5$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_md5(self):
encoded = make_password('lètmein', '', 'unsalted_md5')
@@ -71,6 +96,11 @@ def test_unsalted_md5(self):
self.assertTrue(is_password_usable(alt_encoded))
self.assertTrue(check_password('lètmein', alt_encoded))
self.assertFalse(check_password('lètmeinz', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_md5')
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unsalted_sha1(self):
encoded = make_password('lètmein', '', 'unsalted_sha1')
@@ -82,6 +112,12 @@ def test_unsalted_sha1(self):
# Raw SHA1 isn't acceptable
alt_encoded = encoded[6:]
self.assertFalse(check_password('lètmein', alt_encoded))
+ # Blank passwords
+ blank_encoded = make_password('', '', 'unsalted_sha1')
+ self.assertTrue(blank_encoded.startswith('sha1$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(crypt, "no crypt module to generate password.")
def test_crypt(self):
@@ -91,6 +127,12 @@ def test_crypt(self):
self.assertTrue(check_password('lètmei', encoded))
self.assertFalse(check_password('lètmeiz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "crypt")
+ # Blank passwords
+ blank_encoded = make_password('', 'ab', 'crypt')
+ self.assertTrue(blank_encoded.startswith('crypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt_sha256(self):
@@ -107,6 +149,12 @@ def test_bcrypt_sha256(self):
encoded = make_password(password, hasher='bcrypt_sha256')
self.assertTrue(check_password(password, encoded))
self.assertFalse(check_password(password[:72], encoded))
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt_sha256')
+ self.assertTrue(blank_encoded.startswith('bcrypt_sha256$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
@skipUnless(bcrypt, "bcrypt not installed")
def test_bcrypt(self):
@@ -116,21 +164,31 @@ def test_bcrypt(self):
self.assertTrue(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertEqual(identify_hasher(encoded).algorithm, "bcrypt")
+ # Blank passwords
+ blank_encoded = make_password('', hasher='bcrypt')
+ self.assertTrue(blank_encoded.startswith('bcrypt$'))
+ self.assertTrue(is_password_usable(blank_encoded))
+ self.assertTrue(check_password('', blank_encoded))
+ self.assertFalse(check_password(' ', blank_encoded))
def test_unusable(self):
encoded = make_password(None)
+ self.assertEqual(len(encoded), len(UNUSABLE_PASSWORD_PREFIX) + UNUSABLE_PASSWORD_SUFFIX_LENGTH)
self.assertFalse(is_password_usable(encoded))
self.assertFalse(check_password(None, encoded))
- self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded))
+ self.assertFalse(check_password(encoded, encoded))
+ self.assertFalse(check_password(UNUSABLE_PASSWORD_PREFIX, encoded))
self.assertFalse(check_password('', encoded))
self.assertFalse(check_password('lètmein', encoded))
self.assertFalse(check_password('lètmeinz', encoded))
self.assertRaises(ValueError, identify_hasher, encoded)
+ # Assert that the unusable passwords actually contain a random part.
+ # This might fail one day due to a hash collision.
+ self.assertNotEqual(encoded, make_password(None), "Random password collision?")
def test_bad_algorithm(self):
- def doit():
+ with self.assertRaises(ValueError):
make_password('lètmein', hasher='lolcat')
- self.assertRaises(ValueError, doit)
self.assertRaises(ValueError, identify_hasher, "lolcat$salt$hash")
def test_bad_encoded(self):
@@ -178,3 +236,17 @@ def setter():
state['upgraded'] = True
self.assertFalse(check_password('WRONG', encoded, setter))
self.assertFalse(state['upgraded'])
+
+ def test_load_library_no_algorithm(self):
+ with self.assertRaises(ValueError) as e:
+ BasePasswordHasher()._load_library()
+ self.assertEqual("Hasher 'BasePasswordHasher' doesn't specify a "
+ "library attribute", str(e.exception))
+
+ def test_load_library_importerror(self):
+ PlainHasher = type(str('PlainHasher'), (BasePasswordHasher,),
+ {'algorithm': 'plain', 'library': 'plain'})
+ # Python 3.3 adds quotes around module name
+ with six.assertRaisesRegex(self, ValueError,
+ "Couldn't load 'PlainHasher' algorithm library: No module named '?plain'?"):
+ PlainHasher()._load_library()
View
7 django/contrib/auth/tests/test_management.py
@@ -174,6 +174,13 @@ def test_swappable_user_missing_required_field(self):
class CustomUserModelValidationTestCase(TestCase):
+ @override_settings(AUTH_USER_MODEL='auth.CustomUserNonListRequiredFields')
+ def test_required_fields_is_list(self):
+ "REQUIRED_FIELDS should be a list."
+ new_io = StringIO()
+ get_validation_errors(new_io, get_app('auth'))
+ self.assertIn("The REQUIRED_FIELDS must be a list or tuple.", new_io.getvalue())
+
@override_settings(AUTH_USER_MODEL='auth.CustomUserBadRequiredFields')
def test_username_not_in_required_fields(self):
"USERNAME_FIELD should not appear in REQUIRED_FIELDS."
View
2  django/contrib/auth/tests/test_models.py
@@ -87,7 +87,7 @@ def test_create_user(self):
user = User.objects.create_user('user', email_lowercase)
self.assertEqual(user.email, email_lowercase)
self.assertEqual(user.username, 'user')
- self.assertEqual(user.password, '!')
+ self.assertFalse(user.has_usable_password())
def test_create_user_email_domain_normalize_rfc3696(self):
# According to http://tools.ietf.org/html/rfc3696#section-3
View
7 django/contrib/contenttypes/management.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.db import DEFAULT_DB_ALIAS, router
-from django.db.models import get_apps, get_models, signals
+from django.db.models import get_apps, get_model, get_models, signals, UnavailableApp
from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
@@ -11,6 +11,11 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, *
Creates content types for models in the given app, removing any model
entries that no longer have a matching model class.
"""
+ try:
+ get_model('contenttypes', 'ContentType')
+ except UnavailableApp:
+ return
+
if not router.allow_syncdb(db, ContentType):
return
View
16 django/contrib/flatpages/forms.py
@@ -17,11 +17,17 @@ class Meta:
def clean_url(self):
url = self.cleaned_data['url']
if not url.startswith('/'):
- raise forms.ValidationError(ugettext("URL is missing a leading slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a leading slash."),
+ code='missing_leading_slash',
+ )
if (settings.APPEND_SLASH and
'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and
not url.endswith('/')):
- raise forms.ValidationError(ugettext("URL is missing a trailing slash."))
+ raise forms.ValidationError(
+ ugettext("URL is missing a trailing slash."),
+ code='missing_trailing_slash',
+ )
return url
def clean(self):
@@ -36,7 +42,9 @@ def clean(self):
for site in sites:
if same_url.filter(sites=site).exists():
raise forms.ValidationError(
- _('Flatpage with url %(url)s already exists for site %(site)s') %
- {'url': url, 'site': site})
+ _('Flatpage with url %(url)s already exists for site %(site)s'),
+ code='duplicate_url',
+ params={'url': url, 'site': site},
+ )
return super(FlatpageForm, self).clean()
View
5 django/contrib/formtools/wizard/views.py
@@ -7,6 +7,7 @@
from django.views.generic import TemplateView
from django.utils.datastructures import SortedDict
from django.utils.decorators import classonlymethod
+from django.utils.translation import ugettext as _
from django.utils import six
from django.contrib.formtools.wizard.storage import get_storage
@@ -271,7 +272,9 @@ def post(self, *args, **kwargs):
management_form = ManagementForm(self.request.POST, prefix=self.prefix)
if not management_form.is_valid():
raise ValidationError(
- 'ManagementForm data is missing or has been tampered.')
+ _('ManagementForm data is missing or has been tampered.'),
+ code='missing_management_form',
+ )
form_current_step = management_form.cleaned_data['current_step']
if (form_current_step != self.steps.current and
View
6 django/contrib/gis/forms/fields.py
@@ -50,7 +50,7 @@ def to_python(self, value):
try:
return GEOSGeometry(value)
except (GEOSException, ValueError, TypeError):
- raise forms.ValidationError(self.error_messages['invalid_geom'])
+ raise forms.ValidationError(self.error_messages['invalid_geom'], code='invalid_geom')
def clean(self, value):
"""
@@ -65,7 +65,7 @@ def clean(self, value):
# Ensuring that the geometry is of the correct type (indicated
# using the OGC string label).
if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY':
- raise forms.ValidationError(self.error_messages['invalid_geom_type'])
+ raise forms.ValidationError(self.error_messages['invalid_geom_type'], code='invalid_geom_type')
# Transforming the geometry if the SRID was set.
if self.srid:
@@ -76,7 +76,7 @@ def clean(self, value):
try:
geom.transform(self.srid)
except:
- raise forms.ValidationError(self.error_messages['transform_error'])
+ raise forms.ValidationError(self.error_messages['transform_error'], code='transform_error')
return geom
View
4 django/contrib/sites/models.py
@@ -22,7 +22,9 @@ def _simple_domain_name_validator(value):
checks = ((s in value) for s in string.whitespace)
if any(checks):
raise ValidationError(
- _("The domain name cannot contain any spaces or tabs."))
+ _("The domain name cannot contain any spaces or tabs."),
+ code='invalid',
+ )
class SiteManager(models.Manager):
View
0  tests/shared_models/__init__.py → django/core/compat_checks/__init__.py
File renamed without changes
View
39 django/core/compat_checks/base.py
@@ -0,0 +1,39 @@
+from __future__ import unicode_literals
+import warnings
+
+from django.core.compat_checks import django_1_6_0
+
+
+COMPAT_CHECKS = [
+ # Add new modules at the top, so we keep things in descending order.
+ # After two-three minor releases, old versions should get dropped.
+ django_1_6_0,
+]
+
+
+def check_compatibility():
+ """
+ Runs through compatibility checks to warn the user with an existing install
+ about changes in an up-to-date Django.
+
+ Modules should be located in ``django.core.compat_checks`` (typically one
+ per release of Django) & must have a ``run_checks`` function that runs
+ all the checks.
+
+ Returns a list of informational messages about incompatibilities.
+ """
+ messages = []
+
+ for check_module in COMPAT_CHECKS:
+ check = getattr(check_module, 'run_checks', None)
+
+ if check is None:
+ warnings.warn(
+ "The '%s' module lacks a " % check_module.__name__ +
+ "'run_checks' method, which is needed to verify compatibility."
+ )
+ continue
+
+ messages.extend(check())
+
+ return messages
View
37 django/core/compat_checks/django_1_6_0.py
@@ -0,0 +1,37 @@
+from __future__ import unicode_literals
+
+
+def check_test_runner():
+ """
+ Checks if the user has *not* overridden the ``TEST_RUNNER`` setting &
+ warns them about the default behavior changes.
+
+ If the user has overridden that setting, we presume they know what they're
+ doing & avoid generating a message.
+ """
+ from django.conf import settings
+ new_default = 'django.test.runner.DiscoverRunner'
+ test_runner_setting = getattr(settings, 'TEST_RUNNER', new_default)
+
+ if test_runner_setting == new_default:
+ message = [
+ "You have not explicitly set 'TEST_RUNNER'. In Django 1.6,",
+ "there is a new test runner ('%s')" % new_default,
+ "by default. You should ensure your tests are still all",
+ "running & behaving as expected. See",
+ "https://docs.djangoproject.com/en/dev/releases/1.6/#discovery-of-tests-in-any-test-module",
+ "for more information.",
+ ]
+ return ' '.join(message)
+
+
+def run_checks():
+ """
+ Required by the ``checksetup`` management command, this returns a list of
+ messages from all the relevant check functions for this version of Django.
+ """
+ checks = [
+ check_test_runner()
+ ]
+ # Filter out the ``None`` or empty strings.
+ return [output for output in checks if output]
View
64 django/core/exceptions.py
@@ -3,6 +3,9 @@
"""
import logging
from functools import reduce
+import operator
+
+from django.utils.encoding import force_text
class DjangoRuntimeWarning(RuntimeWarning):
@@ -74,46 +77,65 @@ class FieldError(Exception):
class ValidationError(Exception):
"""An error while validating data."""
def __init__(self, message, code=None, params=None):
- import operator
- from django.utils.encoding import force_text
"""
ValidationError can be passed any object that can be printed (usually
a string), a list of objects or a dictionary.
"""
if isinstance(message, dict):
- self.message_dict = message
- # Reduce each list of messages into a single list.
- message = reduce(operator.add, message.values())
-
- if isinstance(message, list):
- self.messages = [force_text(msg) for msg in message]
+ self.error_dict = message
+ elif isinstance(message, list):
+ self.error_list = message
else:
self.code = code
self.params = params
+ self.message = message
+ self.error_list = [self]
+
+ @property
+ def message_dict(self):
+ message_dict = {}
+ for field, messages in self.error_dict.items():
+ message_dict[field] = []
+ for message in messages:
+ if isinstance(message, ValidationError):
+ message_dict[field].extend(message.messages)
+ else:
+ message_dict[field].append(force_text(message))
+ return message_dict
+
+ @property
+ def messages(self):
+ if hasattr(self, 'error_dict'):
+ message_list = reduce(operator.add, self.error_dict.values())
+ else:
+ message_list = self.error_list
+
+ messages = []
+ for message in message_list:
+ if isinstance(message, ValidationError):
+ params = message.params
+ message = message.message
+ if params:
+ message %= params
message = force_text(message)
- self.messages = [message]
+ messages.append(message)
+ return messages
def __str__(self):
- # This is needed because, without a __str__(), printing an exception
- # instance would result in this:
- # AttributeError: ValidationError instance has no attribute 'args'
- # See http://www.python.org/doc/current/tut/node10.html#handling
- if hasattr(self, 'message_dict'):
+ if hasattr(self, 'error_dict'):
return repr(self.message_dict)
return repr(self.messages)
def __repr__(self):
- if hasattr(self, 'message_dict'):
- return 'ValidationError(%s)' % repr(self.message_dict)
- return 'ValidationError(%s)' % repr(self.messages)
+ return 'ValidationError(%s)' % self
def update_error_dict(self, error_dict):
- if hasattr(self, 'message_dict'):
+ if hasattr(self, 'error_dict'):
if error_dict:
- for k, v in self.message_dict.items():
+ for k, v in self.error_dict.items():
error_dict.setdefault(k, []).extend(v)
else:
- error_dict = self.message_dict
+ error_dict = self.error_dict
else:
- error_dict[NON_FIELD_ERRORS] = self.messages
+ error_dict[NON_FIELD_ERRORS] = self.error_list
return error_dict
View
14 django/core/management/commands/checksetup.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+import warnings
+
+from django.core.compat_checks.base import check_compatibility
+from django.core.management.base import NoArgsCommand
+
+
+class Command(NoArgsCommand):
+ help = "Checks your configuration's compatibility with this version " + \
+ "of Django."
+
+ def handle_noargs(self, **options):
+ for message in check_compatibility():
+ warnings.warn(message)
View
33 django/core/management/commands/flush.py
@@ -32,8 +32,10 @@ def handle_noargs(self, **options):
connection = connections[db]
verbosity = int(options.get('verbosity'))
interactive = options.get('interactive')
- # 'reset_sequences' is a stealth option
+ # The following are stealth options used by Django's internals.
reset_sequences = options.get('reset_sequences', True)
+ allow_cascade = options.get('allow_cascade', False)
+ inhibit_post_syncdb = options.get('inhibit_post_syncdb', False)
self.style = no_style()
@@ -45,7 +47,9 @@ def handle_noargs(self, **options):
except ImportError:
pass
- sql_list = sql_flush(self.style, connection, only_django=True, reset_sequences=reset_sequences)
+ sql_list = sql_flush(self.style, connection, only_django=True,
+ reset_sequences=reset_sequences,
+ allow_cascade=allow_cascade)
if interactive:
confirm = input("""You have requested a flush of the database.
@@ -72,16 +76,9 @@ def handle_noargs(self, **options):
"Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run.\n"
"The full error: %s") % (connection.settings_dict['NAME'], e)
six.reraise(CommandError, CommandError(new_msg), sys.exc_info()[2])
- # Emit the post sync signal. This allows individual
- # applications to respond as if the database had been
- # sync'd from scratch.
- all_models = []
- for app in models.get_apps():
- all_models.extend([
- m for m in models.get_models(app, include_auto_created=True)
- if router.allow_syncdb(db, m)
- ])
- emit_post_sync_signal(set(all_models), verbosity, interactive, db)
+
+ if not inhibit_post_syncdb:
+ self.emit_post_syncdb(verbosity, interactive, db)
# Reinstall the initial_data fixture.
if options.get('load_initial_data'):
@@ -90,3 +87,15 @@ def handle_noargs(self, **options):
else:
self.stdout.write("Flush cancelled.\n")
+
+ @staticmethod
+ def emit_post_syncdb(verbosity, interactive, database):
+ # Emit the post sync signal. This allows individual applications to
+ # respond as if the database had been sync'd from scratch.
+ all_models = []
+ for app in models.get_apps():
+ all_models.extend([
+ m for m in models.get_models(app, include_auto_created=True)
+ if router.allow_syncdb(database, m)
+ ])
+ emit_post_sync_signal(set(all_models), verbosity, interactive, database)
View
5 django/core/management/commands/runserver.py
@@ -40,6 +40,11 @@ def get_handler(self, *args, **options):
return get_internal_wsgi_application()
def handle(self, addrport='', *args, **options):
+ from django.conf import settings
+
+ if not settings.DEBUG and not settings.ALLOWED_HOSTS:
+ raise CommandError('You must set settings.ALLOWED_HOSTS if DEBUG is False.')
+
self.use_ipv6 = options.get('use_ipv6')
if self.use_ipv6 and not socket.has_ipv6:
raise CommandError('Your Python does not support IPv6.')
View
4 django/core/management/sql.py
@@ -102,7 +102,7 @@ def sql_delete(app, style, connection):
return output[::-1] # Reverse it, to deal with table dependencies.
-def sql_flush(style, connection, only_django=False, reset_sequences=True):
+def sql_flush(style, connection, only_django=False, reset_sequences=True, allow_cascade=False):
"""
Returns a list of the SQL statements used to flush the database.
@@ -114,7 +114,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True):
else:
tables = connection.introspection.table_names()
seqs = connection.introspection.sequence_list() if reset_sequences else ()
- statements = connection.ops.sql_flush(style, tables, seqs)
+ statements = connection.ops.sql_flush(style, tables, seqs, allow_cascade)
return statements
View
4 django/core/management/validation.py
@@ -51,6 +51,10 @@ def get_validation_errors(outfile, app=None):
# If this is the current User model, check known validation problems with User models
if settings.AUTH_USER_MODEL == '%s.%s' % (opts.app_label, opts.object_name):
+ # Check that REQUIRED_FIELDS is a list
+ if not isinstance(cls.REQUIRED_FIELDS, (list, tuple)):
+ e.add(opts, 'The REQUIRED_FIELDS must be a list or tuple.')
+
# Check that the USERNAME FIELD isn't included in REQUIRED_FIELDS.
if cls.USERNAME_FIELD in cls.REQUIRED_FIELDS:
e.add(opts, 'The field named as the USERNAME_FIELD should not be included in REQUIRED_FIELDS on a swappable User model.')
View
5 django/core/urlresolvers.py
@@ -422,8 +422,11 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
lookup_view_s = "%s.%s" % (m, n)
else:
lookup_view_s = lookup_view
+
+ patterns = [pattern for (possibility, pattern, defaults) in possibilities]
raise NoReverseMatch("Reverse for '%s' with arguments '%s' and keyword "
- "arguments '%s' not found." % (lookup_view_s, args, kwargs))
+ "arguments '%s' not found. %d pattern(s) tried: %s" %
+ (lookup_view_s, args, kwargs, len(patterns), patterns))
class LocaleRegexURLResolver(RegexURLResolver):
"""
View
8 django/core/validators.py
@@ -76,7 +76,7 @@ def validate_integer(value):
try:
int(value)
except (ValueError, TypeError):
- raise ValidationError('')
+ raise ValidationError(_('Enter a valid integer.'), code='invalid')
class EmailValidator(object):
@@ -188,11 +188,7 @@ def __call__(self, value):
cleaned = self.clean(value)
params = {'limit_value': self.limit_value, 'show_value': cleaned}
if self.compare(cleaned, self.limit_value):
- raise ValidationError(
- self.message % params,
- code=self.code,
- params=params,
- )
+ raise ValidationError(self.message, code=self.code, params=params)
class MaxValueValidator(BaseValidator):
View
8 django/db/backends/__init__.py
@@ -390,7 +390,7 @@ def constraint_checks_disabled(self):
def disable_constraint_checking(self):
"""
Backends can implement as needed to temporarily disable foreign key
- constraint checking. Should return True if the constraints were
+ constraint checking. Should return True if the constraints were
disabled and will need to be reenabled.
"""
return False
@@ -966,7 +966,7 @@ def set_time_zone_sql(self):
"""
return ''
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
"""
Returns a list of SQL statements required to remove all data from
the given database tables (without actually removing the tables
@@ -977,6 +977,10 @@ def sql_flush(self, style, tables, sequences):
The `style` argument is a Style object as returned by either
color_style() or no_style() in django.core.management.color.
+
+ The `allow_cascade` argument determines whether truncation may cascade
+ to tables with foreign keys pointing the tables being truncated.
+ PostgreSQL requires a cascade even if these tables are empty.
"""
raise NotImplementedError()
View
7 django/db/backends/mysql/base.py
@@ -302,14 +302,17 @@ def quote_name(self, name):
def random_function_sql(self):
return 'RAND()'
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# NB: The generated SQL below is specific to MySQL
# 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements
# to clear all tables of all data
if tables:
sql = ['SET FOREIGN_KEY_CHECKS = 0;']
for table in tables:
- sql.append('%s %s;' % (style.SQL_KEYWORD('TRUNCATE'), style.SQL_FIELD(self.quote_name(table))))
+ sql.append('%s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ style.SQL_FIELD(self.quote_name(table)),
+ ))
sql.append('SET FOREIGN_KEY_CHECKS = 1;')
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
View
12 django/db/backends/oracle/base.py
@@ -340,17 +340,17 @@ def savepoint_create_sql(self, sid):
def savepoint_rollback_sql(self, sid):
return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(sid))
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# Return a list of 'TRUNCATE x;', 'TRUNCATE y;',
# 'TRUNCATE z;'... style SQL statements
if tables:
# Oracle does support TRUNCATE, but it seems to get us into
# FK referential trouble, whereas DELETE FROM table works.
- sql = ['%s %s %s;' % \
- (style.SQL_KEYWORD('DELETE'),
- style.SQL_KEYWORD('FROM'),
- style.SQL_FIELD(self.quote_name(table)))
- for table in tables]
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(self.quote_name(table))
+ ) for table in tables]
# Since we've just deleted all the rows, running our sequence
# ALTER code will reset the sequence to 0.
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
View
19 django/db/backends/postgresql_psycopg2/operations.py
@@ -101,15 +101,24 @@ def quote_name(self, name):
def set_time_zone_sql(self):
return "SET TIME ZONE %s"
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
if tables:
# Perform a single SQL 'TRUNCATE x, y, z...;' statement. It allows
# us to truncate tables referenced by a foreign key in any other
# table.
- sql = ['%s %s;' % \
- (style.SQL_KEYWORD('TRUNCATE'),
- style.SQL_FIELD(', '.join([self.quote_name(table) for table in tables]))
- )]
+ tables_sql = ', '.join(
+ style.SQL_FIELD(self.quote_name(table)) for table in tables)
+ if allow_cascade:
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ tables_sql,
+ style.SQL_KEYWORD('CASCADE'),
+ )]
+ else:
+ sql = ['%s %s;' % (
+ style.SQL_KEYWORD('TRUNCATE'),
+ tables_sql,
+ )]
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
return sql
else:
View
12 django/db/backends/sqlite3/base.py
@@ -212,15 +212,15 @@ def quote_name(self, name):
def no_limit_value(self):
return -1
- def sql_flush(self, style, tables, sequences):
+ def sql_flush(self, style, tables, sequences, allow_cascade=False):
# NB: The generated SQL below is specific to SQLite
# Note: The DELETE FROM... SQL generated below works for SQLite databases
# because constraints don't exist
- sql = ['%s %s %s;' % \
- (style.SQL_KEYWORD('DELETE'),
- style.SQL_KEYWORD('FROM'),
- style.SQL_FIELD(self.quote_name(table))
- ) for table in tables]
+ sql = ['%s %s %s;' % (
+ style.SQL_KEYWORD('DELETE'),
+ style.SQL_KEYWORD('FROM'),
+ style.SQL_FIELD(self.quote_name(table))
+ ) for table in tables]
# Note: No requirement for reset of auto-incremented indices (cf. other
# sql_flush() implementations). Just return SQL at this point
return sql
View
2  django/db/models/__init__.py
@@ -1,7 +1,7 @@
from functools import wraps
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
-from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models
+from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models, UnavailableApp
from django.db.models.query import Q
from django.db.models.expressions import F
from django.db.models.manager import Manager
View
43 django/db/models/base.py
@@ -450,16 +450,18 @@ def __reduce__(self):
need to do things manually, as they're dynamically created classes and
only module-level classes can be pickled by the default path.
"""
- if not self._deferred:
- return super(Model, self).__reduce__()
data = self.__dict__
+ if not self._deferred:
+ class_id = self._meta.app_label, self._meta.object_name
+ return model_unpickle, (class_id, [], simple_class_factory), data
defers = []
for field in self._meta.fields:
if isinstance(self.__class__.__dict__.get(field.attname),
- DeferredAttribute):
+ DeferredAttribute):
defers.append(field.attname)
model = self._meta.proxy_for_model
- return (model_unpickle, (model, defers), data)
+ class_id = model._meta.app_label, model._meta.object_name
+ return (model_unpickle, (class_id, defers, deferred_class_factory), data)
def _get_pk_val(self, meta=None):
if not meta:
@@ -907,7 +909,7 @@ def unique_error_message(self, model_class, unique_check):
'field_label': six.text_type(field_labels)
}
- def full_clean(self, exclude=None):
+ def full_clean(self, exclude=None, validate_unique=True):
"""
Calls clean_fields, clean, and validate_unique, on the model,
and raises a ``ValidationError`` for any errors that occurred.
@@ -929,13 +931,14 @@ def full_clean(self, exclude=None):