fix(settings): fix-up user objects for solutions (#17556)

This commit is contained in:
Stuart Taylor
2018-06-12 16:50:35 +01:00
committed by mrugesh mohapatra
parent e10a9fcda0
commit f54b7c07f5
11 changed files with 200 additions and 107 deletions

View File

@ -64,7 +64,7 @@ class Timeline extends PureComponent {
renderCompletion(completed) { renderCompletion(completed) {
const { idToNameMap } = this.props; const { idToNameMap } = this.props;
const { id, completedDate, solution, files } = completed; const { id, completedDate } = completed;
const challengeDashedName = idToNameMap[id]; const challengeDashedName = idToNameMap[id];
return ( return (
<tr key={ id }> <tr key={ id }>
@ -80,28 +80,7 @@ class Timeline extends PureComponent {
} }
</time> </time>
</td> </td>
<td> <td/>
{/* eslint-disable no-nested-ternary */
files ? (
<Button
block={ true }
bsStyle='primary'
onClick={ () => this.viewSolution(id) }
>
View&nbsp;Solution
</Button>
) : solution ? (
<Button
block={ true }
bsStyle='primary'
href={solution}
target='_blank'
>
View&nbsp;Solution
</Button>
) : ''
}
</td>
</tr> </tr>
); );
} }
@ -142,7 +121,7 @@ class Timeline extends PureComponent {
<thead> <thead>
<tr> <tr>
<th>Challenge</th> <th>Challenge</th>
<th className='text-center'>First Completed</th> <th className='text-center'>Completed</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -100,12 +100,15 @@ const propTypes = {
superBlock: PropTypes.string, superBlock: PropTypes.string,
updateUserBackend: PropTypes.func.isRequired, updateUserBackend: PropTypes.func.isRequired,
userProjects: PropTypes.objectOf( userProjects: PropTypes.objectOf(
PropTypes.objectOf(PropTypes.oneOfType( PropTypes.oneOfType(
[ [
// this is really messy, it should be addressed
// in completedChallenges migration to unify to one type
PropTypes.string, PropTypes.string,
PropTypes.arrayOf(PropTypes.object),
PropTypes.object PropTypes.object
] ]
)) )
), ),
username: PropTypes.string username: PropTypes.string
}; };
@ -113,7 +116,6 @@ const propTypes = {
class CertificationSettings extends PureComponent { class CertificationSettings extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.buildProjectForms = this.buildProjectForms.bind(this); this.buildProjectForms = this.buildProjectForms.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
} }

View File

@ -13,7 +13,12 @@ const jsFormPropTypes = {
claimCert: PropTypes.func.isRequired, claimCert: PropTypes.func.isRequired,
hardGoTo: PropTypes.func.isRequired, hardGoTo: PropTypes.func.isRequired,
isCertClaimed: PropTypes.bool, isCertClaimed: PropTypes.bool,
jsProjects: PropTypes.objectOf(PropTypes.object), jsProjects: PropTypes.objectOf(
PropTypes.oneOfType(
PropTypes.arrayOf(PropTypes.object),
PropTypes.string
)
),
projectBlockName: PropTypes.string, projectBlockName: PropTypes.string,
superBlock: PropTypes.string, superBlock: PropTypes.string,
username: PropTypes.string username: PropTypes.string

View File

@ -11,35 +11,59 @@ const prismLang = {
html: 'markup' html: 'markup'
}; };
function getContentString(file) {
return file.trim() || '// We do not have the solution to this challenge';
}
function SolutionViewer({ files }) { function SolutionViewer({ files }) {
const solutions = files && Array.isArray(files) ?
files.map(file => (
<Panel
bsStyle='primary'
className='solution-viewer'
header={ file.ext.toUpperCase() }
key={ file.ext }
>
<pre>
<code
className={ `language-${prismLang[file.ext]}` }
dangerouslySetInnerHTML={{
__html: Prism.highlight(
file.contents.trim(),
Prism.languages[prismLang[file.ext]]
)
}}
/>
</pre>
</Panel>
)) : (
<Panel
bsStyle='primary'
className='solution-viewer'
header='JS'
key={ files.slice(0, 10) }
>
<pre>
<code
className='language-markup'
dangerouslySetInnerHTML={{
__html: Prism.highlight(
getContentString(files),
Prism.languages.js
)
}}
/>
</pre>
</Panel>
)
;
return ( return (
<div> <div>
<Helmet> <Helmet>
<link href='/css/prism.css' rel='stylesheet' /> <link href='/css/prism.css' rel='stylesheet' />
</Helmet> </Helmet>
{ {
Object.keys(files) solutions
.map(key => files[key])
.map(file => (
<Panel
bsStyle='primary'
className='solution-viewer'
header={ file.ext.toUpperCase() }
key={ file.ext }
>
<pre>
<code
className={ `language-${prismLang[file.ext]}` }
dangerouslySetInnerHTML={{
__html: Prism.highlight(
file.contents.trim(),
Prism.languages[prismLang[file.ext]]
)
}}
/>
</pre>
</Panel>
))
} }
</div> </div>
); );
@ -47,7 +71,10 @@ function SolutionViewer({ files }) {
SolutionViewer.displayName = 'SolutionViewer'; SolutionViewer.displayName = 'SolutionViewer';
SolutionViewer.propTypes = { SolutionViewer.propTypes = {
files: PropTypes.object files: PropTypes.oneOfType(
PropTypes.arrayOf(PropTypes.objectOf(PropTypes.string)),
PropTypes.string
)
}; };
export default SolutionViewer; export default SolutionViewer;

View File

@ -21,7 +21,7 @@ export function buildUserProjectsMap(projectBlock, completedChallenges) {
if (completed) { if (completed) {
solution = 'solution' in completed ? solution = 'solution' in completed ?
completed.solution : completed.solution :
completed.files; completed.files || '';
} }
return { return {
...solutions, ...solutions,

View File

@ -1,3 +1,10 @@
/**
*
* Any ref to fixCompletedChallengesItem should be removed post
* a db migration to fix all completedChallenges
*
*/
import { Observable } from 'rx'; import { Observable } from 'rx';
import uuid from 'uuid/v4'; import uuid from 'uuid/v4';
import moment from 'moment'; import moment from 'moment';
@ -10,6 +17,7 @@ import _ from 'lodash';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { fixCompletedChallengeItem } from '../utils';
import { themes } from '../utils/themes'; import { themes } from '../utils/themes';
import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { saveUser, observeMethod } from '../../server/utils/rx.js';
import { blacklistedUsernames } from '../../server/utils/constants.js'; import { blacklistedUsernames } from '../../server/utils/constants.js';
@ -24,7 +32,7 @@ import {
publicUserProps publicUserProps
} from '../../server/utils/publicUserProps'; } from '../../server/utils/publicUserProps';
const debug = debugFactory('fcc:models:user'); const log = debugFactory('fcc:models:user');
const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
const createEmailError = redirectTo => wrapHandledError( const createEmailError = redirectTo => wrapHandledError(
@ -47,7 +55,9 @@ function buildCompletedChallengesUpdate(completedChallenges, project) {
const key = Object.keys(project)[0]; const key = Object.keys(project)[0];
const solutions = project[key]; const solutions = project[key];
const solutionKeys = Object.keys(solutions); const solutionKeys = Object.keys(solutions);
const currentCompletedChallenges = [ ...completedChallenges ]; const currentCompletedChallenges = [
...completedChallenges.map(fixCompletedChallengeItem)
];
const currentCompletedProjects = currentCompletedChallenges const currentCompletedProjects = currentCompletedChallenges
.filter(({id}) => solutionKeys.includes(id)); .filter(({id}) => solutionKeys.includes(id));
const now = Date.now(); const now = Date.now();
@ -59,7 +69,7 @@ function buildCompletedChallengesUpdate(completedChallenges, project) {
const isCurrentlyCompleted = indexOfCurrentId !== -1; const isCurrentlyCompleted = indexOfCurrentId !== -1;
if (isCurrentlyCompleted) { if (isCurrentlyCompleted) {
update[indexOfCurrentId] = { update[indexOfCurrentId] = {
..._.find(update, ({id}) => id === currentId).__data, ..._.find(update, ({id}) => id === currentId),
solution: solutions[currentId] solution: solutions[currentId]
}; };
} }
@ -298,7 +308,7 @@ module.exports = function(User) {
User.observe('before delete', function(ctx, next) { User.observe('before delete', function(ctx, next) {
const UserIdentity = User.app.models.UserIdentity; const UserIdentity = User.app.models.UserIdentity;
const UserCredential = User.app.models.UserCredential; const UserCredential = User.app.models.UserCredential;
debug('removing user', ctx.where); log('removing user', ctx.where);
var id = ctx.where && ctx.where.id ? ctx.where.id : null; var id = ctx.where && ctx.where.id ? ctx.where.id : null;
if (!id) { if (!id) {
return next(); return next();
@ -315,20 +325,20 @@ module.exports = function(User) {
) )
.subscribe( .subscribe(
function(data) { function(data) {
debug('deleted', data); log('deleted', data);
}, },
function(err) { function(err) {
debug('error deleting user %s stuff', id, err); log('error deleting user %s stuff', id, err);
next(err); next(err);
}, },
function() { function() {
debug('user stuff deleted for user %s', id); log('user stuff deleted for user %s', id);
next(); next();
} }
); );
}); });
debug('setting up user hooks'); log('setting up user hooks');
// overwrite lb confirm // overwrite lb confirm
User.confirm = function(uid, token, redirectTo) { User.confirm = function(uid, token, redirectTo) {
return this.findById(uid) return this.findById(uid)
@ -366,6 +376,17 @@ module.exports = function(User) {
}); });
}; };
function manualReload() {
this.reload((err, instance) => {
if (err) {
throw Error('failed to reload user instance');
}
Object.assign(this, instance);
log('user reloaded from db');
});
}
User.prototype.manualReload = manualReload;
User.prototype.loginByRequest = function loginByRequest(req, res) { User.prototype.loginByRequest = function loginByRequest(req, res) {
const { const {
query: { query: {
@ -423,7 +444,7 @@ module.exports = function(User) {
if (!username && (!email || !isEmail(email))) { if (!username && (!email || !isEmail(email))) {
return Promise.resolve(false); return Promise.resolve(false);
} }
debug('checking existence'); log('checking existence');
// check to see if username is on blacklist // check to see if username is on blacklist
if (username && blacklistedUsernames.indexOf(username) !== -1) { if (username && blacklistedUsernames.indexOf(username) !== -1) {
@ -436,7 +457,7 @@ module.exports = function(User) {
} else { } else {
where.email = email ? email.toLowerCase() : email; where.email = email ? email.toLowerCase() : email;
} }
debug('where', where); log('where', where);
return User.count(where) return User.count(where)
.then(count => count > 0); .then(count => count > 0);
}; };
@ -531,10 +552,7 @@ module.exports = function(User) {
} }
}) })
) )
.do(() => { .do(() => this.manualReload());
this.isDonating = true;
this.donationEmails = [ ...this.donationEmails, donation.email ];
});
}; };
User.prototype.getEncodedEmail = function getEncodedEmail(email) { User.prototype.getEncodedEmail = function getEncodedEmail(email) {
@ -677,9 +695,7 @@ module.exports = function(User) {
this.requestAuthEmail(false, newEmail), this.requestAuthEmail(false, newEmail),
(_, message) => message (_, message) => message
) )
.do(() => { .doOnNext(() => this.manualReload());
Object.assign(this, updateConfig);
});
}); });
} else { } else {
@ -715,15 +731,11 @@ module.exports = function(User) {
Observable.from(updates) Observable.from(updates)
.flatMap(({ flag, newValue }) => { .flatMap(({ flag, newValue }) => {
return Observable.fromPromise(User.doesExist(null, this.email)) return Observable.fromPromise(User.doesExist(null, this.email))
.flatMap(() => { .flatMap(() => this.update$({ [flag]: newValue }));
return this.update$({ [flag]: newValue })
.do(() => {
this[flag] = newValue;
});
});
}) })
); );
}) })
.doOnNext(() => this.manualReload())
.map(() => dedent` .map(() => dedent`
We have successfully updated your account. We have successfully updated your account.
`); `);
@ -748,9 +760,7 @@ module.exports = function(User) {
updatedPortfolio[pIndex] = { ...portfolioItem }; updatedPortfolio[pIndex] = { ...portfolioItem };
} }
return this.update$({ portfolio: updatedPortfolio }) return this.update$({ portfolio: updatedPortfolio })
.do(() => { .do(() => this.manualReload())
this.portfolio = updatedPortfolio;
})
.map(() => dedent` .map(() => dedent`
Your portfolio has been updated. Your portfolio has been updated.
`); `);
@ -779,7 +789,7 @@ module.exports = function(User) {
} }
return this.update$(updateData); return this.update$(updateData);
}) })
.do(() => Object.assign(this, updateData)) .doOnNext(() => this.manualReload() )
.map(() => dedent` .map(() => dedent`
Your projects have been updated. Your projects have been updated.
`); `);
@ -795,7 +805,7 @@ module.exports = function(User) {
}; };
return this.update$(update) return this.update$(update)
.do(() => Object.assign(this, update)) .doOnNext(() => this.manualReload())
.map(() => dedent` .map(() => dedent`
Your privacy settings have been updated. Your privacy settings have been updated.
`); `);
@ -824,9 +834,7 @@ module.exports = function(User) {
} }
return this.update$({ username: newUsername }) return this.update$({ username: newUsername })
.do(() => { .do(() => this.manualReload())
this.username = newUsername;
})
.map(() => dedent` .map(() => dedent`
Your username has been updated successfully. Your username has been updated successfully.
`); `);
@ -955,7 +963,7 @@ module.exports = function(User) {
}, },
(e) => cb(e, null, dev ? { giver, receiver, data } : null), (e) => cb(e, null, dev ? { giver, receiver, data } : null),
() => { () => {
debug('brownie points assigned completed'); log('brownie points assigned completed');
} }
); );
}; };
@ -1014,7 +1022,9 @@ module.exports = function(User) {
); );
return Promise.reject(err); return Promise.reject(err);
} }
return this.update$({ theme }).toPromise(); return this.update$({ theme })
.doOnNext(() => this.manualReload())
.toPromise();
}; };
// deprecated. remove once live // deprecated. remove once live

View File

@ -167,25 +167,37 @@
"description": "Camper is information security and quality assurance certified", "description": "Camper is information security and quality assurance certified",
"default": false "default": false
}, },
"completedChallengeCount": {
"type": "number",
"description": "generated per request, not held in db"
},
"completedCertCount": {
"type": "number",
"description": "generated per request, not held in db"
},
"completedProjectCount": {
"type": "number",
"description": "generated per request, not held in db"
},
"completedChallenges": { "completedChallenges": {
"type": [ "type": [
{ {
"completedDate": "number", "completedDate": "number",
"id": "string", "id": "string",
"solution": "string", "solution": "string",
"challengeType": "number" "githubLink": "string",
"challengeType": "number",
"files": {
"type": [
{
"contents": {
"type": "string",
"default": ""
},
"ext": {
"type": "string"
},
"path": {
"type": "string"
},
"name": {
"type": "string"
},
"key": {
"type": "string"
}
}
],
"default": []
}
} }
], ],
"default": [] "default": []

View File

@ -1,3 +1,5 @@
import { pick } from 'lodash';
export function dashify(str) { export function dashify(str) {
return ('' + str) return ('' + str)
.toLowerCase() .toLowerCase()
@ -8,3 +10,8 @@ export function dashify(str) {
// todo: unify with server/utils/index.js:dasherize // todo: unify with server/utils/index.js:dasherize
const dasherize = dashify; const dasherize = dashify;
export { dasherize }; export { dasherize };
export const fixCompletedChallengeItem = obj => pick(
obj,
[ 'id', 'completedDate', 'solution', 'githubLink', 'challengeType', 'files' ]
);

View File

@ -355,8 +355,6 @@ export default function certificate(app) {
) )
.subscribe( .subscribe(
user => { user => {
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`;
if (!user) { if (!user) {
req.flash( req.flash(
'danger', 'danger',
@ -364,6 +362,8 @@ export default function certificate(app) {
); );
return res.redirect('/'); return res.redirect('/');
} }
const { isLocked, showCerts } = user.profileUI;
const profile = `/portfolio/${user.username}`;
if (!user.name) { if (!user.name) {
req.flash( req.flash(
@ -415,7 +415,7 @@ export default function certificate(app) {
} }
if (user[certType]) { if (user[certType]) {
const { completedChallenges = {} } = user; const { completedChallenges = [] } = user;
const { completedDate = new Date() } = _.find( const { completedDate = new Date() } = _.find(
completedChallenges, ({ id }) => certId === id completedChallenges, ({ id }) => certId === id
) || {}; ) || {};

View File

@ -1,3 +1,10 @@
/**
*
* Any ref to fixCompletedChallengesItem should be removed post
* a db migration to fix all completedChallenges
*
*/
import _ from 'lodash'; import _ from 'lodash';
import debug from 'debug'; import debug from 'debug';
import accepts from 'accepts'; import accepts from 'accepts';
@ -8,17 +15,49 @@ import { getChallengeById, cachedMap } from '../utils/map';
import { dasherize } from '../utils'; import { dasherize } from '../utils';
import pathMigrations from '../resources/pathMigration.json'; import pathMigrations from '../resources/pathMigration.json';
import { fixCompletedChallengeItem } from '../../common/utils';
const log = debug('fcc:boot:challenges'); const log = debug('fcc:boot:challenges');
const learnURL = 'https://learn.freecodecamp.org'; const learnURL = 'https://learn.freecodecamp.org';
const jsProjects = [
'aaa48de84e1ecc7c742e1124',
'a7f4d8f2483413a6ce226cac',
'56533eb9ac21ba0edf2244e2',
'aff0395860f5d3034dc0bfc9',
'aa2e6f85cab2ab736c9a9b24'
];
function buildUserUpdate( function buildUserUpdate(
user, user,
challengeId, challengeId,
completedChallenge, _completedChallenge,
timezone timezone
) { ) {
const { files } = _completedChallenge;
let completedChallenge = {};
if (jsProjects.includes(challengeId)) {
completedChallenge = {
..._completedChallenge,
files: Object.keys(files)
.map(key => files[key])
.map(file => _.pick(
file,
[
'contents',
'key',
'index',
'name',
'path',
'ext'
]
))
};
} else {
completedChallenge = _.omit(_completedChallenge, ['files']);
}
let finalChallenge; let finalChallenge;
const updateData = {}; const updateData = {};
const { timezone: userTimezone, completedChallenges = [] } = user; const { timezone: userTimezone, completedChallenges = [] } = user;
@ -46,7 +85,7 @@ function buildUserUpdate(
updateData.$set = { updateData.$set = {
completedChallenges: _.uniqBy( completedChallenges: _.uniqBy(
[finalChallenge, ...completedChallenges], [finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)],
'id' 'id'
) )
}; };
@ -167,6 +206,7 @@ export default function(app) {
const points = alreadyCompleted ? user.points : user.points + 1; const points = alreadyCompleted ? user.points : user.points + 1;
return user.update$(updateData) return user.update$(updateData)
.doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.map(() => { .map(() => {
if (type === 'json') { if (type === 'json') {
@ -199,7 +239,7 @@ export default function(app) {
return req.user.getCompletedChallenges$() return req.user.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { id, solution, timezone } = req.body; const { id, solution, timezone, files } = req.body;
const { const {
alreadyCompleted, alreadyCompleted,
@ -207,7 +247,7 @@ export default function(app) {
} = buildUserUpdate( } = buildUserUpdate(
req.user, req.user,
id, id,
{ id, solution, completedDate }, { id, solution, completedDate, files },
timezone timezone
); );
@ -250,7 +290,7 @@ export default function(app) {
const completedChallenge = _.pick( const completedChallenge = _.pick(
body, body,
[ 'id', 'solution', 'githubLink', 'challengeType' ] [ 'id', 'solution', 'githubLink', 'challengeType', 'files' ]
); );
completedChallenge.completedDate = Date.now(); completedChallenge.completedDate = Date.now();
@ -278,6 +318,7 @@ export default function(app) {
} = buildUserUpdate(user, completedChallenge.id, completedChallenge); } = buildUserUpdate(user, completedChallenge.id, completedChallenge);
return user.update$(updateData) return user.update$(updateData)
.doOnNext(() => user.manualReload())
.doOnNext(({ count }) => log('%s documents updated', count)) .doOnNext(({ count }) => log('%s documents updated', count))
.doOnNext(() => { .doOnNext(() => {
if (type === 'json') { if (type === 'json') {

View File

@ -1,3 +1,10 @@
/**
*
* Any ref to fixCompletedChallengesItem should be removed post
* a db migration to fix all completedChallenges
*
*/
import { Observable } from 'rx'; import { Observable } from 'rx';
import _ from 'lodash'; import _ from 'lodash';
@ -6,6 +13,7 @@ import {
normaliseUserFields, normaliseUserFields,
userPropsForSession userPropsForSession
} from '../utils/publicUserProps'; } from '../utils/publicUserProps';
import { fixCompletedChallengeItem } from '../../common/utils';
export default function userServices() { export default function userServices() {
return { return {
@ -32,7 +40,9 @@ export default function userServices() {
.map(({ completedChallenges, progress }) => ({ .map(({ completedChallenges, progress }) => ({
...queryUser.toJSON(), ...queryUser.toJSON(),
...progress, ...progress,
completedChallenges completedChallenges: completedChallenges.map(
fixCompletedChallengeItem
)
})) }))
.map( .map(
user => ({ user => ({