diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index 934096c129c1..fba3fd35bb4c 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -608,6 +608,8 @@ def do_create_account(form, custom_form=None): user = User( username=proposed_username, email=form.cleaned_data["email"], + first_name=form.cleaned_data["first_name"], + last_name=form.cleaned_data["last_name"], is_active=False ) password = normalize_password(form.cleaned_data["password"]) @@ -646,12 +648,13 @@ def do_create_account(form, custom_form=None): registration.register(user) profile_fields = [ - "name", "level_of_education", "gender", "mailing_address", "city", "country", "goals", + "level_of_education", "gender", "mailing_address", "city", "country", "goals", "year_of_birth" ] profile = UserProfile( user=user, - **{key: form.cleaned_data.get(key) for key in profile_fields} + **{key: form.cleaned_data.get(key) for key in profile_fields}, + name=form.cleaned_data["first_name"] + ' ' + form.cleaned_data["last_name"], ) extended_profile = form.cleaned_extended_profile if extended_profile: diff --git a/lms/djangoapps/dashboard/sysadmin.py b/lms/djangoapps/dashboard/sysadmin.py index 469a22223be5..1add452c223d 100644 --- a/lms/djangoapps/dashboard/sysadmin.py +++ b/lms/djangoapps/dashboard/sysadmin.py @@ -212,11 +212,11 @@ def post(self, request): track.views.server_track(request, action, {}, page='user_sysdashboard') if action == 'download_users': - header = [_('username'), _('email'), _('name'), _('country') ] + header = [_('username'), _('email'), _('first_name'), _('last_name'), _('country') ] data = [] for u in (User.objects.select_related('profile').iterator()): try: - data.append([u.username, u.email, u.profile.name, u.profile.country]) + data.append([u.username, u.email, u.first_name, u.last_name, u.profile.country]) except UserProfile.DoesNotExist as err: data.append([u.username, u.email, '', '']) msg = _(u'Cannot find user profile with username {username} - {error}').format( diff --git a/lms/envs/common.py b/lms/envs/common.py index 31092d9301d8..d29f5604804c 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3293,7 +3293,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # The list of all fields that can be shared selectively with other users using the 'custom' privacy setting ACCOUNT_VISIBILITY_CONFIGURATION["custom_shareable_fields"] = ( ACCOUNT_VISIBILITY_CONFIGURATION["bulk_shareable_fields"] + [ - "name", + "first_name", + "last_name", ] ) diff --git a/lms/static/js/student_account/models/user_account_model.js b/lms/static/js/student_account/models/user_account_model.js index 0bf577db3aec..4b2f19fe8507 100644 --- a/lms/static/js/student_account/models/user_account_model.js +++ b/lms/static/js/student_account/models/user_account_model.js @@ -7,7 +7,8 @@ idAttribute: 'username', defaults: { username: '', - name: '', + first_name: '', + last_name: '', email: '', password: '', language: null, diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js index 670f039914f4..58798726e25a 100644 --- a/lms/static/js/student_account/views/account_settings_factory.js +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -36,8 +36,8 @@ accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage, showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField, emailFieldView, secondaryEmailFieldView, socialFields, accountDeletionFields, platformData, - aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView, - fullNameFieldData, emailFieldData, secondaryEmailFieldData, countryFieldData, additionalFields, + aboutSectionMessageType, aboutSectionMessage, firstNameFieldView, lastNameFieldView, countryFieldView, + firstNameFieldData, lastNameFieldData, emailFieldData, secondaryEmailFieldData, countryFieldData, additionalFields, fieldItem, emailFieldViewIndex, focusId, tabIndex = 0; @@ -95,20 +95,37 @@ persistChanges: true }; - fullNameFieldData = { + firstNameFieldData = { model: userAccountModel, - title: gettext('Full Name'), - valueAttribute: 'name', - helpMessage: gettext('The name that is used for ID verification and that appears on your certificates.'), // eslint-disable-line max-len, + title: gettext('First Name'), + valueAttribute: 'first_name', + helpMessage: gettext('Your first name.'), // eslint-disable-line max-len, persistChanges: true }; - if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('name') !== -1) { - fullnameFieldView = { - view: new AccountSettingsFieldViews.ReadonlyFieldView(fullNameFieldData) + if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('first_name') !== -1) { + firstNameFieldView = { + view: new AccountSettingsFieldViews.ReadonlyFieldView(firstNameFieldData) }; } else { - fullnameFieldView = { - view: new AccountSettingsFieldViews.TextFieldView(fullNameFieldData) + firstNameFieldView = { + view: new AccountSettingsFieldViews.TextFieldView(firstNameFieldData) + }; + } + + lastNameFieldData = { + model: userAccountModel, + title: gettext('Last Name'), + valueAttribute: 'last_name', + helpMessage: gettext('Your last name.'), // eslint-disable-line max-len, + persistChanges: true + } + if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('last_name') !== -1) { + lastNameFieldView = { + view: new AccountSettingsFieldViews.ReadonlyFieldView(lastNameFieldData) + }; + } else { + lastNameFieldView = { + view: new AccountSettingsFieldViews.TextFieldView(lastNameFieldData) }; } @@ -154,7 +171,8 @@ ) }) }, - fullnameFieldView, + firstNameFieldView, + lastNameFieldView, emailFieldView, { view: new AccountSettingsFieldViews.PasswordFieldView({ diff --git a/openedx/core/djangoapps/user_api/accounts/api.py b/openedx/core/djangoapps/user_api/accounts/api.py index 62d341c8a67d..554c9afc884a 100644 --- a/openedx/core/djangoapps/user_api/accounts/api.py +++ b/openedx/core/djangoapps/user_api/accounts/api.py @@ -138,12 +138,18 @@ def update_account_settings(requesting_user, update, username=None): user_serializer = AccountUserSerializer(user, data=update) legacy_profile_serializer = AccountLegacyProfileSerializer(user_profile, data=update) + + if 'first_name' in update or 'last_name' in update: # store first and last name in user profile as well + update['name'] = update.get('first_name', user.first_name) + ' ' + update.get('last_name', user.last_name) + for serializer in user_serializer, legacy_profile_serializer: add_serializer_errors(serializer, update, field_errors) _validate_email_change(user, update, field_errors) _validate_secondary_email(user, update, field_errors) - old_name = _validate_name_change(user_profile, update, field_errors) + _validate_name_change(user_profile, "first_name", update, field_errors) + _validate_name_change(user_profile, "last_name", update, field_errors) + old_name = _get_oldname(user_profile, update) old_language_proficiencies = _get_old_language_proficiencies_if_updating(user_profile, update) if field_errors: @@ -236,23 +242,33 @@ def _validate_secondary_email(user, data, field_errors): del data["secondary_email"] -def _validate_name_change(user_profile, data, field_errors): - # If user has requested to change name, store old name because we must update associated metadata - # after the save process is complete. - if "name" not in data: - return None +def _validate_name_change(user_profile, name_field, data, field_errors): + if name_field not in data: + return - old_name = user_profile.name try: - validate_name(data['name']) + validate_name(data[name_field]) + _validate_not_empty(data[name_field]) except ValidationError as err: - field_errors["name"] = { + field_errors[name_field] = { "developer_message": u"Error thrown from validate_name: '{}'".format(err.message), "user_message": err.message } return None - return old_name + +def _get_oldname(user_profile, data): + # If user has requested to change name, store old name because we must update associated metadata + # after the save process is complete. + if "name" not in data: + return None + + return user_profile.name + +def _validate_not_empty(value): + if not value: + raise ValidationError(u"This field cannot be empty") + def _get_old_language_proficiencies_if_updating(user_profile, data): diff --git a/openedx/core/djangoapps/user_api/accounts/serializers.py b/openedx/core/djangoapps/user_api/accounts/serializers.py index 7f197cb8b6e7..bd483765a041 100644 --- a/openedx/core/djangoapps/user_api/accounts/serializers.py +++ b/openedx/core/djangoapps/user_api/accounts/serializers.py @@ -141,6 +141,8 @@ def to_representation(self, user): "profile_image": None, "language_proficiencies": None, "name": None, + "first_name": user.first_name, + "last_name": user.last_name, "gender": None, "goals": None, "year_of_birth": None, @@ -222,7 +224,7 @@ class AccountUserSerializer(serializers.HyperlinkedModelSerializer, ReadOnlyFiel """ class Meta(object): model = User - fields = ("username", "email", "date_joined", "is_active") + fields = ("username", "first_name", "last_name", "email", "date_joined", "is_active") read_only_fields = ("username", "email", "date_joined", "is_active") explicit_read_only_fields = () diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index 4ce0a8917c22..b79a941587bc 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -97,7 +97,7 @@ def validate_name(name): name (unicode): The name to validate. """ if contains_html(name): - raise forms.ValidationError(_('Full Name cannot contain the following characters: < >')) + raise forms.ValidationError(_('First/last Name cannot contain the following characters: < >')) class UsernameField(forms.CharField): @@ -136,7 +136,8 @@ class AccountCreationForm(forms.Form): """ _EMAIL_INVALID_MSG = _(u"A properly formatted e-mail is required") - _NAME_TOO_SHORT_MSG = _(u"Your legal name must be a minimum of one character long") + _FIRST_NAME_TOO_SHORT_MSG = _(u"Your legal first name must be a minimum of one character long") + _LAST_NAME_TOO_SHORT_MSG = _(u"Your legal last name must be a minimum of one character long") # TODO: Resolve repetition @@ -154,11 +155,19 @@ class AccountCreationForm(forms.Form): password = forms.CharField() - name = forms.CharField( + first_name = forms.CharField( min_length=accounts.NAME_MIN_LENGTH, error_messages={ - "required": _NAME_TOO_SHORT_MSG, - "min_length": _NAME_TOO_SHORT_MSG, + "required": _FIRST_NAME_TOO_SHORT_MSG, + "min_length": _FIRST_NAME_TOO_SHORT_MSG, + }, + validators=[validate_name] + ) + last_name = forms.CharField( + min_length=accounts.NAME_MIN_LENGTH, + error_messages={ + "required": _LAST_NAME_TOO_SHORT_MSG, + "min_length": _LAST_NAME_TOO_SHORT_MSG, }, validators=[validate_name] ) @@ -294,12 +303,10 @@ class RegistrationFormFactory(object): Construct Registration forms and associated fields. """ - DEFAULT_FIELDS = ["email", "name", "username", "password"] + DEFAULT_FIELDS = ["email", "first_name", "last_name", "username", "password"] EXTRA_FIELDS = [ "confirm_email", - "first_name", - "last_name", "city", "state", "country",