diff --git a/wagtail/tests/customuser/migrations/0001_initial.py b/wagtail/tests/customuser/migrations/0001_initial.py index a09a9251cc..65456fad4a 100644 --- a/wagtail/tests/customuser/migrations/0001_initial.py +++ b/wagtail/tests/customuser/migrations/0001_initial.py @@ -46,4 +46,29 @@ class Migration(migrations.Migration): }, bases=(models.Model,), ), + migrations.CreateModel( + name='EmailUser', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('password', models.CharField(max_length=128, verbose_name='password')), + ( + 'last_login', + models.DateTimeField(null=True, verbose_name='last login', blank=True) + if django.VERSION >= (1, 8) else + models.DateTimeField(default=django.utils.timezone.now, verbose_name='last login') + ), + ('email', models.EmailField(unique=True, max_length=255)), + ('is_staff', models.BooleanField(default=True)), + ('is_active', models.BooleanField(default=True)), + ('first_name', models.CharField(max_length=50, blank=True)), + ('last_name', models.CharField(max_length=50, blank=True)), + ('is_superuser', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(related_name='+', to='auth.Group', blank=True)), + ('user_permissions', models.ManyToManyField(related_name='+', to='auth.Permission', blank=True)), + ], + options={ + 'abstract': False, + }, + bases=(models.Model,), + ), ] diff --git a/wagtail/tests/customuser/models.py b/wagtail/tests/customuser/models.py index cd09fa6e38..5b6a0c4155 100644 --- a/wagtail/tests/customuser/models.py +++ b/wagtail/tests/customuser/models.py @@ -1,6 +1,9 @@ +import sys + from django.db import models -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager +from django.contrib.auth.models import ( + Group, Permission, AbstractBaseUser, PermissionsMixin, BaseUserManager) class CustomUserManager(BaseUserManager): @@ -37,6 +40,7 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): last_name = models.CharField(max_length=50, blank=True) USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] objects = CustomUserManager() @@ -45,3 +49,61 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): def get_short_name(self): return self.first_name + + +class EmailUserManager(BaseUserManager): + def _create_user(self, email, password, + is_staff, is_superuser, **extra_fields): + """ + Creates and saves a User with the given email and password. + """ + email = self.normalize_email(email) + user = self.model(email=email, is_staff=is_staff, is_active=True, + is_superuser=is_superuser, **extra_fields) + user.set_password(password) + user.save(using=self._db) + return user + + def create_user(self, email=None, password=None, **extra_fields): + return self._create_user(email, password, False, False, + **extra_fields) + + def create_superuser(self, email, password, **extra_fields): + return self._create_user(email, password, True, True, + **extra_fields) + + +class EmailUser(AbstractBaseUser): + # Cant inherit from PermissionsMixin because of clashes with + # groups/user_permissions related_names. + email = models.EmailField(max_length=255, unique=True) + is_staff = models.BooleanField(default=True) + is_active = models.BooleanField(default=True) + first_name = models.CharField(max_length=50, blank=True) + last_name = models.CharField(max_length=50, blank=True) + + is_superuser = models.BooleanField(default=False) + groups = models.ManyToManyField(Group, related_name='+', blank=True) + user_permissions = models.ManyToManyField(Permission, related_name='+', blank=True) + + USERNAME_FIELD = 'email' + + objects = EmailUserManager() + + def get_full_name(self): + return self.first_name + ' ' + self.last_name + + def get_short_name(self): + return self.first_name + + +def steal_method(name): + func = getattr(PermissionsMixin, name) + if sys.version_info < (3,): + func = func.__func__ + setattr(EmailUser, name, func) + +methods = ['get_group_permissions', 'get_all_permissions', 'has_perm', + 'has_perms', 'has_module_perms'] +for method in methods: + steal_method(method) diff --git a/wagtail/wagtailusers/forms.py b/wagtail/wagtailusers/forms.py index 3fc1b3096e..304655de27 100644 --- a/wagtail/wagtailusers/forms.py +++ b/wagtail/wagtailusers/forms.py @@ -1,7 +1,6 @@ from django import forms -from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm -from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model +from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import Group, Permission from django.forms.models import inlineformset_factory @@ -13,9 +12,37 @@ from wagtail.wagtailcore.models import UserPagePermissionsProxy, GroupPagePermis User = get_user_model() +# The standard fields each user model is expected to have, as a minimum. +standard_fields = set(['email', 'first_name', 'last_name', 'is_superuser', 'groups']) -# extend Django's UserCreationForm with an 'is_superuser' field -class UserCreationForm(BaseUserCreationForm): + +class UsernameForm(forms.ModelForm): + """ + Intelligently sets up the username field if it is infact a username. If the + User model has been swapped out, and the username field is an email or + something else, dont touch it. + """ + def __init__(self, *args, **kwargs): + super(UsernameForm, self).__init__(*args, **kwargs) + if User.USERNAME_FIELD == 'username': + field = self.fields['username'] + field.regex = r"^[\w.@+-]+$" + field.help_text = _("Required. 30 characters or fewer. Letters, " + "digits and @/./+/-/_ only.") + field.error_messages = field.error_messages.copy() + field.error_messages.update({ + 'invalid': _("This value may contain only letters, numbers " + "and @/./+/-/_ characters.")}) + + @property + def username_field(self): + return self[User.USERNAME_FIELD] + + def separate_username_field(self): + return User.USERNAME_FIELD not in standard_fields + + +class UserCreationForm(UsernameForm): required_css_class = "required" is_superuser = forms.BooleanField( @@ -24,26 +51,32 @@ class UserCreationForm(BaseUserCreationForm): help_text=_("If ticked, this user has the ability to manage user accounts.") ) + password1 = forms.CharField( + label=_("Password"), + required=False, + widget=forms.PasswordInput, + help_text=_("Leave blank if not changing.")) + password2 = forms.CharField( + label=_("Password confirmation"), required=False, + widget=forms.PasswordInput, + help_text=_("Enter the same password as above, for verification.")) + email = forms.EmailField(required=True, label=_("Email")) first_name = forms.CharField(required=True, label=_("First Name")) last_name = forms.CharField(required=True, label=_("Last Name")) class Meta: model = User - fields = ("username", "email", "first_name", "last_name", "is_superuser", "groups") + fields = set([User.USERNAME_FIELD]) | standard_fields widgets = { 'groups': forms.CheckboxSelectMultiple } def clean_username(self): - # Method copied from parent - - username = self.cleaned_data["username"] + username_field = User.USERNAME_FIELD + username = self.cleaned_data[username_field] try: - # When called from BaseUserCreationForm, the method fails if using a AUTH_MODEL_MODEL, - # This is because the following line tries to perform a lookup on - # the default "auth_user" table. - User._default_manager.get(username=username) + User._default_manager.get(**{username_field: username}) except User.DoesNotExist: return username raise forms.ValidationError( @@ -51,8 +84,19 @@ class UserCreationForm(BaseUserCreationForm): code='duplicate_username', ) + def clean_password2(self): + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + def save(self, commit=True): user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) # users can access django-admin iff they are a superuser user.is_staff = user.is_superuser @@ -65,21 +109,13 @@ class UserCreationForm(BaseUserCreationForm): # Largely the same as django.contrib.auth.forms.UserCreationForm, but with enough subtle changes # (to make password non-required) that it isn't worth inheriting... -class UserEditForm(forms.ModelForm): +class UserEditForm(UsernameForm): required_css_class = "required" error_messages = { 'duplicate_username': _("A user with that username already exists."), 'password_mismatch': _("The two password fields didn't match."), } - username = forms.RegexField( - label=_("Username"), - max_length=30, - regex=r'^[\w.@+-]+$', - help_text=_("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."), - error_messages={ - 'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.") - }) email = forms.EmailField(required=True, label=_("Email")) first_name = forms.CharField(required=True, label=_("First Name")) @@ -103,7 +139,7 @@ class UserEditForm(forms.ModelForm): class Meta: model = User - fields = ("username", "email", "first_name", "last_name", "is_active", "is_superuser", "groups") + fields = set([User.USERNAME_FIELD, "is_active"]) | standard_fields widgets = { 'groups': forms.CheckboxSelectMultiple } @@ -112,8 +148,10 @@ class UserEditForm(forms.ModelForm): # Since User.username is unique, this check is redundant, # but it sets a nicer error message than the ORM. See #13147. username = self.cleaned_data["username"] + username_field = User.USERNAME_FIELD try: - User._default_manager.exclude(id=self.instance.id).get(username=username) + User._default_manager.exclude(id=self.instance.id).get(**{ + username_field: username}) except User.DoesNotExist: return username raise forms.ValidationError(self.error_messages['duplicate_username']) diff --git a/wagtail/wagtailusers/templates/wagtailusers/users/create.html b/wagtail/wagtailusers/templates/wagtailusers/users/create.html index 02e7ecec1a..93ed45c704 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/users/create.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/create.html @@ -18,7 +18,9 @@ {% csrf_token %}