Repository: mdn/kuma

Issue:
T - Remove option to *change* email in sign up form
Basically, Option 1 in https://docs.google.com/document/d/17FUhSfPQ5uPyA5VvctYFZZ0tPhc205gr4nZM8Nsu2KI/edit#

* No "change username" hyperlink.
* No `<input>` elements at all for email. Hidden or could-be hidden. 
* No form validation of email at all. 
* Remove the last two words on the paragraph line above "... choose username and email." to "... choose username."

Notes-to-self;

* There might be a slight conflict with the necessary work on https://github.com/mdn/kuma/issues/6541 since the form, before save, should display the *correct* email in the form-preview.
* Working on this issue, might "automatically" cover all of https://github.com/mdn/kuma/issues/6513 too. 


Discussion:
More note-to-self;
Completeting this will free up the draft of https://github.com/mdn/kuma/pull/6530 since it now doesn't need to worry itself with the email which was the blocker of that PR progress.
Also, the for-loop in https://github.com/mdn/kuma/pull/6496 will not be necessary any more. 
Hi @peterbe - Can you confirm an estimated for this task? Thanks! 

Resolving patch:
diff --git a/kuma/users/signup.py b/kuma/users/signup.py
--- a/kuma/users/signup.py
+++ b/kuma/users/signup.py
@@ -24,18 +24,9 @@ class SignupForm(BaseSignupForm):
     This overrides the default error messages for the username form field
     with our own strings.
 
-    It has an additional other_email form field to handle the case of GitHub
-    which may deliver a number of emails for users to choose from upon signup.
-
     The heavy lifting happens in the view.
     """
 
-    email = forms.CharField(
-        required=False, widget=forms.TextInput(attrs={"type": "email"})
-    )
-    other_email = forms.CharField(
-        required=False, widget=forms.TextInput(attrs={"type": "email"})
-    )
     terms = forms.BooleanField(
         label=_("I agree"), required=True, error_messages={"required": TERMS_REQUIRED}
     )
@@ -43,12 +34,10 @@ class SignupForm(BaseSignupForm):
         label=_("I would like to make my GitHub profile URL public"), required=False
     )
     is_newsletter_subscribed = forms.BooleanField(required=False)
-    other_email_value = "_other"
     duplicate_email_error_label = "_duplicate_email"
 
     def __init__(self, *args, **kwargs):
         super(SignupForm, self).__init__(*args, **kwargs)
-        self.other_email_used = False
         self.fields["username"].error_messages = {
             "required": USERNAME_REQUIRED,
             "min_length": USERNAME_SHORT,
@@ -57,39 +46,8 @@ def __init__(self, *args, **kwargs):
 
     def clean_email(self):
         value = self.cleaned_data["email"]
-        # if the selected email address is "other" we cut things short
-        # and clean the value in the form's clean method instead
-        if value == self.other_email_value:
-            return value
-
-        # otherwise we emmulate the functionality of the EmailField here
-        # stripping whitespaces, validating the content and then
-        # run allauth's own email value cleanup
-        self.cleaned_data["email"] = value.strip()
         validators.validate_email(value)
         return super(SignupForm, self).clean_email()
 
-    def clean(self):
-        """
-        Cleans the email field data given the other email address field
-        if given.
-        """
-        cleaned_data = super(SignupForm, self).clean()
-        # let's see if the email value was "other"
-        if cleaned_data.get("email") == self.other_email_value:
-            # and set the cleaned data to the cleaned other_email value
-            self.cleaned_data["email"] = self.cleaned_data["other_email"]
-            # also store the fact of using the other value in an attribute
-            # to be used in the view to check for it
-            self.other_email_used = True
-            # then run the usual email clean method again to apply
-            # the regular email validation and put the error into the
-            # email field specific value
-            try:
-                self.clean_email()
-            except forms.ValidationError as e:
-                self._errors["email"] = self.error_class(e.messages)
-        return cleaned_data
-
     def raise_duplicate_email_error(self):
         raise forms.ValidationError(self.duplicate_email_error_label)
diff --git a/kuma/users/views.py b/kuma/users/views.py
--- a/kuma/users/views.py
+++ b/kuma/users/views.py
@@ -9,7 +9,6 @@
 from allauth.socialaccount.views import ConnectionsView
 from allauth.socialaccount.views import SignupView as BaseSignupView
 from constance import config
-from django import forms
 from django.conf import settings
 from django.contrib.auth import get_user_model, login
 from django.contrib.auth.decorators import permission_required
@@ -660,22 +659,6 @@ def get_form(self, form_class=None):
                     "verified": False,
                     "primary": False,
                 }
-            choices = []
-            verified_emails = []
-            for email_data in self.email_addresses.values():
-                email_address = email_data["email"]
-                if email_data["verified"]:
-                    verified_emails.append(email_address)
-                choices.append((email_address, email_address))
-            if extra_email_addresses:
-                choices.append((form.other_email_value, _("Other:")))
-            else:
-                choices.append((form.other_email_value, _("Email:")))
-            email_select = forms.RadioSelect(choices=choices, attrs={"id": "email"})
-            form.fields["email"].widget = email_select
-            form.initial.update(email=choices[0])
-            if not email and len(verified_emails) == 1:
-                form.initial.update(email=verified_emails[0])
         return form
 
     def form_valid(self, form):
@@ -686,28 +669,30 @@ def form_valid(self, form):
         We send our welcome email via celery during complete_signup.
         So, we need to manually commit the user to the db for it.
         """
+
         selected_email = form.cleaned_data["email"]
-        if form.other_email_used:
+        if selected_email in self.email_addresses:
+            data = self.email_addresses[selected_email]
+        elif selected_email == self.default_email:
             data = {
                 "email": selected_email,
-                "verified": False,
+                "verified": True,
                 "primary": True,
             }
         else:
-            data = self.email_addresses.get(selected_email, None)
+            return HttpResponseBadRequest("email not a valid choice")
 
-        if data:
-            primary_email_address = EmailAddress(
-                email=data["email"], verified=data["verified"], primary=True
-            )
-            form.sociallogin.email_addresses = self.sociallogin.email_addresses = [
-                primary_email_address
-            ]
-            if data["verified"]:
-                # we have to stash the selected email address here
-                # so that no email verification is sent again
-                # this is done by adding the email address to the session
-                get_adapter().stash_verified_email(self.request, data["email"])
+        primary_email_address = EmailAddress(
+            email=data["email"], verified=data["verified"], primary=True
+        )
+        form.sociallogin.email_addresses = self.sociallogin.email_addresses = [
+            primary_email_address
+        ]
+        if data["verified"]:
+            # we have to stash the selected email address here
+            # so that no email verification is sent again
+            # this is done by adding the email address to the session
+            get_adapter().stash_verified_email(self.request, data["email"])
 
         with transaction.atomic():
             form.save(self.request)
@@ -717,13 +702,12 @@ def form_invalid(self, form):
         """
         This is called on POST but only when the form is invalid. We're
         overriding this method simply to send GA events when we find an
-        error in the username and/or email fields.
+        error in the username field.
         """
-        for name in ("username", "email"):
-            if form.errors.get(name) is not None:
-                track_event(
-                    CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, name,
-                )
+        if form.errors.get("username") is not None:
+            track_event(
+                CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username",
+            )
         return super().form_invalid(form)
 
     def get_context_data(self, **kwargs):


Test patch:
diff --git a/kuma/users/tests/test_templates.py b/kuma/users/tests/test_templates.py
--- a/kuma/users/tests/test_templates.py
+++ b/kuma/users/tests/test_templates.py
@@ -136,10 +136,10 @@ def test_auth_success_username_available(self):
         parsed = pq(response.content)
         username = parsed("#id_username")[0]
         assert username.value == self.github_profile_data["login"]
-        email0 = parsed("#email_0")[0].attrib["value"]
-        assert email0 == self.github_email_data[0]["email"]
-        email1 = parsed("#email_1")[0].attrib["value"]
-        assert email1 == self.github_profile_data["email"]
+        assert (
+            parsed.find('input[name="email"]').val()
+            == self.github_email_data[0]["email"]
+        )
 
     def test_signin(self):
         """Successful auth to existing account is reflected in tools."""
diff --git a/kuma/users/tests/test_views.py b/kuma/users/tests/test_views.py
--- a/kuma/users/tests/test_views.py
+++ b/kuma/users/tests/test_views.py
@@ -46,7 +46,6 @@
 
 from . import SampleRevisionsMixin, SocialTestMixin, user, UserTestCase
 from ..models import User, UserBan
-from ..signup import SignupForm
 from ..views import delete_document, revert_document
 
 
@@ -1110,20 +1109,6 @@ def test_matching_user(self):
         assert_no_cache_header(response)
         assert response.context["matching_user"] == octocat
 
-    def test_clashing_username(self):
-        """First a GitHub user exists. Then a Google user tries to sign up
-        whose email address, when `email.split('@')[0]` would become the same
-        as the existing GitHub user.
-        """
-        user(username="octocat", save=True)
-        self.google_login(
-            profile_data=dict(self.google_profile_data, email="octocat@gmail.com",)
-        )
-        response = self.client.get(self.signup_url)
-        assert response.status_code == 200
-        doc = pq(response.content)
-        assert doc.find('input[name="username"]').val() == "octocat2"
-
     def test_email_addresses(self):
         public_email = "octocat-public@example.com"
         private_email = "octocat-private@example.com"
@@ -1132,75 +1117,44 @@ def test_email_addresses(self):
         profile_data = self.github_profile_data.copy()
         profile_data["email"] = public_email
         email_data = [
-            {"email": private_email, "verified": True, "primary": True},
+            # It might be unrealistic but let's make sure the primary email
+            # is NOT first in the list. Just to prove that pick that email not
+            # on it coming first but that's the primary verified one.
             {"email": unverified_email, "verified": False, "primary": False},
+            {"email": private_email, "verified": True, "primary": True},
             {"email": invalid_email, "verified": False, "primary": False},
         ]
         self.github_login(profile_data=profile_data, email_data=email_data)
         response = self.client.get(self.signup_url)
         assert response.status_code == 200
         assert_no_cache_header(response)
-        assert private_email not in response.context
-        email_address = response.context["email_addresses"]
-
-        # first check if the public email address has been found
-        assert public_email in email_address
-        assert email_address[public_email] == {
-            "verified": False,
-            "email": public_email,
-            "primary": False,
-        }
-        # then check if the private and verified-at-GitHub email address
-        # has been found
-        assert private_email in email_address
-        assert email_address[private_email] == {
-            "verified": True,
-            "email": private_email,
-            "primary": True,
-        }
-        # then check that the invalid email is not present
-        assert invalid_email not in email_address
-        # then check if the radio button's default value is the primary email
-        # address
-        assert response.context["form"].initial["email"][1] == private_email
+        doc = pq(response.content)
+
+        # The hidden input should display the primary verified email
+        assert doc.find('input[name="email"]').val() == email_data[1]["email"]
+        # But whatever's in the hidden email input is always displayed to the user
+        # as "plain text". Check that that also is right.
+        assert doc.find("#email-static-container").text() == email_data[1]["email"]
 
         unverified_email = "o.ctocat@gmail.com"
         data = {
             "website": "",
             "username": "octocat",
-            "email": SignupForm.other_email_value,  # = use other_email
-            "other_email": unverified_email,
+            "email": email_data[1]["email"],
             "terms": True,
         }
         assert not EmailAddress.objects.filter(email=unverified_email).exists()
         response = self.client.post(self.signup_url, data=data)
         assert response.status_code == 302
         assert_no_cache_header(response)
-        unverified_email_addresses = EmailAddress.objects.filter(email=unverified_email)
-        assert unverified_email_addresses.exists()
-        assert unverified_email_addresses.count() == 1
-        assert unverified_email_addresses[0].primary
-        assert not unverified_email_addresses[0].verified
 
-    def test_email_addresses_with_no_public(self):
-        profile_data = self.github_profile_data.copy()
-        profile_data["email"] = None
-        email_data = self.github_email_data[:]
-        private_email = "octocat.private@example.com"
-        email_data[0]["email"] = private_email
-        self.github_login(profile_data=profile_data, email_data=email_data)
-        response = self.client.get(self.signup_url)
-        assert response.status_code == 200
-        assert_no_cache_header(response)
-        assert response.context["form"].initial["email"] == private_email
-
-    def test_email_addresses_with_no_alternatives(self):
-        private_email = self.github_profile_data["email"]
-        self.github_login(email_data=[])
-        response = self.client.get(self.signup_url)
-        assert response.status_code == 200
-        assert_no_cache_header(response)
-        assert response.context["form"].initial["email"] == private_email
+        # Check that the user.email field became the primary verified one.
+        user = User.objects.get(username=data["username"])
+        assert user.email == email_data[1]["email"]
+        assert user.emailaddress_set.count() == 1
+        assert user.emailaddress_set.first().email == user.email
+        assert user.emailaddress_set.first().verified
+        assert user.emailaddress_set.first().primary
 
     def test_signup_public_github(self, is_public=True):
         resp = self.github_login()
@@ -1209,7 +1163,7 @@ def test_signup_public_github(self, is_public=True):
         data = {
             "website": "",
             "username": "octocat",
-            "email": "octo.cat@github-inc.com",
+            "email": "octocat-private@example.com",
             "terms": True,
             "is_github_url_public": is_public,
         }
@@ -1239,7 +1193,7 @@ def test_signup_github_event_tracking(self):
                 data = {
                     "website": "",
                     "username": "octocat",
-                    "email": "octo.cat@github-inc.com",
+                    "email": "octocat-private@example.com",
                     "terms": True,
                     "is_github_url_public": True,
                 }
@@ -1268,50 +1222,25 @@ def test_signup_github_event_tracking(self):
                     CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_AUDIT, "github"
                 )
 
-    def test_signup_username_and_email_errors_event_tracking(self):
+    def test_signup_github_email_manual_override(self):
+        """Tests if a POST request comes in with an email that is NOT one of the
+        options, it should reject it.
+        Basically, in the sign up, you are shown what you primary default is and
+        it's also in a hidden input.
+        So, the only want to try to sign up with anything outside of that would
+        be if you manually control the POST request or fiddle with the DOM to
+        edit the hidden email input.
         """
-        Tests that GA tracking events are sent for errors in the username
-        and/or email fields submitted when signing-up with a new account.
-        """
-        user(username="octocat", save=True)
-        with self.settings(
-            GOOGLE_ANALYTICS_ACCOUNT="UA-XXXX-1",
-            GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS=True,
-        ):
-            p1 = mock.patch("kuma.users.signal_handlers.track_event")
-            p2 = mock.patch("kuma.users.views.track_event")
-            p3 = mock.patch("kuma.users.providers.google.views.track_event")
-            with p1 as track_event_mock_signals, p2 as track_event_mock_views, p3 as track_event_mock_google:
-
-                self.google_login()
-
-                data = {
-                    "website": "",
-                    "username": "octocat",
-                    "email": "invalid@email",
-                    "terms": True,
-                    "is_newsletter_subscribed": True,
-                }
-                response = self.client.post(self.signup_url, data=data)
-                assert response.status_code == 200
-
-                track_event_mock_signals.assert_called_with(
-                    CATEGORY_SIGNUP_FLOW, ACTION_AUTH_SUCCESSFUL, "google"
-                )
-                track_event_mock_google.assert_called_with(
-                    CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "google"
-                )
-                track_event_mock_views.assert_has_calls(
-                    [
-                        mock.call(CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_AUDIT, "google"),
-                        mock.call(
-                            CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username"
-                        ),
-                        mock.call(
-                            CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "email"
-                        ),
-                    ]
-                )
+        self.github_login()
+        data = {
+            "website": "",
+            "username": "octocat",
+            "email": "wasnot@anoption.biz",
+            "terms": True,
+            "is_github_url_public": True,
+        }
+        response = self.client.post(self.signup_url, data=data)
+        assert response.status_code == 400
 
     def test_signin_github_event_tracking(self):
         """Tests that kuma.core.ga_tracking.track_event is called when you
@@ -1321,7 +1250,7 @@ def test_signin_github_event_tracking(self):
         data = {
             "website": "",
             "username": "octocat",
-            "email": "octo.cat@github-inc.com",
+            "email": "octocat-private@example.com",
             "terms": True,
             "is_github_url_public": True,
         }
@@ -1415,6 +1344,122 @@ def test_account_refresh_token_saved_next_login(self):
         assert refresh_token == social_token.token_secret
 
 
+class KumaGoogleTests(UserTestCase, SocialTestMixin):
+    def setUp(self):
+        self.signup_url = reverse("socialaccount_signup")
+
+    def test_signup_google(self):
+        response = self.google_login()
+        assert response.status_code == 200
+
+        doc = pq(response.content)
+        # The default suggested username should be the `email.split('@')[0]`
+        email = self.google_profile_data["email"]
+        username = email.split("@")[0]
+        assert doc.find('input[name="username"]').val() == username
+        # first remove the button from that container
+        doc("#username-static-container button").remove()
+        # so that what's left is just the username
+        assert doc.find("#username-static-container").text() == username
+
+        # The hidden input should display the primary verified email
+        assert doc.find('input[name="email"]').val() == email
+        # But whatever's in the hidden email input is always displayed to the user
+        # as "plain text". Check that that also is right.
+        assert doc.find("#email-static-container").text() == email
+
+        data = {
+            "website": "",  # for the honeypot
+            "username": username,
+            "email": email,
+            "terms": True,
+        }
+        response = self.client.post(self.signup_url, data=data)
+        assert response.status_code == 302
+        assert_no_cache_header(response)
+
+        user = User.objects.get(username=username)
+        assert user.email == email
+
+        assert EmailAddress.objects.filter(
+            email=email, primary=True, verified=True
+        ).exists()
+
+    def test_signup_google_changed_email(self):
+        """When you load the signup form, our backend recognizes what your valid
+        email address can be. But what if someone changes the hidden input to
+        something other that what's there by default. That should get kicked out.
+        """
+        self.google_login()
+        email = self.google_profile_data["email"]
+        username = email.split("@")[0]
+
+        data = {
+            "website": "",  # for the honeypot
+            "username": username,
+            "email": "somethingelse@example.biz",
+            "terms": True,
+        }
+        response = self.client.post(self.signup_url, data=data)
+        assert response.status_code == 400
+
+    def test_clashing_username(self):
+        """First a GitHub user exists. Then a Google user tries to sign up
+        whose email address, when `email.split('@')[0]` would become the same
+        as the existing GitHub user.
+        """
+        user(username="octocat", save=True)
+        self.google_login(
+            profile_data=dict(self.google_profile_data, email="octocat@gmail.com",)
+        )
+        response = self.client.get(self.signup_url)
+        assert response.status_code == 200
+        doc = pq(response.content)
+        assert doc.find('input[name="username"]').val() == "octocat2"
+
+    def test_signup_username_error_event_tracking(self):
+        """
+        Tests that GA tracking events are sent for errors in the username
+        field submitted when signing-up with a new account.
+        """
+        user(username="octocat", save=True)
+        with self.settings(
+            GOOGLE_ANALYTICS_ACCOUNT="UA-XXXX-1",
+            GOOGLE_ANALYTICS_TRACKING_RAISE_ERRORS=True,
+        ):
+            p1 = mock.patch("kuma.users.signal_handlers.track_event")
+            p2 = mock.patch("kuma.users.views.track_event")
+            p3 = mock.patch("kuma.users.providers.google.views.track_event")
+            with p1 as track_event_mock_signals, p2 as track_event_mock_views, p3 as track_event_mock_google:
+
+                self.google_login()
+
+                data = {
+                    "website": "",
+                    "username": "octocat",
+                    "email": "octocat-private@example.com",
+                    "terms": True,
+                    "is_newsletter_subscribed": True,
+                }
+                response = self.client.post(self.signup_url, data=data)
+                assert response.status_code == 200
+
+                track_event_mock_signals.assert_called_with(
+                    CATEGORY_SIGNUP_FLOW, ACTION_AUTH_SUCCESSFUL, "google"
+                )
+                track_event_mock_google.assert_called_with(
+                    CATEGORY_SIGNUP_FLOW, ACTION_AUTH_STARTED, "google"
+                )
+                track_event_mock_views.assert_has_calls(
+                    [
+                        mock.call(CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_AUDIT, "google"),
+                        mock.call(
+                            CATEGORY_SIGNUP_FLOW, ACTION_PROFILE_EDIT_ERROR, "username"
+                        ),
+                    ]
+                )
+
+
 def test_delete_user_login_always_required(db, client):
     # Anonymous client gets redirected to sign in page.
     url = reverse("users.user_delete", kwargs={"username": "missing"})