Repository: inducer/relate

Issue:
Bug: institutional_id can't be empty when saving profile form
It raises "field is required" or "input not matched". Why that bug had not been reported before?


Discussion:

Resolving patch:
diff --git a/course/auth.py b/course/auth.py
--- a/course/auth.py
+++ b/course/auth.py
@@ -34,13 +34,13 @@
 from django.core.exceptions import (PermissionDenied, SuspiciousOperation,
         ObjectDoesNotExist)
 from crispy_forms.helper import FormHelper
-from crispy_forms.layout import Submit, Layout, Div
+from crispy_forms.layout import Submit, Layout, Div, Button
 from django.conf import settings
 from django.contrib.auth import (get_user_model, REDIRECT_FIELD_NAME,
         login as auth_login, logout as auth_logout)
 from django.contrib.auth.forms import \
         AuthenticationForm as AuthenticationFormBase
-from django.contrib.auth.decorators import user_passes_test
+from django.contrib.auth.decorators import user_passes_test, login_required
 from django.urls import reverse
 from django.contrib.auth.validators import ASCIIUsernameValidator
 from django.utils.http import is_safe_url
@@ -872,8 +872,10 @@ def sign_in_stage2_with_token(request, user_id, sign_in_key):
 
 # {{{ user profile
 
-EDITABLE_INST_ID_BEFORE_VERIFICATION = \
-        settings.RELATE_EDITABLE_INST_ID_BEFORE_VERIFICATION
+def is_inst_id_editable_before_validation():
+    # type: () -> bool
+    return getattr(
+        settings, "RELATE_EDITABLE_INST_ID_BEFORE_VERIFICATION", True)
 
 
 class UserForm(StyledModelForm):
@@ -881,12 +883,6 @@ class UserForm(StyledModelForm):
             max_length=100,
             label=_("Institutional ID Confirmation"),
             required=False)
-    no_institutional_id = forms.BooleanField(
-            label=_("I have no Institutional ID"),
-            help_text=_("Check the checkbox if you are not a student "
-                        "or you forget your institutional id."),
-            required=False,
-            initial=False)
 
     class Meta:
         model = get_user_model()
@@ -894,15 +890,19 @@ class Meta:
                 "editor_mode")
 
     def __init__(self, *args, **kwargs):
-        self.is_inst_id_locked = is_inst_id_locked =\
-                kwargs.pop('is_inst_id_locked')
+        self.is_inst_id_locked = kwargs.pop('is_inst_id_locked')
         super(UserForm, self).__init__(*args, **kwargs)
 
-        self.helper.layout = Layout(
-                Div("last_name", "first_name", css_class="well"),
-                Div("institutional_id", css_class="well"),
-                Div("editor_mode", css_class="well")
-                )
+        if self.instance.name_verified:
+            self.fields["first_name"].disabled = True
+            self.fields["last_name"].disabled = True
+
+        if self.is_inst_id_locked:
+            self.fields["institutional_id"].disabled = True
+            self.fields["institutional_id_confirm"].disabled = True
+        else:
+            self.fields["institutional_id_confirm"].initial = (
+                self.instance.institutional_id)
 
         self.fields["institutional_id"].help_text = (
                 _("The unique ID your university or school provided, "
@@ -911,92 +911,70 @@ def __init__(self, *args, **kwargs):
                     "<b>Once %(submitted_or_verified)s, it cannot be "
                     "changed</b>.")
                 % {"submitted_or_verified":
-                    EDITABLE_INST_ID_BEFORE_VERIFICATION
-                    and _("verified") or _("submitted")})
-
-        def adjust_layout(is_inst_id_locked):
-            if not is_inst_id_locked:
-                self.helper.layout[1].insert(1, "institutional_id_confirm")
-                self.helper.layout[1].insert(0, "no_institutional_id")
-                self.fields["institutional_id_confirm"].initial = \
-                        self.instance.institutional_id
-            else:
-                self.fields["institutional_id"].widget.\
-                        attrs['disabled'] = True
-            if not settings.RELATE_SHOW_INST_ID_FORM:
-                self.helper.layout[1].css_class = 'well hidden'
-            if not settings.RELATE_SHOW_EDITOR_FORM:
-                self.helper.layout[2].css_class = 'well hidden'
+                   is_inst_id_editable_before_validation()
+                   and _("verified") or _("submitted")})
+
+        # {{ build layout
+        name_fields_layout = ["last_name", "first_name"]
+        fields_layout = [Div(*name_fields_layout, css_class="well")]
+
+        if getattr(settings, "RELATE_SHOW_INST_ID_FORM", True):
+            inst_field_group_layout = ["institutional_id"]
+            if not self.is_inst_id_locked:
+                inst_field_group_layout.append("institutional_id_confirm")
+            fields_layout.append(Div(*inst_field_group_layout, css_class="well",
+                                     css_id="institutional_id_block"))
+        else:
+            # This is needed for django-crispy-form version < 1.7
+            self.fields["institutional_id"].widget = forms.HiddenInput()
 
-        if self.instance.name_verified:
-            self.fields["first_name"].widget.attrs['disabled'] = True
-            self.fields["last_name"].widget.attrs['disabled'] = True
+        if getattr(settings, "RELATE_SHOW_EDITOR_FORM", True):
+            fields_layout.append(Div("editor_mode", css_class="well"))
+        else:
+            # This is needed for django-crispy-form version < 1.7
+            self.fields["editor_mode"].widget = forms.HiddenInput()
 
-        adjust_layout(is_inst_id_locked)
+        self.helper.layout = Layout(*fields_layout)
 
         self.helper.add_input(
                 Submit("submit_user", _("Update")))
 
-    def clean_institutional_id(self):
-        inst_id = self.cleaned_data['institutional_id']
-
-        if inst_id is not None:
-            inst_id = inst_id.strip()
-
-        if self.is_inst_id_locked:
-            # Disabled fields are not part of form submit--so simply
-            # assume old value. At the same time, prevent smuggled-in
-            # POST parameters.
-            return self.instance.institutional_id
-        else:
-            return inst_id
-
-    def clean_first_name(self):
-        first_name = self.cleaned_data['first_name']
-        if self.instance.name_verified:
-            # Disabled fields are not part of form submit--so simply
-            # assume old value. At the same time, prevent smuggled-in
-            # POST parameters.
-            return self.instance.first_name
-        else:
-            return first_name
-
-    def clean_last_name(self):
-        last_name = self.cleaned_data['last_name']
-        if self.instance.name_verified:
-            # Disabled fields are not part of form submit--so simply
-            # assume old value. At the same time, prevent smuggled-in
-            # POST parameters.
-            return self.instance.last_name
-        else:
-            return last_name
+        self.helper.add_input(
+                Button("signout", _("Sign out"), css_class="btn btn-danger",
+                       onclick=(
+                           "window.location.href=%s"
+                           % reverse("relate-logout"))))
+        # }}}
 
     def clean_institutional_id_confirm(self):
-        inst_id_confirmed = self.cleaned_data.get(
-                "institutional_id_confirm")
+        inst_id_confirmed = self.cleaned_data.get("institutional_id_confirm")
 
         if not self.is_inst_id_locked:
             inst_id = self.cleaned_data.get("institutional_id")
             if inst_id and not inst_id_confirmed:
                 raise forms.ValidationError(_("This field is required."))
-            if not inst_id == inst_id_confirmed:
+            if any([inst_id, inst_id_confirmed]) and inst_id != inst_id_confirmed:
                 raise forms.ValidationError(_("Inputs do not match."))
         return inst_id_confirmed
 
 
+@login_required
 def user_profile(request):
-    if not request.user.is_authenticated:
-        raise PermissionDenied()
-
     user_form = None
 
     def is_inst_id_locked(user):
-        if EDITABLE_INST_ID_BEFORE_VERIFICATION:
+        if is_inst_id_editable_before_validation():
             return True if (user.institutional_id
                     and user.institutional_id_verified) else False
         else:
             return True if user.institutional_id else False
 
+    def is_requesting_inst_id():
+        return not is_inst_id_locked(request.user) and (
+            request.GET.get("first_login")
+            or (request.GET.get("set_inst_id")
+                and request.GET.get("referer")))
+
     if request.method == "POST":
         if "submit_user" in request.POST:
             user_form = UserForm(
@@ -1005,34 +983,42 @@ def is_inst_id_locked(user):
                     is_inst_id_locked=is_inst_id_locked(request.user),
             )
             if user_form.is_valid():
-                user_form.save()
+                if user_form.has_changed():
+                    user_form.save()
+                    messages.add_message(request, messages.SUCCESS,
+                            _("Profile data updated."))
+                    request.user.refresh_from_db()
+
+                else:
+                    messages.add_message(request, messages.INFO,
+                            _("No change was made on your profile."))
 
-                messages.add_message(request, messages.INFO,
-                        _("Profile data saved."))
                 if request.GET.get("first_login"):
                     return redirect("relate-home")
+
                 if (request.GET.get("set_inst_id")
-                        and request.GET["referer"]):
+                        and request.GET.get("referer")):
                     return redirect(request.GET["referer"])
 
                 user_form = UserForm(
-                        instance=request.user,
-                        is_inst_id_locked=is_inst_id_locked(request.user))
-
-    if user_form is None:
-            user_form = UserForm(
                     instance=request.user,
                     is_inst_id_locked=is_inst_id_locked(request.user),
-            )
+                )
+
+    if user_form is None:
+        request.user.refresh_from_db()
+        user_form = UserForm(
+            instance=request.user,
+            is_inst_id_locked=is_inst_id_locked(request.user),
+        )
 
     return render(request, "user-profile-form.html", {
-        "is_inst_id_locked": is_inst_id_locked(request.user),
-        "enable_inst_id_if_not_locked": (
-            request.GET.get("first_login")
-            or (request.GET.get("set_inst_id")
-                and request.GET["referer"])
-            ),
-        "user_form": user_form,
+        "form": user_form,
+        "form_description": _("User Profile"),
+        "is_requesting_inst_id": is_requesting_inst_id(),
+        "enable_profile_form_js": (
+            not is_inst_id_locked(request.user) and
+            getattr(settings, "RELATE_SHOW_INST_ID_FORM", True))
         })
 
 # }}}


Test patch:
diff --git a/tests/base_test_mixins.py b/tests/base_test_mixins.py
--- a/tests/base_test_mixins.py
+++ b/tests/base_test_mixins.py
@@ -304,6 +304,7 @@ def get_profile(self, follow=True):
         return self.c.get(self.get_profile_view_url(), follow=follow)
 
     def post_profile(self, data, follow=True):
+        data.update({"submit_user": [""]})
         return self.c.post(self.get_profile_view_url(), data, follow=follow)
 
     def post_signout(self, data, follow=True):
diff --git a/tests/test_auth.py b/tests/test_auth.py
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -26,7 +26,7 @@
 import itertools
 from six.moves.urllib.parse import ParseResult, quote, urlparse
 from djangosaml2.urls import urlpatterns as djsaml2_urlpatterns
-from django.test import TestCase, override_settings, mock
+from django.test import TestCase, override_settings, mock, RequestFactory
 from django.contrib import messages
 from django.conf import settings
 from django.core import mail
@@ -47,6 +47,11 @@
 from .utils import (
     LocmemBackendTestsMixin, load_url_pattern_names, reload_urlconf)
 
+# settings names
+EDITABLE_INST_ID_BEFORE_VERI = "RELATE_EDITABLE_INST_ID_BEFORE_VERIFICATION"
+SHOW_INST_ID_FORM = "RELATE_SHOW_INST_ID_FORM"
+SHOW_EDITOR_FORM = "RELATE_SHOW_EDITOR_FORM"
+
 NOT_IMPERSONATING_MESSAGE = "Not currently impersonating anyone."
 NO_LONGER_IMPERSONATING_MESSAGE = "No longer impersonating anyone."
 ALREADY_IMPERSONATING_SOMEONE_MESSAGE = "Already impersonating someone."
@@ -334,6 +339,10 @@ def setUpTestData(cls):  # noqa
             get_user_model().objects.create_user(**cls._user_create_kwargs))
         cls.existing_user_count = get_user_model().objects.count()
 
+    def setUp(self):
+        super(AuthTestMixin, self).setUp()
+        self.test_user.refresh_from_db()
+
     def get_sign_in_data(self):
         return self._user_create_kwargs.copy()
 
@@ -1048,3 +1057,428 @@ def test_sign_out_confirmation_with_redirect_to(self):
                     self.get_sign_out_view_url(), redirect_to
                 ),
                 resp.content.decode())
+
+
+class UserProfileTest(CoursesTestMixinBase, AuthTestMixin,
+                      FallBackStorageMessageTestMixin, TestCase):
+
+    def setUp(self):
+        super(UserProfileTest, self).setUp()
+        self.rf = RequestFactory()
+
+    def generate_profile_data(self, **kwargs):
+        profile_data = {
+            "first_name": "",
+            "last_name": "",
+            "institutional_id": "",
+            "editor_mode": "default"}
+        profile_data.update(kwargs)
+        return profile_data
+
+    def post_profile_by_request_factory(self, data, query_string_dict=None):
+        data.update({"submit_user": [""]})
+        url = self.get_profile_view_url()
+        if query_string_dict is not None:
+            url = (
+                "%s?%s" % (
+                    url,
+                    "&".join(["%s=%s" % (k, v)
+                              for k, v in six.iteritems(query_string_dict)])))
+        request = self.rf.post(url, data)
+        request.user = self.test_user
+        request.session = mock.MagicMock()
+
+        from course.auth import user_profile
+        response = user_profile(request)
+        return response
+
+    def get_profile_by_request_factory(self):
+        request = self.rf.get(self.get_profile_view_url())
+        request.user = self.test_user
+        request.session = mock.MagicMock()
+
+        from course.auth import user_profile
+        response = user_profile(request)
+        return response
+
+    def generate_profile_form_data(self, **kwargs):
+        form_data = {
+            "first_name": "",
+            "last_name": "",
+            "institutional_id": "",
+            "institutional_id_confirm": "",
+            "no_institutional_id": True,
+            "editor_mode": "default"}
+        form_data.update(kwargs)
+        return form_data
+
+    def test_not_authenticated(self):
+        with self.temporarily_switch_to_user(None):
+            resp = self.get_profile()
+            self.assertTrue(resp.status_code, 403)
+
+            data = self.generate_profile_form_data()
+            resp = self.post_profile(data)
+            self.assertTrue(resp.status_code, 403)
+
+    def test_get_profile(self):
+        with self.temporarily_switch_to_user(self.test_user):
+            resp = self.get_profile()
+            self.assertTrue(resp.status_code, 200)
+
+    def test_post_profile_without_submit_user(self):
+        # Only POST with 'submit_user' works
+        with self.temporarily_switch_to_user(self.test_user):
+            resp = self.get_profile()
+            self.assertTrue(resp.status_code, 200)
+            data = self.generate_profile_form_data(first_name="foo")
+
+            # No 'submit_user' in POST
+            resp = self.c.post(self.get_profile_view_url(), data)
+            self.test_user.refresh_from_db()
+            self.assertEqual(self.test_user.first_name, "")
+
+    def update_profile_by_post_form(self, user_profile_dict=None,
+                                    update_profile_dict=None,
+                                    query_string_dict=None):
+        if user_profile_dict:
+            assert isinstance(user_profile_dict, dict)
+        else:
+            user_profile_dict = {}
+
+        if update_profile_dict:
+            assert isinstance(update_profile_dict, dict)
+        else:
+            update_profile_dict = {}
+
+        user_profile = self.generate_profile_data(**user_profile_dict)
+        get_user_model().objects.filter(pk=self.test_user.pk).update(**user_profile)
+        self.test_user.refresh_from_db()
+        form_data = self.generate_profile_form_data(**update_profile_dict)
+        return self.post_profile_by_request_factory(form_data, query_string_dict)
+
+    @skipIf(six.PY2, "Python2 doesn't support subTest")
+    def test_update_profile_with_different_settings(self):
+        disabled_inst_id_html_pattern = (
+            '<input type="text" name="institutional_id" value="%s" '
+            'class="textinput textInput form-control" id="id_institutional_id" '
+            'maxlength="100" disabled />')
+
+        enabled_inst_id_html_pattern = (
+            '<input type="text" name="institutional_id" value="%s" '
+            'class="textinput textInput form-control" id="id_institutional_id" '
+            'maxlength="100" />')
+
+        expected_success_msg = "Profile data updated."
+        expected_unchanged_msg = "No change was made on your profile."
+        from collections import namedtuple
+        Conf = namedtuple(
+            'Conf', [
+                'id',
+                'override_settings_dict',
+                'user_profile_dict',
+                'update_profile_dict',
+                'expected_result_dict',
+                'assert_in_html_kwargs_list',
+                'expected_msg',
+            ])
+
+        test_confs = (
+            # {{{ basic test
+            Conf("basic_1",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True}, {},
+                 {}, {}, [], expected_unchanged_msg),
+
+            Conf("basic_2",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: False}, {},
+                 {}, {}, [], expected_unchanged_msg),
+
+            Conf("basic_3",
+                 {EDITABLE_INST_ID_BEFORE_VERI: False, SHOW_INST_ID_FORM: True}, {},
+                 {}, {}, [], expected_unchanged_msg),
+
+            Conf("basic_4",
+                 {EDITABLE_INST_ID_BEFORE_VERI: False, SHOW_INST_ID_FORM: False},
+                 {}, {}, {}, [], expected_unchanged_msg),
+            # }}}
+
+            # {{{ update first_name
+            Conf("first_name_1",
+                 {},
+                 {"first_name": "foo", "name_verified": False},
+                 {"first_name": "bar"},
+                 {"first_name": "bar"},
+                 [],
+                 expected_success_msg),
+
+            Conf("first_name_2", {},
+                 {"first_name": "foo", "name_verified": True},
+                 {"first_name": "bar"},
+                 {"first_name": "foo"},
+                 [],
+                 expected_unchanged_msg),
+
+            # test strip
+            Conf("first_name_3", {},
+                 {"first_name": "foo", "name_verified": False},
+                 {"first_name": "   bar  "},
+                 {"first_name": "bar"},
+                 [],
+                 expected_success_msg),
+
+            # }}}
+
+            # {{{ update last_name
+            Conf("last_name_1", {},
+                 {"last_name": "foo", "name_verified": False},
+                 {"last_name": "bar"},
+                 {"last_name": "bar"},
+                 [],
+                 expected_success_msg),
+
+            Conf("last_name_2", {},
+                 {"last_name": "foo", "name_verified": True},
+                 {"last_name": "bar"},
+                 {"last_name": "foo"},
+                 [],
+                 expected_unchanged_msg),
+
+            # test strip
+            Conf("last_name_3", {},
+                 {"last_name": "foo", "name_verified": False},
+                 {"last_name": "  bar  "},
+                 {"last_name": "bar"},
+                 [],
+                 expected_success_msg),
+
+            # }}}
+
+            # {{{ update institutional_id update
+            Conf("institutional_id_update_1",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "1234", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [{"needle": enabled_inst_id_html_pattern % "1234", "count": 1}],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_2",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "123", "institutional_id_confirm": "123"},
+                 {"institutional_id": "123"},
+                 [{"needle": enabled_inst_id_html_pattern % "123", "count": 1}],
+                 expected_success_msg),
+
+            Conf("institutional_id_update_3",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: False},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "1234", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_4",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: False},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "123", "institutional_id_confirm": "123"},
+                 {"institutional_id": "123"},
+                 [],
+                 expected_success_msg),
+
+            Conf("institutional_id_update_5",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": True},
+                 {"institutional_id": "1234", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [{"needle": disabled_inst_id_html_pattern % "1234", "count": 1}],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_6",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": True},
+                 {"institutional_id": "123", "institutional_id_confirm": "123"},
+                 {"institutional_id": "1234"},
+                 [{"needle": disabled_inst_id_html_pattern % "1234", "count": 1}],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_7",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: False},
+                 {"institutional_id": "1234", "institutional_id_verified": True},
+                 {"institutional_id": "1234", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_8",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: False},
+                 {"institutional_id": "1234", "institutional_id_verified": True},
+                 {"institutional_id": "123", "institutional_id_confirm": "123"},
+                 {"institutional_id": "1234"},
+                 [{"needle": enabled_inst_id_html_pattern % "1234", "count": 0}],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_9",
+                 {EDITABLE_INST_ID_BEFORE_VERI: False, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "1234", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [{"needle": disabled_inst_id_html_pattern % "1234", "count": 1}],
+                 expected_unchanged_msg),
+
+            Conf("institutional_id_update_10",
+                 {EDITABLE_INST_ID_BEFORE_VERI: False, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "123", "institutional_id_confirm": "123"},
+                 {"institutional_id": "1234"},
+                 [{"needle": disabled_inst_id_html_pattern % "1234", "count": 1}],
+                 expected_unchanged_msg),
+            # }}}
+
+            # {{{ institutional_id clean
+            Conf("institutional_id_clean_1",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "123", "institutional_id_confirm": "1234"},
+                 {"institutional_id": "1234"},
+                 [{"needle": "Inputs do not match.", "count": 1}],
+                 None),
+
+            Conf("institutional_id_clean_2",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "123"},
+                 {"institutional_id": "1234"},
+                 [{"needle": "This field is required.", "count": 1}],
+                 None),
+            # }}}
+
+            # Update with blank value
+            # https://github.com/inducer/relate/pull/145
+            Conf("clear_institutional_id",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {"institutional_id": "1234", "institutional_id_verified": False},
+                 {"institutional_id": "", "institutional_id_confirm": ""},
+                 {"institutional_id": None},
+                 [],
+                 expected_success_msg),
+
+            # test post value with leading/trailing spaces (striped when saving)
+            Conf("strip_institutional_id",
+                 {EDITABLE_INST_ID_BEFORE_VERI: True, SHOW_INST_ID_FORM: True},
+                 {},
+                 {"institutional_id": "123   ",
+                    "institutional_id_confirm": "   123"},
+                 {"institutional_id": "123"},
+                 [],
+                 expected_success_msg),
+
+        )
+
+        with mock.patch("course.auth.messages.add_message") as mock_add_msg:
+            for conf in test_confs:
+                with self.subTest(conf.id):
+                    with override_settings(**conf.override_settings_dict):
+                        resp = self.update_profile_by_post_form(
+                            conf.user_profile_dict,
+                            conf.update_profile_dict)
+
+                        self.assertTrue(resp.status_code, 200)
+                        self.test_user.refresh_from_db()
+                        for k, v in six.iteritems(conf.expected_result_dict):
+                            self.assertEqual(self.test_user.__dict__[k], v)
+
+                        if conf.assert_in_html_kwargs_list:
+                            kwargs_list = conf.assert_in_html_kwargs_list
+                            for kwargs in kwargs_list:
+                                self.assertInHTML(haystack=resp.content.decode(),
+                                                  **kwargs)
+
+                        if conf.expected_msg is not None:
+                            self.assertIn(conf.expected_msg,
+                                          mock_add_msg.call_args[0])
+                        else:
+                            if mock_add_msg.call_count > 0:
+                                self.fail("Message was actually called with %s."
+                                          % repr(mock_add_msg.call_args[0][2]))
+
+                        mock_add_msg.reset_mock()
+
+    def test_profile_page_hide_institutional_id_or_editor_mode(self):
+        """
+        Test whether the response content contains <input type="hidden"
+        for specific field
+        """
+        field_div_with_id_pattern = (
+            ".*(<div\s+[^\>]*id\s*=\s*['\"]div_id_%s['\"][^>]*\/?>).*")
+
+        def assertFieldDiv(field_name, exist=True):  # noqa
+            resp = self.get_profile_by_request_factory()
+            self.assertEqual(resp.status_code, 200)
+            pattern = field_div_with_id_pattern % field_name
+            if exist:
+                self.assertRegex(resp.content.decode(), pattern,
+                                 msg=("Field Div of '%s' is expected to exist."
+                                      % field_name))
+            else:
+                self.assertNotRegex(resp.content.decode(), pattern,
+                                    msg=("Field Div of '%s' is not expected "
+                                         "to exist." % field_name))
+
+        with override_settings(
+                RELATE_SHOW_INST_ID_FORM=True, RELATE_SHOW_EDITOR_FORM=True):
+            assertFieldDiv("institutional_id", True)
+            assertFieldDiv("editor_mode", True)
+
+        with override_settings(
+                RELATE_SHOW_INST_ID_FORM=False, RELATE_SHOW_EDITOR_FORM=True):
+            assertFieldDiv("institutional_id", False)
+            assertFieldDiv("editor_mode", True)
+
+        with override_settings(
+                RELATE_SHOW_INST_ID_FORM=True, RELATE_SHOW_EDITOR_FORM=False):
+            assertFieldDiv("institutional_id", True)
+            assertFieldDiv("editor_mode", False)
+
+        with override_settings(
+                RELATE_SHOW_INST_ID_FORM=False, RELATE_SHOW_EDITOR_FORM=False):
+            assertFieldDiv("institutional_id", False)
+            assertFieldDiv("editor_mode", False)
+
+    def test_update_profile_for_first_login(self):
+        data = self.generate_profile_data(first_name="foo")
+        expected_msg = "Profile data updated."
+        with mock.patch("course.auth.messages.add_message") as mock_add_msg:
+            resp = self.post_profile_by_request_factory(
+                data, query_string_dict={"first_login": "1"})
+            self.assertRedirects(resp, reverse("relate-home"),
+                                 fetch_redirect_response=False)
+            self.assertIn(expected_msg,
+                          mock_add_msg.call_args[0])
+
+    def test_update_profile_for_referer(self):
+        data = self.generate_profile_data(first_name="foo")
+        with mock.patch("course.auth.messages.add_message") as mock_add_msg:
+            expected_msg = "Profile data updated."
+            resp = self.post_profile_by_request_factory(
+                data, query_string_dict={"referer": "/some/where/",
+                                         "set_inst_id": "1"})
+            self.assertRedirects(resp, "/some/where/",
+                                 fetch_redirect_response=False)
+            self.assertIn(expected_msg,
+                          mock_add_msg.call_args[0])
+
+    def test_update_profile_for_referer_wrong_spell(self):
+        data = self.generate_profile_data(first_name="foo")
+        with mock.patch("course.auth.messages.add_message") as mock_add_msg:
+            expected_msg = "Profile data updated."
+
+            # "Wrong" spell of referer, no redirect
+            resp = self.post_profile_by_request_factory(
+                data, query_string_dict={"referrer": "/some/where/",
+                                         "set_inst_id": "1"})
+            self.assertTrue(resp.status_code, 200)
+            self.assertIn(expected_msg,
+                          mock_add_msg.call_args[0])
+
+# vim: foldmethod=marker