Skip to content

Commit

Permalink
Use first and last name fields for user (#311)
Browse files Browse the repository at this point in the history
* feat: ask first and last name on account creation

* feat: add first and last name in sysadmin report

* feat: add first and last name in accounts page
  • Loading branch information
ahmed-arb authored Oct 17, 2023
1 parent 8968f4c commit 6b17bff
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 37 deletions.
7 changes: 5 additions & 2 deletions common/djangoapps/student/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/dashboard/sysadmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
)

Expand Down
3 changes: 2 additions & 1 deletion lms/static/js/student_account/models/user_account_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
idAttribute: 'username',
defaults: {
username: '',
name: '',
first_name: '',
last_name: '',
email: '',
password: '',
language: null,
Expand Down
42 changes: 30 additions & 12 deletions lms/static/js/student_account/views/account_settings_factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
};
}

Expand Down Expand Up @@ -154,7 +171,8 @@
)
})
},
fullnameFieldView,
firstNameFieldView,
lastNameFieldView,
emailFieldView,
{
view: new AccountSettingsFieldViews.PasswordFieldView({
Expand Down
36 changes: 26 additions & 10 deletions openedx/core/djangoapps/user_api/accounts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 3 additions & 1 deletion openedx/core/djangoapps/user_api/accounts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 = ()

Expand Down
23 changes: 15 additions & 8 deletions openedx/core/djangoapps/user_authn/views/registration_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand All @@ -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]
)
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit 6b17bff

Please sign in to comment.