Repository: wagtail/wagtail

Issue:
Error in Django1.8 and custom user model
I'm using a custom user model with no username field in wagtail 1.0b2. In Django 1.7 wagtail worked fine except the user editing code (which I don't really need) 

In Django 1.8  I get the error:

```
Unknown field(s) (username) specified for User
```

My User Model:

```
class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('Email address'), unique=True)
   (...)
    USERNAME_FIELD = 'email'

```

Traceback:

```
Traceback:
File "/virtualenvs/sswmainvenv/local/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
  119.                 resolver_match = resolver.resolve(request.path_info)
File "/virtualenvs/sswmainvenv/local/lib/python2.7/site-packages/django/core/urlresolvers.py" in resolve
  366.             for pattern in self.url_patterns:
File "/virtualenvs/sswmainvenv/local/lib/python2.7/site-packages/django/core/urlresolvers.py" in url_patterns
  402.         patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
File "/virtualenvs/sswmainvenv/local/lib/python2.7/site-packages/django/core/urlresolvers.py" in urlconf_module
  396.             self._urlconf_module = import_module(self.urlconf_name)
File "/usr/lib/python2.7/importlib/__init__.py" in import_module
  37.     __import__(name)
File "/workspace/sswmain/src/sswmain/urls.py" in <module>
  7. from wagtail.wagtailadmin import urls as wagtailadmin_urls
File "/workspace/sswmain/src/wagtail/wagtailadmin/urls.py" in <module>
  7. from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar, page_privacy
File "/workspace/sswmain/src/wagtail/wagtailadmin/views/account.py" in <module>
  12. from wagtail.wagtailusers.forms import NotificationPreferencesForm
File "/workspace/sswmain/src/wagtail/wagtailusers/forms.py" in <module>
  18. class UserCreationForm(BaseUserCreationForm):
File "/virtualenvs/sswmainvenv/local/lib/python2.7/site-packages/django/forms/models.py" in __new__
  295.                 raise FieldError(message)

Exception Type: FieldError at /ssw-admin/dashboard/
Exception Value: Unknown field(s) (username) specified for User
```



Discussion:
#1380 


Resolving patch:
diff --git a/wagtail/wagtailusers/forms.py b/wagtail/wagtailusers/forms.py
--- 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 @@
 
 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'])
+
+
+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
+
 
-# extend Django's UserCreationForm with an 'is_superuser' field
-class UserCreationForm(BaseUserCreationForm):
+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 @@ def clean_username(self):
             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 @@ def save(self, commit=True):
 
 # 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 @@ def clean_username(self):
         # 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'])


Test patch:
diff --git a/wagtail/tests/customuser/migrations/0001_initial.py b/wagtail/tests/customuser/migrations/0001_initial.py
--- 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
--- 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 @@ def get_full_name(self):
 
     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)