From b1b32b2074a1aa906ba02fe221bfabef25617026 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 17 Sep 2012 22:02:16 -0700 Subject: Added myself as a committer. --- docs/internals/committers.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'docs/internals') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index ca56d36880..7900dd8cd0 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -407,6 +407,18 @@ Jeremy Dunck .. _vlogger: http://youtube.com/bryanveloso/ .. _shoutcaster: http://twitch.tv/vlogalonstar/ +`Preston Holmes`_ + Preston is a recovering neuroscientist who originally discovered Django as + part of a sweeping move to Python from a grab bag of half a dozen + languages. He was drawn to Django's balance of practical batteries included + philosophy, care and thought in code design, and strong open source + community. In addition to his current job in private progressive education, + Preston contributes some developer time to local non-profits. + + Preston lives with his family and animal menagerie in Santa Barbara, CA, USA. + +.. _Preston Holmes: http://www.ptone.com/ + Specialists ----------- -- cgit v1.3 From e06b54391dd06a0448b7676ec38f3734a4f86300 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 20 Sep 2012 13:49:26 -0700 Subject: Removed an excess colon. Thanks to jMyles for the patch. --- docs/internals/deprecation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/internals') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4add751912..976371516e 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -134,7 +134,7 @@ these changes. * The function-based generic view modules will be removed in favor of their class-based equivalents, outlined :doc:`here - `: + `. * The :class:`~django.core.servers.basehttp.AdminMediaHandler` will be removed. In its place use -- cgit v1.3 From 70a0de37d132e5f1514fb939875f69649f103124 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 26 Sep 2012 18:48:09 +0800 Subject: Fixed #3011 -- Added swappable auth.User models. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to the many people that contributed to the development and review of this patch, including (but not limited to) Jacob Kaplan-Moss, Anssi Kääriäinen, Ramiro Morales, Preston Holmes, Josh Ourisman, Thomas Sutton, and Roger Barnes, as well as the many, many people who have contributed to the design discussion around this ticket over many years. Squashed commit of the following: commit d84749a0f034a0a6906d20df047086b1219040d0 Merge: 531e771 7c11b1a Author: Russell Keith-Magee Date: Wed Sep 26 18:37:04 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 531e7715da545f930c49919a19e954d41c59b446 Merge: 29d1abb 1f84b04 Author: Russell Keith-Magee Date: Wed Sep 26 07:09:23 2012 +0800 Merged recent trunk changes. commit 29d1abbe351fd5da855fe5ce09e24227d90ddc91 Merge: 8a527dd 54c81a1 Author: Russell Keith-Magee Date: Mon Sep 24 07:49:46 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 8a527dda13c9bec955b1f7e8db5822d1d9b32a01 Author: Russell Keith-Magee Date: Mon Sep 24 07:48:05 2012 +0800 Ensure sequences are reset correctly in the presence of swapped models. commit e2b6e22f298eb986d74d28b8d9906f37f5ff8eb8 Author: Russell Keith-Magee Date: Sun Sep 23 17:53:05 2012 +0800 Modifications to the handling and docs for auth forms. commit 98aba856b534620aea9091f824b442b47d2fdb3c Author: Russell Keith-Magee Date: Sun Sep 23 15:28:57 2012 +0800 Improved error handling and docs for get_user_model() commit 0229209c844f06dfeb33b0b8eeec000c127695b6 Merge: 6494bf9 8599f64 Author: Russell Keith-Magee Date: Sun Sep 23 14:50:11 2012 +0800 Merged recent Django trunk changes. commit 6494bf91f2ddaaabec3ec017f2e3131937c35517 Author: Russell Keith-Magee Date: Mon Sep 17 21:38:44 2012 +0800 Improved validation of swappable model settings. commit 5a04cde342cc860384eb844cfda5af55204564ad Author: Russell Keith-Magee Date: Mon Sep 17 07:15:14 2012 +0800 Removed some unused imports. commit ffd535e4136dc54f084b6ac467e81444696e1c8a Author: Russell Keith-Magee Date: Sun Sep 16 20:31:28 2012 +0800 Corrected attribute access on for get_by_natural_key commit 913e1ac84c3d9c7c58a9b3bdbbb15ebccd8a8c0a Author: Russell Keith-Magee Date: Sun Sep 16 20:12:34 2012 +0800 Added test for proxy model safeguards on swappable models. commit 280bf19e94d0d534d0e51bae485c1842558f4ff4 Merge: dbb3900 935a863 Author: Russell Keith-Magee Date: Sun Sep 16 18:16:49 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit dbb3900775a99df8b6cb1d7063cf364eab55621a Author: Russell Keith-Magee Date: Sun Sep 16 18:09:27 2012 +0800 Fixes for Python 3 compatibility. commit dfd72131d8664615e245aa0f95b82604ba6b3821 Author: Russell Keith-Magee Date: Sun Sep 16 15:54:30 2012 +0800 Added protection against proxying swapped models. commit abcb027190e53613e7f1734e77ee185b2587de31 Author: Russell Keith-Magee Date: Sun Sep 16 15:11:10 2012 +0800 Cleanup and documentation of AbstractUser base class. commit a9491a87763e307f0eb0dc246f54ac865a6ffb34 Merge: fd8bb4e 08bcb4a Author: Russell Keith-Magee Date: Sun Sep 16 14:46:49 2012 +0800 Merge commit '08bcb4aec1ed154cefc631b8510ee13e9af0c19d' into t3011 commit fd8bb4e3e498a92d7a8b340f0684d5f088aa4c92 Author: Russell Keith-Magee Date: Sun Sep 16 14:20:14 2012 +0800 Documentation improvements coming from community review. commit b550a6d06d016ab6a0198c4cb2dffe9cceabe8a5 Author: Russell Keith-Magee Date: Sun Sep 16 13:52:47 2012 +0800 Refactored skipIfCustomUser into the contrib.auth tests. commit 52a02f11107c3f0d711742b8ca65b75175b79d6a Author: Russell Keith-Magee Date: Sun Sep 16 13:46:10 2012 +0800 Refactored common 'get' pattern into manager method. commit b441a6bbc7d6065175715cb09316b9f13268171b Author: Russell Keith-Magee Date: Sun Sep 16 13:41:33 2012 +0800 Added note about backwards incompatible change to admin login messages. commit 08bcb4aec1ed154cefc631b8510ee13e9af0c19d Author: Anssi Kääriäinen Date: Sat Sep 15 18:30:33 2012 +0300 Splitted User to AbstractUser and User commit d9f5e5addbad5e1a01f67e7358e4f5091c3cad81 Author: Anssi Kääriäinen Date: Sat Sep 15 18:30:02 2012 +0300 Reworked REQUIRED_FIELDS + create_user() interaction commit 579f152e4a6e06671e1ac1e59e2b43cf4d764bf4 Merge: 9184972 93e6733 Author: Russell Keith-Magee Date: Sat Sep 15 20:18:37 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 918497218c58227f5032873ff97261627b2ceab2 Author: Russell Keith-Magee Date: Sat Sep 15 20:18:19 2012 +0800 Deprecate AUTH_PROFILE_MODULE and get_profile(). commit 334cdfc1bb6a6794791497cdefda843bca2ea57a Author: Russell Keith-Magee Date: Sat Sep 15 20:00:12 2012 +0800 Added release notes for new swappable User feature. commit 5d7bb22e8d913b51aba1c3360e7af8b01b6c0ab6 Author: Russell Keith-Magee Date: Sat Sep 15 19:59:49 2012 +0800 Ensure swapped models can't be queried. commit 57ac6e3d32605a67581e875b37ec5b2284711a32 Merge: f2ec915 abfba3b Author: Russell Keith-Magee Date: Sat Sep 15 14:31:54 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit f2ec915b20f81c8afeaa3df25f80689712f720f8 Merge: 1952656 5e99a3d Author: Russell Keith-Magee Date: Sun Sep 9 08:29:51 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 19526563b54fa300785c49cfb625c0c6158ced67 Merge: 2c5e833 c4aa26a Author: Russell Keith-Magee Date: Sun Sep 9 08:22:26 2012 +0800 Merge recent changes from master. commit 2c5e833a30bef4305d55eacc0703533152f5c427 Author: Russell Keith-Magee Date: Sun Sep 9 07:53:46 2012 +0800 Corrected admin_views tests following removal of the email fallback on admin logins. commit 20d1892491839d6ef21f37db4ca136935c2076bf Author: Russell Keith-Magee Date: Sun Sep 9 01:00:37 2012 +0800 Added conditional skips for all tests dependent on the default User model commit 40ea8b888284775481fc1eaadeff267dbd7e3dfa Author: Russell Keith-Magee Date: Sat Sep 8 23:47:02 2012 +0800 Added documentation for REQUIRED_FIELDS in custom auth. commit e6aaf659708cf6491f5485d3edfa616cb9214cc0 Author: Russell Keith-Magee Date: Sat Sep 8 23:20:02 2012 +0800 Added first draft of custom User docs. Thanks to Greg Turner for the initial text. commit 75118bd242eec87649da2859e8c50a199a8a1dca Author: Thomas Sutton Date: Mon Aug 20 11:17:26 2012 +0800 Admin app should not allow username discovery The admin app login form should not allow users to discover the username associated with an email address. commit d088b3af58dad7449fc58493193a327725c57c22 Author: Thomas Sutton Date: Mon Aug 20 10:32:13 2012 +0800 Admin app login form should use swapped user model commit 7e82e83d67ee0871a72e1a3a723afdd214fcefc3 Merge: e29c010 39aa890 Author: Russell Keith-Magee Date: Fri Sep 7 23:45:03 2012 +0800 Merged master changes. commit e29c010beb96ca07697c4e3e0c0d5d3ffdc4c0a3 Merge: 8e3fd70 30bdf22 Author: Russell Keith-Magee Date: Mon Aug 20 13:12:57 2012 +0800 Merge remote-tracking branch 'django/master' into t3011 commit 8e3fd703d02c31a4c3ac9f51f5011d03c0bd47f6 Merge: 507bb50 26e0ba0 Author: Russell Keith-Magee Date: Mon Aug 20 13:09:09 2012 +0800 Merged recent changes from trunk. commit 507bb50a9291bfcdcfa1198f9fea21d4e3b1e762 Author: Russell Keith-Magee Date: Mon Jun 4 20:41:37 2012 +0800 Modified auth app so that login with alternate auth app is possible. commit dabe3628362ab7a4a6c9686dd874803baa997eaa Author: Russell Keith-Magee Date: Mon Jun 4 20:10:51 2012 +0800 Modified auth management commands to handle custom user definitions. commit 7cc0baf89d490c92ef3f1dc909b8090191a1294b Author: Russell Keith-Magee Date: Mon Jun 4 14:17:28 2012 +0800 Added model Meta option for swappable models, and made auth.User a swappable model --- django/conf/global_settings.py | 2 + django/contrib/admin/forms.py | 15 +- django/contrib/admin/models.py | 6 +- django/contrib/admin/sites.py | 34 +- django/contrib/admin/templates/admin/base.html | 2 +- django/contrib/admin/templates/admin/login.html | 2 +- django/contrib/admin/views/decorators.py | 1 + django/contrib/auth/__init__.py | 23 +- django/contrib/auth/backends.py | 24 +- django/contrib/auth/fixtures/custom_user.json | 14 + django/contrib/auth/forms.py | 15 +- django/contrib/auth/management/__init__.py | 32 +- .../auth/management/commands/changepassword.py | 16 +- .../auth/management/commands/createsuperuser.py | 122 +++---- django/contrib/auth/models.py | 173 ++++++---- django/contrib/auth/tests/__init__.py | 37 +-- django/contrib/auth/tests/auth_backends.py | 5 + django/contrib/auth/tests/basic.py | 45 ++- django/contrib/auth/tests/context_processors.py | 3 +- django/contrib/auth/tests/custom_user.py | 75 +++++ django/contrib/auth/tests/decorators.py | 4 +- django/contrib/auth/tests/forms.py | 8 +- django/contrib/auth/tests/management.py | 100 +++++- django/contrib/auth/tests/models.py | 6 + django/contrib/auth/tests/remote_user.py | 4 + django/contrib/auth/tests/signals.py | 2 + django/contrib/auth/tests/tokens.py | 2 + django/contrib/auth/tests/utils.py | 9 + django/contrib/auth/tests/views.py | 30 ++ django/contrib/auth/tokens.py | 1 + django/contrib/auth/views.py | 19 +- django/contrib/comments/models.py | 17 +- django/core/exceptions.py | 13 +- django/core/management/commands/sqlall.py | 1 + django/core/management/commands/syncdb.py | 2 +- django/core/management/commands/validate.py | 1 + django/core/management/sql.py | 12 +- django/core/management/validation.py | 27 +- django/core/validators.py | 33 +- django/db/backends/__init__.py | 10 +- django/db/backends/creation.py | 16 +- django/db/models/base.py | 20 +- django/db/models/fields/related.py | 35 +- django/db/models/loading.py | 1 + django/db/models/manager.py | 9 +- django/db/models/options.py | 21 +- django/test/__init__.py | 3 +- django/test/testcases.py | 2 + django/test/utils.py | 7 +- docs/internals/deprecation.txt | 3 + docs/ref/settings.txt | 27 +- docs/releases/1.5.txt | 42 +++ docs/topics/auth.txt | 359 +++++++++++++++++++++ .../invalid_models/invalid_models/models.py | 110 ++++++- tests/modeltests/invalid_models/tests.py | 13 +- tests/modeltests/proxy_models/tests.py | 38 ++- tests/regressiontests/admin_views/tests.py | 102 +++--- 57 files changed, 1412 insertions(+), 343 deletions(-) create mode 100644 django/contrib/auth/fixtures/custom_user.json create mode 100644 django/contrib/auth/tests/custom_user.py create mode 100644 django/contrib/auth/tests/utils.py (limited to 'docs/internals') diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 13f7991b57..4d5dc49ee0 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -488,6 +488,8 @@ PROFANITIES_LIST = () # AUTHENTICATION # ################## +AUTH_USER_MODEL = 'auth.User' + AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) LOGIN_URL = '/accounts/login/' diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py index 398af075b1..f1e7076ece 100644 --- a/django/contrib/admin/forms.py +++ b/django/contrib/admin/forms.py @@ -4,12 +4,12 @@ from django import forms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy, ugettext as _ +from django.utils.translation import ugettext_lazy ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password " "for a staff account. Note that both fields are case-sensitive.") + class AdminAuthenticationForm(AuthenticationForm): """ A custom authentication form used in the admin app. @@ -26,17 +26,6 @@ class AdminAuthenticationForm(AuthenticationForm): if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - if '@' in username: - # Mistakenly entered e-mail address instead of username? Look it up. - try: - user = User.objects.get(email=username) - except (User.DoesNotExist, User.MultipleObjectsReturned): - # Nothing to do here, moving along. - pass - else: - if user.check_password(password): - message = _("Your e-mail address is not your username." - " Try '%s' instead.") % user.username raise forms.ValidationError(message) elif not self.user_cache.is_active or not self.user_cache.is_staff: raise forms.ValidationError(message) diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py index 2b12edd4e2..e1d3b40d01 100644 --- a/django/contrib/admin/models.py +++ b/django/contrib/admin/models.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.db import models +from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.contrib.auth.models import User from django.contrib.admin.util import quote from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import smart_text @@ -12,15 +12,17 @@ ADDITION = 1 CHANGE = 2 DELETION = 3 + class LogEntryManager(models.Manager): def log_action(self, user_id, content_type_id, object_id, object_repr, action_flag, change_message=''): e = self.model(None, None, user_id, content_type_id, smart_text(object_id), object_repr[:200], action_flag, change_message) e.save() + @python_2_unicode_compatible class LogEntry(models.Model): action_time = models.DateTimeField(_('action time'), auto_now=True) - user = models.ForeignKey(User) + user = models.ForeignKey(settings.AUTH_USER_MODEL) content_type = models.ForeignKey(ContentType, blank=True, null=True) object_id = models.TextField(_('object id'), blank=True, null=True) object_repr = models.CharField(_('object repr'), max_length=200) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 05773ceac0..e375bc608f 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -9,7 +9,6 @@ from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse, NoReverseMatch from django.template.response import TemplateResponse -from django.utils.safestring import mark_safe from django.utils import six from django.utils.text import capfirst from django.utils.translation import ugettext as _ @@ -18,12 +17,15 @@ from django.conf import settings LOGIN_FORM_KEY = 'this_is_the_login_form' + class AlreadyRegistered(Exception): pass + class NotRegistered(Exception): pass + class AdminSite(object): """ An AdminSite object encapsulates an instance of the Django admin application, ready @@ -41,7 +43,7 @@ class AdminSite(object): password_change_done_template = None def __init__(self, name='admin', app_name='admin'): - self._registry = {} # model_class class -> admin_class instance + self._registry = {} # model_class class -> admin_class instance self.name = name self.app_name = app_name self._actions = {'delete_selected': actions.delete_selected} @@ -80,20 +82,23 @@ class AdminSite(object): if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) - # If we got **options then dynamically construct a subclass of - # admin_class with those **options. - if options: - # For reasons I don't quite understand, without a __module__ - # the created class appears to "live" in the wrong place, - # which causes issues later on. - options['__module__'] = __name__ - admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) + # Ignore the registration if the model has been + # swapped out. + if not model._meta.swapped: + # If we got **options then dynamically construct a subclass of + # admin_class with those **options. + if options: + # For reasons I don't quite understand, without a __module__ + # the created class appears to "live" in the wrong place, + # which causes issues later on. + options['__module__'] = __name__ + admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + # Validate (which might be a no-op) + validate(admin_class, model) - # Instantiate the admin class to save in the registry - self._registry[model] = admin_class(model, self) + # Instantiate the admin class to save in the registry + self._registry[model] = admin_class(model, self) def unregister(self, model_or_iterable): """ @@ -319,6 +324,7 @@ class AdminSite(object): REDIRECT_FIELD_NAME: request.get_full_path(), } context.update(extra_context or {}) + defaults = { 'extra_context': context, 'current_app': self.name, diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index caa26744d4..3d2a07eba2 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -26,7 +26,7 @@ {% if user.is_active and user.is_staff %}
{% trans 'Welcome,' %} - {% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}. + {% filter force_escape %}{% firstof user.get_short_name user.username %}{% endfilter %}. {% block userlinks %} {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 06fe4c8160..4690363891 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -30,7 +30,7 @@
{% csrf_token %}
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %} - {{ form.username }} + {{ form.username }}
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %} diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index b5313a162e..e19265fc83 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -4,6 +4,7 @@ from django.contrib.admin.forms import AdminAuthenticationForm from django.contrib.auth.views import login from django.contrib.auth import REDIRECT_FIELD_NAME + def staff_member_required(view_func): """ Decorator for views that checks that the user is logged in and is a staff diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 0b3ccf7d8c..1050d1d1bb 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -6,9 +6,10 @@ SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' REDIRECT_FIELD_NAME = 'next' + def load_backend(path): i = path.rfind('.') - module, attr = path[:i], path[i+1:] + module, attr = path[:i], path[i + 1:] try: mod = import_module(module) except ImportError as e: @@ -21,6 +22,7 @@ def load_backend(path): raise ImproperlyConfigured('Module "%s" does not define a "%s" authentication backend' % (module, attr)) return cls() + def get_backends(): from django.conf import settings backends = [] @@ -30,6 +32,7 @@ def get_backends(): raise ImproperlyConfigured('No authentication backends have been defined. Does AUTHENTICATION_BACKENDS contain anything?') return backends + def authenticate(**credentials): """ If the given credentials are valid, return a User object. @@ -46,6 +49,7 @@ def authenticate(**credentials): user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) return user + def login(request, user): """ Persist a user id and a backend in the request. This way a user doesn't @@ -69,6 +73,7 @@ def login(request, user): request.user = user user_logged_in.send(sender=user.__class__, request=request, user=user) + def logout(request): """ Removes the authenticated user's ID from the request and flushes their @@ -86,6 +91,22 @@ def logout(request): from django.contrib.auth.models import AnonymousUser request.user = AnonymousUser() + +def get_user_model(): + "Return the User model that is active in this project" + from django.conf import settings + from django.db.models import get_model + + try: + app_label, model_name = settings.AUTH_USER_MODEL.split('.') + except ValueError: + raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form 'app_label.model_name'") + user_model = get_model(app_label, model_name) + if user_model is None: + raise ImproperlyConfigured("AUTH_USER_MODEL refers to model '%s' that has not been installed" % settings.AUTH_USER_MODEL) + return user_model + + def get_user(request): from django.contrib.auth.models import AnonymousUser try: diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 9088e2fbf6..d103f32eb5 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals - -from django.contrib.auth.models import User, Permission +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission class ModelBackend(object): @@ -12,10 +12,11 @@ class ModelBackend(object): # configurable. def authenticate(self, username=None, password=None): try: - user = User.objects.get(username=username) + UserModel = get_user_model() + user = UserModel.objects.get_by_natural_key(username) if user.check_password(password): return user - except User.DoesNotExist: + except UserModel.DoesNotExist: return None def get_group_permissions(self, user_obj, obj=None): @@ -60,8 +61,9 @@ class ModelBackend(object): def get_user(self, user_id): try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: + UserModel = get_user_model() + return UserModel.objects.get(pk=user_id) + except UserModel.DoesNotExist: return None @@ -94,17 +96,21 @@ class RemoteUserBackend(ModelBackend): user = None username = self.clean_username(remote_user) + UserModel = get_user_model() + # Note that this could be accomplished in one try-except clause, but # instead we use get_or_create when creating unknown users since it has # built-in safeguards for multiple threads. if self.create_unknown_user: - user, created = User.objects.get_or_create(username=username) + user, created = UserModel.objects.get_or_create(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) if created: user = self.configure_user(user) else: try: - user = User.objects.get(username=username) - except User.DoesNotExist: + user = UserModel.objects.get_by_natural_key(username) + except UserModel.DoesNotExist: pass return user diff --git a/django/contrib/auth/fixtures/custom_user.json b/django/contrib/auth/fixtures/custom_user.json new file mode 100644 index 0000000000..770bea6541 --- /dev/null +++ b/django/contrib/auth/fixtures/custom_user.json @@ -0,0 +1,14 @@ +[ + { + "pk": "1", + "model": "auth.customuser", + "fields": { + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "last_login": "2006-12-17 07:03:31", + "email": "staffmember@example.com", + "is_active": true, + "is_admin": false, + "date_of_birth": "1976-11-08" + } + } +] \ No newline at end of file diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 08488237c7..a430f042e9 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -7,9 +7,10 @@ from django.utils.datastructures import SortedDict from django.utils.html import format_html, format_html_join from django.utils.http import int_to_base36 from django.utils.safestring import mark_safe +from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ -from django.contrib.auth import authenticate +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.tokens import default_token_generator @@ -135,7 +136,7 @@ class AuthenticationForm(forms.Form): Base class for authenticating users. Extend this to get a form that accepts username/password logins. """ - username = forms.CharField(label=_("Username"), max_length=30) + username = forms.CharField(max_length=30) password = forms.CharField(label=_("Password"), widget=forms.PasswordInput) error_messages = { @@ -157,6 +158,11 @@ class AuthenticationForm(forms.Form): self.user_cache = None super(AuthenticationForm, self).__init__(*args, **kwargs) + # Set the label for the "username" field. + UserModel = get_user_model() + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + self.fields['username'].label = capfirst(username_field.verbose_name) + def clean(self): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') @@ -198,9 +204,10 @@ class PasswordResetForm(forms.Form): """ Validates that an active user exists with the given email address. """ + UserModel = get_user_model() email = self.cleaned_data["email"] - self.users_cache = User.objects.filter(email__iexact=email, - is_active=True) + self.users_cache = UserModel.objects.filter(email__iexact=email, + is_active=True) if not len(self.users_cache): raise forms.ValidationError(self.error_messages['unknown']) if any((user.password == UNUSABLE_PASSWORD) diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 23a053d985..2ada789cae 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -6,9 +6,10 @@ from __future__ import unicode_literals import getpass import locale import unicodedata -from django.contrib.auth import models as auth_app + +from django.contrib.auth import models as auth_app, get_user_model +from django.core import exceptions from django.db.models import get_models, signals -from django.contrib.auth.models import User from django.utils import six from django.utils.six.moves import input @@ -64,7 +65,9 @@ def create_permissions(app, created_models, verbosity, **kwargs): def create_superuser(app, created_models, verbosity, db, **kwargs): from django.core.management import call_command - if auth_app.User in created_models and kwargs.get('interactive', True): + UserModel = get_user_model() + + if UserModel in created_models and kwargs.get('interactive', True): msg = ("\nYou just installed Django's auth system, which means you " "don't have any superusers defined.\nWould you like to create one " "now? (yes/no): ") @@ -113,28 +116,35 @@ def get_default_username(check_db=True): :returns: The username, or an empty string if no username can be determined. """ - from django.contrib.auth.management.commands.createsuperuser import ( - RE_VALID_USERNAME) + # If the User model has been swapped out, we can't make any assumptions + # about the default user name. + if auth_app.User._meta.swapped: + return '' + default_username = get_system_username() try: default_username = unicodedata.normalize('NFKD', default_username)\ .encode('ascii', 'ignore').decode('ascii').replace(' ', '').lower() except UnicodeDecodeError: return '' - if not RE_VALID_USERNAME.match(default_username): + + # Run the username validator + try: + auth_app.User._meta.get_field('username').run_validators(default_username) + except exceptions.ValidationError: return '' + # Don't return the default username if it is already taken. if check_db and default_username: try: - User.objects.get(username=default_username) - except User.DoesNotExist: + auth_app.User.objects.get(username=default_username) + except auth_app.User.DoesNotExist: pass else: return '' return default_username - signals.post_syncdb.connect(create_permissions, - dispatch_uid = "django.contrib.auth.management.create_permissions") + dispatch_uid="django.contrib.auth.management.create_permissions") signals.post_syncdb.connect(create_superuser, - sender=auth_app, dispatch_uid = "django.contrib.auth.management.create_superuser") + sender=auth_app, dispatch_uid="django.contrib.auth.management.create_superuser") diff --git a/django/contrib/auth/management/commands/changepassword.py b/django/contrib/auth/management/commands/changepassword.py index d125dfe5b6..1a2387442c 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -1,8 +1,8 @@ import getpass from optparse import make_option +from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User from django.db import DEFAULT_DB_ALIAS @@ -30,12 +30,16 @@ class Command(BaseCommand): else: username = getpass.getuser() + UserModel = get_user_model() + try: - u = User.objects.using(options.get('database')).get(username=username) - except User.DoesNotExist: + u = UserModel.objects.using(options.get('database')).get(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) + except UserModel.DoesNotExist: raise CommandError("user '%s' does not exist" % username) - self.stdout.write("Changing password for user '%s'\n" % u.username) + self.stdout.write("Changing password for user '%s'\n" % u) MAX_TRIES = 3 count = 0 @@ -48,9 +52,9 @@ class Command(BaseCommand): count = count + 1 if count == MAX_TRIES: - raise CommandError("Aborting password change for user '%s' after %s attempts" % (username, count)) + raise CommandError("Aborting password change for user '%s' after %s attempts" % (u, count)) u.set_password(p1) u.save() - return "Password changed successfully for user '%s'" % u.username + return "Password changed successfully for user '%s'" % u diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index 6e0d0bc754..c5f6469548 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -3,109 +3,114 @@ Management utility to create superusers. """ import getpass -import re import sys from optparse import make_option -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.contrib.auth.management import get_default_username from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS from django.utils.six.moves import input -from django.utils.translation import ugettext as _ - -RE_VALID_USERNAME = re.compile('[\w.@+-]+$') - -EMAIL_RE = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain - - -def is_valid_email(value): - if not EMAIL_RE.search(value): - raise exceptions.ValidationError(_('Enter a valid e-mail address.')) +from django.utils.text import capfirst class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('--username', dest='username', default=None, help='Specifies the username for the superuser.'), - make_option('--email', dest='email', default=None, - help='Specifies the email address for the superuser.'), make_option('--noinput', action='store_false', dest='interactive', default=True, help=('Tells Django to NOT prompt the user for input of any kind. ' - 'You must use --username and --email with --noinput, and ' - 'superusers created with --noinput will not be able to log ' - 'in until they\'re given a valid password.')), + 'You must use --username with --noinput, along with an option for ' + 'any other required field. Superusers created with --noinput will ' + ' not be able to log in until they\'re given a valid password.')), make_option('--database', action='store', dest='database', default=DEFAULT_DB_ALIAS, help='Specifies the database to use. Default is "default".'), + ) + tuple( + make_option('--%s' % field, dest=field, default=None, + help='Specifies the %s for the superuser.' % field) + for field in get_user_model().REQUIRED_FIELDS ) + help = 'Used to create a superuser.' def handle(self, *args, **options): username = options.get('username', None) - email = options.get('email', None) interactive = options.get('interactive') verbosity = int(options.get('verbosity', 1)) database = options.get('database') - # Do quick and dirty validation if --noinput - if not interactive: - if not username or not email: - raise CommandError("You must use --username and --email with --noinput.") - if not RE_VALID_USERNAME.match(username): - raise CommandError("Invalid username. Use only letters, digits, and underscores") - try: - is_valid_email(email) - except exceptions.ValidationError: - raise CommandError("Invalid email address.") + UserModel = get_user_model() + + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) + other_fields = UserModel.REQUIRED_FIELDS # If not provided, create the user with an unusable password password = None + other_data = {} - # Prompt for username/email/password. Enclose this whole thing in a - # try/except to trap for a keyboard interrupt and exit gracefully. - if interactive: + # Do quick and dirty validation if --noinput + if not interactive: + try: + if not username: + raise CommandError("You must use --username with --noinput.") + username = username_field.clean(username, None) + + for field_name in other_fields: + if options.get(field_name): + field = UserModel._meta.get_field(field_name) + other_data[field_name] = field.clean(options[field_name], None) + else: + raise CommandError("You must use --%s with --noinput." % field_name) + except exceptions.ValidationError as e: + raise CommandError('; '.join(e.messages)) + + else: + # Prompt for username/password, and any other required fields. + # Enclose this whole thing in a try/except to trap for a + # keyboard interrupt and exit gracefully. default_username = get_default_username() try: # Get a username - while 1: + while username is None: + username_field = UserModel._meta.get_field(getattr(UserModel, 'USERNAME_FIELD', 'username')) if not username: - input_msg = 'Username' + input_msg = capfirst(username_field.verbose_name) if default_username: input_msg += ' (leave blank to use %r)' % default_username - username = input(input_msg + ': ') - if default_username and username == '': + raw_value = input(input_msg + ': ') + if default_username and raw_value == '': username = default_username - if not RE_VALID_USERNAME.match(username): - self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.") + try: + username = username_field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) username = None continue try: - User.objects.using(database).get(username=username) - except User.DoesNotExist: - break + UserModel.objects.using(database).get(**{ + getattr(UserModel, 'USERNAME_FIELD', 'username'): username + }) + except UserModel.DoesNotExist: + pass else: self.stderr.write("Error: That username is already taken.") username = None - # Get an email - while 1: - if not email: - email = input('E-mail address: ') - try: - is_valid_email(email) - except exceptions.ValidationError: - self.stderr.write("Error: That e-mail address is invalid.") - email = None - else: - break + for field_name in other_fields: + field = UserModel._meta.get_field(field_name) + other_data[field_name] = options.get(field_name) + while other_data[field_name] is None: + raw_value = input(capfirst(field.verbose_name + ': ')) + try: + other_data[field_name] = field.clean(raw_value, None) + except exceptions.ValidationError as e: + self.stderr.write("Error: %s" % '; '.join(e.messages)) + other_data[field_name] = None # Get a password - while 1: + while password is None: if not password: password = getpass.getpass() password2 = getpass.getpass('Password (again): ') @@ -117,12 +122,11 @@ class Command(BaseCommand): self.stderr.write("Error: Blank passwords aren't allowed.") password = None continue - break + except KeyboardInterrupt: self.stderr.write("\nOperation cancelled.") sys.exit(1) - User.objects.db_manager(database).create_superuser(username, email, password) + UserModel.objects.db_manager(database).create_superuser(username=username, password=password, **other_data) if verbosity >= 1: - self.stdout.write("Superuser created successfully.") - + self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 98eb44ea05..abcc7ceafc 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +import re +import warnings from django.core.exceptions import ImproperlyConfigured from django.core.mail import send_mail +from django.core import validators from django.db import models from django.db.models.manager import EmptyManager from django.utils.crypto import get_random_string @@ -96,6 +99,7 @@ class GroupManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + @python_2_unicode_compatible class Group(models.Model): """ @@ -131,7 +135,7 @@ class Group(models.Model): return (self.name,) -class UserManager(models.Manager): +class BaseUserManager(models.Manager): @classmethod def normalize_email(cls, email): @@ -148,7 +152,25 @@ class UserManager(models.Manager): email = '@'.join([email_name, domain_part.lower()]) return email - def create_user(self, username, email=None, password=None): + def make_random_password(self, length=10, + allowed_chars='abcdefghjkmnpqrstuvwxyz' + 'ABCDEFGHJKLMNPQRSTUVWXYZ' + '23456789'): + """ + Generates a random password with the given length and given + allowed_chars. Note that the default value of allowed_chars does not + have "I" or "O" or letters and digits that look similar -- just to + avoid confusion. + """ + return get_random_string(length, allowed_chars) + + def get_by_natural_key(self, username): + return self.get(**{getattr(self.model, 'USERNAME_FIELD', 'username'): username}) + + +class UserManager(BaseUserManager): + + def create_user(self, username, email=None, password=None, **extra_fields): """ Creates and saves a User with the given username, email and password. """ @@ -158,35 +180,20 @@ class UserManager(models.Manager): email = UserManager.normalize_email(email) user = self.model(username=username, email=email, is_staff=False, is_active=True, is_superuser=False, - last_login=now, date_joined=now) + last_login=now, date_joined=now, **extra_fields) user.set_password(password) user.save(using=self._db) return user - def create_superuser(self, username, email, password): - u = self.create_user(username, email, password) + def create_superuser(self, username, email, password, **extra_fields): + u = self.create_user(username, email, password, **extra_fields) u.is_staff = True u.is_active = True u.is_superuser = True u.save(using=self._db) return u - def make_random_password(self, length=10, - allowed_chars='abcdefghjkmnpqrstuvwxyz' - 'ABCDEFGHJKLMNPQRSTUVWXYZ' - '23456789'): - """ - Generates a random password with the given length and given - allowed_chars. Note that the default value of allowed_chars does not - have "I" or "O" or letters and digits that look similar -- just to - avoid confusion. - """ - return get_random_string(length, allowed_chars) - - def get_by_natural_key(self, username): - return self.get(username=username) - # A few helper functions for common logic between User and AnonymousUser. def _user_get_all_permissions(user, obj): @@ -201,8 +208,6 @@ def _user_get_all_permissions(user, obj): def _user_has_perm(user, perm, obj): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_perm"): if obj is not None: @@ -215,8 +220,6 @@ def _user_has_perm(user, perm, obj): def _user_has_module_perms(user, app_label): - anon = user.is_anonymous() - active = user.is_active for backend in auth.get_backends(): if hasattr(backend, "has_module_perms"): if backend.has_module_perms(user, app_label): @@ -224,21 +227,73 @@ def _user_has_module_perms(user, app_label): return False +class AbstractBaseUser(models.Model): + password = models.CharField(_('password'), max_length=128) + last_login = models.DateTimeField(_('last login'), default=timezone.now) + + REQUIRED_FIELDS = [] + + class Meta: + abstract = True + + def is_anonymous(self): + """ + Always returns False. This is a way of comparing User objects to + anonymous users. + """ + return False + + def is_authenticated(self): + """ + Always return True. This is a way to tell if the user has been + authenticated in templates. + """ + return True + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + """ + Returns a boolean of whether the raw_password was correct. Handles + hashing formats behind the scenes. + """ + def setter(raw_password): + self.set_password(raw_password) + self.save(update_fields=["password"]) + return check_password(raw_password, self.password, setter) + + def set_unusable_password(self): + # Sets a value that will never be a valid hash + self.password = make_password(None) + + def has_usable_password(self): + return is_password_usable(self.password) + + def get_full_name(self): + raise NotImplementedError() + + def get_short_name(self): + raise NotImplementedError() + + @python_2_unicode_compatible -class User(models.Model): +class AbstractUser(AbstractBaseUser): """ - Users within the Django authentication system are represented by this - model. + An abstract base class implementing a fully featured User model with + admin-compliant permissions. - Username and password are required. Other fields are optional. + Username, password and email are required. Other fields are optional. """ username = models.CharField(_('username'), max_length=30, unique=True, help_text=_('Required. 30 characters or fewer. Letters, numbers and ' - '@/./+/-/_ characters')) + '@/./+/-/_ characters'), + validators=[ + validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid') + ]) first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) - email = models.EmailField(_('e-mail address'), blank=True) - password = models.CharField(_('password'), max_length=128) + email = models.EmailField(_('email address'), blank=True) is_staff = models.BooleanField(_('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.')) @@ -248,7 +303,6 @@ class User(models.Model): is_superuser = models.BooleanField(_('superuser status'), default=False, help_text=_('Designates that this user has all permissions without ' 'explicitly assigning them.')) - last_login = models.DateTimeField(_('last login'), default=timezone.now) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) groups = models.ManyToManyField(Group, verbose_name=_('groups'), blank=True, help_text=_('The groups this user belongs to. A user will ' @@ -257,11 +311,15 @@ class User(models.Model): user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, help_text='Specific permissions for this user.') + objects = UserManager() + REQUIRED_FIELDS = ['email'] + class Meta: verbose_name = _('user') verbose_name_plural = _('users') + abstract = True def __str__(self): return self.username @@ -272,20 +330,6 @@ class User(models.Model): def get_absolute_url(self): return "/users/%s/" % urlquote(self.username) - def is_anonymous(self): - """ - Always returns False. This is a way of comparing User objects to - anonymous users. - """ - return False - - def is_authenticated(self): - """ - Always return True. This is a way to tell if the user has been - authenticated in templates. - """ - return True - def get_full_name(self): """ Returns the first_name plus the last_name, with a space in between. @@ -293,25 +337,9 @@ class User(models.Model): full_name = '%s %s' % (self.first_name, self.last_name) return full_name.strip() - def set_password(self, raw_password): - self.password = make_password(raw_password) - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. Handles - hashing formats behind the scenes. - """ - def setter(raw_password): - self.set_password(raw_password) - self.save(update_fields=["password"]) - return check_password(raw_password, self.password, setter) - - def set_unusable_password(self): - # Sets a value that will never be a valid hash - self.password = make_password(None) - - def has_usable_password(self): - return is_password_usable(self.password) + def get_short_name(self): + "Returns the short name for the user." + return self.first_name def get_group_permissions(self, obj=None): """ @@ -381,6 +409,8 @@ class User(models.Model): Returns site-specific profile for this user. Raises SiteProfileNotAvailable if this site does not allow profiles. """ + warnings.warn("The use of AUTH_PROFILE_MODULE to define user profiles has been deprecated.", + PendingDeprecationWarning) if not hasattr(self, '_profile_cache'): from django.conf import settings if not getattr(settings, 'AUTH_PROFILE_MODULE', False): @@ -407,6 +437,17 @@ class User(models.Model): return self._profile_cache +class User(AbstractUser): + """ + Users within the Django authentication system are represented by this + model. + + Username, password and email are required. Other fields are optional. + """ + class Meta: + swappable = 'AUTH_USER_MODEL' + + @python_2_unicode_compatible class AnonymousUser(object): id = None @@ -431,7 +472,7 @@ class AnonymousUser(object): return not self.__eq__(other) def __hash__(self): - return 1 # instances always return the same hash value + return 1 # instances always return the same hash value def save(self): raise NotImplementedError diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 16eaa5c5b4..094a595238 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -1,26 +1,15 @@ -from django.contrib.auth.tests.auth_backends import (BackendTest, - RowlevelBackendTest, AnonymousUserBackendTest, NoBackendsTest, - InActiveUserBackendTest) -from django.contrib.auth.tests.basic import BasicTestCase -from django.contrib.auth.tests.context_processors import AuthContextProcessorTests -from django.contrib.auth.tests.decorators import LoginRequiredTestCase -from django.contrib.auth.tests.forms import (UserCreationFormTest, - AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, - UserChangeFormTest, PasswordResetFormTest) -from django.contrib.auth.tests.remote_user import (RemoteUserTest, - RemoteUserNoCreateTest, RemoteUserCustomTest) -from django.contrib.auth.tests.management import ( - GetDefaultUsernameTestCase, - ChangepasswordManagementCommandTestCase, -) -from django.contrib.auth.tests.models import (ProfileTestCase, NaturalKeysTestCase, - LoadDataWithoutNaturalKeysTestCase, LoadDataWithNaturalKeysTestCase, - UserManagerTestCase) -from django.contrib.auth.tests.hashers import TestUtilsHashPass -from django.contrib.auth.tests.signals import SignalTestCase -from django.contrib.auth.tests.tokens import TokenGeneratorTest -from django.contrib.auth.tests.views import (AuthViewNamedURLTests, - PasswordResetTest, ChangePasswordTest, LoginTest, LogoutTest, - LoginURLSettings) +from django.contrib.auth.tests.custom_user import * +from django.contrib.auth.tests.auth_backends import * +from django.contrib.auth.tests.basic import * +from django.contrib.auth.tests.context_processors import * +from django.contrib.auth.tests.decorators import * +from django.contrib.auth.tests.forms import * +from django.contrib.auth.tests.remote_user import * +from django.contrib.auth.tests.management import * +from django.contrib.auth.tests.models import * +from django.contrib.auth.tests.hashers import * +from django.contrib.auth.tests.signals import * +from django.contrib.auth.tests.tokens import * +from django.contrib.auth.tests.views import * # The password for the fixture data users is 'password' diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index 9a4d8f9b3a..a6be985412 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.auth.models import User, Group, Permission, AnonymousUser +from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured from django.test import TestCase from django.test.utils import override_settings +@skipIfCustomUser class BackendTest(TestCase): backend = 'django.contrib.auth.backends.ModelBackend' @@ -151,6 +153,7 @@ class SimpleRowlevelBackend(object): return ['none'] +@skipIfCustomUser class RowlevelBackendTest(TestCase): """ Tests for auth backend that supports object level permissions @@ -223,6 +226,7 @@ class AnonymousUserBackendTest(TestCase): self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon'])) +@skipIfCustomUser @override_settings(AUTHENTICATION_BACKENDS=[]) class NoBackendsTest(TestCase): """ @@ -235,6 +239,7 @@ class NoBackendsTest(TestCase): self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) +@skipIfCustomUser class InActiveUserBackendTest(TestCase): """ Tests for a inactive user diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 710754b8f1..ed1d0674fc 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -1,13 +1,18 @@ import locale -import traceback +from django.contrib.auth import get_user_model from django.contrib.auth.management.commands import createsuperuser from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.tests.custom_user import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django.test import TestCase +from django.test.utils import override_settings from django.utils.six import StringIO +@skipIfCustomUser class BasicTestCase(TestCase): def test_user(self): "Check that users can be created and can set their password" @@ -33,7 +38,7 @@ class BasicTestCase(TestCase): self.assertFalse(u.is_superuser) # Check API-based user creation with no password - u2 = User.objects.create_user('testuser2', 'test2@example.com') + User.objects.create_user('testuser2', 'test2@example.com') self.assertFalse(u.has_usable_password()) def test_user_no_email(self): @@ -98,7 +103,6 @@ class BasicTestCase(TestCase): self.assertEqual(u.email, 'joe2@somewhere.org') self.assertFalse(u.has_usable_password()) - new_io = StringIO() call_command("createsuperuser", interactive=False, @@ -124,15 +128,21 @@ class BasicTestCase(TestCase): # Temporarily replace getpass to allow interactive code to be used # non-interactively - class mock_getpass: pass + class mock_getpass: + pass mock_getpass.getpass = staticmethod(lambda p=None: "nopasswd") createsuperuser.getpass = mock_getpass # Call the command in this new environment new_io = StringIO() - call_command("createsuperuser", interactive=True, username="nolocale@somewhere.org", email="nolocale@somewhere.org", stdout=new_io) - - except TypeError as e: + call_command("createsuperuser", + interactive=True, + username="nolocale@somewhere.org", + email="nolocale@somewhere.org", + stdout=new_io + ) + + except TypeError: self.fail("createsuperuser fails if the OS provides no information about the current locale") finally: @@ -143,3 +153,24 @@ class BasicTestCase(TestCase): # If we were successful, a user should have been created u = User.objects.get(username="nolocale@somewhere.org") self.assertEqual(u.email, 'nolocale@somewhere.org') + + def test_get_user_model(self): + "The current user model can be retrieved" + self.assertEqual(get_user_model(), User) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "The current user model can be swapped out for another" + self.assertEqual(get_user_model(), CustomUser) + + @override_settings(AUTH_USER_MODEL='badsetting') + def test_swappable_user_bad_setting(self): + "The alternate user setting must point to something in the format app.model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() + + @override_settings(AUTH_USER_MODEL='thismodel.doesntexist') + def test_swappable_user_nonexistent_model(self): + "The current user model must point to an installed model" + with self.assertRaises(ImproperlyConfigured): + get_user_model() diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py index 6c824e831b..4e914133d0 100644 --- a/django/contrib/auth/tests/context_processors.py +++ b/django/contrib/auth/tests/context_processors.py @@ -2,12 +2,13 @@ import os from django.conf import global_settings from django.contrib.auth import authenticate +from django.contrib.auth.tests.utils import skipIfCustomUser from django.db.models import Q -from django.template import context from django.test import TestCase from django.test.utils import override_settings +@skipIfCustomUser @override_settings( TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py new file mode 100644 index 0000000000..3e7fa097b5 --- /dev/null +++ b/django/contrib/auth/tests/custom_user.py @@ -0,0 +1,75 @@ +# The custom User uses email as the unique identifier, and requires +# that every user provide a date of birth. This lets us test +# changes in username datatype, and non-text required fields. + +from django.db import models +from django.contrib.auth.models import BaseUserManager, AbstractBaseUser + + +class CustomUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=CustomUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, password, date_of_birth): + u = self.create_user(username, password=password, date_of_birth=date_of_birth) + u.is_admin = True + u.save(using=self._db) + return u + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + date_of_birth = models.DateField() + + objects = CustomUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + class Meta: + app_label = 'auth' + + def get_full_name(self): + return self.email + + def get_short_name(self): + return self.email + + def __unicode__(self): + return self.email + + # Maybe required? + def get_group_permissions(self, obj=None): + return set() + + def get_all_permissions(self, obj=None): + return set() + + def has_perm(self, perm, obj=None): + return True + + def has_perms(self, perm_list, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + # Admin required fields + @property + def is_staff(self): + return self.is_admin diff --git a/django/contrib/auth/tests/decorators.py b/django/contrib/auth/tests/decorators.py index cefc310e40..be99e7abb6 100644 --- a/django/contrib/auth/tests/decorators.py +++ b/django/contrib/auth/tests/decorators.py @@ -1,7 +1,9 @@ -from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.tests.views import AuthViewsTestCase +from django.contrib.auth.tests.utils import skipIfCustomUser + +@skipIfCustomUser class LoginRequiredTestCase(AuthViewsTestCase): """ Tests the login_required decorators diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 74aa47e199..7c6410da0f 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -4,16 +4,17 @@ import os from django.contrib.auth.models import User from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm, PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.core import mail from django.forms.fields import Field, EmailField from django.test import TestCase from django.test.utils import override_settings from django.utils.encoding import force_text -from django.utils import six from django.utils import translation from django.utils.translation import ugettext as _ +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserCreationFormTest(TestCase): @@ -81,6 +82,7 @@ class UserCreationFormTest(TestCase): self.assertEqual(repr(u), '') +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AuthenticationFormTest(TestCase): @@ -133,6 +135,7 @@ class AuthenticationFormTest(TestCase): self.assertEqual(form.non_field_errors(), []) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SetPasswordFormTest(TestCase): @@ -160,6 +163,7 @@ class SetPasswordFormTest(TestCase): self.assertTrue(form.is_valid()) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordChangeFormTest(TestCase): @@ -208,6 +212,7 @@ class PasswordChangeFormTest(TestCase): ['old_password', 'new_password1', 'new_password2']) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserChangeFormTest(TestCase): @@ -261,6 +266,7 @@ class UserChangeFormTest(TestCase): form.as_table()) +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class PasswordResetFormTest(TestCase): diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index ac83086dc3..60c05a0255 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -1,13 +1,20 @@ from __future__ import unicode_literals +from datetime import date from django.contrib.auth import models, management from django.contrib.auth.management.commands import changepassword +from django.contrib.auth.models import User +from django.contrib.auth.tests import CustomUser +from django.contrib.auth.tests.utils import skipIfCustomUser +from django.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase +from django.test.utils import override_settings from django.utils import six from django.utils.six import StringIO +@skipIfCustomUser class GetDefaultUsernameTestCase(TestCase): def setUp(self): @@ -36,6 +43,7 @@ class GetDefaultUsernameTestCase(TestCase): self.assertEqual(management.get_default_username(), 'julia') +@skipIfCustomUser class ChangepasswordManagementCommandTestCase(TestCase): def setUp(self): @@ -48,7 +56,7 @@ class ChangepasswordManagementCommandTestCase(TestCase): self.stderr.close() def test_that_changepassword_command_changes_joes_password(self): - " Executing the changepassword management command should change joe's password " + "Executing the changepassword management command should change joe's password" self.assertTrue(self.user.check_password('qwerty')) command = changepassword.Command() command._get_pass = lambda *args: 'not qwerty' @@ -69,3 +77,93 @@ class ChangepasswordManagementCommandTestCase(TestCase): with self.assertRaises(CommandError): command.execute("joe", stdout=self.stdout, stderr=self.stderr) + + +@skipIfCustomUser +class CreatesuperuserManagementCommandTestCase(TestCase): + + def test_createsuperuser(self): + "Check the operation of the createsuperuser management command" + # We can use the management command to create a superuser + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe", + email="joe@somewhere.org", + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = User.objects.get(username="joe") + self.assertEqual(u.email, 'joe@somewhere.org') + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + def test_verbosity_zero(self): + # We can supress output on the management command + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe2", + email="joe2@somewhere.org", + verbosity=0, + stdout=new_io + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, '') + u = User.objects.get(username="joe2") + self.assertEqual(u.email, 'joe2@somewhere.org') + self.assertFalse(u.has_usable_password()) + + def test_email_in_username(self): + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe+admin@somewhere.org", + email="joe@somewhere.org", + stdout=new_io + ) + u = User.objects.get(username="joe+admin@somewhere.org") + self.assertEqual(u.email, 'joe@somewhere.org') + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + call_command("createsuperuser", + interactive=False, + username="joe@somewhere.org", + date_of_birth="1976-04-01", + stdout=new_io, + skip_validation=True + ) + command_output = new_io.getvalue().strip() + self.assertEqual(command_output, 'Superuser created successfully.') + u = CustomUser.objects.get(email="joe@somewhere.org") + self.assertEqual(u.date_of_birth, date(1976, 4, 1)) + + # created password should be unusable + self.assertFalse(u.has_usable_password()) + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_swappable_user_missing_required_field(self): + "A superuser can be created when a custom User model is in use" + # We can use the management command to create a superuser + # We skip validation because the temporary substitution of the + # swappable User model messes with validation. + new_io = StringIO() + with self.assertRaises(CommandError): + call_command("createsuperuser", + interactive=False, + username="joe@somewhere.org", + stdout=new_io, + stderr=new_io, + skip_validation=True + ) + + self.assertEqual(CustomUser.objects.count(), 0) diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py index e4efee4339..252a0887c8 100644 --- a/django/contrib/auth/tests/models.py +++ b/django/contrib/auth/tests/models.py @@ -1,11 +1,13 @@ from django.conf import settings from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable, UserManager) +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.test.utils import override_settings from django.utils import six +@skipIfCustomUser @override_settings(USE_TZ=False, AUTH_PROFILE_MODULE='') class ProfileTestCase(TestCase): @@ -31,6 +33,7 @@ class ProfileTestCase(TestCase): user.get_profile() +@skipIfCustomUser @override_settings(USE_TZ=False) class NaturalKeysTestCase(TestCase): fixtures = ['authtestdata.json'] @@ -45,6 +48,7 @@ class NaturalKeysTestCase(TestCase): self.assertEqual(Group.objects.get_by_natural_key('users'), users_group) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithoutNaturalKeysTestCase(TestCase): fixtures = ['regular.json'] @@ -55,6 +59,7 @@ class LoadDataWithoutNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser @override_settings(USE_TZ=False) class LoadDataWithNaturalKeysTestCase(TestCase): fixtures = ['natural.json'] @@ -65,6 +70,7 @@ class LoadDataWithNaturalKeysTestCase(TestCase): self.assertEqual(group, user.groups.get()) +@skipIfCustomUser class UserManagerTestCase(TestCase): def test_create_user(self): diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index fa324781d2..9b0f6f8be3 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -3,10 +3,12 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import timezone +@skipIfCustomUser class RemoteUserTest(TestCase): urls = 'django.contrib.auth.tests.urls' @@ -106,6 +108,7 @@ class RemoteUserNoCreateBackend(RemoteUserBackend): create_unknown_user = False +@skipIfCustomUser class RemoteUserNoCreateTest(RemoteUserTest): """ Contains the same tests as RemoteUserTest, but using a custom auth backend @@ -142,6 +145,7 @@ class CustomRemoteUserBackend(RemoteUserBackend): return user +@skipIfCustomUser class RemoteUserCustomTest(RemoteUserTest): """ Tests a custom RemoteUserBackend subclass that overrides the clean_username diff --git a/django/contrib/auth/tests/signals.py b/django/contrib/auth/tests/signals.py index e570280ada..c597aa9ed0 100644 --- a/django/contrib/auth/tests/signals.py +++ b/django/contrib/auth/tests/signals.py @@ -1,10 +1,12 @@ from django.contrib.auth import signals from django.contrib.auth.models import User +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +@skipIfCustomUser @override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SignalTestCase(TestCase): urls = 'django.contrib.auth.tests.urls' diff --git a/django/contrib/auth/tests/tokens.py b/django/contrib/auth/tests/tokens.py index 44117a4f84..e8aeb46326 100644 --- a/django/contrib/auth/tests/tokens.py +++ b/django/contrib/auth/tests/tokens.py @@ -4,10 +4,12 @@ from datetime import date, timedelta from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.contrib.auth.tests.utils import skipIfCustomUser from django.test import TestCase from django.utils import unittest +@skipIfCustomUser class TokenGeneratorTest(TestCase): def test_make_token(self): diff --git a/django/contrib/auth/tests/utils.py b/django/contrib/auth/tests/utils.py new file mode 100644 index 0000000000..6bb3d9994f --- /dev/null +++ b/django/contrib/auth/tests/utils.py @@ -0,0 +1,9 @@ +from django.conf import settings +from django.utils.unittest import skipIf + + +def skipIfCustomUser(test_func): + """ + Skip a test if a custom user model is in use. + """ + return skipIf(settings.AUTH_USER_MODEL != 'auth.User', 'Custom user model in use')(test_func) diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index e3402b13b9..5727dc289f 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -16,6 +16,7 @@ from django.test.utils import override_settings from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, SetPasswordForm, PasswordResetForm) +from django.contrib.auth.tests.utils import skipIfCustomUser @override_settings( @@ -50,6 +51,7 @@ class AuthViewsTestCase(TestCase): return self.assertContains(response, escape(force_text(text)), **kwargs) +@skipIfCustomUser class AuthViewNamedURLTests(AuthViewsTestCase): urls = 'django.contrib.auth.urls' @@ -75,6 +77,7 @@ class AuthViewNamedURLTests(AuthViewsTestCase): self.fail("Reversal of url named '%s' failed with NoReverseMatch" % name) +@skipIfCustomUser class PasswordResetTest(AuthViewsTestCase): def test_email_not_found(self): @@ -172,6 +175,30 @@ class PasswordResetTest(AuthViewsTestCase): self.assertContainsEscaped(response, SetPasswordForm.error_messages['password_mismatch']) +@override_settings(AUTH_USER_MODEL='auth.CustomUser') +class CustomUserPasswordResetTest(AuthViewsTestCase): + fixtures = ['custom_user.json'] + + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertEqual(len(mail.outbox), 1) + return self._read_signup_email(mail.outbox[0]) + + def _read_signup_email(self, email): + urlmatch = re.search(r"https?://[^/]*(/.*reset/\S*)", email.body) + self.assertTrue(urlmatch is not None, "No URL found in sent email") + return urlmatch.group(), urlmatch.groups()[0] + + def test_confirm_valid_custom_user(self): + url, path = self._test_confirm_start() + response = self.client.get(path) + # redirect to a 'complete' page: + self.assertContains(response, "Please enter your new password") + + +@skipIfCustomUser class ChangePasswordTest(AuthViewsTestCase): def fail_login(self, password='password'): @@ -231,6 +258,7 @@ class ChangePasswordTest(AuthViewsTestCase): self.assertTrue(response['Location'].endswith('/login/?next=/password_change/done/')) +@skipIfCustomUser class LoginTest(AuthViewsTestCase): def test_current_site_in_context_after_login(self): @@ -289,6 +317,7 @@ class LoginTest(AuthViewsTestCase): "%s should be allowed" % good_url) +@skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): def setUp(self): @@ -347,6 +376,7 @@ class LoginURLSettings(AuthViewsTestCase): querystring.urlencode('/'))) +@skipIfCustomUser class LogoutTest(AuthViewsTestCase): def confirm_logged_out(self): diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py index 9b2eda83d4..930c70012b 100644 --- a/django/contrib/auth/tokens.py +++ b/django/contrib/auth/tokens.py @@ -4,6 +4,7 @@ from django.utils.http import int_to_base36, base36_to_int from django.utils.crypto import constant_time_compare, salted_hmac from django.utils import six + class PasswordResetTokenGenerator(object): """ Strategy object used to generate and check tokens for the password diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 024be5e46d..747b5c0991 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -15,10 +15,9 @@ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect # Avoid shadowing the login() and logout() views below. -from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout +from django.contrib.auth import REDIRECT_FIELD_NAME, login as auth_login, logout as auth_logout, get_user_model from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, PasswordChangeForm -from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.models import get_current_site @@ -74,6 +73,7 @@ def login(request, template_name='registration/login.html', return TemplateResponse(request, template_name, context, current_app=current_app) + def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME, @@ -104,6 +104,7 @@ def logout(request, next_page=None, # Redirect to this page until the session has been cleared. return HttpResponseRedirect(next_page or request.path) + def logout_then_login(request, login_url=None, current_app=None, extra_context=None): """ Logs out the user if he is logged in. Then redirects to the log-in page. @@ -113,6 +114,7 @@ def logout_then_login(request, login_url=None, current_app=None, extra_context=N login_url = resolve_url(login_url) return logout(request, login_url, current_app=current_app, extra_context=extra_context) + def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): """ @@ -128,6 +130,7 @@ def redirect_to_login(next, login_url=None, return HttpResponseRedirect(urlunparse(login_url_parts)) + # 4 views for password reset: # - password_reset sends the mail # - password_reset_done shows a success message for the above @@ -173,6 +176,7 @@ def password_reset(request, is_admin_site=False, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_done(request, template_name='registration/password_reset_done.html', current_app=None, extra_context=None): @@ -182,6 +186,7 @@ def password_reset_done(request, return TemplateResponse(request, template_name, context, current_app=current_app) + # Doesn't need csrf_protect since no-one can guess the URL @sensitive_post_parameters() @never_cache @@ -195,13 +200,14 @@ def password_reset_confirm(request, uidb36=None, token=None, View that checks the hash in a password reset link and presents a form for entering a new password. """ - assert uidb36 is not None and token is not None # checked by URLconf + UserModel = get_user_model() + assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') try: uid_int = base36_to_int(uidb36) - user = User.objects.get(id=uid_int) - except (ValueError, OverflowError, User.DoesNotExist): + user = UserModel.objects.get(id=uid_int) + except (ValueError, OverflowError, UserModel.DoesNotExist): user = None if user is not None and token_generator.check_token(user, token): @@ -225,6 +231,7 @@ def password_reset_confirm(request, uidb36=None, token=None, return TemplateResponse(request, template_name, context, current_app=current_app) + def password_reset_complete(request, template_name='registration/password_reset_complete.html', current_app=None, extra_context=None): @@ -236,6 +243,7 @@ def password_reset_complete(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @sensitive_post_parameters() @csrf_protect @login_required @@ -261,6 +269,7 @@ def password_change(request, return TemplateResponse(request, template_name, context, current_app=current_app) + @login_required def password_change_done(request, template_name='registration/password_change_done.html', diff --git a/django/contrib/comments/models.py b/django/contrib/comments/models.py index b043b4187a..a39c2622dd 100644 --- a/django/contrib/comments/models.py +++ b/django/contrib/comments/models.py @@ -1,16 +1,16 @@ -from django.contrib.auth.models import User +from django.conf import settings from django.contrib.comments.managers import CommentManager from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site -from django.db import models from django.core import urlresolvers +from django.db import models from django.utils.translation import ugettext_lazy as _ from django.utils import timezone -from django.conf import settings from django.utils.encoding import python_2_unicode_compatible -COMMENT_MAX_LENGTH = getattr(settings,'COMMENT_MAX_LENGTH',3000) +COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000) + class BaseCommentAbstractModel(models.Model): """ @@ -40,6 +40,7 @@ class BaseCommentAbstractModel(models.Model): args=(self.content_type_id, self.object_pk) ) + @python_2_unicode_compatible class Comment(BaseCommentAbstractModel): """ @@ -49,7 +50,7 @@ class Comment(BaseCommentAbstractModel): # Who posted this comment? If ``user`` is set then it was an authenticated # user; otherwise at least user_name should have been set and the comment # was posted by a non-authenticated user. - user = models.ForeignKey(User, verbose_name=_('user'), + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), blank=True, null=True, related_name="%(class)s_comments") user_name = models.CharField(_("user's name"), max_length=50, blank=True) user_email = models.EmailField(_("user's email address"), blank=True) @@ -117,6 +118,7 @@ class Comment(BaseCommentAbstractModel): def _get_name(self): return self.userinfo["name"] + def _set_name(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -126,6 +128,7 @@ class Comment(BaseCommentAbstractModel): def _get_email(self): return self.userinfo["email"] + def _set_email(self, val): if self.user_id: raise AttributeError(_("This comment was posted by an authenticated "\ @@ -135,6 +138,7 @@ class Comment(BaseCommentAbstractModel): def _get_url(self): return self.userinfo["url"] + def _set_url(self, val): self.user_url = val url = property(_get_url, _set_url, doc="The URL given by the user who posted this comment") @@ -155,6 +159,7 @@ class Comment(BaseCommentAbstractModel): } return _('Posted by %(user)s at %(date)s\n\n%(comment)s\n\nhttp://%(domain)s%(url)s') % d + @python_2_unicode_compatible class CommentFlag(models.Model): """ @@ -169,7 +174,7 @@ class CommentFlag(models.Model): design users are only allowed to flag a comment with a given flag once; if you want rating look elsewhere. """ - user = models.ForeignKey(User, verbose_name=_('user'), related_name="comment_flags") + user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('user'), related_name="comment_flags") comment = models.ForeignKey(Comment, verbose_name=_('comment'), related_name="flags") flag = models.CharField(_('flag'), max_length=30, db_index=True) flag_date = models.DateTimeField(_('date'), default=None) diff --git a/django/core/exceptions.py b/django/core/exceptions.py index f0f14cffda..233af40f88 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -3,42 +3,54 @@ Global Django exception and warning classes. """ from functools import reduce + class DjangoRuntimeWarning(RuntimeWarning): pass + class ObjectDoesNotExist(Exception): "The requested object does not exist" silent_variable_failure = True + class MultipleObjectsReturned(Exception): "The query returned multiple objects when only one was expected." pass + class SuspiciousOperation(Exception): "The user did something suspicious" pass + class PermissionDenied(Exception): "The user did not have permission to do that" pass + class ViewDoesNotExist(Exception): "The requested view does not exist" pass + class MiddlewareNotUsed(Exception): "This middleware is not used in this server configuration" pass + class ImproperlyConfigured(Exception): "Django is somehow improperly configured" pass + class FieldError(Exception): """Some kind of problem with a model field.""" pass + NON_FIELD_ERRORS = '__all__' + + class ValidationError(Exception): """An error while validating data.""" def __init__(self, message, code=None, params=None): @@ -85,4 +97,3 @@ class ValidationError(Exception): else: error_dict[NON_FIELD_ERRORS] = self.messages return error_dict - diff --git a/django/core/management/commands/sqlall.py b/django/core/management/commands/sqlall.py index 6d0735a6f9..0e2c05ba82 100644 --- a/django/core/management/commands/sqlall.py +++ b/django/core/management/commands/sqlall.py @@ -6,6 +6,7 @@ from django.core.management.base import AppCommand from django.core.management.sql import sql_all from django.db import connections, DEFAULT_DB_ALIAS + class Command(AppCommand): help = "Prints the CREATE TABLE, custom SQL and CREATE INDEX SQL statements for the given model module name(s)." diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index cceec07be8..4ce2910fb5 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -68,6 +68,7 @@ class Command(NoArgsCommand): if router.allow_syncdb(db, m)]) for app in models.get_apps() ] + def model_installed(model): opts = model._meta converter = connection.introspection.table_name_converter @@ -101,7 +102,6 @@ class Command(NoArgsCommand): cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - transaction.commit_unless_managed(using=db) # Send the post_syncdb signal, so individual apps can do whatever they need diff --git a/django/core/management/commands/validate.py b/django/core/management/commands/validate.py index 760d41c5bf..0dec3ea8b9 100644 --- a/django/core/management/commands/validate.py +++ b/django/core/management/commands/validate.py @@ -1,5 +1,6 @@ from django.core.management.base import NoArgsCommand + class Command(NoArgsCommand): help = "Validates all installed models." diff --git a/django/core/management/sql.py b/django/core/management/sql.py index b02a548314..ac16a5b358 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -9,6 +9,7 @@ from django.core.management.base import CommandError from django.db import models from django.db.models import get_models + def sql_create(app, style, connection): "Returns a list of the CREATE TABLE SQL statements for the given app." @@ -55,6 +56,7 @@ def sql_create(app, style, connection): return final_output + def sql_delete(app, style, connection): "Returns a list of the DROP TABLE SQL statements for the given app." @@ -83,7 +85,7 @@ def sql_delete(app, style, connection): opts = model._meta for f in opts.local_fields: if f.rel and f.rel.to not in to_delete: - references_to_delete.setdefault(f.rel.to, []).append( (model, f) ) + references_to_delete.setdefault(f.rel.to, []).append((model, f)) to_delete.add(model) @@ -97,7 +99,8 @@ def sql_delete(app, style, connection): cursor.close() connection.close() - return output[::-1] # Reverse it, to deal with table dependencies. + return output[::-1] # Reverse it, to deal with table dependencies. + def sql_flush(style, connection, only_django=False, reset_sequences=True): """ @@ -114,6 +117,7 @@ def sql_flush(style, connection, only_django=False, reset_sequences=True): statements = connection.ops.sql_flush(style, tables, seqs) return statements + def sql_custom(app, style, connection): "Returns a list of the custom table modifying SQL statements for the given app." output = [] @@ -125,6 +129,7 @@ def sql_custom(app, style, connection): return output + def sql_indexes(app, style, connection): "Returns a list of the CREATE INDEX SQL statements for all models in the given app." output = [] @@ -132,10 +137,12 @@ def sql_indexes(app, style, connection): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_all(app, style, connection): "Returns a list of CREATE TABLE SQL, initial-data inserts, and CREATE INDEX SQL for the given module." return sql_create(app, style, connection) + sql_custom(app, style, connection) + sql_indexes(app, style, connection) + def _split_statements(content): comment_re = re.compile(r"^((?:'[^']*'|[^'])*?)--.*$") statements = [] @@ -150,6 +157,7 @@ def _split_statements(content): statement = "" return statements + def custom_sql_for_model(model, style, connection): opts = model._meta app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql')) diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 6cd66f3a6a..fa3edb4430 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -5,6 +5,7 @@ from django.utils.encoding import force_str from django.utils.itercompat import is_iterable from django.utils import six + class ModelErrorCollection: def __init__(self, outfile=sys.stdout): self.errors = [] @@ -15,6 +16,7 @@ class ModelErrorCollection: self.errors.append((context, error)) self.outfile.write(self.style.ERROR(force_str("%s: %s\n" % (context, error)))) + def get_validation_errors(outfile, app=None): """ Validates all models that are part of the specified app. If no app name is provided, @@ -56,7 +58,7 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name) if isinstance(f, models.DecimalField): decimalp_ok, mdigits_ok = False, False - decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' + decimalp_msg = '"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' try: decimal_places = int(f.decimal_places) if decimal_places < 0: @@ -123,6 +125,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Make sure the related field specified by a ForeignKey is unique if not f.rel.to._meta.get_field(f.rel.field_name).unique: e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__)) @@ -165,6 +171,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f.rel.to, six.string_types): continue + # Make sure the model we're related hasn't been swapped out + if f.rel.to._meta.swapped: + e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable)) + # Check that the field is not set to unique. ManyToManyFields do not support unique. if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) @@ -176,7 +186,7 @@ def get_validation_errors(outfile, app=None): seen_from, seen_to, seen_self = False, False, 0 for inter_field in f.rel.through._meta.fields: rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self + if from_model == to_model: # relation to self if rel_to == from_model: seen_self += 1 if seen_self > 2: @@ -275,10 +285,21 @@ def get_validation_errors(outfile, app=None): if r.get_accessor_name() == rel_query_name: e.add(opts, "Reverse query name for m2m field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + # Check swappable attribute. + if opts.swapped: + try: + app_label, model_name = opts.swapped.split('.') + except ValueError: + e.add(opts, "%s is not of the form 'app_label.app_name'." % opts.swappable) + continue + if not models.get_model(app_label, model_name): + e.add(opts, "Model has been swapped out for '%s' which has not been installed or is abstract." % opts.swapped) + # Check ordering attribute. if opts.ordering: for field_name in opts.ordering: - if field_name == '?': continue + if field_name == '?': + continue if field_name.startswith('-'): field_name = field_name[1:] if opts.order_with_respect_to and field_name == '_order': diff --git a/django/core/validators.py b/django/core/validators.py index 317e3880bf..cf12f8c9fc 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -15,6 +15,7 @@ from django.utils import six # These values, if given to validate(), will trigger the self.required check. EMPTY_VALUES = (None, '', [], (), {}) + class RegexValidator(object): regex = '' message = _('Enter a valid value.') @@ -39,14 +40,15 @@ class RegexValidator(object): if not self.regex.search(force_text(value)): raise ValidationError(self.message, code=self.code) + class URLValidator(RegexValidator): regex = re.compile( - r'^(?:http|ftp)s?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... - r'localhost|' #localhost... - r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 - r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 - r'(?::\d+)?' # optional port + r'^(?:http|ftp)s?://' # http:// or https:// + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... + r'localhost|' # localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 + r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 + r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) def __call__(self, value): @@ -58,8 +60,8 @@ class URLValidator(RegexValidator): value = force_text(value) scheme, netloc, path, query, fragment = urlsplit(value) try: - netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE - except UnicodeError: # invalid domain part + netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE + except UnicodeError: # invalid domain part raise e url = urlunsplit((scheme, netloc, path, query, fragment)) super(URLValidator, self).__call__(url) @@ -75,6 +77,7 @@ def validate_integer(value): except (ValueError, TypeError): raise ValidationError('') + class EmailValidator(RegexValidator): def __call__(self, value): @@ -106,10 +109,12 @@ validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of le ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') validate_ipv4_address = RegexValidator(ipv4_re, _('Enter a valid IPv4 address.'), 'invalid') + def validate_ipv6_address(value): if not is_valid_ipv6_address(value): raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid') + def validate_ipv46_address(value): try: validate_ipv4_address(value) @@ -125,6 +130,7 @@ ip_address_validator_map = { 'ipv6': ([validate_ipv6_address], _('Enter a valid IPv6 address.')), } + def ip_address_validators(protocol, unpack_ipv4): """ Depending on the given parameters returns the appropriate validators for @@ -147,7 +153,7 @@ validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_ class BaseValidator(object): compare = lambda self, a, b: a is not b - clean = lambda self, x: x + clean = lambda self, x: x message = _('Ensure this value is %(limit_value)s (it is %(show_value)s).') code = 'limit_value' @@ -164,25 +170,28 @@ class BaseValidator(object): params=params, ) + class MaxValueValidator(BaseValidator): compare = lambda self, a, b: a > b message = _('Ensure this value is less than or equal to %(limit_value)s.') code = 'max_value' + class MinValueValidator(BaseValidator): compare = lambda self, a, b: a < b message = _('Ensure this value is greater than or equal to %(limit_value)s.') code = 'min_value' + class MinLengthValidator(BaseValidator): compare = lambda self, a, b: a < b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).') code = 'min_length' + class MaxLengthValidator(BaseValidator): compare = lambda self, a, b: a > b - clean = lambda self, x: len(x) + clean = lambda self, x: len(x) message = _('Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).') code = 'max_length' - diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 9b0f495749..02d2a16a46 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -319,6 +319,7 @@ class BaseDatabaseWrapper(object): def make_debug_cursor(self, cursor): return util.CursorDebugWrapper(cursor, self) + class BaseDatabaseFeatures(object): allows_group_by_pk = False # True if django.db.backend.utils.typecast_timestamp is used on values @@ -776,7 +777,7 @@ class BaseDatabaseOperations(object): The `style` argument is a Style object as returned by either color_style() or no_style() in django.core.management.color. """ - return [] # No sequence reset required by default. + return [] # No sequence reset required by default. def start_transaction_sql(self): """ @@ -915,6 +916,7 @@ class BaseDatabaseOperations(object): conn = ' %s ' % connector return conn.join(sub_expressions) + class BaseDatabaseIntrospection(object): """ This class encapsulates all backend-specific introspection utilities @@ -1010,12 +1012,14 @@ class BaseDatabaseIntrospection(object): for model in models.get_models(app): if not model._meta.managed: continue + if model._meta.swapped: + continue if not router.allow_syncdb(self.connection.alias, model): continue for f in model._meta.local_fields: if isinstance(f, models.AutoField): sequence_list.append({'table': model._meta.db_table, 'column': f.column}) - break # Only one AutoField is allowed per model, so don't bother continuing. + break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.local_many_to_many: # If this is an m2m using an intermediate table, @@ -1052,6 +1056,7 @@ class BaseDatabaseIntrospection(object): """ raise NotImplementedError + class BaseDatabaseClient(object): """ This class encapsulates all backend-specific methods for opening a @@ -1068,6 +1073,7 @@ class BaseDatabaseClient(object): def runshell(self): raise NotImplementedError() + class BaseDatabaseValidation(object): """ This class encapsualtes all backend-specific model validation. diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 52d5ac0547..3262a8922f 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -40,7 +40,7 @@ class BaseDatabaseCreation(object): (list_of_sql, pending_references_dict) """ opts = model._meta - if not opts.managed or opts.proxy: + if not opts.managed or opts.proxy or opts.swapped: return [], {} final_output = [] table_output = [] @@ -92,9 +92,9 @@ class BaseDatabaseCreation(object): full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] - for i, line in enumerate(table_output): # Combine and add commas. + for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( @@ -143,11 +143,11 @@ class BaseDatabaseCreation(object): """ from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + opts = model._meta + if not opts.managed or opts.proxy or opts.swapped: return [] qn = self.connection.ops.quote_name final_output = [] - opts = model._meta if model in pending_references: for rel_class, f in pending_references[model]: rel_opts = rel_class._meta @@ -172,7 +172,7 @@ class BaseDatabaseCreation(object): """ Returns the CREATE INDEX SQL statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] for f in model._meta.local_fields: @@ -211,7 +211,7 @@ class BaseDatabaseCreation(object): Return the DROP TABLE and restraint dropping statements for a single model. """ - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] # Drop the table now qn = self.connection.ops.quote_name @@ -228,7 +228,7 @@ class BaseDatabaseCreation(object): def sql_remove_table_constraints(self, model, references_to_delete, style): from django.db.backends.util import truncate_name - if not model._meta.managed or model._meta.proxy: + if not model._meta.managed or model._meta.proxy or model._meta.swapped: return [] output = [] qn = self.connection.ops.quote_name diff --git a/django/db/models/base.py b/django/db/models/base.py index 62024c8ee4..a1f9e2f26e 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -5,7 +5,7 @@ import sys from functools import update_wrapper from django.utils.six.moves import zip -import django.db.models.manager # Imported to register signal handler. +import django.db.models.manager # Imported to register signal handler. from django.conf import settings from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS) @@ -108,6 +108,11 @@ class ModelBase(type): is_proxy = new_class._meta.proxy + # If the model is a proxy, ensure that the base class + # hasn't been swapped out. + if is_proxy and base_meta and base_meta.swapped: + raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped)) + if getattr(new_class, '_default_manager', None): if not is_proxy: # Multi-table inheritance doesn't inherit default manager from @@ -262,6 +267,7 @@ class ModelBase(type): if opts.order_with_respect_to: cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) + # defer creating accessors on the foreign class until we are # certain it has been created def make_foreign_order_accessors(field, model, cls): @@ -292,6 +298,7 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) + class ModelState(object): """ A class for storing instance state @@ -303,6 +310,7 @@ class ModelState(object): # This impacts validation only; it has no effect on the actual save. self.adding = True + class Model(six.with_metaclass(ModelBase, object)): _deferred = False @@ -632,7 +640,6 @@ class Model(six.with_metaclass(ModelBase, object)): signals.post_save.send(sender=origin, instance=self, created=(not record_exists), update_fields=update_fields, raw=raw, using=using) - save_base.alters_data = True def delete(self, using=None): @@ -656,7 +663,7 @@ class Model(six.with_metaclass(ModelBase, object)): order = not is_next and '-' or '' param = force_text(getattr(self, field.attname)) q = Q(**{'%s__%s' % (field.name, op): param}) - q = q|Q(**{field.name: param, 'pk__%s' % op: self.pk}) + q = q | Q(**{field.name: param, 'pk__%s' % op: self.pk}) qs = self.__class__._default_manager.using(self._state.db).filter(**kwargs).filter(q).order_by('%s%s' % (order, field.name), '%spk' % order) try: return qs[0] @@ -849,7 +856,7 @@ class Model(six.with_metaclass(ModelBase, object)): field = opts.get_field(field_name) field_label = capfirst(field.verbose_name) # Insert the error into the error dict, very sneaky - return field.error_messages['unique'] % { + return field.error_messages['unique'] % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_label) } @@ -857,7 +864,7 @@ class Model(six.with_metaclass(ModelBase, object)): else: field_labels = [capfirst(opts.get_field(f).verbose_name) for f in unique_check] field_labels = get_text_list(field_labels, _('and')) - return _("%(model_name)s with this %(field_label)s already exists.") % { + return _("%(model_name)s with this %(field_label)s already exists.") % { 'model_name': six.text_type(model_name), 'field_label': six.text_type(field_labels) } @@ -921,7 +928,6 @@ class Model(six.with_metaclass(ModelBase, object)): raise ValidationError(errors) - ############################################ # HELPER FUNCTIONS (CURRIED MODEL METHODS) # ############################################ @@ -963,6 +969,7 @@ def get_absolute_url(opts, func, self, *args, **kwargs): class Empty(object): pass + def model_unpickle(model, attrs): """ Used to unpickle Model subclasses with deferred fields. @@ -971,6 +978,7 @@ def model_unpickle(model, attrs): return cls.__new__(cls) model_unpickle.__safe_for_unpickle__ = True + def unpickle_inner_exception(klass, exception_name): # Get the exception class from the class it is attached to: exception = getattr(klass, exception_name) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 08cc0a747f..c065162aa0 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -21,6 +21,7 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self' pending_lookups = {} + def add_lazy_relation(cls, field, relation, operation): """ Adds a lookup on ``cls`` when a related field is defined using a string, @@ -77,6 +78,7 @@ def add_lazy_relation(cls, field, relation, operation): value = (cls, field, operation) pending_lookups.setdefault(key, []).append(value) + def do_pending_lookups(sender, **kwargs): """ Handle any pending relations to the sending model. Sent from class_prepared. @@ -87,6 +89,7 @@ def do_pending_lookups(sender, **kwargs): signals.class_prepared.connect(do_pending_lookups) + #HACK class RelatedField(object): def contribute_to_class(self, cls, name): @@ -220,6 +223,7 @@ class RelatedField(object): # "related_name" option. return self.rel.related_name or self.opts.object_name.lower() + class SingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -306,6 +310,7 @@ class SingleRelatedObjectDescriptor(object): setattr(instance, self.cache_name, value) setattr(value, self.related.field.get_cache_name(), instance) + class ReverseSingleRelatedObjectDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -430,6 +435,7 @@ class ReverseSingleRelatedObjectDescriptor(object): if value is not None and not self.field.rel.multiple: setattr(value, self.field.related.get_cache_name(), instance) + class ForeignRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -660,7 +666,7 @@ def create_many_related_manager(superclass, rel): for obj in objs: if isinstance(obj, self.model): if not router.allow_relation(obj, self.instance): - raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % + raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' % (obj, self.instance._state.db, obj._state.db)) new_ids.add(obj.pk) elif isinstance(obj, Model): @@ -752,6 +758,7 @@ def create_many_related_manager(superclass, rel): return ManyRelatedManager + class ManyRelatedObjectsDescriptor(object): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have @@ -860,12 +867,13 @@ class ReverseManyRelatedObjectsDescriptor(object): manager.clear() manager.add(*value) + class ManyToOneRel(object): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): try: to._meta - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT self.to, self.field_name = to, field_name self.related_name = related_name @@ -891,6 +899,7 @@ class ManyToOneRel(object): self.field_name) return data[0] + class OneToOneRel(ManyToOneRel): def __init__(self, to, field_name, related_name=None, limit_choices_to=None, parent_link=False, on_delete=None): @@ -900,6 +909,7 @@ class OneToOneRel(ManyToOneRel): ) self.multiple = False + class ManyToManyRel(object): def __init__(self, to, related_name=None, limit_choices_to=None, symmetrical=True, through=None): @@ -924,16 +934,18 @@ class ManyToManyRel(object): """ return self.to._meta.pk + class ForeignKey(RelatedField, Field): empty_strings_allowed = False default_error_messages = { 'invalid': _('Model %(model)s with pk %(pk)r does not exist.') } description = _("Foreign Key (type determined by related field)") + def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: to_name = to._meta.object_name.lower() - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) @@ -1050,6 +1062,7 @@ class ForeignKey(RelatedField, Field): return IntegerField().db_type(connection=connection) return rel_field.db_type(connection=connection) + class OneToOneField(ForeignKey): """ A OneToOneField is essentially the same as a ForeignKey, with the exception @@ -1058,6 +1071,7 @@ class OneToOneField(ForeignKey): rather than returning a list. """ description = _("One-to-one relationship") + def __init__(self, to, to_field=None, **kwargs): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) @@ -1077,12 +1091,14 @@ class OneToOneField(ForeignKey): else: setattr(instance, self.attname, data) + def create_many_to_many_intermediary_model(field, klass): from django.db import models managed = True if isinstance(field.rel.to, six.string_types) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: to_model = field.rel.to to = to_model.split('.')[-1] + def set_managed(field, model, cls): field.rel.through._meta.managed = model._meta.managed or cls._meta.managed add_lazy_relation(klass, field, to_model, set_managed) @@ -1119,12 +1135,14 @@ def create_many_to_many_intermediary_model(field, klass): to: models.ForeignKey(to_model, related_name='%s+' % name, db_tablespace=field.db_tablespace) }) + class ManyToManyField(RelatedField, Field): description = _("Many-to-many relationship") + def __init__(self, to, **kwargs): try: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT + except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ManyToManyField must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) # Python 2.6 and earlier require dictionary keys to be of str type, # not unicode and class names must be ASCII (in Python 2.x), so we @@ -1135,7 +1153,7 @@ class ManyToManyField(RelatedField, Field): kwargs['rel'] = ManyToManyRel(to, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), - symmetrical=kwargs.pop('symmetrical', to==RECURSIVE_RELATIONSHIP_CONSTANT), + symmetrical=kwargs.pop('symmetrical', to == RECURSIVE_RELATIONSHIP_CONSTANT), through=kwargs.pop('through', None)) self.db_table = kwargs.pop('db_table', None) @@ -1166,7 +1184,7 @@ class ManyToManyField(RelatedField, Field): if hasattr(self, cache_attr): return getattr(self, cache_attr) for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.model: setattr(self, cache_attr, getattr(f, attr)) return getattr(self, cache_attr) @@ -1177,7 +1195,7 @@ class ManyToManyField(RelatedField, Field): return getattr(self, cache_attr) found = False for f in self.rel.through._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model: + if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model: if related.model == related.parent_model: # If this is an m2m-intermediate to self, # the first foreign key you find will be @@ -1222,7 +1240,8 @@ class ManyToManyField(RelatedField, Field): # The intermediate m2m model is not auto created if: # 1) There is a manually specified intermediate, or # 2) The class owning the m2m field is abstract. - if not self.rel.through and not cls._meta.abstract: + # 3) The class owning the m2m field has been swapped out. + if not self.rel.through and not cls._meta.abstract and not cls._meta.swapped: self.rel.through = create_many_to_many_intermediary_model(self, cls) # Add the descriptor for the m2m relation diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 7a9cb2cb41..8a0e796f4b 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -14,6 +14,7 @@ import os __all__ = ('get_apps', 'get_app', 'get_models', 'get_model', 'register_models', 'load_app', 'app_cache_ready') + class AppCache(object): """ A cache that stores installed applications and their models. Used to diff --git a/django/db/models/manager.py b/django/db/models/manager.py index e1bbf6ebc5..522a8a2306 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -13,7 +13,7 @@ def ensure_default_manager(sender, **kwargs): _default_manager if it's not a subclass of Manager). """ cls = sender - if cls._meta.abstract: + if cls._meta.abstract or cls._meta.swapped: return if not getattr(cls, '_default_manager', None): # Create the default manager, if needed. @@ -42,6 +42,7 @@ def ensure_default_manager(sender, **kwargs): signals.class_prepared.connect(ensure_default_manager) + class Manager(object): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 @@ -56,7 +57,9 @@ class Manager(object): def contribute_to_class(self, model, name): # TODO: Use weakref because of possible memory leak / circular reference. self.model = model - setattr(model, name, ManagerDescriptor(self)) + # Only contribute the manager if the model is concrete + if not model._meta.abstract and not model._meta.swapped: + setattr(model, name, ManagerDescriptor(self)) if not getattr(model, '_default_manager', None) or self.creation_counter < model._default_manager.creation_counter: model._default_manager = self if model._meta.abstract or (self._inherited and not self.model._meta.proxy): @@ -208,6 +211,7 @@ class Manager(object): def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) + class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. # For example, Poll.objects works, but poll_obj.objects raises AttributeError. @@ -219,6 +223,7 @@ class ManagerDescriptor(object): raise AttributeError("Manager isn't accessible via %s instances" % type.__name__) return self.manager + class EmptyManager(Manager): def get_query_set(self): return self.get_empty_query_set() diff --git a/django/db/models/options.py b/django/db/models/options.py index 6814ce27ff..d2de96ea5c 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,8 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy', 'auto_created') + 'abstract', 'managed', 'proxy', 'swappable', 'auto_created') + @python_2_unicode_compatible class Options(object): @@ -32,8 +33,8 @@ class Options(object): self.verbose_name_plural = None self.db_table = '' self.ordering = [] - self.unique_together = [] - self.permissions = [] + self.unique_together = [] + self.permissions = [] self.object_name, self.app_label = None, app_label self.get_latest_by = None self.order_with_respect_to = None @@ -55,6 +56,7 @@ class Options(object): # in the end of the proxy_for_model chain. In particular, for # concrete models, the concrete_model is always the class itself. self.concrete_model = None + self.swappable = None self.parents = SortedDict() self.duplicate_targets = {} self.auto_created = False @@ -218,6 +220,19 @@ class Options(object): return raw verbose_name_raw = property(verbose_name_raw) + def _swapped(self): + """ + Has this model been swapped out for another? If so, return the model + name of the replacement; otherwise, return None. + """ + if self.swappable: + model_label = '%s.%s' % (self.app_label, self.object_name) + swapped_for = getattr(settings, self.swappable, None) + if swapped_for not in (None, model_label): + return swapped_for + return None + swapped = property(_swapped) + def _fields(self): """ The getter for self.fields. This returns the list of field objects diff --git a/django/test/__init__.py b/django/test/__init__.py index 21a4841a6b..7a4987508e 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -5,5 +5,6 @@ Django Unit Test and Doctest framework. from django.test.client import Client, RequestFactory from django.test.testcases import (TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase, skipIfDBFeature, - skipUnlessDBFeature) + skipUnlessDBFeature +) from django.test.utils import Approximate diff --git a/django/test/testcases.py b/django/test/testcases.py index d37be58a71..2b1ef912b6 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -44,6 +44,7 @@ from django.utils import unittest as ut2 from django.utils.encoding import force_text from django.utils import six from django.utils.unittest.util import safe_repr +from django.utils.unittest import skipIf from django.views.static import serve __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase', @@ -53,6 +54,7 @@ normalize_long_ints = lambda s: re.sub(r'(?` for details. -.. setting:: AUTH_PROFILE_MODULE +.. setting:: AUTH_USER_MODEL -AUTH_PROFILE_MODULE -------------------- +AUTH_USER_MODEL +--------------- -Default: Not defined +Default: 'auth.User' -The site-specific user profile model used by this site. See -:ref:`auth-profiles`. +The model to use to represent a User. See :ref:`auth-custom-user`. .. setting:: CACHES @@ -2209,6 +2208,22 @@ ADMIN_MEDIA_PREFIX integration. See the :doc:`Django 1.4 release notes` for more information. +.. setting:: AUTH_PROFILE_MODULE + +AUTH_PROFILE_MODULE +------------------- + +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + +Default: Not defined + +The site-specific user profile model used by this site. See +:ref:`auth-profiles`. + .. setting:: IGNORABLE_404_ENDS IGNORABLE_404_ENDS diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 528a44c5a1..df8d89c185 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -34,6 +34,23 @@ release featuring 2.7 support. What's new in Django 1.5 ======================== +Configurable User model +~~~~~~~~~~~~~~~~~~~~~~~ + +In Django 1.5, you can now use your own model as the store for user-related +data. If your project needs a username with more than 30 characters, or if +you want to store usernames in a format other than first name/last name, or +you want to put custom profile information onto your User object, you can +now do so. + +If you have a third-party reusable application that references the User model, +you may need to make some changes to the way you reference User instances. You +should also document any specific features of the User model that your +application relies upon. + +See the :ref:`documentation on custom User models ` for +more details. + Support for saving a subset of model's fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -277,6 +294,18 @@ Session not saved on 500 responses Django's session middleware will skip saving the session data if the response's status code is 500. +Email checks on failed admin login +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to Django 1.5, if you attempted to log into the admin interface and +mistakenly used your email address instead of your username, the admin +interface would provide a warning advising that your email address was +not your username. In Django 1.5, the introduction of +:ref:`custom User models ` has required the removal of this +warning. This doesn't change the login behavior of the admin site; it only +affects the warning message that is displayed under one particular mode of +login failure. + Changes in tests execution ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -394,3 +423,16 @@ The markup contrib module has been deprecated and will follow an accelerated deprecation schedule. Direct use of python markup libraries or 3rd party tag libraries is preferred to Django maintaining this functionality in the framework. + +:setting:`AUTH_PROFILE_MODULE` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the introduction of :ref:`custom User models `, there is +no longer any need for a built-in mechanism to store user profile data. + +You can still define user profiles models that have a one-to-one relation with +the User model - in fact, for many applications needing to associate data with +a User account, this will be an appropriate design pattern to follow. However, +the :setting:`AUTH_PROFILE_MODULE` setting, and the +:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing +the user profile model, should not be used any longer. diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 88372af149..a767b5a93f 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -250,6 +250,12 @@ Methods .. method:: models.User.get_profile() + .. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + Returns a site-specific profile for this user. Raises :exc:`django.contrib.auth.models.SiteProfileNotAvailable` if the current site doesn't allow profiles, or @@ -582,6 +588,12 @@ correct path and environment for you. Storing additional information about users ------------------------------------------ +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + If you'd like to store additional information related to your users, Django provides a method to specify a site-specific related model -- termed a "user profile" -- for this purpose. @@ -1345,6 +1357,9 @@ Helper functions URL to redirect to after log out. Overrides ``next`` if the given ``GET`` parameter is passed. + +.. _built-in-auth-forms: + Built-in forms -------------- @@ -1735,6 +1750,350 @@ Fields group.permissions.remove(permission, permission, ...) group.permissions.clear() +.. _auth-custom-user: + +Customizing the User model +========================== + +.. versionadded:: 1.5 + +Some kinds of projects may have authentication requirements for which Django's +built-in :class:`~django.contrib.auth.models.User` model is not always +appropriate. For instance, on some sites it makes more sense to use an email +address as your identification token instead of a username. + +Django allows you to override the default User model by providing a value for +the :setting:`AUTH_USER_MODEL` setting that references a custom model:: + + AUTH_USER_MODEL = 'myapp.MyUser' + +This dotted pair describes the name of the Django app, and the name of the Django +model that you wish to use as your User model. + +.. admonition:: Warning + + Changing :setting:`AUTH_USER_MODEL` has a big effect on your database + structure. It changes the tables that are available, and it will affect the + construction of foreign keys and many-to-many relationships. If you intend + to set :setting:`AUTH_USER_MODEL`, you should set it before running + ``manage.py syncdb`` for the first time. + + If you have an existing project and you want to migrate to using a custom + User model, you may need to look into using a migration tool like South_ + to ease the transition. + +.. _South: http://south.aeracode.org + +Referencing the User model +-------------------------- + +If you reference :class:`~django.contrib.auth.models.User` directly (for +example, by referring to it in a foreign key), your code will not work in +projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a +different User model. + +Instead of referring to :class:`~django.contrib.auth.models.User` directly, +you should reference the user model using +:func:`django.contrib.auth.get_user_model()`. This method will return the +currently active User model -- the custom User model if one is specified, or +:class:`~django.contrib.auth.User` otherwise. + +In relations to the User model, you should specify the custom model using +the :setting:`AUTH_USER_MODEL` setting. For example:: + + from django.conf import settings + from django.db import models + + class Article(models.Model) + author = models.ForeignKey(settings.AUTH_USER_MODEL) + +Specifying a custom User model +------------------------------ + +.. admonition:: Model design considerations + + Think carefully before handling information not directly related to + authentication in your custom User Model. + + It may be better to store app-specific user information in a model + that has a relation with the User model. That allows each app to specify + its own user data requirements without risking conflicts with other + apps. On the other hand, queries to retrieve this related information + will involve a database join, which may have an effect on performance. + +Django expects your custom User model to meet some minimum requirements. + +1. Your model must have a single unique field that can be used for + identification purposes. This can be a username, an email address, + or any other unique attribute. + +2. Your model must provide a way to address the user in a "short" and + "long" form. The most common interpretation of this would be to use + the user's given name as the "short" identifier, and the user's full + name as the "long" identifier. However, there are no constraints on + what these two methods return - if you want, they can return exactly + the same value. + +The easiest way to construct a compliant custom User model is to inherit from +:class:`~django.contrib.auth.models.AbstractBaseUser`. +:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core +implementation of a `User` model, including hashed passwords and tokenized +password resets. You must then provide some key implementation details: + +.. attribute:: User.USERNAME_FIELD + + A string describing the name of the field on the User model that is + used as the unique identifier. This will usually be a username of + some kind, but it can also be an email address, or any other unique + identifier. In the following example, the field `identifier` is used + as the identifying field:: + + class MyUser(AbstractBaseUser): + identfier = models.CharField(max_length=40, unique=True, db_index=True) + ... + USERNAME_FIELD = 'identifier' + +.. attribute:: User.REQUIRED_FIELDS + + A list of the field names that *must* be provided when creating + a user. For example, here is the partial definition for a User model + that defines two required fields - a date of birth and height:: + + class MyUser(AbstractBaseUser): + ... + date_of_birth = models.DateField() + height = models.FloatField() + ... + REQUIRED_FIELDS = ['date_of_birth', 'height'] + +.. method:: User.get_full_name(): + + A longer formal identifier for the user. A common interpretation + would be the full name name of the user, but it can be any string that + identifies the user. + +.. method:: User.get_short_name(): + + A short, informal identifier for the user. A common interpretation + would be the first name of the user, but it can be any string that + identifies the user in an informal way. It may also return the same + value as :meth:`django.contrib.auth.User.get_full_name()`. + +You should also define a custom manager for your User model. If your User +model defines `username` and `email` fields the same as Django's default User, +you can just install Django's +:class:`~django.contrib.auth.models.UserManager`; however, if your User model +defines different fields, you will need to define a custom manager that +extends :class:`~django.contrib.auth.models.BaseUserManager` providing two +additional methods: + +.. method:: UserManager.create_user(username, password=None, **other_fields) + + The prototype of `create_user()` should accept all required fields + as arguments. For example, if your user model defines `username`, + and `date_of_birth` as required fields, then create_user should be + defined as:: + + def create_user(self, username, date_of_birth, password=None): + # create user here + +.. method:: UserManager.create_superuser(username, password, **other_fields) + + The prototype of `create_superuser()` should accept all required fields + as arguments. For example, if your user model defines `username`, + and `date_of_birth` as required fields, then create_user should be + defined as:: + + def create_superuser(self, username, date_of_birth, password): + # create superuser here + + Unlike `create_user()`, `create_superuser()` *must* require the caller + to provider a password. + +Extending Django's default User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you're entirely happy with Django's :class:`~django.contrib.auth.models.User` +model and you just want to add some additional profile information, you can +simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your +custom profile fields. + +Custom users and the built-in auth forms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you may expect, built-in Django's :ref:`forms <_built-in-auth-forms>` +and :ref:`views ` make certain assumptions about +the user model that they are working with. + +If your user model doesn't follow the same assumptions, it may be necessary to define +a replacement form, and pass that form in as part of the configuration of the +auth views. + +* :class:`~django.contrib.auth.forms.UserCreationForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.UserChangeForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.AuthenticationForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, + and will adapt to use the field defined in `USERNAME_FIELD`. + +* :class:`~django.contrib.auth.forms.PasswordResetForm` + + Assumes that the user model has an integer primary key, has a field named + `email` that can be used to identify the user, and a boolean field + named `is_active` to prevent password resets for inactive users. + +* :class:`~django.contrib.auth.forms.SetPasswordForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.PasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + + +Custom users and django.contrib.admin +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want your custom User model to also work with Admin, your User model must +define some additional attributes and methods. These methods allow the admin to +control access of the User to admin content: + +.. attribute:: User.is_staff + + Returns True if the user is allowed to have access to the admin site. + +.. attribute:: User.is_active + + Returns True if the user account is currently active. + +.. method:: User.has_perm(perm, obj=None): + + Returns True if the user has the named permission. If `obj` is + provided, the permission needs to be checked against a specific object + instance. + +.. method:: User.has_module_perms(app_label): + + Returns True if the user has permission to access models in + the given app. + + +Custom users and Proxy models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One limitation of custom User models is that installing a custom User model +will break any proxy model extending :class:`~django.contrib.auth.models.User`. +Proxy models must be based on a concrete base class; by defining a custom User +model, you remove the ability of Django to reliably identify the base class. + +If your project uses proxy models, you must either modify the proxy to extend +the User model that is currently in use in your project, or merge your proxy's +behavior into your User subclass. + +A full example +-------------- + +Here is an example of a full models.py for an admin-compliant custom +user app. This user model uses an email address as the username, and has a +required date of birth; it provides no permission checking, beyond a simple +`admin` flag on the user account. This model would be compatible with all +the built-in auth forms and views, except for the User creation forms. + +This code would all live in a ``models.py`` file for a custom +authentication app:: + + from django.db import models + from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser + ) + + + class MyUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email, date of + birth and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=MyUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, date_of_birth, password): + """ + Creates and saves a superuser with the given email, date of + birth and password. + """ + u = self.create_user(username, + password=password, + date_of_birth=date_of_birth + ) + u.is_admin = True + u.save(using=self._db) + return u + + + class MyUser(AbstractBaseUser): + email = models.EmailField( + verbose_name='email address', + max_length=255 + ) + date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = MyUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + def get_full_name(self): + # The user is identified by their email address + return self.email + + def get_short_name(self): + # The user is identified by their email address + return self.email + + def __unicode__(self): + return self.email + + def has_perm(self, perm, obj=None): + "Does the user have a specific permission?" + # Simplest possible answer: Yes, always + return True + + def has_module_perms(self, app_label): + "Does the user have permissions to view the app `app_label`?" + # Simplest possible answer: Yes, always + return True + + @property + def is_staff(self): + "Is the user a member of staff?" + # Simplest possible answer: All admins are staff + return self.is_admin + + .. _authentication-backends: Other authentication sources diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py index b2ba253c5d..3f95d314e3 100644 --- a/tests/modeltests/invalid_models/invalid_models/models.py +++ b/tests/modeltests/invalid_models/invalid_models/models.py @@ -21,11 +21,12 @@ class FieldErrors(models.Model): decimalfield5 = models.DecimalField(max_digits=10, decimal_places=10) filefield = models.FileField() choices = models.CharField(max_length=10, choices='bad') - choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) + choices2 = models.CharField(max_length=10, choices=[(1, 2, 3), (1, 2, 3)]) index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + class Target(models.Model): tgt_safe = models.CharField(max_length=10) clash1 = models.CharField(max_length=10) @@ -33,12 +34,14 @@ class Target(models.Model): clash1_set = models.CharField(max_length=10) + class Clash1(models.Model): src_safe = models.CharField(max_length=10) foreign = models.ForeignKey(Target) m2m = models.ManyToManyField(Target) + class Clash2(models.Model): src_safe = models.CharField(max_length=10) @@ -48,6 +51,7 @@ class Clash2(models.Model): m2m_1 = models.ManyToManyField(Target, related_name='id') m2m_2 = models.ManyToManyField(Target, related_name='src_safe') + class Target2(models.Model): clash3 = models.CharField(max_length=10) foreign_tgt = models.ForeignKey(Target) @@ -56,6 +60,7 @@ class Target2(models.Model): m2m_tgt = models.ManyToManyField(Target) clashm2m_set = models.ManyToManyField(Target) + class Clash3(models.Model): src_safe = models.CharField(max_length=10) @@ -65,12 +70,15 @@ class Clash3(models.Model): m2m_1 = models.ManyToManyField(Target2, related_name='foreign_tgt') m2m_2 = models.ManyToManyField(Target2, related_name='m2m_tgt') + class ClashForeign(models.Model): foreign = models.ForeignKey(Target2) + class ClashM2M(models.Model): m2m = models.ManyToManyField(Target2) + class SelfClashForeign(models.Model): src_safe = models.CharField(max_length=10) selfclashforeign = models.CharField(max_length=10) @@ -79,6 +87,7 @@ class SelfClashForeign(models.Model): foreign_1 = models.ForeignKey("SelfClashForeign", related_name='id') foreign_2 = models.ForeignKey("SelfClashForeign", related_name='src_safe') + class ValidM2M(models.Model): src_safe = models.CharField(max_length=10) validm2m = models.CharField(max_length=10) @@ -94,6 +103,7 @@ class ValidM2M(models.Model): m2m_3 = models.ManyToManyField('self') m2m_4 = models.ManyToManyField('self') + class SelfClashM2M(models.Model): src_safe = models.CharField(max_length=10) selfclashm2m = models.CharField(max_length=10) @@ -108,120 +118,148 @@ class SelfClashM2M(models.Model): m2m_3 = models.ManyToManyField('self', symmetrical=False) m2m_4 = models.ManyToManyField('self', symmetrical=False) + class Model(models.Model): "But it's valid to call a model Model." - year = models.PositiveIntegerField() #1960 - make = models.CharField(max_length=10) #Aston Martin - name = models.CharField(max_length=10) #DB 4 GT + year = models.PositiveIntegerField() # 1960 + make = models.CharField(max_length=10) # Aston Martin + name = models.CharField(max_length=10) # DB 4 GT + class Car(models.Model): colour = models.CharField(max_length=5) model = models.ForeignKey(Model) + class MissingRelations(models.Model): rel1 = models.ForeignKey("Rel1") rel2 = models.ManyToManyField("Rel2") + class MissingManualM2MModel(models.Model): name = models.CharField(max_length=5) missing_m2m = models.ManyToManyField(Model, through="MissingM2MModel") + class Person(models.Model): name = models.CharField(max_length=5) + class Group(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership", related_name="primary") secondary = models.ManyToManyField(Person, through="Membership", related_name="secondary") tertiary = models.ManyToManyField(Person, through="RelationshipDoubleFK", related_name="tertiary") + class GroupTwo(models.Model): name = models.CharField(max_length=5) primary = models.ManyToManyField(Person, through="Membership") secondary = models.ManyToManyField(Group, through="MembershipMissingFK") + class Membership(models.Model): person = models.ForeignKey(Person) group = models.ForeignKey(Group) not_default_or_null = models.CharField(max_length=5) + class MembershipMissingFK(models.Model): person = models.ForeignKey(Person) + class PersonSelfRefM2M(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="Relationship") too_many_friends = models.ManyToManyField('self', through="RelationshipTripleFK") + class PersonSelfRefM2MExplicit(models.Model): name = models.CharField(max_length=5) friends = models.ManyToManyField('self', through="ExplicitRelationship", symmetrical=True) + class Relationship(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set") date_added = models.DateTimeField() + class ExplicitRelationship(models.Model): first = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_from_set") second = models.ForeignKey(PersonSelfRefM2MExplicit, related_name="rel_to_set") date_added = models.DateTimeField() + class RelationshipTripleFK(models.Model): first = models.ForeignKey(PersonSelfRefM2M, related_name="rel_from_set_2") second = models.ForeignKey(PersonSelfRefM2M, related_name="rel_to_set_2") third = models.ForeignKey(PersonSelfRefM2M, related_name="too_many_by_far") date_added = models.DateTimeField() + class RelationshipDoubleFK(models.Model): first = models.ForeignKey(Person, related_name="first_related_name") second = models.ForeignKey(Person, related_name="second_related_name") third = models.ForeignKey(Group, related_name="rel_to_set") date_added = models.DateTimeField() + class AbstractModel(models.Model): name = models.CharField(max_length=10) + class Meta: abstract = True + class AbstractRelationModel(models.Model): fk1 = models.ForeignKey('AbstractModel') fk2 = models.ManyToManyField('AbstractModel') + class UniqueM2M(models.Model): """ Model to test for unique ManyToManyFields, which are invalid. """ unique_people = models.ManyToManyField(Person, unique=True) + class NonUniqueFKTarget1(models.Model): """ Model to test for non-unique FK target in yet-to-be-defined model: expect an error """ tgt = models.ForeignKey('FKTarget', to_field='bad') + class UniqueFKTarget1(models.Model): """ Model to test for unique FK target in yet-to-be-defined model: expect no error """ tgt = models.ForeignKey('FKTarget', to_field='good') + class FKTarget(models.Model): bad = models.IntegerField() good = models.IntegerField(unique=True) + class NonUniqueFKTarget2(models.Model): """ Model to test for non-unique FK target in previously seen model: expect an error """ tgt = models.ForeignKey(FKTarget, to_field='bad') + class UniqueFKTarget2(models.Model): """ Model to test for unique FK target in previously seen model: expect no error """ tgt = models.ForeignKey(FKTarget, to_field='good') + class NonExistingOrderingWithSingleUnderscore(models.Model): class Meta: ordering = ("does_not_exist",) + class InvalidSetNull(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_NULL) + class InvalidSetDefault(models.Model): fk = models.ForeignKey('self', on_delete=models.SET_DEFAULT) + class UnicodeForeignKeys(models.Model): """Foreign keys which can translate to ascii should be OK, but fail if they're not.""" @@ -232,9 +270,11 @@ class UnicodeForeignKeys(models.Model): # when adding the errors in core/management/validation.py #bad = models.ForeignKey('★') + class PrimaryKeyNull(models.Model): my_pk_field = models.IntegerField(primary_key=True, null=True) + class OrderByPKModel(models.Model): """ Model to test that ordering by pk passes validation. @@ -245,6 +285,62 @@ class OrderByPKModel(models.Model): class Meta: ordering = ('pk',) + +class SwappableModel(models.Model): + """A model that can be, but isn't swapped out. + + References to this model *shoudln't* raise any validation error. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + +class SwappedModel(models.Model): + """A model that is swapped out. + + References to this model *should* raise a validation error. + Requires TEST_SWAPPED_MODEL to be defined in the test environment; + this is guaranteed by the test runner using @override_settings. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL' + + +class BadSwappableValue(models.Model): + """A model that can be swapped out; during testing, the swappable + value is not of the format app.model + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_VALUE' + + +class BadSwappableModel(models.Model): + """A model that can be swapped out; during testing, the swappable + value references an unknown model. + """ + name = models.CharField(max_length=100) + + class Meta: + swappable = 'TEST_SWAPPED_MODEL_BAD_MODEL' + + +class HardReferenceModel(models.Model): + fk_1 = models.ForeignKey(SwappableModel, related_name='fk_hardref1') + fk_2 = models.ForeignKey('invalid_models.SwappableModel', related_name='fk_hardref2') + fk_3 = models.ForeignKey(SwappedModel, related_name='fk_hardref3') + fk_4 = models.ForeignKey('invalid_models.SwappedModel', related_name='fk_hardref4') + m2m_1 = models.ManyToManyField(SwappableModel, related_name='m2m_hardref1') + m2m_2 = models.ManyToManyField('invalid_models.SwappableModel', related_name='m2m_hardref2') + m2m_3 = models.ManyToManyField(SwappedModel, related_name='m2m_hardref3') + m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4') + + model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer. invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer. @@ -353,6 +449,12 @@ invalid_models.nonuniquefktarget2: Field 'bad' under model 'FKTarget' must have invalid_models.nonexistingorderingwithsingleunderscore: "ordering" refers to "does_not_exist", a field that doesn't exist. invalid_models.invalidsetnull: 'fk' specifies on_delete=SET_NULL, but cannot be null. invalid_models.invalidsetdefault: 'fk' specifies on_delete=SET_DEFAULT, but has no default value. +invalid_models.hardreferencemodel: 'fk_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'fk_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL. +invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'. +invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract. """ if not connection.features.interprets_empty_strings_as_nulls: diff --git a/tests/modeltests/invalid_models/tests.py b/tests/modeltests/invalid_models/tests.py index e1fc68743e..6050a20880 100644 --- a/tests/modeltests/invalid_models/tests.py +++ b/tests/modeltests/invalid_models/tests.py @@ -4,6 +4,7 @@ import sys from django.core.management.validation import get_validation_errors from django.db.models.loading import cache, load_app +from django.test.utils import override_settings from django.utils import unittest from django.utils.six import StringIO @@ -31,14 +32,22 @@ class InvalidModelTestCase(unittest.TestCase): cache._get_models_cache = {} sys.stdout = self.old_stdout + # Technically, this isn't an override -- TEST_SWAPPED_MODEL must be + # set to *something* in order for the test to work. However, it's + # easier to set this up as an override than to require every developer + # to specify a value in their test settings. + @override_settings( + TEST_SWAPPED_MODEL='invalid_models.Target', + TEST_SWAPPED_MODEL_BAD_VALUE='not-a-model', + TEST_SWAPPED_MODEL_BAD_MODEL='not_an_app.Target', + ) def test_invalid_models(self): - try: module = load_app("modeltests.invalid_models.invalid_models") except Exception: self.fail('Unable to load invalid model module') - count = get_validation_errors(self.stdout, module) + get_validation_errors(self.stdout, module) self.stdout.seek(0) error_log = self.stdout.read() actual = error_log.split('\n') diff --git a/tests/modeltests/proxy_models/tests.py b/tests/modeltests/proxy_models/tests.py index 7ec86e9b22..d1c95467ee 100644 --- a/tests/modeltests/proxy_models/tests.py +++ b/tests/modeltests/proxy_models/tests.py @@ -1,10 +1,13 @@ from __future__ import absolute_import, unicode_literals +import copy +from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core import management from django.core.exceptions import FieldError from django.db import models, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.models.loading import cache from django.test import TestCase @@ -13,6 +16,7 @@ from .models import (MyPerson, Person, StatusPerson, LowerStatusPerson, Country, State, StateProxy, TrackerUser, BaseUser, Bug, ProxyTrackerUser, Improvement, ProxyProxyBug, ProxyBug, ProxyImprovement) + class ProxyModelTests(TestCase): def test_same_manager_queries(self): """ @@ -91,7 +95,7 @@ class ProxyModelTests(TestCase): ) self.assertRaises(Person.MultipleObjectsReturned, MyPersonProxy.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) self.assertRaises(Person.DoesNotExist, StatusPerson.objects.get, @@ -104,7 +108,7 @@ class ProxyModelTests(TestCase): self.assertRaises(Person.MultipleObjectsReturned, StatusPerson.objects.get, - id__lt=max_id+1 + id__lt=max_id + 1 ) def test_abc(self): @@ -138,10 +142,40 @@ class ProxyModelTests(TestCase): def build_new_fields(): class NoNewFields(Person): newfield = models.BooleanField() + class Meta: proxy = True self.assertRaises(FieldError, build_new_fields) + def test_swappable(self): + try: + # This test adds dummy applications to the app cache. These + # need to be removed in order to prevent bad interactions + # with the flush operation in other tests. + old_app_models = copy.deepcopy(cache.app_models) + old_app_store = copy.deepcopy(cache.app_store) + + settings.TEST_SWAPPABLE_MODEL = 'proxy_models.AlternateModel' + + class SwappableModel(models.Model): + + class Meta: + swappable = 'TEST_SWAPPABLE_MODEL' + + class AlternateModel(models.Model): + pass + + # You can't proxy a swapped model + with self.assertRaises(TypeError): + class ProxyModel(SwappableModel): + + class Meta: + proxy = True + finally: + del settings.TEST_SWAPPABLE_MODEL + cache.app_models = old_app_models + cache.app_store = old_app_store + def test_myperson_manager(self): Person.objects.create(name="fred") Person.objects.create(name="wilma") diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 8151c8c854..284ea94226 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -52,6 +52,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, ERROR_MESSAGE = "Please enter the correct username and password \ for a staff account. Note that both fields are case-sensitive." + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', @@ -141,7 +142,7 @@ class AdminViewBasicTest(TestCase): "article_set-MAX_NUM_FORMS": "0", } response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testPopupAddPost(self): """ @@ -205,7 +206,7 @@ class AdminViewBasicTest(TestCase): A smoke test to ensure POST on edit_view works. """ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testEditSaveAs(self): """ @@ -221,7 +222,7 @@ class AdminViewBasicTest(TestCase): "article_set-5-section": "1", }) response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testChangeListSortingCallable(self): """ @@ -308,7 +309,7 @@ class AdminViewBasicTest(TestCase): self.assertContentBefore(response, link2, link1) # Test we can override with query string - response = self.client.get('/test_admin/admin/admin_views/language/', {'o':'-1'}) + response = self.client.get('/test_admin/admin/admin_views/language/', {'o': '-1'}) self.assertContentBefore(response, link1, link2) def testChangeListSortingOverrideModelAdmin(self): @@ -358,13 +359,13 @@ class AdminViewBasicTest(TestCase): kinds of 'ordering' fields: field names, method on the model admin and model itself, and other callables. See #17252. """ - models = [(AdminOrderedField, 'adminorderedfield' ), + models = [(AdminOrderedField, 'adminorderedfield'), (AdminOrderedModelMethod, 'adminorderedmodelmethod'), (AdminOrderedAdminMethod, 'adminorderedadminmethod'), - (AdminOrderedCallable, 'adminorderedcallable' )] + (AdminOrderedCallable, 'adminorderedcallable')] for model, url in models: - a1 = model.objects.create(stuff='The Last Item', order=3) - a2 = model.objects.create(stuff='The First Item', order=1) + a1 = model.objects.create(stuff='The Last Item', order=3) + a2 = model.objects.create(stuff='The First Item', order=1) a3 = model.objects.create(stuff='The Middle Item', order=2) response = self.client.get('/test_admin/admin/admin_views/%s/' % url, {}) self.assertEqual(response.status_code, 200) @@ -671,7 +672,6 @@ class AdminJavaScriptTest(TestCase): '' ) - def test_js_minified_only_if_debug_is_false(self): """ Ensure that the minified versions of the JS files are only used when @@ -709,7 +709,7 @@ class AdminJavaScriptTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class SaveAsTests(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml','admin-views-person.xml'] + fixtures = ['admin-views-users.xml', 'admin-views-person.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -719,7 +719,7 @@ class SaveAsTests(TestCase): def test_save_as_duplication(self): """Ensure save as actually creates a new person""" - post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 1, 'age': 42} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=1)), 1) @@ -732,10 +732,11 @@ class SaveAsTests(TestCase): """ response = self.client.get('/test_admin/admin/admin_views/person/1/') self.assertTrue(response.context['save_as']) - post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'} + post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(response.context['form_url'], '/test_admin/admin/admin_views/person/add/') + class CustomModelAdminTest(AdminViewBasicTest): urls = "regressiontests.admin_views.urls" urlbit = "admin2" @@ -791,11 +792,13 @@ class CustomModelAdminTest(AdminViewBasicTest): response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) self.assertEqual(response.content, b"Django is a magical pony!") + def get_perm(Model, perm): """Return the permission object, for the Model""" ct = ContentType.objects.get_for_model(Model) return Permission.objects.get(content_type=ct, codename=perm) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewPermissionsTest(TestCase): """Tests for Admin Views Permissions.""" @@ -898,7 +901,7 @@ class AdminViewPermissionsTest(TestCase): response = self.client.get('/test_admin/admin/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) - self.assertContains(login, "Your e-mail address is not your username") + self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post('/test_admin/admin/', self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) @@ -959,7 +962,7 @@ class AdminViewPermissionsTest(TestCase): def testAddView(self): """Test add view restricts access and actually adds items.""" - add_dict = {'title' : 'Døm ikke', + add_dict = {'title': 'Døm ikke', 'content': '

great article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1014,7 +1017,7 @@ class AdminViewPermissionsTest(TestCase): def testChangeView(self): """Change view should restrict access and allow users to edit items.""" - change_dict = {'title' : 'Ikke fordømt', + change_dict = {'title': 'Ikke fordømt', 'content': '

edited article

', 'date_0': '2008-03-18', 'date_1': '10:54:39', 'section': 1} @@ -1346,6 +1349,7 @@ class AdminViewDeletedObjectsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3)) self.assertContains(response, should_contain) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewStringPrimaryKeyTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -1400,7 +1404,7 @@ class AdminViewStringPrimaryKeyTest(TestCase): response = self.client.get('/test_admin/admin/') should_contain = """%s""" % (escape(quote(self.pk)), escape(self.pk)) self.assertContains(response, should_contain) - should_contain = "Model with string primary key" # capitalized in Recent Actions + should_contain = "Model with string primary key" # capitalized in Recent Actions self.assertContains(response, should_contain) logentry = LogEntry.objects.get(content_type__name__iexact=should_contain) # http://code.djangoproject.com/ticket/10275 @@ -1522,7 +1526,7 @@ class SecureViewTests(TestCase): def test_secure_view_shows_login_if_not_logged_in(self): "Ensure that we see the login form" - response = self.client.get('/test_admin/admin/secure-view/' ) + response = self.client.get('/test_admin/admin/secure-view/') self.assertTemplateUsed(response, 'admin/login.html') def test_secure_view_login_successfully_redirects_to_original_url(self): @@ -1556,7 +1560,7 @@ class SecureViewTests(TestCase): response = self.client.get('/test_admin/admin/secure-view/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) - self.assertContains(login, "Your e-mail address is not your username") + self.assertContains(login, ERROR_MESSAGE) # only correct passwords get a username hint login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login) self.assertContains(login, ERROR_MESSAGE) @@ -1626,6 +1630,7 @@ class SecureViewTests(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(response['Location'], 'http://example.com/users/super/') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminViewUnicodeTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -1668,7 +1673,7 @@ class AdminViewUnicodeTest(TestCase): } response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testUnicodeDelete(self): """ @@ -2035,7 +2040,7 @@ class AdminViewListEditable(TestCase): story1 = Story.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...') story2 = Story.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...') response = self.client.get('/test_admin/admin/admin_views/story/') - self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '
\n\n
' % (story2.id, story1.id), html=True) self.assertContains(response, '%d' % story1.id, 1) @@ -2051,7 +2056,7 @@ class AdminViewListEditable(TestCase): link1 = reverse('admin:admin_views_otherstory_change', args=(story1.pk,)) link2 = reverse('admin:admin_views_otherstory_change', args=(story2.pk,)) response = self.client.get('/test_admin/admin/admin_views/otherstory/') - self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. self.assertContains(response, 'id="id_form-1-id"', 1) self.assertContains(response, '
\n\n
' % (story2.id, story1.id), html=True) self.assertContains(response, '%d' % (link1, story1.id), 1) @@ -2109,7 +2114,7 @@ class AdminSearchTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): urls = "regressiontests.admin_views.urls" - fixtures = ['admin-views-users.xml',] + fixtures = ['admin-views-users.xml'] def setUp(self): self.client.login(username='super', password='secret') @@ -2146,7 +2151,7 @@ class AdminInheritedInlinesTest(TestCase): } response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere self.assertEqual(Persona.objects.count(), 1) self.assertEqual(FooAccount.objects.count(), 1) self.assertEqual(BarAccount.objects.count(), 1) @@ -2193,6 +2198,7 @@ class AdminInheritedInlinesTest(TestCase): self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminActionsTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2208,7 +2214,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'mail_admin', + 'action': 'mail_admin', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2219,12 +2225,12 @@ class AdminActionsTest(TestCase): "Tests the default delete action defined as a ModelAdmin method" action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } delete_confirmation_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'post': 'yes', } confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2248,12 +2254,12 @@ class AdminActionsTest(TestCase): subscriber.save() action_data = { ACTION_CHECKBOX_NAME: [9999, 2], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) self.assertTemplateUsed(response, 'admin/delete_selected_confirmation.html') - self.assertContains(response, 'value="9999"') # Instead of 9,999 + self.assertContains(response, 'value="9999"') # Instead of 9,999 self.assertContains(response, 'value="2"') settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR settings.USE_L10N = self.old_USE_L10N @@ -2270,7 +2276,7 @@ class AdminActionsTest(TestCase): action_data = { ACTION_CHECKBOX_NAME: [q1.pk, q2.pk], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } @@ -2284,7 +2290,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2295,7 +2301,7 @@ class AdminActionsTest(TestCase): "Tests a custom action defined in a function" action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'redirect_to', + 'action': 'redirect_to', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) @@ -2309,7 +2315,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1], - 'action' : 'external_mail', + 'action': 'external_mail', 'index': 0, } url = '/test_admin/admin/admin_views/externalsubscriber/?o=1' @@ -2374,7 +2380,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [], - 'action' : 'delete_selected', + 'action': 'delete_selected', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2388,7 +2394,7 @@ class AdminActionsTest(TestCase): """ action_data = { ACTION_CHECKBOX_NAME: [1, 2], - 'action' : '', + 'action': '', 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) @@ -2432,7 +2438,7 @@ class TestCustomChangeList(TestCase): # Insert some data post_data = {"name": "First Gadget"} response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) - self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere # Hit the page once to get messages out of the queue message list response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) # Ensure that that data is still not visible on the page @@ -2460,6 +2466,7 @@ class TestInlineNotEditable(TestCase): response = self.client.get('/test_admin/admin/admin_views/parent/add/') self.assertEqual(response.status_code, 200) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomQuerysetTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2516,6 +2523,7 @@ class AdminCustomQuerysetTest(TestCase): # Message should contain non-ugly model name. Instance representation is set by model's __unicode__() self.assertContains(response, '
  • The cover letter "John Doe II" was changed successfully.
  • ', html=True) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInlineFileUploadTest(TestCase): urls = "regressiontests.admin_views.urls" @@ -2656,7 +2664,7 @@ class AdminInlineTests(TestCase): result = self.client.login(username='super', password='secret') self.assertEqual(result, True) - self.collector = Collector(pk=1,name='John Fowles') + self.collector = Collector(pk=1, name='John Fowles') self.collector.save() def tearDown(self): @@ -2982,14 +2990,14 @@ class PrePopulatedTest(TestCase): self.assertNotContains(response, "field['dependency_ids'].push('#id_title');") self.assertNotContains(response, "id: '#id_prepopulatedsubpost_set-0-subslug',") - @override_settings(USE_THOUSAND_SEPARATOR = True, USE_L10N = True) + @override_settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=True) def test_prepopulated_maxlength_localized(self): """ Regression test for #15938: if USE_THOUSAND_SEPARATOR is set, make sure that maxLength (in the JavaScript) is rendered without separators. """ response = self.client.get('/test_admin/admin/admin_views/prepopulatedpostlargeslug/add/') - self.assertContains(response, "maxLength: 1000") # instead of 1,000 + self.assertContains(response, "maxLength: 1000") # instead of 1,000 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -3035,8 +3043,8 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-name').send_keys(' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooog text... ') slug1 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug1').get_attribute('value') slug2 = self.selenium.find_element_by_css_selector('#id_relatedprepopulated_set-1-slug2').get_attribute('value') - self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field - self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field + self.assertEqual(slug1, 'now-you-have-another-stacked-inline-very-loooooooo') # 50 characters maximum for slug1 field + self.assertEqual(slug2, 'option-two-now-you-have-another-stacked-inline-very-looooooo') # 60 characters maximum for slug2 field # Tabular inlines ---------------------------------------------------- # Initial inline @@ -3087,7 +3095,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): slug2='option-one-here-stacked-inline', ) RelatedPrepopulated.objects.get( - name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field + name=' now you haVe anöther sŤāÇkeð inline with a very ... loooooooooooooooooo', # 75 characters in name field pubdate='1999-01-25', status='option two', slug1='now-you-have-another-stacked-inline-very-loooooooo', @@ -3112,6 +3120,7 @@ class SeleniumPrePopulatedFirefoxTests(AdminSeleniumWebDriverTestCase): class SeleniumPrePopulatedChromeTests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' + class SeleniumPrePopulatedIETests(SeleniumPrePopulatedFirefoxTests): webdriver_class = 'selenium.webdriver.ie.webdriver.WebDriver' @@ -3172,7 +3181,7 @@ class ReadonlyTest(TestCase): p = Post.objects.get() self.assertEqual(p.posted, datetime.date.today()) - data["posted"] = "10-8-1990" # some date that's not today + data["posted"] = "10-8-1990" # some date that's not today response = self.client.post('/test_admin/admin/admin_views/post/add/', data) self.assertEqual(response.status_code, 302) self.assertEqual(Post.objects.count(), 2) @@ -3214,7 +3223,7 @@ class RawIdFieldsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/sketch/add/') # Find the link m = re.search(br']* id="lookup_id_inquisition"', response.content) - self.assertTrue(m) # Got a match + self.assertTrue(m) # Got a match popup_url = m.groups()[0].decode().replace("&", "&") # Handle relative links @@ -3224,6 +3233,7 @@ class RawIdFieldsTest(TestCase): self.assertContains(response2, "Spain") self.assertNotContains(response2, "England") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserAdminTest(TestCase): """ @@ -3378,7 +3388,7 @@ class CSSTest(TestCase): self.assertContains(response, 'class="form-row field-awesomeness_level"') self.assertContains(response, 'class="form-row field-coolness"') self.assertContains(response, 'class="form-row field-value"') - self.assertContains(response, 'class="form-row"') # The lambda function + self.assertContains(response, 'class="form-row"') # The lambda function # The tabular inline self.assertContains(response, '') @@ -3390,6 +3400,7 @@ try: except ImportError: docutils = None + @unittest.skipUnless(docutils, "no docutils installed.") @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminDocsTest(TestCase): @@ -3448,7 +3459,7 @@ class ValidXHTMLTests(TestCase): @override_settings( TEMPLATE_CONTEXT_PROCESSORS=filter( - lambda t:t!='django.core.context_processors.i18n', + lambda t: t != 'django.core.context_processors.i18n', global_settings.TEMPLATE_CONTEXT_PROCESSORS), USE_I18N=False, ) @@ -3585,6 +3596,7 @@ class DateHierarchyTests(TestCase): self.assert_non_localized_year(response, 2003) self.assert_non_localized_year(response, 2005) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminCustomSaveRelatedTests(TestCase): """ -- cgit v1.3 From 2c8267bf3db608b99c04ae903c424b60cafaaf93 Mon Sep 17 00:00:00 2001 From: Adrien Lemaire Date: Wed, 26 Sep 2012 14:14:51 +0200 Subject: Fixed #17899 -- Rewrote [Ee]-mail to [Ee]mail --- django/conf/global_settings.py | 2 +- django/contrib/admin/templates/admin/500.html | 2 +- .../registration/password_reset_done.html | 2 +- .../registration/password_reset_email.html | 2 +- .../registration/password_reset_form.html | 4 ++-- django/contrib/auth/forms.py | 6 ++--- django/contrib/auth/tests/forms.py | 2 +- .../registration/password_reset_done.html | 2 +- django/core/validators.py | 2 +- django/db/models/fields/__init__.py | 2 +- django/forms/fields.py | 2 +- docs/index.txt | 2 +- docs/internals/deprecation.txt | 2 +- docs/ref/contrib/syndication.txt | 4 ++-- docs/ref/forms/api.txt | 10 ++++---- docs/ref/forms/fields.txt | 2 +- docs/ref/forms/validation.txt | 4 ++-- docs/topics/testing.txt | 2 +- tests/modeltests/test_client/models.py | 4 ++-- tests/regressiontests/admin_views/tests.py | 8 +++---- tests/regressiontests/forms/tests/extra.py | 2 +- tests/regressiontests/forms/tests/fields.py | 28 +++++++++++----------- tests/regressiontests/test_client_regress/tests.py | 4 ++-- tests/regressiontests/test_utils/tests.py | 2 +- 24 files changed, 51 insertions(+), 51 deletions(-) (limited to 'docs/internals') diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 4d5dc49ee0..708e9c9f70 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -144,7 +144,7 @@ DEFAULT_CHARSET = 'utf-8' # Encoding of files read from disk (template and initial SQL files). FILE_CHARSET = 'utf-8' -# E-mail address that error messages come from. +# Email address that error messages come from. SERVER_EMAIL = 'root@localhost' # Whether to send broken-link emails. diff --git a/django/contrib/admin/templates/admin/500.html b/django/contrib/admin/templates/admin/500.html index 9a3b636346..4842faa656 100644 --- a/django/contrib/admin/templates/admin/500.html +++ b/django/contrib/admin/templates/admin/500.html @@ -12,6 +12,6 @@ {% block content %}

    {% trans 'Server Error (500)' %}

    -

    {% trans "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." %}

    +

    {% trans "There's been an error. It's been reported to the site administrators via email and should be fixed shortly. Thanks for your patience." %}

    {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_done.html b/django/contrib/admin/templates/registration/password_reset_done.html index 3c9796e63c..7584c8393a 100644 --- a/django/contrib/admin/templates/registration/password_reset_done.html +++ b/django/contrib/admin/templates/registration/password_reset_done.html @@ -14,6 +14,6 @@

    {% trans 'Password reset successful' %}

    -

    {% trans "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." %}

    +

    {% trans "We've emailed you instructions for setting your password to the email address you submitted. You should be receiving it shortly." %}

    {% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 4f002fe5bb..0eef4a7f9d 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -1,5 +1,5 @@ {% load i18n %}{% autoescape off %} -{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} +{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index ca9ff115bc..c9998a1a3b 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -14,11 +14,11 @@

    {% trans "Password reset" %}

    -

    {% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

    +

    {% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}

    {% csrf_token %} {{ form.email.errors }} -

    {{ form.email }}

    +

    {{ form.email }}

    {% endblock %} diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index a430f042e9..c114c18afe 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -193,12 +193,12 @@ class AuthenticationForm(forms.Form): class PasswordResetForm(forms.Form): error_messages = { - 'unknown': _("That e-mail address doesn't have an associated " + 'unknown': _("That email address doesn't have an associated " "user account. Are you sure you've registered?"), - 'unusable': _("The user account associated with this e-mail " + 'unusable': _("The user account associated with this email " "address cannot reset the password."), } - email = forms.EmailField(label=_("E-mail"), max_length=75) + email = forms.EmailField(label=_("Email"), max_length=75) def clean_email(self): """ diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py index 7c6410da0f..6be6249711 100644 --- a/django/contrib/auth/tests/forms.py +++ b/django/contrib/auth/tests/forms.py @@ -344,4 +344,4 @@ class PasswordResetFormTest(TestCase): form = PasswordResetForm(data) self.assertFalse(form.is_valid()) self.assertEqual(form["email"].errors, - [_("The user account associated with this e-mail address cannot reset the password.")]) + [_("The user account associated with this email address cannot reset the password.")]) diff --git a/django/contrib/auth/tests/templates/registration/password_reset_done.html b/django/contrib/auth/tests/templates/registration/password_reset_done.html index d56b10f0d5..c3d1d0c7b0 100644 --- a/django/contrib/auth/tests/templates/registration/password_reset_done.html +++ b/django/contrib/auth/tests/templates/registration/password_reset_done.html @@ -1 +1 @@ -E-mail sent \ No newline at end of file +Email sent \ No newline at end of file diff --git a/django/core/validators.py b/django/core/validators.py index cf12f8c9fc..c7bda682ac 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -101,7 +101,7 @@ email_re = re.compile( r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' r')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?$)' # domain r'|\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$', re.IGNORECASE) # literal form, ipv4 address (SMTP 4.1.3) -validate_email = EmailValidator(email_re, _('Enter a valid e-mail address.'), 'invalid') +validate_email = EmailValidator(email_re, _('Enter a valid email address.'), 'invalid') slug_re = re.compile(r'^[-a-zA-Z0-9_]+$') validate_slug = RegexValidator(slug_re, _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."), 'invalid') diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 58ae3413f3..94abfd784c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -905,7 +905,7 @@ class DecimalField(Field): class EmailField(CharField): default_validators = [validators.validate_email] - description = _("E-mail address") + description = _("Email address") def __init__(self, *args, **kwargs): # max_length should be overridden to 254 characters to be fully diff --git a/django/forms/fields.py b/django/forms/fields.py index 0075325288..4438812a37 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -461,7 +461,7 @@ class RegexField(CharField): class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] diff --git a/docs/index.txt b/docs/index.txt index 8b29c95fa2..ce84f79d43 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -241,7 +241,7 @@ applications: * :doc:`Authentication ` * :doc:`Caching ` * :doc:`Logging ` -* :doc:`Sending e-mails ` +* :doc:`Sending emails ` * :doc:`Syndication feeds (RSS/Atom) ` * :doc:`Comments `, :doc:`comment moderation ` and :doc:`custom comments ` * :doc:`Pagination ` diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4e341c6953..6387c87d1d 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -23,7 +23,7 @@ these changes. * The :mod:`django.contrib.gis.db.backend` module will be removed in favor of the specific backends. -* ``SMTPConnection`` will be removed in favor of a generic E-mail backend API. +* ``SMTPConnection`` will be removed in favor of a generic Email backend API. * The many to many SQL generation functions on the database backends will be removed. diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 5653397748..27b8fc0875 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -455,7 +455,7 @@ This example illustrates all possible attributes and methods for a author_name = 'Sally Smith' # Hard-coded author name. - # AUTHOR E-MAIL --One of the following three is optional. The framework + # AUTHOR EMAIL --One of the following three is optional. The framework # looks for them in this order. def author_email(self, obj): @@ -635,7 +635,7 @@ This example illustrates all possible attributes and methods for a item_author_name = 'Sally Smith' # Hard-coded author name. - # ITEM AUTHOR E-MAIL --One of the following three is optional. The + # ITEM AUTHOR EMAIL --One of the following three is optional. The # framework looks for them in this order. # # If you specify this, you must specify item_author_name. diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 2323425277..dffef314b7 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -105,7 +105,7 @@ Access the :attr:`~Form.errors` attribute to get a dictionary of error messages:: >>> f.errors - {'sender': [u'Enter a valid e-mail address.'], 'subject': [u'This field is required.']} + {'sender': [u'Enter a valid email address.'], 'subject': [u'This field is required.']} In this dictionary, the keys are the field names, and the values are lists of Unicode strings representing the error messages. The error messages are stored @@ -538,18 +538,18 @@ method you're using:: >>> print(f.as_table()) Subject:
    • This field is required.
    Message: - Sender:
    • Enter a valid e-mail address.
    + Sender:
    • Enter a valid email address.
    Cc myself: >>> print(f.as_ul())
    • This field is required.
    Subject:
  • Message:
  • -
    • Enter a valid e-mail address.
    Sender:
  • +
    • Enter a valid email address.
    Sender:
  • Cc myself:
  • >>> print(f.as_p())

    • This field is required.

    Subject:

    Message:

    -

    • Enter a valid e-mail address.

    +

    • Enter a valid email address.

    Sender:

    Cc myself:

    @@ -572,7 +572,7 @@ pass that in at construction time::
    This field is required.

    Subject:

    Message:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Sender:

    Cc myself:

    diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 9f3dc68b4d..82a3ea9ab3 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -28,7 +28,7 @@ exception or returns the clean value:: >>> f.clean('invalid email address') Traceback (most recent call last): ... - ValidationError: [u'Enter a valid e-mail address.'] + ValidationError: [u'Enter a valid email address.'] Core field arguments -------------------- diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 1af32da875..e89bce748f 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -185,7 +185,7 @@ a look at Django's ``EmailField``:: class EmailField(CharField): default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] @@ -198,7 +198,7 @@ on field definition so:: is equivalent to:: email = forms.CharField(validators=[validators.validate_email], - error_messages={'invalid': _('Enter a valid e-mail address.')}) + error_messages={'invalid': _('Enter a valid email address.')}) Form field default cleaning diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 117dfbe591..2bc8410745 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1622,7 +1622,7 @@ your test suite. "a@a.com" as a valid email address, but rejects "aaa" with a reasonable error message:: - self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid e-mail address.']}) + self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) .. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index 1d9c999f21..0f3cba7e88 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -215,7 +215,7 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_valid_form_with_template(self): "POST valid data to a form using multiple templates" @@ -263,7 +263,7 @@ class ClientTest(TestCase): self.assertTemplateUsed(response, 'base.html') self.assertTemplateNotUsed(response, "Invalid POST Template") - self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.') + self.assertFormError(response, 'form', 'email', 'Enter a valid email address.') def test_unknown_page(self): "GET an invalid URL" diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 284ea94226..72dc6a3f97 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -897,7 +897,7 @@ class AdminViewPermissionsTest(TestCase): self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') - # Test if user enters e-mail address + # Test if user enters email address response = self.client.get('/test_admin/admin/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) @@ -907,7 +907,7 @@ class AdminViewPermissionsTest(TestCase): self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() - # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/', self.super_email_login) self.assertContains(login, ERROR_MESSAGE) @@ -1556,7 +1556,7 @@ class SecureViewTests(TestCase): # make sure the view removes test cookie self.assertEqual(self.client.session.test_cookie_worked(), False) - # Test if user enters e-mail address + # Test if user enters email address response = self.client.get('/test_admin/admin/secure-view/') self.assertEqual(response.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) @@ -1566,7 +1566,7 @@ class SecureViewTests(TestCase): self.assertContains(login, ERROR_MESSAGE) new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() - # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + # check to ensure if there are multiple email addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) self.assertContains(login, ERROR_MESSAGE) diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 2ab5d40942..44d6778aa2 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -613,7 +613,7 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): data = dict(email='invalid') f = CommentForm(data, auto_id=False, error_class=DivErrorList) self.assertHTMLEqual(f.as_p(), """

    Name:

    -
    Enter a valid e-mail address.
    +
    Enter a valid email address.

    Email:

    This field is required.

    Comment:

    """) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index 989acbc496..8695256d64 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -507,16 +507,16 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('person@example.com', f.clean('person@example.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@invalid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@-invalid.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.alid-.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@inv-.-alid.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@invalid-.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@-invalid.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.alid-.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@inv-.-alid.com') self.assertEqual('example@valid-----hyphens.com', f.clean('example@valid-----hyphens.com')) self.assertEqual('example@valid-with-hyphens.com', f.clean('example@valid-with-hyphens.com')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'example@.com') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'example@.com') self.assertEqual('local@domain.with.idn.xyz\xe4\xf6\xfc\xdfabc.part.com', f.clean('local@domain.with.idn.xyzäöüßabc.part.com')) def test_email_regexp_for_performance(self): @@ -525,7 +525,7 @@ class FieldsTests(SimpleTestCase): # if the security fix isn't in place. self.assertRaisesMessage( ValidationError, - "'Enter a valid e-mail address.'", + "'Enter a valid email address.'", f.clean, 'viewx3dtextx26qx3d@yahoo.comx26latlngx3d15854521645943074058' ) @@ -536,9 +536,9 @@ class FieldsTests(SimpleTestCase): self.assertEqual('', f.clean(None)) self.assertEqual('person@example.com', f.clean('person@example.com')) self.assertEqual('example@example.com', f.clean(' example@example.com \t \t ')) - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'foo@bar') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'foo@bar') def test_emailfield_3(self): f = EmailField(min_length=10, max_length=15) @@ -926,7 +926,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()]) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) @@ -934,7 +934,7 @@ class FieldsTests(SimpleTestCase): f = ComboField(fields=[CharField(max_length=20), EmailField()], required=False) self.assertEqual('test@example.com', f.clean('test@example.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 28).'", f.clean, 'longemailaddress@example.com') - self.assertRaisesMessage(ValidationError, "'Enter a valid e-mail address.'", f.clean, 'not an e-mail') + self.assertRaisesMessage(ValidationError, "'Enter a valid email address.'", f.clean, 'not an email') self.assertEqual('', f.clean('')) self.assertEqual('', f.clean(None)) diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py index 9deb8a4755..c741903c34 100644 --- a/tests/regressiontests/test_client_regress/tests.py +++ b/tests/regressiontests/test_client_regress/tests.py @@ -499,11 +499,11 @@ class AssertFormErrorTests(TestCase): try: self.assertFormError(response, 'form', 'email', 'Some error.') except AssertionError as e: - self.assertIn(str_prefix("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid e-mail address.'])"), str(e)) + self.assertIn(str_prefix("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid email address.'])"), str(e)) try: self.assertFormError(response, 'form', 'email', 'Some error.', msg_prefix='abc') except AssertionError as e: - self.assertIn(str_prefix("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid e-mail address.'])"), str(e)) + self.assertIn(str_prefix("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [%(_)s'Enter a valid email address.'])"), str(e)) def test_unknown_nonfield_error(self): """ diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index 468af77f44..12c639cee1 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -476,7 +476,7 @@ class AssertRaisesMsgTest(SimpleTestCase): class AssertFieldOutputTests(SimpleTestCase): def test_assert_field_output(self): - error_invalid = ['Enter a valid e-mail address.'] + error_invalid = ['Enter a valid email address.'] self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': error_invalid}) self.assertRaises(AssertionError, self.assertFieldOutput, EmailField, {'a@a.com': 'a@a.com'}, {'aaa': error_invalid + ['Another error']}) self.assertRaises(AssertionError, self.assertFieldOutput, EmailField, {'a@a.com': 'Wrong output'}, {'aaa': error_invalid}) -- cgit v1.3 From 1df58968a4b2247aff91db40f1325f079ba3cdce Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 27 Sep 2012 13:19:04 -0700 Subject: Added a note regarding interaction between GitHub and Trac Plugin --- docs/internals/contributing/committing-code.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/committing-code.txt b/docs/internals/contributing/committing-code.txt index d36bc78fe1..67dda02f8b 100644 --- a/docs/internals/contributing/committing-code.txt +++ b/docs/internals/contributing/committing-code.txt @@ -187,7 +187,15 @@ Django's Git repository: For the curious, we're using a `Trac plugin`_ for this. - .. _Trac plugin: https://github.com/aaugustin/trac-github +.. note:: + + Note that the Trac integration doesn't know anything about pull requests. + So if you try to close a pull request with the phrase "closes #400" in your + commit message, GitHub will close the pull request, but the Trac plugin + will also close the same numbered ticket in Trac. + + +.. _Trac plugin: https://github.com/aaugustin/trac-github * If your commit references a ticket in the Django `ticket tracker`_ but does *not* close the ticket, include the phrase "Refs #xxxxx", where "xxxxx" -- cgit v1.3 From 14681eaa53a035fa1c8fc390d5cca0b340fdcdb3 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Mon, 1 Oct 2012 09:00:41 -0700 Subject: Fixed #19045 -- removed 'fixed on a branch' from triage docs --- docs/internals/contributing/triaging-tickets.txt | 9 --------- 1 file changed, 9 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index ab879e5caf..84f70fd731 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -171,15 +171,6 @@ concrete actionable issues. They are enhancement requests that we might consider adding someday to the framework if an excellent patch is submitted. These tickets are not a high priority. -Fixed on a branch -~~~~~~~~~~~~~~~~~ - -Used to indicate that a ticket is resolved as part of a major body of work -that will eventually be merged to trunk. Tickets in this stage generally -don't need further work. This may happen in the case of major -features/refactors in each release cycle, or as part of the annual Google -Summer of Code efforts. - Other triage attributes ----------------------- -- cgit v1.3 From f315006d50a1391393507cef1c52147cb766e45d Mon Sep 17 00:00:00 2001 From: "Daniel D. Beck" Date: Wed, 10 Oct 2012 20:38:48 -0300 Subject: Remove heteronormativity from coding style doc --- docs/internals/contributing/writing-code/coding-style.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index 2fa0233e3d..a699e39bd8 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -140,9 +140,9 @@ Model style a tuple of tuples, with an all-uppercase name, either near the top of the model module or just above the model class. Example:: - GENDER_CHOICES = ( - ('M', 'Male'), - ('F', 'Female'), + DIRECTION_CHOICES = ( + ('U', 'Up'), + ('D', 'Down'), ) Use of ``django.conf.settings`` -- cgit v1.3 From 06f5da3d7813e9a23b1e98ecf8b75fc6073800e9 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 11 Oct 2012 06:11:52 -0400 Subject: Fixed #16817 - Added a guide of code coverage to contributing docs. Thanks Pedro Lima for the draft patch. --- .gitignore | 2 ++ .hgignore | 2 ++ .../contributing/writing-code/unit-tests.txt | 20 ++++++++++++++++++++ docs/topics/testing.txt | 2 ++ tests/.coveragerc | 5 +++++ 5 files changed, 31 insertions(+) create mode 100644 tests/.coveragerc (limited to 'docs/internals') diff --git a/.gitignore b/.gitignore index 17e39abd38..2d028c7287 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ MANIFEST dist/ docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/.hgignore b/.hgignore index 765a29d091..3dc253a3c1 100644 --- a/.hgignore +++ b/.hgignore @@ -4,3 +4,5 @@ syntax:glob *.pot *.py[co] docs/_build/ +tests/coverage_html/ +tests/.coverage \ No newline at end of file diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 4de506a654..a828b06b36 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -163,6 +163,26 @@ associated tests will be skipped. .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html .. _selenium: http://pypi.python.org/pypi/selenium +Code coverage +~~~~~~~~~~~~~ + +Contributors are encouraged to run coverage on the test suite to identify areas +that need additional tests. The coverage tool installation and use is described +in :ref:`testing code coverage`. + +To run coverage on the Django test suite using the standard test settings:: + + coverage run ./runtests.py --settings=test_sqlite + +After running coverage, generate the html report by running:: + + coverage html + +When running coverage for the Django tests, the included ``.coveragerc`` +settings file defines ``coverage_html`` as the output directory for the report +and also excludes several directories not relevant to the results +(test code or external code included in Django). + .. _contrib-apps: Contrib apps diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index f907c72a5e..d0b2e7cdf9 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -589,6 +589,8 @@ to a faster hashing algorithm:: Don't forget to also include in :setting:`PASSWORD_HASHERS` any hashing algorithm used in fixtures, if any. +.. _topics-testing-code-coverage: + Integration with coverage.py ---------------------------- diff --git a/tests/.coveragerc b/tests/.coveragerc new file mode 100644 index 0000000000..b979e94c58 --- /dev/null +++ b/tests/.coveragerc @@ -0,0 +1,5 @@ +[run] +omit = runtests,test_sqlite,regressiontests*,modeltests*,*/django/contrib/*/tests*,*/django/utils/unittest*,*/django/utils/simplejson*,*/django/utils/importlib.py,*/django/test/_doctest.py,*/django/core/servers/fastcgi.py,*/django/utils/autoreload.py,*/django/utils/dictconfig.py + +[html] +directory = coverage_html -- cgit v1.3 From a451d2b4a21af062bd1295f71ae62ef770963d4f Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sun, 14 Oct 2012 23:03:01 +0200 Subject: Replaced mentions of Subversion by Git in docs --- docs/internals/contributing/localizing.txt | 2 +- docs/misc/distributions.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/localizing.txt b/docs/internals/contributing/localizing.txt index 263087b5fa..0cde77882c 100644 --- a/docs/internals/contributing/localizing.txt +++ b/docs/internals/contributing/localizing.txt @@ -55,7 +55,7 @@ The format files aren't managed by the use of Transifex. To change them, you must :doc:`create a patch` against the Django source tree, as for any code change: -* Create a diff against the current Subversion trunk. +* Create a diff against the current Git master branch. * Open a ticket in Django's ticket system, set its ``Component`` field to ``Translations``, and attach the patch to it. diff --git a/docs/misc/distributions.txt b/docs/misc/distributions.txt index 729ce0717b..1b324234d1 100644 --- a/docs/misc/distributions.txt +++ b/docs/misc/distributions.txt @@ -11,7 +11,7 @@ requires. Typically, these packages are based on the latest stable release of Django, so if you want to use the development version of Django you'll need to follow the instructions for :ref:`installing the development version -` from our Subversion repository. +` from our Git repository. If you're using Linux or a Unix installation, such as OpenSolaris, check with your distributor to see if they already package Django. If -- cgit v1.3 From 495a8b8107dbd4fb511954bcd2322d125addd94e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 23 Oct 2012 22:25:38 +0200 Subject: Fixed #6527 -- Provided repeatable content access in HttpResponses instantiated with iterators. --- django/http/response.py | 30 +++++++++++++--- docs/internals/deprecation.txt | 4 +++ docs/ref/request-response.txt | 23 +++++++----- docs/releases/1.5.txt | 54 ++++++++++++++++++----------- tests/regressiontests/httpwrappers/tests.py | 31 ++++++++++++++--- 5 files changed, 104 insertions(+), 38 deletions(-) (limited to 'docs/internals') diff --git a/django/http/response.py b/django/http/response.py index 4a5c479419..e9cc3f70a9 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -246,8 +246,18 @@ class HttpResponse(HttpResponseBase): else: __str__ = serialize + def _consume_content(self): + # If the response was instantiated with an iterator, when its content + # is accessed, the iterator is going be exhausted and the content + # loaded in memory. At this point, it's better to abandon the original + # iterator and save the content for later reuse. This is a temporary + # solution. See the comment in __iter__ below for the long term plan. + if self._base_content_is_iter: + self.content = b''.join(self.make_bytes(e) for e in self._container) + @property def content(self): + self._consume_content() return b''.join(self.make_bytes(e) for e in self._container) @content.setter @@ -262,6 +272,17 @@ class HttpResponse(HttpResponseBase): self._base_content_is_iter = False def __iter__(self): + # Raise a deprecation warning only if the content wasn't consumed yet, + # because the response may be intended to be streamed. + # Once the deprecation completes, iterators should be consumed upon + # assignment rather than upon access. The _consume_content method + # should be removed. See #6527. + if self._base_content_is_iter: + warnings.warn( + 'Creating streaming responses with `HttpResponse` is ' + 'deprecated. Use `StreamingHttpResponse` instead ' + 'if you need the streaming behavior.', + PendingDeprecationWarning, stacklevel=2) self._iterator = iter(self._container) return self @@ -277,14 +298,12 @@ class HttpResponse(HttpResponseBase): next = __next__ # Python 2 compatibility def write(self, content): - if self._base_content_is_iter: - raise Exception("This %s instance is not writable" % self.__class__.__name__) + self._consume_content() self._container.append(content) def tell(self): - if self._base_content_is_iter: - raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) - return sum([len(chunk) for chunk in self]) + self._consume_content() + return sum(len(chunk) for chunk in self) class StreamingHttpResponse(HttpResponseBase): @@ -389,6 +408,7 @@ class HttpResponseNotModified(HttpResponse): if value: raise AttributeError("You cannot set content to a 304 (Not Modified) response") self._container = [] + self._base_content_is_iter = False class HttpResponseBadRequest(HttpResponse): diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 6387c87d1d..014ea05a51 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -286,6 +286,10 @@ these changes. * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__`` will be removed (``content_type`` should be used instead). +* When :class:`~django.http.HttpResponse` is instantiated with an iterator, + or when :attr:`~django.http.HttpResponse.content` is set to an iterator, + that iterator will be immediately consumed. + * The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on the User model, will be removed. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index a4b8e9aa66..c3ba99168d 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -569,18 +569,25 @@ Passing iterators Finally, you can pass ``HttpResponse`` an iterator rather than strings. If you use this technique, the iterator should return strings. +Passing an iterator as content to :class:`HttpResponse` creates a +streaming response if (and only if) no middleware accesses the +:attr:`HttpResponse.content` attribute before the response is returned. + .. versionchanged:: 1.5 - Passing an iterator as content to :class:`HttpResponse` creates a - streaming response if (and only if) no middleware accesses the - :attr:`HttpResponse.content` attribute before the response is returned. +This technique is fragile and was deprecated in Django 1.5. If you need the +response to be streamed from the iterator to the client, you should use the +:class:`StreamingHttpResponse` class instead. + +As of Django 1.7, when :class:`HttpResponse` is instantiated with an +iterator, it will consume it immediately, store the response content as a +string, and discard the iterator. - If you want to guarantee that your response will stream to the client, you - should use the new :class:`StreamingHttpResponse` class instead. +.. versionchanged:: 1.5 -If an :class:`HttpResponse` instance has been initialized with an iterator as -its content, you can't use it as a file-like object. Doing so will raise an -exception. +You can now use :class:`HttpResponse` as a file-like object even if it was +instantiated with an iterator. Django will consume and save the content of +the iterator on first access. Setting headers ~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index f7467bc06a..ac61fb363b 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -84,6 +84,8 @@ For one-to-one relationships, both sides can be cached. For many-to-one relationships, only the single side of the relationship can be cached. This is particularly helpful in combination with ``prefetch_related``. +.. _explicit-streaming-responses: + Explicit support for streaming responses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -98,7 +100,7 @@ You can now explicitly generate a streaming response with the new is an iterator. Since :class:`~django.http.StreamingHttpResponse` does not have a ``content`` -attribute, middleware that need access to the response content must test for +attribute, middleware that needs access to the response content must test for streaming responses and behave accordingly. See :ref:`response-middleware` for more information. @@ -483,6 +485,30 @@ Features deprecated in 1.5 .. _simplejson-deprecation: +:setting:`AUTH_PROFILE_MODULE` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +With the introduction of :ref:`custom User models `, there is +no longer any need for a built-in mechanism to store user profile data. + +You can still define user profiles models that have a one-to-one relation with +the User model - in fact, for many applications needing to associate data with +a User account, this will be an appropriate design pattern to follow. However, +the :setting:`AUTH_PROFILE_MODULE` setting, and the +:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing +the user profile model, should not be used any longer. + +Streaming behavior of :class:`HttpResponse` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 1.5 deprecates the ability to stream a response by passing an iterator +to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to +:class:`~django.http.StreamingHttpResponse`. See :ref:`explicit-streaming- +responses` above. + +In Django 1.7 and above, the iterator will be consumed immediately by +:class:`~django.http.HttpResponse`. + ``django.utils.simplejson`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -497,12 +523,6 @@ incompatibilities between versions of :mod:`simplejson` -- see the If you rely on features added to :mod:`simplejson` after it became Python's :mod:`json`, you should import :mod:`simplejson` explicitly. -``itercompat.product`` -~~~~~~~~~~~~~~~~~~~~~~ - -The :func:`~django.utils.itercompat.product` function has been deprecated. Use -the built-in :func:`itertools.product` instead. - ``django.utils.encoding.StrAndUnicode`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -510,6 +530,13 @@ The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated. Define a ``__str__`` method and apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead. +``django.utils.itercompat.product`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~django.utils.itercompat.product` function has been deprecated. Use +the built-in :func:`itertools.product` instead. + + ``django.utils.markup`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -517,16 +544,3 @@ The markup contrib module has been deprecated and will follow an accelerated deprecation schedule. Direct use of python markup libraries or 3rd party tag libraries is preferred to Django maintaining this functionality in the framework. - -:setting:`AUTH_PROFILE_MODULE` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -With the introduction of :ref:`custom User models `, there is -no longer any need for a built-in mechanism to store user profile data. - -You can still define user profiles models that have a one-to-one relation with -the User model - in fact, for many applications needing to associate data with -a User account, this will be an appropriate design pattern to follow. However, -the :setting:`AUTH_PROFILE_MODULE` setting, and the -:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing -the user profile model, should not be used any longer. diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index bfb4ae1fd5..7f61a3074f 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import copy import os import pickle +import warnings from django.core.exceptions import SuspiciousOperation from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, @@ -313,11 +314,17 @@ class HttpResponseTests(unittest.TestCase): r.content = [1, 2, 3] self.assertEqual(r.content, b'123') - #test retrieval explicitly using iter and odd inputs + #test retrieval explicitly using iter (deprecated) and odd inputs r = HttpResponse() r.content = ['1', '2', 3, '\u079e'] - my_iter = r.__iter__() - result = list(my_iter) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + my_iter = iter(r) + self.assertEqual(w[0].category, PendingDeprecationWarning) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", PendingDeprecationWarning) + result = list(my_iter) + self.assertEqual(w[0].category, PendingDeprecationWarning) #'\xde\x9e' == unichr(1950).encode('utf-8') self.assertEqual(result, [b'1', b'2', b'3', b'\xde\x9e']) self.assertEqual(r.content, b'123\xde\x9e') @@ -330,6 +337,16 @@ class HttpResponseTests(unittest.TestCase): self.assertRaises(UnicodeEncodeError, getattr, r, 'content') + # content can safely be accessed multiple times. + r = HttpResponse(iter(['hello', 'world'])) + self.assertEqual(r.content, r.content) + self.assertEqual(r.content, b'helloworld') + + # additional content can be written to the response. + r.write('!') + self.assertEqual(r.content, b'helloworld!') + + def test_file_interface(self): r = HttpResponse() r.write(b"hello") @@ -338,7 +355,9 @@ class HttpResponseTests(unittest.TestCase): self.assertEqual(r.tell(), 17) r = HttpResponse(['abc']) - self.assertRaises(Exception, r.write, 'def') + r.write('def') + self.assertEqual(r.tell(), 6) + self.assertEqual(r.content, b'abcdef') def test_unsafe_redirect(self): bad_urls = [ @@ -447,7 +466,9 @@ class FileCloseTests(TestCase): file1 = open(filename) r = HttpResponse(file1) self.assertFalse(file1.closed) - list(r) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + list(r) self.assertFalse(file1.closed) r.close() self.assertTrue(file1.closed) -- cgit v1.3 From e8b258895adec3d892ada30722517976b298e420 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Thu, 25 Oct 2012 12:02:22 -0700 Subject: Tweaked tense of URL tag changes for clarity --- docs/internals/deprecation.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 014ea05a51..10bbfe1a91 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -141,9 +141,9 @@ these changes. :class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`. * The :ttag:`url` and :ttag:`ssi` template tags will be - modified so that the first argument to each tag is a - template variable, not an implied string. Until then, the new-style - behavior is provided in the ``future`` template tag library. + modified so that the first argument to each tag is a template variable, not + an implied string. In 1.4, this behavior is provided by a version of the tag + in the ``future`` template tag library. * The :djadmin:`reset` and :djadmin:`sqlreset` management commands will be removed. -- cgit v1.3 From 5c143cb340df6825714a6317901b6df03d4ef4b0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 25 Oct 2012 06:51:19 -0400 Subject: Fixed #19180 - Clarified policy regarding older versions of the docs. --- docs/internals/contributing/writing-documentation.txt | 5 +++-- docs/intro/whatsnext.txt | 14 ++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/writing-documentation.txt b/docs/internals/contributing/writing-documentation.txt index c8d7039a68..469f8614b9 100644 --- a/docs/internals/contributing/writing-documentation.txt +++ b/docs/internals/contributing/writing-documentation.txt @@ -30,8 +30,9 @@ If you'd like to start contributing to our docs, get the development version of Django from the source code repository (see :ref:`installing-development-version`). The development version has the latest-and-greatest documentation, just as it has latest-and-greatest code. -Generally, we only revise documentation in the development version, as our -policy is to freeze documentation for existing releases (see +We also backport documentation fixes and improvements, at the discretion of the +committer, to the last release branch. That's because it's highly advantageous +to have the docs for the last release be up-to-date and correct (see :ref:`differences-between-doc-versions`). Getting started with Sphinx diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index ea4b18de03..500a858d47 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -216,15 +216,13 @@ We follow this policy: "New in version X.Y", being X.Y the next release version (hence, the one being developed). -* Documentation for a particular Django release is frozen once the version - has been released officially. It remains a snapshot of the docs as of the - moment of the release. We will make exceptions to this rule in - the case of retroactive security updates or other such retroactive - changes. Once documentation is frozen, we add a note to the top of each - frozen document that says "These docs are frozen for Django version XXX" - and links to the current version of that document. +* Documentation fixes and improvements may be backported to the last release + branch, at the discretion of the committer, however, once a version of + Django is :ref:`no longer supported`, that + version of the docs won't get any further updates. * The `main documentation Web page`_ includes links to documentation for - all previous versions. + all previous versions. Be sure you are using the version of the docs + corresponding to the version of Django you are using! .. _main documentation Web page: https://docs.djangoproject.com/en/dev/ -- cgit v1.3