Added displayUsername and username update functionality (#35699)

* Added displayUsername and username update functionality

* fix: move username assignment to safe place

moved the username assignment down a statement so that it doesn't cause exception 

* fix: handle missing username or displayUsername

* refactor: remove redundant code
This commit is contained in:
Catalina
2019-12-11 06:43:06 -05:00
committed by Oliver Eyton-Williams
parent 0f5b9f8764
commit e154f38118
13 changed files with 87 additions and 37 deletions

View File

@ -127,6 +127,9 @@ function nextTick(fn) {
const getRandomNumber = () => Math.random();
function populateRequiredFields(user) {
// by default, the displayUsername will have
// the same value as the username
user.displayUsername = user.username;
user.username = user.username.trim().toLowerCase();
user.email =
typeof user.email === 'string'
@ -347,6 +350,7 @@ export default function(User) {
if (!username && (!email || !isEmail(email))) {
return Promise.resolve(false);
}
username = username.toLowerCase();
log('checking existence');
// check to see if username is on blacklist
@ -395,6 +399,7 @@ export default function(User) {
cb(null, {});
});
}
username = username.toLowerCase();
return User.findOne({ where: { username } }, (err, user) => {
if (err) {
return cb(err);
@ -724,13 +729,13 @@ export default function(User) {
User.prototype.updateMyUsername = function updateMyUsername(newUsername) {
return Observable.defer(() => {
const isOwnUsername = isTheSame(newUsername, this.username);
const isOwnUsername = isTheSame(newUsername.toLowerCase(), this.username);
if (isOwnUsername) {
return Observable.of(dedent`
${newUsername} is already associated with this account.
`);
}
return Observable.fromPromise(User.doesExist(newUsername));
return Observable.fromPromise(User.doesExist(newUsername.toLowerCase()));
}).flatMap(boolOrMessage => {
if (typeof boolOrMessage === 'string') {
return Observable.of(boolOrMessage);
@ -741,14 +746,20 @@ export default function(User) {
`);
}
const usernameUpdate = new Promise((resolve, reject) =>
this.updateAttribute('username', newUsername, err => {
const usernameUpdate = new Promise((resolve, reject) => {
this.updateAttributes(
{
username: newUsername.toLowerCase(),
displayUsername: newUsername
},
err => {
if (err) {
return reject(err);
}
return resolve();
})
}
);
});
return Observable.fromPromise(usernameUpdate).map(
() => dedent`

View File

@ -74,6 +74,10 @@
},
"require": true
},
"displayUsername": {
"type": "string",
"default":""
},
"about": {
"type": "string",
"default": ""

View File

@ -194,7 +194,7 @@ function createUpdateMyUsername(app) {
user,
body: { username }
} = req;
if (username === user.username) {
if (username.toLowerCase() === user.username) {
return res.json({
type: 'info',
message: 'Username is already associated with this account'
@ -209,7 +209,7 @@ function createUpdateMyUsername(app) {
});
}
const exists = await User.doesExist(username);
const exists = await User.doesExist(username.toLowerCase());
if (exists) {
return res.json({
@ -218,7 +218,9 @@ function createUpdateMyUsername(app) {
});
}
return user.updateAttribute('username', username, err => {
return user.updateAttributes(
{ username: username.toLowerCase(), displayUsername: username },
err => {
if (err) {
res.status(500).json(standardErrorMessage);
return next(err);
@ -227,7 +229,8 @@ function createUpdateMyUsername(app) {
type: 'success',
message: `We have updated your username to ${username}`
});
});
}
);
};
}

View File

@ -37,6 +37,7 @@ function createProfileAttributesFromGithub(profile) {
return {
name,
username: username.toLowerCase(),
displayUsername: username,
location,
bio,
website,

View File

@ -11,6 +11,7 @@ export const publicUserProps = [
'about',
'calendar',
'completedChallenges',
'displayUsername',
'githubProfile',
'isApisMicroservicesCert',
'isBackEndCert',

View File

@ -22,6 +22,7 @@ const propTypes = {
navigate: PropTypes.func.isRequired,
requestedUser: PropTypes.shape({
username: PropTypes.string,
displayUsername: PropTypes.string,
profileUI: PropTypes.object
}),
showLoading: PropTypes.bool

View File

@ -48,6 +48,7 @@ const propTypes = {
files: PropTypes.array
})
),
displayUsername: PropTypes.string,
email: PropTypes.string,
githubProfile: PropTypes.string,
is2018DataVisCert: PropTypes.bool,
@ -121,6 +122,7 @@ export function ShowSettings(props) {
toggleNightMode,
user: {
completedChallenges,
displayUsername,
email,
is2018DataVisCert,
isApisMicroservicesCert,
@ -191,9 +193,12 @@ export function ShowSettings(props) {
</Button>
</FullWidthRow>
<Spacer />
<h1 className='text-center' style={{ overflowWrap: 'break-word' }}>
{`Account Settings for ${username}`}
</h1>
<h1
className='text-center'
style={{ overflowWrap: 'break-word' }}
>{`Account Settings for ${
displayUsername ? displayUsername : username
}`}</h1>
<About
about={about}
currentTheme={theme}
@ -203,7 +208,7 @@ export function ShowSettings(props) {
points={points}
submitNewAbout={submitNewAbout}
toggleNightMode={toggleNightMode}
username={username}
username={displayUsername ? displayUsername : username}
/>
<Spacer />
<Privacy />

View File

@ -27,6 +27,7 @@ const propTypes = {
showPortfolio: PropTypes.bool,
showTimeLine: PropTypes.bool
}),
displayUsername: PropTypes.string,
calendar: PropTypes.object,
streak: PropTypes.shape({
current: PropTypes.number,
@ -121,13 +122,15 @@ function renderProfile(user) {
picture,
portfolio,
about,
yearsTopContributor
yearsTopContributor,
displayUsername
} = user;
return (
<Fragment>
<Camper
about={showAbout ? about : null}
displayUsername={displayUsername}
githubProfile={githubProfile}
isGithub={isGithub}
isLinkedIn={isLinkedIn}
@ -147,7 +150,11 @@ function renderProfile(user) {
{showCerts ? <Certifications username={username} /> : null}
{showPortfolio ? <Portfolio portfolio={portfolio} /> : null}
{showTimeLine ? (
<Timeline completedMap={completedChallenges} username={username} />
<Timeline
completedMap={completedChallenges}
displayUsername={displayUsername}
username={username}
/>
) : null}
<Spacer />
</Fragment>

View File

@ -11,6 +11,7 @@ import './camper.css';
const propTypes = {
about: PropTypes.string,
displayUsername: PropTypes.string,
githubProfile: PropTypes.string,
isGithub: PropTypes.bool,
isLinkedIn: PropTypes.bool,
@ -48,6 +49,7 @@ function joinArray(array) {
function Camper({
name,
username,
displayUsername,
location,
points,
picture,
@ -74,7 +76,7 @@ function Camper({
/>
) : (
<Image
alt={username + "'s avatar"}
alt={displayUsername ? displayUsername : username + "'s avatar"}
className='avatar'
responsive={true}
src={picture}
@ -99,7 +101,9 @@ function Camper({
website={website}
/>
<br />
<h2 className='text-center username'>@{username}</h2>
<h2 className='text-center username'>
@{displayUsername ? displayUsername : username}
</h2>
{name && <p className='text-center name'>{name}</p>}
{location && <p className='text-center location'>{location}</p>}
{about && <p className='bio text-center'>{about}</p>}

View File

@ -27,6 +27,7 @@ const propTypes = {
)
})
),
displayUsername: PropTypes.string,
username: PropTypes.string
};
@ -130,6 +131,7 @@ class TimelineInner extends Component {
render() {
const {
completedMap,
displayUsername,
idToNameMap,
username,
sortedTimeline,
@ -171,9 +173,9 @@ class TimelineInner extends Component {
>
<Modal.Header closeButton={true}>
<Modal.Title id='contained-modal-title'>
{`${username}'s Solution to ${
idToNameMap.get(id).challengeTitle
}`}
{`${
displayUsername ? displayUsername : username
}'s Solution to ${idToNameMap.get(id).challengeTitle}`}
</Modal.Title>
</Modal.Header>
<Modal.Body>

View File

@ -20,6 +20,7 @@ import FullWidthRow from '../helpers/FullWidthRow';
import { isValidUsername } from '../../../../utils/validate';
const propTypes = {
displayUsername: PropTypes.string,
isValidUsername: PropTypes.bool,
submitNewUsername: PropTypes.func.isRequired,
username: PropTypes.string,
@ -54,6 +55,9 @@ class UsernameSettings extends Component {
this.state = {
isFormPristine: true,
formValue: props.username,
formDisplayValue: props.displayUsername
? props.displayUsername
: props.username,
characterValidation: { valid: false, error: null },
submitClicked: false,
isUserNew: tempUserRegex.test(props.username)
@ -84,21 +88,23 @@ class UsernameSettings extends Component {
e.preventDefault();
const { submitNewUsername } = this.props;
const {
formValue,
formDisplayValue,
characterValidation: { valid }
} = this.state;
return this.setState({ submitClicked: true }, () =>
valid ? submitNewUsername(formValue) : null
valid ? submitNewUsername(formDisplayValue) : null
);
}
handleChange(e) {
e.preventDefault();
const { username, validateUsername } = this.props;
const newValue = e.target.value.toLowerCase();
const newDisplayUsernameValue = e.target.value;
const newValue = newDisplayUsernameValue.toLowerCase();
return this.setState(
{
formDisplayValue: newDisplayUsernameValue,
formValue: newValue,
isFormPristine: username === newValue,
characterValidation: this.validateFormInput(newValue),
@ -160,7 +166,7 @@ class UsernameSettings extends Component {
render() {
const {
isFormPristine,
formValue,
formDisplayValue,
characterValidation: { valid, error },
submitClicked
} = this.state;
@ -177,7 +183,7 @@ class UsernameSettings extends Component {
<FormControl
name='username-settings'
onChange={this.handleChange}
value={formValue}
value={formDisplayValue}
/>
</FormGroup>
</FullWidthRow>

View File

@ -20,7 +20,11 @@ function* fetchSessionUser() {
} = yield call(getSessionUser);
const appUser = user[result] || {};
yield put(
fetchUserComplete({ user: appUser, username: result, sessionMeta })
fetchUserComplete({
user: appUser,
username: result,
sessionMeta
})
);
} catch (e) {
yield put(fetchUserError(e));

View File

@ -45,6 +45,7 @@ MongoClient.connect(MONGOHQ_URL, { useNewUrlParser: true }, function(
isBanned: false,
isCheater: false,
username: 'developmentuser',
displayUsername: 'DevelopmentUser',
about: '',
name: 'Development User',
location: '',