Repository: pennersr/django-allauth

Issue:
New configuration setting ACCOUNT_SIGNUP_EMAIL_VERIFICATION
Add a configuration settings `ACCOUNT_SIGNUP_EMAIL_VERIFICATION` so when users are signing up, it will let the user type in their email twice to avoid typos.

Similar to preexisting setting `ACCOUNT_SIGNUP_PASSWORD_VERIFICATION`  - When signing up, let the user type in their password twice to avoid typos.

**Use case**:
Users mistype their emails when signing up, sometimes as much as their password. Having them type in their email twice avoids adding bad emails to your system.



Discussion:
@pennersr What are your thoughts on this or accepting such a PR?

so the user copies the emailadress from field one to field two?

@mfa we started using this measure on our site client side and it has significantly decreased invalid emails (measured by decreases in bounces when sending welcome emails).

I agree with you that a lot of people will copy the email address from field one to field two but at least they are giving it a second look. 

This feature is common enough for it to be included out-of-the-box.


Resolving patch:
diff --git a/allauth/account/app_settings.py b/allauth/account/app_settings.py
--- a/allauth/account/app_settings.py
+++ b/allauth/account/app_settings.py
@@ -124,6 +124,13 @@ def UNIQUE_EMAIL(self):
         """
         return self._setting("UNIQUE_EMAIL", True)
 
+    @property
+    def SIGNUP_EMAIL_VERIFICATION(self):
+        """
+        Signup email verification
+        """
+        return self._setting("SIGNUP_EMAIL_VERIFICATION", False)
+
     @property
     def SIGNUP_PASSWORD_VERIFICATION(self):
         """
diff --git a/allauth/account/forms.py b/allauth/account/forms.py
--- a/allauth/account/forms.py
+++ b/allauth/account/forms.py
@@ -29,6 +29,18 @@
     from django.utils.importlib import import_module
 
 
+class PasswordVerificationMixin(object):
+    def clean(self):
+        cleaned_data = super(PasswordVerificationMixin, self).clean()
+        password1 = cleaned_data.get('password1', None)
+        password2 = cleaned_data.get('password2', None)
+        if (password1 and password2) and password1 != password2:
+            self.add_error(
+                'password2', _("You must type the same password each time.")
+            )
+        return cleaned_data
+
+
 class PasswordField(forms.CharField):
 
     def __init__(self, *args, **kwargs):
@@ -238,17 +250,40 @@ def __init__(self, *args, **kwargs):
 
         # field order may contain additional fields from our base class,
         # so take proper care when reordering...
+        self.email_field = 'email'
         field_order = ['email', 'username']
+        if app_settings.SIGNUP_EMAIL_VERIFICATION:
+            del self.fields["email"]
+            self.fields["email1"] = forms.EmailField(
+                widget=forms.TextInput(
+                    attrs={
+                        'type': 'email', 'placeholder': _('E-mail address')
+                    }
+                )
+            )
+            self.fields["email2"] = forms.EmailField(
+                widget=forms.TextInput(
+                    attrs={
+                        'type': 'email',
+                        'placeholder': _('E-mail address confirmation')
+                    }
+                )
+            )
+            field_order = ['email1', 'email2', 'username']
+            self.email_field = 'email1'
         merged_field_order = list(self.fields.keys())
         if email_required:
-            self.fields["email"].label = ugettext("E-mail")
-            self.fields["email"].required = True
+            self.fields[self.email_field].label = ugettext("E-mail")
+            self.fields[self.email_field].required = True
         else:
-            self.fields["email"].label = ugettext("E-mail (optional)")
-            self.fields["email"].required = False
-            self.fields["email"].widget.is_required = False
+            self.fields[self.email_field].label = ugettext("E-mail (optional)")
+            self.fields[self.email_field].required = False
+            self.fields[self.email_field].widget.is_required = False
             if self.username_required:
-                field_order = ['username', 'email']
+                if app_settings.SIGNUP_EMAIL_VERIFICATION:
+                    field_order = ['username', 'email1', 'email2']
+                else:
+                    field_order = ['username', 'email']
 
         # Merge our email and username fields in if they are not
         # currently in the order.  This is to allow others to
@@ -268,12 +303,23 @@ def clean_username(self):
         return value
 
     def clean_email(self):
-        value = self.cleaned_data["email"]
+        value = self.cleaned_data[self.email_field]
         value = get_adapter().clean_email(value)
         if value and app_settings.UNIQUE_EMAIL:
             value = get_adapter().validate_unique_email(value)
         return value
 
+    def clean(self):
+        cleaned_data = super(BaseSignupForm, self).clean()
+        if app_settings.SIGNUP_EMAIL_VERIFICATION:
+            email1 = cleaned_data.get('email1', None)
+            email2 = cleaned_data.get('email2', None)
+            if (email1 and email2) and email1 != email2:
+                self.add_error(
+                    'email2', _("You must type the same email each time.")
+                )
+        return cleaned_data
+
     def custom_signup(self, request, user):
         custom_form = super(BaseSignupForm, self)
         if hasattr(custom_form, 'signup') and callable(custom_form.signup):
@@ -381,7 +427,7 @@ def save(self, request):
                                               confirm=True)
 
 
-class ChangePasswordForm(UserForm):
+class ChangePasswordForm(PasswordVerificationMixin, UserForm):
 
     oldpassword = PasswordField(label=_("Current Password"))
     password1 = SetPasswordField(label=_("New Password"))
@@ -397,20 +443,11 @@ def clean_oldpassword(self):
                                           " password."))
         return self.cleaned_data["oldpassword"]
 
-    def clean_password2(self):
-        if ("password1" in self.cleaned_data and
-                "password2" in self.cleaned_data):
-            if (self.cleaned_data["password1"] !=
-                    self.cleaned_data["password2"]):
-                raise forms.ValidationError(_("You must type the same password"
-                                              " each time."))
-        return self.cleaned_data["password2"]
-
     def save(self):
         get_adapter().set_password(self.user, self.cleaned_data["password1"])
 
 
-class SetPasswordForm(UserForm):
+class SetPasswordForm(PasswordVerificationMixin, UserForm):
 
     password1 = SetPasswordField(label=_("Password"))
     password2 = PasswordField(label=_("Password (again)"))
@@ -419,15 +456,6 @@ def __init__(self, *args, **kwargs):
         super(SetPasswordForm, self).__init__(*args, **kwargs)
         self.fields['password1'].user = self.user
 
-    def clean_password2(self):
-        if ("password1" in self.cleaned_data and
-                "password2" in self.cleaned_data):
-            if (self.cleaned_data["password1"] !=
-                    self.cleaned_data["password2"]):
-                raise forms.ValidationError(_("You must type the same password"
-                                              " each time."))
-        return self.cleaned_data["password2"]
-
     def save(self):
         get_adapter().set_password(self.user, self.cleaned_data["password1"])
 
@@ -495,7 +523,7 @@ def deprecated_site():
         return self.cleaned_data["email"]
 
 
-class ResetPasswordKeyForm(forms.Form):
+class ResetPasswordKeyForm(PasswordVerificationMixin, forms.Form):
 
     password1 = SetPasswordField(label=_("New Password"))
     password2 = PasswordField(label=_("New Password (again)"))
@@ -506,16 +534,6 @@ def __init__(self, *args, **kwargs):
         super(ResetPasswordKeyForm, self).__init__(*args, **kwargs)
         self.fields['password1'].user = self.user
 
-    # FIXME: Inspecting other fields -> should be put in def clean(self) ?
-    def clean_password2(self):
-        if ("password1" in self.cleaned_data and
-                "password2" in self.cleaned_data):
-            if (self.cleaned_data["password1"] != self.cleaned_data[
-                    "password2"]):
-                raise forms.ValidationError(_("You must type the same"
-                                              " password each time."))
-        return self.cleaned_data["password2"]
-
     def save(self):
         get_adapter().set_password(self.user, self.cleaned_data["password1"])
 
diff --git a/allauth/socialaccount/providers/eveonline/provider.py b/allauth/socialaccount/providers/eveonline/provider.py
--- a/allauth/socialaccount/providers/eveonline/provider.py
+++ b/allauth/socialaccount/providers/eveonline/provider.py
@@ -7,12 +7,12 @@
 class EveOnlineAccount(ProviderAccount):
     def get_profile_url(self):
         return 'https://gate.eveonline.com/Profile/{char_name}'.format(
-                char_name=self.account.extra_data.get('CharacterName'))
+            char_name=self.account.extra_data.get('CharacterName'))
 
     def get_avatar_url(self):
         return ('https://image.eveonline.com/Character/'
                 '{char_id}_128.jpg').format(
-                char_id=self.account.extra_data.get('CharacterID', 1))
+                    char_id=self.account.extra_data.get('CharacterID', 1))
 
     def to_str(self):
         dflt = super(EveOnlineAccount, self).to_str()


Test patch:
diff --git a/allauth/account/tests.py b/allauth/account/tests.py
--- a/allauth/account/tests.py
+++ b/allauth/account/tests.py
@@ -787,6 +787,29 @@ def test_username_maxlength(self):
         widget = field.widget
         self.assertEqual(widget.attrs.get('maxlength'), str(max_length))
 
+    @override_settings(
+        ACCOUNT_USERNAME_REQUIRED=True,
+        ACCOUNT_SIGNUP_EMAIL_VERIFICATION=True)
+    def test_signup_email_verification(self):
+        data = {
+            'username': 'username',
+            'email': 'user@example.com',
+        }
+        form = BaseSignupForm(data, email_required=True)
+        self.assertFalse(form.is_valid())
+
+        data = {
+            'username': 'username',
+            'email1': 'user@example.com',
+            'email2': 'user@example.com',
+        }
+        form = BaseSignupForm(data, email_required=True)
+        self.assertTrue(form.is_valid())
+
+        data['email2'] = 'anotheruser@example.com'
+        form = BaseSignupForm(data, email_required=True)
+        self.assertFalse(form.is_valid())
+
 
 class AuthenticationBackendTests(TestCase):