From dabe3628362ab7a4a6c9686dd874803baa997eaa Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 4 Jun 2012 20:10:51 +0800 Subject: [PATCH] Modified auth management commands to handle custom user definitions. --- django/contrib/admin/views/decorators.py | 1 + django/contrib/auth/forms.py | 10 +- django/contrib/auth/management/__init__.py | 27 ++-- .../management/commands/changepassword.py | 16 ++- .../management/commands/createsuperuser.py | 122 +++++++++--------- django/contrib/auth/models.py | 58 ++++++--- django/contrib/auth/tests/__init__.py | 37 ++---- django/contrib/auth/tests/basic.py | 53 +------- django/contrib/auth/tests/custom_user.py | 78 +++++++++++ django/contrib/auth/tests/management.py | 96 +++++++++++++- django/contrib/auth/tokens.py | 1 + django/core/exceptions.py | 13 +- django/core/validators.py | 31 +++-- 13 files changed, 357 insertions(+), 186 deletions(-) create mode 100644 django/contrib/auth/tests/custom_user.py 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/forms.py b/django/contrib/auth/forms.py index f0ef124b20..2cb7da0772 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -64,16 +64,16 @@ class UserCreationForm(forms.ModelForm): } username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^[\w.@+-]+$', - help_text = _("Required. 30 characters or fewer. Letters, digits and " + help_text=_("Required. 30 characters or fewer. Letters, digits and " "@/./+/-/_ only."), - error_messages = { + error_messages={ 'invalid': _("This value may contain only letters, numbers and " "@/./+/-/_ characters.")}) password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, - help_text = _("Enter the same password as above, for verification.")) + help_text=_("Enter the same password as above, for verification.")) class Meta: model = User @@ -108,9 +108,9 @@ class UserCreationForm(forms.ModelForm): class UserChangeForm(forms.ModelForm): username = forms.RegexField( label=_("Username"), max_length=30, regex=r"^[\w.@+-]+$", - help_text = _("Required. 30 characters or fewer. Letters, digits and " + help_text=_("Required. 30 characters or fewer. Letters, digits and " "@/./+/-/_ only."), - error_messages = { + error_messages={ 'invalid': _("This value may contain only letters, numbers and " "@/./+/-/_ characters.")}) password = ReadOnlyPasswordHashField(label=_("Password"), diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 66f54f18a8..8287c97d58 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -4,9 +4,11 @@ Creates permissions for all installed apps that need permissions. import getpass import locale import unicodedata + from django.contrib.auth import models as auth_app +from django.core import exceptions from django.db.models import get_models, signals -from django.contrib.auth.models import User +from django.contrib.auth.models import User, get_user_model def _get_permission_codename(action, opts): @@ -60,7 +62,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): ") @@ -100,16 +104,24 @@ 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 User._meta.swapped: + return '' + default_username = get_system_username() try: default_username = unicodedata.normalize('NFKD', default_username)\ .encode('ascii', 'ignore').replace(' ', '').lower() except UnicodeDecodeError: return '' - if not RE_VALID_USERNAME.match(default_username): + + # Run the username validator + try: + 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: @@ -120,8 +132,7 @@ def get_default_username(check_db=True): 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..2a8a19e6cb 100644 --- a/django/contrib/auth/management/commands/changepassword.py +++ b/django/contrib/auth/management/commands/changepassword.py @@ -2,7 +2,7 @@ import getpass from optparse import make_option from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import User +from django.contrib.auth.models import get_user_model 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 f3f1a7b671..ffe248753a 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -3,108 +3,113 @@ 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.management import get_default_username +from django.contrib.auth.models import get_user_model from django.core import exceptions from django.core.management.base import BaseCommand, CommandError from django.db import DEFAULT_DB_ALIAS -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 getattr(get_user_model(), 'REQUIRED_FIELDS', ['email']) ) + 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 = getattr(UserModel, 'REQUIRED_FIELDS', ['email']) # 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, 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 = raw_input(input_msg + ': ') - if default_username and username == '': + raw_value = raw_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, 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 = raw_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] = None + while other_data[field_name] is None: + raw_value = raw_input(capfirst(field.verbose_name + ': ')) + try: + other_data[field_name] = field.clean(raw_value, None) + except exceptions.ValidationError, 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): ') @@ -116,12 +121,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 ec2ee19d3b..62bf68afbc 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,7 +1,10 @@ +import re import urllib +from django.conf import settings 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 @@ -128,7 +131,7 @@ class Group(models.Model): return (self.name,) -class UserManager(models.Manager): +class BaseUserManager(models.Manager): @classmethod def normalize_email(cls, email): @@ -145,6 +148,24 @@ class UserManager(models.Manager): email = '@'.join([email_name, domain_part.lower()]) return email + 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, 'USERNAME_FIELD', 'username'): username}) + + +class UserManager(BaseUserManager): + def create_user(self, username, email=None, password=None): """ Creates and saves a User with the given username, email and password. @@ -169,21 +190,6 @@ class UserManager(models.Manager): 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): @@ -219,6 +225,7 @@ def _user_has_module_perms(user, app_label): class AbstractBaseUser(models.Model): password = models.CharField(_('password'), max_length=128) + last_login = models.DateTimeField(_('last login'), default=timezone.now) class Meta: abstract = True @@ -264,6 +271,12 @@ class AbstractBaseUser(models.Model): raise NotImplementedError() +def get_user_model(): + "Return the User model that is active in this project" + app_label, model_name = settings.AUTH_USER_MODEL.split('.') + return models.get_model(app_label, model_name) + + class User(AbstractBaseUser): """ Users within the Django authentication system are represented by this @@ -273,10 +286,13 @@ class User(AbstractBaseUser): """ 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.@+-]+$'), _(u'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) + 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.')) @@ -286,7 +302,6 @@ class User(AbstractBaseUser): 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 ' @@ -295,6 +310,7 @@ class User(AbstractBaseUser): user_permissions = models.ManyToManyField(Permission, verbose_name=_('user permissions'), blank=True, help_text='Specific permissions for this user.') + objects = UserManager() class Meta: @@ -318,6 +334,10 @@ class User(AbstractBaseUser): full_name = u'%s %s' % (self.first_name, self.last_name) return full_name.strip() + def get_short_name(self): + "Returns the short name for the user." + return self.first_name + def get_group_permissions(self, obj=None): """ Returns a list of permission strings that this user has through his/her 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/basic.py b/django/contrib/auth/tests/basic.py index 4c2e8cd8aa..3b3cd103f3 100644 --- a/django/contrib/auth/tests/basic.py +++ b/django/contrib/auth/tests/basic.py @@ -1,14 +1,8 @@ from django.test import TestCase -from django.utils.unittest import skipUnless from django.contrib.auth.models import User, AnonymousUser from django.core.management import call_command from StringIO import StringIO -try: - import crypt as crypt_module -except ImportError: - crypt_module = None - class BasicTestCase(TestCase): def test_user(self): @@ -35,7 +29,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): @@ -66,48 +60,3 @@ class BasicTestCase(TestCase): self.assertTrue(super.is_superuser) self.assertTrue(super.is_active) self.assertTrue(super.is_staff) - - def test_createsuperuser_management_command(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()) - - # 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()) - - - 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()) diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py new file mode 100644 index 0000000000..775a4fed24 --- /dev/null +++ b/django/contrib/auth/tests/custom_user.py @@ -0,0 +1,78 @@ +# 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_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 + + @property + def is_active(self): + return True diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 81ab0aa052..20f4cde8f9 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -1,9 +1,14 @@ +from datetime import date from StringIO import StringIO 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.core.management import call_command from django.core.management.base import CommandError from django.test import TestCase +from django.test.utils import override_settings class GetDefaultUsernameTestCase(TestCase): @@ -43,7 +48,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' @@ -64,3 +69,92 @@ class ChangepasswordManagementCommandTestCase(TestCase): with self.assertRaises(CommandError): command.execute("joe", stdout=self.stdout, stderr=self.stderr) + + +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/tokens.py b/django/contrib/auth/tokens.py index fcc8c94011..9c3e9aefee 100644 --- a/django/contrib/auth/tokens.py +++ b/django/contrib/auth/tokens.py @@ -3,6 +3,7 @@ from django.conf import settings from django.utils.http import int_to_base36, base36_to_int from django.utils.crypto import constant_time_compare, salted_hmac + class PasswordResetTokenGenerator(object): """ Strategy object used to generate and check tokens for the password diff --git a/django/core/exceptions.py b/django/core/exceptions.py index e3d1dc9c7e..4856ada911 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/validators.py b/django/core/validators.py index 3d4bcc86c8..02ed4fb86a 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -9,6 +9,7 @@ from django.utils.ipv6 import is_valid_ipv6_address # These values, if given to validate(), will trigger the self.required check. EMPTY_VALUES = (None, '', [], (), {}) + class RegexValidator(object): regex = '' message = _(u'Enter a valid value.') @@ -33,13 +34,14 @@ class RegexValidator(object): if not self.regex.search(smart_unicode(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 ip - 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 ip + r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) def __call__(self, value): @@ -51,8 +53,8 @@ class URLValidator(RegexValidator): value = smart_unicode(value) scheme, netloc, path, query, fragment = urlparse.urlsplit(value) try: - netloc = netloc.encode('idna') # IDN -> ACE - except UnicodeError: # invalid domain part + netloc = netloc.encode('idna') # IDN -> ACE + except UnicodeError: # invalid domain part raise e url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) super(URLValidator, self).__call__(url) @@ -68,6 +70,7 @@ def validate_integer(value): except (ValueError, TypeError): raise ValidationError('') + class EmailValidator(RegexValidator): def __call__(self, value): @@ -99,10 +102,12 @@ validate_slug = RegexValidator(slug_re, _(u"Enter a valid 'slug' consisting of l 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, _(u'Enter a valid IPv4 address.'), 'invalid') + def validate_ipv6_address(value): if not is_valid_ipv6_address(value): raise ValidationError(_(u'Enter a valid IPv6 address.'), code='invalid') + def validate_ipv46_address(value): try: validate_ipv4_address(value) @@ -118,6 +123,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 @@ -140,7 +146,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 = _(u'Ensure this value is %(limit_value)s (it is %(show_value)s).') code = 'limit_value' @@ -157,25 +163,28 @@ class BaseValidator(object): params=params, ) + class MaxValueValidator(BaseValidator): compare = lambda self, a, b: a > b message = _(u'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 = _(u'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 = _(u'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 = _(u'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).') code = 'max_length' -