Chore: Update User model (#17171)

* fix(logs): Remove console.log's

* chore(challengeMap): challengeMap -> completedChallenges

* chore(userModel): Update user model

* feat(userIDs): Add user ident fields

* chore(github): Remove more refs to github data
This commit is contained in:
Stuart Taylor
2018-05-15 14:56:26 +01:00
committed by mrugesh mohapatra
parent 156ea1af76
commit f916204ba5
23 changed files with 233 additions and 742 deletions

View File

@ -15,7 +15,7 @@ export default function fetchMapUiEpic(
_, _,
{ services } { services }
) { ) {
return actions.do(console.log)::ofType( return actions::ofType(
appTypes.appMounted, appTypes.appMounted,
types.fetchMapUi.start types.fetchMapUi.start
) )
@ -25,7 +25,6 @@ export default function fetchMapUiEpic(
}; };
return services.readService$(options) return services.readService$(options)
.retry(3) .retry(3)
.do(console.info)
.map(({ entities, ...res }) => ({ .map(({ entities, ...res }) => ({
entities: shapeChallenges( entities: shapeChallenges(
entities, entities,

View File

@ -12,7 +12,7 @@ import { userByNameSelector } from '../../../redux';
const propTypes = { const propTypes = {
email: PropTypes.string, email: PropTypes.string,
githubURL: PropTypes.string, githubProfile: PropTypes.string,
isGithub: PropTypes.bool, isGithub: PropTypes.bool,
isLinkedIn: PropTypes.bool, isLinkedIn: PropTypes.bool,
isTwitter: PropTypes.bool, isTwitter: PropTypes.bool,
@ -26,7 +26,7 @@ const propTypes = {
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userByNameSelector, userByNameSelector,
({ ({
githubURL, githubProfile,
isLinkedIn, isLinkedIn,
isGithub, isGithub,
isTwitter, isTwitter,
@ -35,7 +35,7 @@ const mapStateToProps = createSelector(
twitter, twitter,
website website
}) => ({ }) => ({
githubURL, githubProfile,
isLinkedIn, isLinkedIn,
isGithub, isGithub,
isTwitter, isTwitter,
@ -97,7 +97,7 @@ function TwitterIcon(handle) {
function SocialIcons(props) { function SocialIcons(props) {
const { const {
githubURL, githubProfile,
isLinkedIn, isLinkedIn,
isGithub, isGithub,
isTwitter, isTwitter,
@ -121,7 +121,7 @@ function SocialIcons(props) {
isLinkedIn ? LinkedInIcon(linkedIn) : null isLinkedIn ? LinkedInIcon(linkedIn) : null
} }
{ {
isGithub ? githubIcon(githubURL) : null isGithub ? githubIcon(githubProfile) : null
} }
{ {
isWebsite ? WebsiteIcon(website) : null isWebsite ? WebsiteIcon(website) : null

View File

@ -5,8 +5,6 @@ import { connect } from 'react-redux';
import format from 'date-fns/format'; import format from 'date-fns/format';
import { reverse, sortBy } from 'lodash'; import { reverse, sortBy } from 'lodash';
import { import {
Button,
Modal,
Table Table
} from 'react-bootstrap'; } from 'react-bootstrap';
@ -16,48 +14,40 @@ import { homeURL } from '../../../../utils/constantStrings.json';
import blockNameify from '../../../utils/blockNameify'; import blockNameify from '../../../utils/blockNameify';
import { FullWidthRow } from '../../../helperComponents'; import { FullWidthRow } from '../../../helperComponents';
import { Link } from '../../../Router'; import { Link } from '../../../Router';
import SolutionViewer from '../../Settings/components/SolutionViewer.jsx';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
challengeIdToNameMapSelector, challengeIdToNameMapSelector,
userByNameSelector, userByNameSelector,
( (
idToNameMap, idToNameMap,
{ challengeMap: completedMap = {}, username } { completedChallenges: completedMap = [] }
) => ({ ) => ({
completedMap, completedMap,
idToNameMap, idToNameMap
username
}) })
); );
const propTypes = { const propTypes = {
completedMap: PropTypes.shape({ completedMap: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
completedDate: PropTypes.number, completedDate: PropTypes.number,
lastUpdated: PropTypes.number challengeType: PropTypes.number
}), })
idToNameMap: PropTypes.objectOf(PropTypes.string), ),
username: PropTypes.string idToNameMap: PropTypes.objectOf(PropTypes.string)
}; };
class Timeline extends PureComponent { class Timeline extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
solutionToView: null,
solutionOpen: false
};
this.closeSolution = this.closeSolution.bind(this);
this.renderCompletion = this.renderCompletion.bind(this); this.renderCompletion = this.renderCompletion.bind(this);
this.viewSolution = this.viewSolution.bind(this);
} }
renderCompletion(completed) { renderCompletion(completed) {
const { idToNameMap } = this.props; const { idToNameMap } = this.props;
const { id, completedDate, lastUpdated, files } = completed; const { id, completedDate } = completed;
return ( return (
<tr key={ id }> <tr key={ id }>
<td>{ blockNameify(idToNameMap[id]) }</td> <td>{ blockNameify(idToNameMap[id]) }</td>
@ -68,53 +58,12 @@ class Timeline extends PureComponent {
} }
</time> </time>
</td> </td>
<td>
{
lastUpdated ?
<time dateTime={ format(lastUpdated, 'YYYY-MM-DDTHH:MM:SSZ') }>
{
format(lastUpdated, 'MMMM DD YYYY')
}
</time> :
''
}
</td>
<td>
{
files ?
<Button
block={ true }
bsStyle='primary'
onClick={ () => this.viewSolution(id) }
>
View&nbsp;Solution
</Button> :
''
}
</td>
</tr> </tr>
); );
} }
viewSolution(id) {
this.setState(state => ({
...state,
solutionToView: id,
solutionOpen: true
}));
}
closeSolution() {
this.setState(state => ({
...state,
solutionToView: null,
solutionOpen: false
}));
}
render() { render() {
const { completedMap, idToNameMap, username } = this.props; const { completedMap, idToNameMap } = this.props;
const { solutionToView: id, solutionOpen } = this.state;
if (!Object.keys(idToNameMap).length) { if (!Object.keys(idToNameMap).length) {
return null; return null;
} }
@ -122,7 +71,7 @@ class Timeline extends PureComponent {
<FullWidthRow> <FullWidthRow>
<h2 className='text-center'>Timeline</h2> <h2 className='text-center'>Timeline</h2>
{ {
Object.keys(completedMap).length === 0 ? completedMap.length === 0 ?
<p className='text-center'> <p className='text-center'>
No challenges have been completed yet.&nbsp; No challenges have been completed yet.&nbsp;
<Link to={ homeURL }> <Link to={ homeURL }>
@ -134,45 +83,21 @@ class Timeline extends PureComponent {
<tr> <tr>
<th>Challenge</th> <th>Challenge</th>
<th>First Completed</th> <th>First Completed</th>
<th>Last Changed</th>
<th />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
reverse( reverse(
sortBy( sortBy(
Object.keys(completedMap) completedMap,
.filter(key => key in idToNameMap)
.map(key => completedMap[key]),
[ 'completedDate' ] [ 'completedDate' ]
) ).filter(({id}) => id in idToNameMap)
) )
.map(this.renderCompletion) .map(this.renderCompletion)
} }
</tbody> </tbody>
</Table> </Table>
} }
{
id &&
<Modal
aria-labelledby='contained-modal-title'
onHide={this.closeSolution}
show={ solutionOpen }
>
<Modal.Header closeButton={ true }>
<Modal.Title id='contained-modal-title'>
{ `${username}'s Solution to ${blockNameify(idToNameMap[id])}` }
</Modal.Title>
</Modal.Header>
<Modal.Body>
<SolutionViewer files={ completedMap[id].files }/>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.closeSolution}>Close</Button>
</Modal.Footer>
</Modal>
}
</FullWidthRow> </FullWidthRow>
); );
} }

View File

@ -27,7 +27,7 @@ const mapStateToProps = createSelector(
projectsSelector, projectsSelector,
( (
{ {
challengeMap, completedChallenges,
isRespWebDesignCert, isRespWebDesignCert,
is2018DataVisCert, is2018DataVisCert,
isFrontEndLibsCert, isFrontEndLibsCert,
@ -45,7 +45,7 @@ const mapStateToProps = createSelector(
legacyProjects: projects.filter(p => p.superBlock.includes('legacy')), legacyProjects: projects.filter(p => p.superBlock.includes('legacy')),
modernProjects: projects.filter(p => !p.superBlock.includes('legacy')), modernProjects: projects.filter(p => !p.superBlock.includes('legacy')),
userProjects: projects userProjects: projects
.map(block => buildUserProjectsMap(block, challengeMap)) .map(block => buildUserProjectsMap(block, completedChallenges))
.reduce((projects, current) => ({ .reduce((projects, current) => ({
...projects, ...projects,
...current ...current

View File

@ -13,13 +13,13 @@ import { updateUserBackend } from '../redux';
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
userSelector, userSelector,
({ ({
githubURL = '', githubProfile = '',
linkedin = '', linkedin = '',
twitter = '', twitter = '',
website = '' website = ''
}) => ({ }) => ({
initialValues: { initialValues: {
githubURL, githubProfile,
linkedin, linkedin,
twitter, twitter,
website website
@ -27,7 +27,7 @@ const mapStateToProps = createSelector(
}) })
); );
const formFields = [ 'githubURL', 'linkedin', 'twitter', 'website' ]; const formFields = [ 'githubProfile', 'linkedin', 'twitter', 'website' ];
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return bindActionCreators({ return bindActionCreators({
@ -37,7 +37,7 @@ function mapDispatchToProps(dispatch) {
const propTypes = { const propTypes = {
fields: PropTypes.object, fields: PropTypes.object,
githubURL: PropTypes.string, githubProfile: PropTypes.string,
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
linkedin: PropTypes.string, linkedin: PropTypes.string,
twitter: PropTypes.string, twitter: PropTypes.string,

View File

@ -36,7 +36,7 @@ export const types = createTypes([
createAsyncTypes('updateMyPortfolio'), createAsyncTypes('updateMyPortfolio'),
'updateNewUsernameValidity', 'updateNewUsernameValidity',
createAsyncTypes('validateUsername'), createAsyncTypes('validateUsername'),
createAsyncTypes('refetchChallengeMap'), createAsyncTypes('refetchCompletedChallenges'),
createAsyncTypes('deleteAccount'), createAsyncTypes('deleteAccount'),
createAsyncTypes('resetProgress'), createAsyncTypes('resetProgress'),
@ -103,8 +103,8 @@ export const updateNewUsernameValidity = createAction(
types.updateNewUsernameValidity types.updateNewUsernameValidity
); );
export const refetchChallengeMap = createAction( export const refetchCompletedChallenges = createAction(
types.refetchChallengeMap.start types.refetchCompletedChallenges.start
); );
export const validateUsername = createAction(types.validateUsername.start); export const validateUsername = createAction(types.validateUsername.start);

View File

@ -3,7 +3,7 @@ import { combineEpics, ofType } from 'redux-epic';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { import {
types, types,
refetchChallengeMap, refetchCompletedChallenges,
updateUserBackendComplete, updateUserBackendComplete,
updateMyPortfolioComplete updateMyPortfolioComplete
} from './'; } from './';
@ -90,7 +90,7 @@ function backendUserUpdateEpic(actions$, { getState }) {
const complete = actions$::ofType(types.updateUserBackend.complete) const complete = actions$::ofType(types.updateUserBackend.complete)
.flatMap(({ payload: { message } }) => Observable.if( .flatMap(({ payload: { message } }) => Observable.if(
() => message.includes('project'), () => message.includes('project'),
Observable.of(refetchChallengeMap(), makeToast({ message })), Observable.of(refetchCompletedChallenges(), makeToast({ message })),
Observable.of(makeToast({ message })) Observable.of(makeToast({ message }))
) )
); );
@ -98,16 +98,16 @@ function backendUserUpdateEpic(actions$, { getState }) {
return Observable.merge(server, optimistic, complete); return Observable.merge(server, optimistic, complete);
} }
function refetchChallengeMapEpic(actions$, { getState }) { function refetchCompletedChallengesEpic(actions$, { getState }) {
return actions$::ofType(types.refetchChallengeMap.start) return actions$::ofType(types.refetchCompletedChallenges.start)
.flatMap(() => { .flatMap(() => {
const { const {
app: { csrfToken: _csrf } app: { csrfToken: _csrf }
} = getState(); } = getState();
const username = usernameSelector(getState()); const username = usernameSelector(getState());
return postJSON$('/refetch-user-challenge-map', { _csrf }) return postJSON$('/refetch-user-completed-challenges', { _csrf })
.map(({ challengeMap }) => .map(({ completedChallenges }) =>
updateMultipleUserFlags({ username, flags: { challengeMap } }) updateMultipleUserFlags({ username, flags: { completedChallenges } })
) )
.catch(createErrorObservable); .catch(createErrorObservable);
}); });
@ -190,7 +190,7 @@ function updateUserEmailEpic(actions, { getState }) {
export default combineEpics( export default combineEpics(
backendUserUpdateEpic, backendUserUpdateEpic,
refetchChallengeMapEpic, refetchCompletedChallengesEpic,
updateMyPortfolioEpic, updateMyPortfolioEpic,
updateUserEmailEpic updateUserEmailEpic
); );

View File

@ -1,6 +1,8 @@
import { find } from 'lodash';
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures'; export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
export function buildUserProjectsMap(projectBlock, challengeMap) { export function buildUserProjectsMap(projectBlock, completedChallenges) {
const { const {
challenges, challenges,
superBlock superBlock
@ -8,7 +10,10 @@ export function buildUserProjectsMap(projectBlock, challengeMap) {
return { return {
[superBlock]: challenges.reduce((solutions, current) => { [superBlock]: challenges.reduce((solutions, current) => {
const { id } = current; const { id } = current;
const completed = challengeMap[id]; const completed = find(
completedChallenges,
({ id: completedId }) => completedId === id
);
let solution = ''; let solution = '';
if (superBlock === jsProjectSuperBlock) { if (superBlock === jsProjectSuperBlock) {
solution = {}; solution = {};

View File

@ -1,5 +1,5 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import uuid from 'uuid'; import uuid from 'uuid/v4';
import moment from 'moment'; import moment from 'moment';
import dedent from 'dedent'; import dedent from 'dedent';
import debugFactory from 'debug'; import debugFactory from 'debug';
@ -7,6 +7,7 @@ import { isEmail } from 'validator';
import path from 'path'; import path from 'path';
import loopback from 'loopback'; import loopback from 'loopback';
import _ from 'lodash'; import _ from 'lodash';
import { ObjectId } from 'mongodb';
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';
@ -41,51 +42,51 @@ function destroyAll(id, Model) {
)({ userId: id }); )({ userId: id });
} }
function buildChallengeMapUpdate(challengeMap, project) { 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 currentChallengeMap = { ...challengeMap }; const currentCompletedChallenges = [ ...completedChallenges ];
const currentCompletedProjects = _.pick( const currentCompletedProjects = currentCompletedChallenges
currentChallengeMap, .filter(({id}) => Object.keys(solutions).includes(id));
Object.keys(solutions)
);
const now = Date.now(); const now = Date.now();
const update = Object.keys(solutions).reduce((update, currentId) => { const update = Object.keys(solutions).reduce((update, currentId) => {
const indexOfCurrentId = _.findIndex(
currentCompletedProjects,
({id}) => id === currentId
);
const isCurrentlyCompleted = indexOfCurrentId !== -1;
if ( if (
currentId in currentCompletedProjects && isCurrentlyCompleted &&
currentCompletedProjects[currentId].solution !== solutions[currentId] currentCompletedProjects[
indexOfCurrentId
].solution !== solutions[currentId]
) { ) {
return { update[indexOfCurrentId] = {
...update, ...update[indexOfCurrentId],
[currentId]: { solution: solutions[currentId]
...currentCompletedProjects[currentId],
solution: solutions[currentId],
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
}
}; };
} }
if (!(currentId in currentCompletedProjects)) { if (!isCurrentlyCompleted) {
return { return [
...update, ...update,
[currentId]: { {
id: currentId, id: currentId,
solution: solutions[currentId], solution: solutions[currentId],
challengeType: 3, challengeType: 3,
completedDate: now, completedDate: now
numOfAttempts: 1
} }
}; ];
} }
return update; return update;
}, {}); }, currentCompletedProjects);
const updatedExisting = { const updatedExisting = _.uniqBy(
...currentCompletedProjects, [
...update ...update,
}; ...currentCompletedChallenges
return { ],
...currentChallengeMap, 'id'
...updatedExisting );
}; return updatedExisting;
} }
function isTheSame(val1, val2) { function isTheSame(val1, val2) {
@ -219,7 +220,14 @@ module.exports = function(User) {
// assign random username to new users // assign random username to new users
// actual usernames will come from github // actual usernames will come from github
// use full uuid to ensure uniqueness // use full uuid to ensure uniqueness
user.username = 'fcc' + uuid.v4(); user.username = 'fcc' + uuid();
if (!user.externalId) {
user.externalId = uuid();
}
if (!user.unsubscribeId) {
user.unsubscribeId = new ObjectId();
}
if (!user.progressTimestamps) { if (!user.progressTimestamps) {
user.progressTimestamps = []; user.progressTimestamps = [];
@ -269,7 +277,15 @@ module.exports = function(User) {
} }
if (user.progressTimestamps.length === 0) { if (user.progressTimestamps.length === 0) {
user.progressTimestamps.push({ timestamp: Date.now() }); user.progressTimestamps.push(Date.now());
}
if (!user.externalId) {
user.externalId = uuid();
}
if (!user.unsubscribeId) {
user.unsubscribeId = new ObjectId();
} }
}) })
.ignoreElements(); .ignoreElements();
@ -645,9 +661,11 @@ module.exports = function(User) {
}); });
}; };
User.prototype.requestChallengeMap = function requestChallengeMap() { function requestCompletedChallenges() {
return this.getChallengeMap$(); return this.getCompletedChallenges$();
}; }
User.prototype.requestCompletedChallenges = requestCompletedChallenges;
User.prototype.requestUpdateFlags = function requestUpdateFlags(values) { User.prototype.requestUpdateFlags = function requestUpdateFlags(values) {
const flagsToCheck = Object.keys(values); const flagsToCheck = Object.keys(values);
@ -715,10 +733,10 @@ module.exports = function(User) {
User.prototype.updateMyProjects = function updateMyProjects(project) { User.prototype.updateMyProjects = function updateMyProjects(project) {
const updateData = {}; const updateData = {};
return this.getChallengeMap$() return this.getCompletedChallenges$()
.flatMap(challengeMap => { .flatMap(completedChallenges => {
updateData.challengeMap = buildChallengeMapUpdate( updateData.completedChallenges = buildCompletedChallengesUpdate(
challengeMap, completedChallenges,
project project
); );
return this.update$(updateData); return this.update$(updateData);
@ -767,18 +785,18 @@ module.exports = function(User) {
if (!user) { if (!user) {
return Observable.of({}); return Observable.of({});
} }
const { challengeMap, progressTimestamps, timezone } = user; const { completedChallenges, progressTimestamps, timezone } = user;
return Observable.of({ return Observable.of({
entities: { entities: {
user: { user: {
[user.username]: { [user.username]: {
..._.pick(user, publicUserProps), ..._.pick(user, publicUserProps),
isGithub: !!user.githubURL, isGithub: !!user.githubProfile,
isLinkedIn: !!user.linkedIn, isLinkedIn: !!user.linkedIn,
isTwitter: !!user.twitter, isTwitter: !!user.twitter,
isWebsite: !!user.website, isWebsite: !!user.website,
points: progressTimestamps.length, points: progressTimestamps.length,
challengeMap, completedChallenges,
...getProgress(progressTimestamps, timezone), ...getProgress(progressTimestamps, timezone),
...normaliseUserFields(user) ...normaliseUserFields(user)
} }
@ -1000,16 +1018,16 @@ module.exports = function(User) {
return user.progressTimestamps; return user.progressTimestamps;
}); });
}; };
User.prototype.getChallengeMap$ = function getChallengeMap$() { User.prototype.getCompletedChallenges$ = function getCompletedChallenges$() {
const id = this.getId(); const id = this.getId();
const filter = { const filter = {
where: { id }, where: { id },
fields: { challengeMap: true } fields: { completedChallenges: true }
}; };
return this.constructor.findOne$(filter) return this.constructor.findOne$(filter)
.map(user => { .map(user => {
this.challengeMap = user.challengeMap; this.completedChallenges = user.completedChallenges;
return user.challengeMap; return user.completedChallenges;
}); });
}; };

View File

@ -29,8 +29,17 @@
"emailAuthLinkTTL": { "emailAuthLinkTTL": {
"type": "date" "type": "date"
}, },
"externalId": {
"type": "string",
"description": "A uuid/v4 used to identify user accounts"
},
"unsubscribeId": {
"type": "string",
"description": "An ObjectId used to unsubscribe users from the mailing list(s)"
},
"password": { "password": {
"type": "string" "type": "string",
"description": "No longer used for new accounts"
}, },
"progressTimestamps": { "progressTimestamps": {
"type": "array", "type": "array",
@ -46,35 +55,15 @@
"description": "Users who are confirmed to have broken academic honesty policy are marked as cheaters", "description": "Users who are confirmed to have broken academic honesty policy are marked as cheaters",
"default": false "default": false
}, },
"isGithubCool": { "githubProfile": {
"type": "boolean",
"default": false
},
"githubId": {
"type": "string" "type": "string"
}, },
"githubURL": {
"type": "string"
},
"githubEmail": {
"type": "string"
},
"joinedGithubOn": {
"type": "date"
},
"website": { "website": {
"type": "string" "type": "string"
}, },
"githubProfile": {
"type": "string"
},
"_csrf": { "_csrf": {
"type": "string" "type": "string"
}, },
"isMigrationGrandfathered": {
"type": "boolean",
"default": false
},
"username": { "username": {
"type": "string", "type": "string",
"index": { "index": {
@ -114,22 +103,6 @@
"twitter": { "twitter": {
"type": "string" "type": "string"
}, },
"currentStreak": {
"type": "number",
"default": 0
},
"longestStreak": {
"type": "number",
"default": 0
},
"sendMonthlyEmail": {
"type": "boolean",
"default": true
},
"sendNotificationEmail": {
"type": "boolean",
"default": true
},
"sendQuincyEmail": { "sendQuincyEmail": {
"type": "boolean", "type": "boolean",
"default": true "default": true
@ -144,15 +117,6 @@
"description": "The challenge last visited by the user", "description": "The challenge last visited by the user",
"default": "" "default": ""
}, },
"currentChallenge": {
"type": {},
"description": "deprecated"
},
"isUniqMigrated": {
"type": "boolean",
"description": "Campers completedChallenges array is free of duplicates",
"default": false
},
"isHonest": { "isHonest": {
"type": "boolean", "type": "boolean",
"description": "Camper has signed academic honesty policy", "description": "Camper has signed academic honesty policy",
@ -208,35 +172,25 @@
"description": "Camper is information security and quality assurance certified", "description": "Camper is information security and quality assurance certified",
"default": false "default": false
}, },
"isChallengeMapMigrated": {
"type": "boolean",
"description": "Migrate completedChallenges array to challenge map",
"default": false
},
"challengeMap": {
"type": "object",
"description": "A map by ID of all the user completed challenges",
"default": {}
},
"completedChallengeCount": { "completedChallengeCount": {
"type": "number" "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",
"lastUpdated": "number",
"numOfAttempts": "number",
"id": "string", "id": "string",
"name": "string",
"completedWith": "string",
"solution": "string", "solution": "string",
"githubLink": "string", "challengeType": "number"
"verified": "boolean",
"challengeType": {
"type": "number",
"default": 0
}
} }
], ],
"default": [] "default": []
@ -249,9 +203,6 @@
"type": "number", "type": "number",
"index": true "index": true
}, },
"tshirtVote": {
"type": "number"
},
"timezone": { "timezone": {
"type": "string" "type": "string"
}, },

View File

@ -13,7 +13,7 @@ MongoClient.connect(secrets.db, function(err, database) {
{$match: { 'email': { $exists: true } } }, {$match: { 'email': { $exists: true } } },
{$match: { 'email': { $ne: '' } } }, {$match: { 'email': { $ne: '' } } },
{$match: { 'email': { $ne: null } } }, {$match: { 'email': { $ne: null } } },
{$match: { 'sendMonthlyEmail': true } }, {$match: { 'sendQuincyEmail': true } },
{$match: { 'email': { $not: /(test|fake)/i } } }, {$match: { 'email': { $not: /(test|fake)/i } } },
{$group: { '_id': 1, 'emails': {$addToSet: '$email' } } } {$group: { '_id': 1, 'emails': {$addToSet: '$email' } } }
], function(err, results) { ], function(err, results) {

View File

@ -1,231 +0,0 @@
/* eslint-disable no-process-exit */
require('dotenv').load();
var Rx = require('rx'),
uuid = require('uuid'),
assign = require('lodash/object/assign'),
mongodb = require('mongodb'),
secrets = require('../config/secrets');
const batchSize = 20;
var MongoClient = mongodb.MongoClient;
Rx.config.longStackSupport = true;
var providers = [
'facebook',
'twitter',
'google',
'github',
'linkedin'
];
// create async console.logs
function debug() {
var args = [].slice.call(arguments);
process.nextTick(function() {
console.log.apply(console, args);
});
}
function createConnection(URI) {
return Rx.Observable.create(function(observer) {
debug('connecting to db');
MongoClient.connect(URI, function(err, database) {
if (err) {
return observer.onError(err);
}
debug('db connected');
observer.onNext(database);
observer.onCompleted();
});
});
}
function createQuery(db, collection, options, batchSize) {
return Rx.Observable.create(function(observer) {
var cursor = db.collection(collection).find({}, options);
cursor.batchSize(batchSize || 20);
// Cursor.each will yield all doc from a batch in the same tick,
// or schedule getting next batch on nextTick
debug('opening cursor for %s', collection);
cursor.each(function(err, doc) {
if (err) {
return observer.onError(err);
}
if (!doc) {
console.log('onCompleted');
return observer.onCompleted();
}
observer.onNext(doc);
});
return Rx.Disposable.create(function() {
debug('closing cursor for %s', collection);
cursor.close();
});
});
}
function getUserCount(db) {
return Rx.Observable.create(function(observer) {
var cursor = db.collection('users').count(function(err, count) {
if (err) {
return observer.onError(err);
}
observer.onNext(count);
observer.onCompleted();
return Rx.Disposable.create(function() {
debug('closing user count');
cursor.close();
});
});
});
}
function insertMany(db, collection, users, options) {
return Rx.Observable.create(function(observer) {
db.collection(collection).insertMany(users, options, function(err) {
if (err) {
return observer.onError(err);
}
observer.onNext();
observer.onCompleted();
});
});
}
var count = 0;
// will supply our db object
var dbObservable = createConnection(secrets.db).replay();
var totalUser = dbObservable
.flatMap(function(db) {
return getUserCount(db);
})
.shareReplay();
var users = dbObservable
.flatMap(function(db) {
// returns user document, n users per loop where n is the batchsize.
return createQuery(db, 'users', {}, batchSize);
})
.map(function(user) {
// flatten user
assign(user, user.portfolio, user.profile);
if (!user.username) {
user.username = 'fcc' + uuid.v4().slice(0, 8);
}
if (user.github) {
user.isGithubCool = true;
} else {
user.isMigrationGrandfathered = true;
}
providers.forEach(function(provider) {
user[provider + 'id'] = user[provider];
user[provider] = null;
});
user.rand = Math.random();
return user;
})
.shareReplay();
// batch them into arrays of twenty documents
var userSavesCount = users
.bufferWithCount(batchSize)
// get bd object ready for insert
.withLatestFrom(dbObservable, function(users, db) {
return {
users: users,
db: db
};
})
.flatMap(function(dats) {
// bulk insert into new collection for loopback
return insertMany(dats.db, 'user', dats.users, { w: 1 });
})
.flatMap(function() {
return totalUser;
})
.doOnNext(function(totalUsers) {
count = count + batchSize;
debug('user progress %s', count / totalUsers * 100);
})
// count how many times insert completes
.count();
// create User Identities
var userIdentityCount = users
.flatMap(function(user) {
var ids = providers
.map(function(provider) {
return {
provider: provider,
externalId: user[provider + 'id'],
userId: user._id || user.id
};
})
.filter(function(ident) {
return !!ident.externalId;
});
return Rx.Observable.from(ids);
})
.bufferWithCount(batchSize)
.withLatestFrom(dbObservable, function(identities, db) {
return {
identities: identities,
db: db
};
})
.flatMap(function(dats) {
// bulk insert into new collection for loopback
return insertMany(dats.db, 'userIdentity', dats.identities, { w: 1 });
})
// count how many times insert completes
.count();
/*
var storyCount = dbObservable
.flatMap(function(db) {
return createQuery(db, 'stories', {}, batchSize);
})
.bufferWithCount(batchSize)
.withLatestFrom(dbObservable, function(stories, db) {
return {
stories: stories,
db: db
};
})
.flatMap(function(dats) {
return insertMany(dats.db, 'story', dats.stories, { w: 1 });
})
.count();
*/
Rx.Observable.combineLatest(
userIdentityCount,
userSavesCount,
// storyCount,
function(userIdentCount, userCount) {
return {
userIdentCount: userIdentCount * batchSize,
userCount: userCount * batchSize
// storyCount: storyCount * batchSize
};
})
.subscribe(
function(countObj) {
console.log('next');
count = countObj;
},
function(err) {
console.error('an error occured', err, err.stack);
},
function() {
console.log('finished with ', count);
process.exit(0);
}
);
dbObservable.connect();

View File

@ -40,8 +40,14 @@ const renderCertifedEmail = loopback.template(path.join(
'certified.ejs' 'certified.ejs'
)); ));
function isCertified(ids, challengeMap = {}) { function isCertified(ids, completedChallenges = []) {
return _.every(ids, ({ id }) => _.has(challengeMap, id)); return _.every(
ids,
({ id }) => _.find(
completedChallenges,
({ id: completedId }) => completedId === id
)
);
} }
const certIds = { const certIds = {
@ -73,17 +79,17 @@ const certViews = {
}; };
const certText = { const certText = {
[certTypes.frontEnd]: 'Legacy Front End certified', [certTypes.frontEnd]: 'Legacy Front End',
[certTypes.backEnd]: 'Legacy Back End Certified', [certTypes.backEnd]: 'Legacy Back End',
[certTypes.dataVis]: 'Legacy Data Visualization Certified', [certTypes.dataVis]: 'Legacy Data Visualization',
[certTypes.fullStack]: 'Legacy Full Stack Certified', [certTypes.fullStack]: 'Legacy Full Stack',
[certTypes.respWebDesign]: 'Responsive Web Design Certified', [certTypes.respWebDesign]: 'Responsive Web Design',
[certTypes.frontEndLibs]: 'Front End Libraries Certified', [certTypes.frontEndLibs]: 'Front End Libraries',
[certTypes.jsAlgoDataStruct]: [certTypes.jsAlgoDataStruct]:
'JavaScript Algorithms and Data Structures Certified', 'JavaScript Algorithms and Data Structures',
[certTypes.dataVis2018]: 'Data Visualization Certified', [certTypes.dataVis2018]: 'Data Visualization',
[certTypes.apisMicroservices]: 'APIs and Microservices Certified', [certTypes.apisMicroservices]: 'APIs and Microservices',
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified' [certTypes.infosecQa]: 'Information Security and Quality Assurance'
}; };
function getIdsForCert$(id, Challenge) { function getIdsForCert$(id, Challenge) {
@ -101,19 +107,6 @@ function getIdsForCert$(id, Challenge) {
.shareReplay(); .shareReplay();
} }
// sendCertifiedEmail(
// {
// email: String,
// username: String,
// isRespWebDesignCert: Boolean,
// isFrontEndLibsCert: Boolean,
// isJsAlgoDataStructCert: Boolean,
// isDataVisCert: Boolean,
// isApisMicroservicesCert: Boolean,
// isInfosecQaCert: Boolean
// },
// send$: Observable
// ) => Observable
function sendCertifiedEmail( function sendCertifiedEmail(
{ {
email, email,
@ -230,46 +223,49 @@ export default function certificate(app) {
log(superBlock); log(superBlock);
let certType = superBlockCertTypeMap[superBlock]; let certType = superBlockCertTypeMap[superBlock];
log(certType); log(certType);
return user.getChallengeMap$() return user.getCompletedChallenges$()
.flatMap(() => certTypeIds[certType]) .flatMap(() => certTypeIds[certType])
.flatMap(challenge => { .flatMap(challenge => {
const { const {
id, id,
tests, tests,
name,
challengeType challengeType
} = challenge; } = challenge;
const certName = certText[certType];
if (user[certType]) { if (user[certType]) {
return Observable.just(alreadyClaimedMessage(name)); return Observable.just(alreadyClaimedMessage(certName));
} }
if (!user[certType] && !isCertified(tests, user.challengeMap)) { if (!user[certType] && !isCertified(tests, user.completedChallenges)) {
return Observable.just(notCertifiedMessage(name)); return Observable.just(notCertifiedMessage(certName));
} }
if (!user.name) { if (!user.name) {
return Observable.just(noNameMessage); return Observable.just(noNameMessage);
} }
const updateData = { const updateData = {
$set: { $push: {
[`challengeMap.${id}`]: { completedChallenges: {
id, id,
name,
completedDate: new Date(), completedDate: new Date(),
challengeType challengeType
}
}, },
$set: {
[certType]: true [certType]: true
} }
}; };
// set here so sendCertifiedEmail works properly // set here so sendCertifiedEmail works properly
// not used otherwise // not used otherwise
user[certType] = true; user[certType] = true;
user.challengeMap[id] = { completedDate: new Date() }; user.completedChallenges[
user.completedChallenges.length - 1
] = { id, completedDate: new Date() };
return Observable.combineLatest( return Observable.combineLatest(
// update user data // update user data
user.update$(updateData), user.update$(updateData),
// If user has committed to nonprofit, // If user has committed to nonprofit,
// this will complete their pledge // this will complete their pledge
completeCommitment$(user), completeCommitment$(user),
// sends notification email is user has all three certs // sends notification email is user has all 6 certs
// if not it noop // if not it noop
sendCertifiedEmail(user, Email.send$), sendCertifiedEmail(user, Email.send$),
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage }) ({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
@ -280,7 +276,7 @@ export default function certificate(app) {
log(pledgeOrMessage); log(pledgeOrMessage);
} }
log(`${count} documents updated`); log(`${count} documents updated`);
return successMessage(user.username, name); return successMessage(user.username, certName);
} }
); );
}) })
@ -326,12 +322,12 @@ export default function certificate(app) {
isHonest: true, isHonest: true,
username: true, username: true,
name: true, name: true,
challengeMap: true completedChallenges: true
} }
) )
.subscribe( .subscribe(
user => { user => {
const profile = `/${user.username}`; const profile = `/portfolio/${user.username}`;
if (!user) { if (!user) {
req.flash( req.flash(
'danger', 'danger',
@ -352,7 +348,7 @@ export default function certificate(app) {
} }
if (user.isCheater) { if (user.isCheater) {
return res.redirect(`/${user.username}`); return res.redirect(profile);
} }
if (user.isLocked) { if (user.isLocked) {
@ -378,8 +374,10 @@ export default function certificate(app) {
} }
if (user[certType]) { if (user[certType]) {
const { challengeMap = {} } = user; const { completedChallenges = {} } = user;
const { completedDate = new Date() } = challengeMap[certId] || {}; const { completedDate = new Date() } = _.find(
completedChallenges, ({ id }) => certId === id
) || {};
return res.render( return res.render(
certViews[certType], certViews[certType],
@ -392,7 +390,7 @@ export default function certificate(app) {
} }
req.flash( req.flash(
'danger', 'danger',
`Looks like user ${username} is not ${certText[certType]}` `Looks like user ${username} is not ${certText[certType]} certified`
); );
return res.redirect(profile); return res.redirect(profile);
}, },

View File

@ -20,39 +20,33 @@ function buildUserUpdate(
timezone timezone
) { ) {
let finalChallenge; let finalChallenge;
let numOfAttempts = 1; const updateData = { $set: {}, $push: {} };
const updateData = { $set: {} }; const { timezone: userTimezone, completedChallenges = [] } = user;
const { timezone: userTimezone, challengeMap = {} } = user;
const oldChallenge = challengeMap[challengeId]; const oldChallenge = _.find(
completedChallenges,
({ id }) => challengeId === id
);
const alreadyCompleted = !!oldChallenge; const alreadyCompleted = !!oldChallenge;
if (alreadyCompleted) { if (alreadyCompleted) {
// add data from old challenge
if (oldChallenge.numOfAttempts) {
numOfAttempts = oldChallenge.numOfAttempts + 1;
}
finalChallenge = { finalChallenge = {
...completedChallenge, ...completedChallenge,
completedDate: oldChallenge.completedDate, completedDate: oldChallenge.completedDate
lastUpdated: completedChallenge.completedDate,
numOfAttempts
}; };
} else { } else {
updateData.$push = { updateData.$push = {
progressTimestamps: { ...updateData.$push,
timestamp: Date.now(), progressTimestamps: Date.now()
completedChallenge: challengeId
}
}; };
finalChallenge = { finalChallenge = {
...completedChallenge, ...completedChallenge
numOfAttempts
}; };
} }
updateData.$set = { updateData.$push = {
[`challengeMap.${challengeId}`]: finalChallenge ...updateData.$push,
completedChallenges: finalChallenge
}; };
if ( if (
@ -71,8 +65,7 @@ function buildUserUpdate(
return { return {
alreadyCompleted, alreadyCompleted,
updateData, updateData,
completedDate: finalChallenge.completedDate, completedDate: finalChallenge.completedDate
lastUpdated: finalChallenge.lastUpdated
}; };
} }
@ -153,7 +146,7 @@ export default function(app) {
} }
const user = req.user; const user = req.user;
return user.getChallengeMap$() return user.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { const {
@ -163,8 +156,7 @@ export default function(app) {
const { const {
alreadyCompleted, alreadyCompleted,
updateData, updateData
lastUpdated
} = buildUserUpdate( } = buildUserUpdate(
user, user,
id, id,
@ -180,8 +172,7 @@ export default function(app) {
return res.json({ return res.json({
points, points,
alreadyCompleted, alreadyCompleted,
completedDate, completedDate
lastUpdated
}); });
} }
return res.sendStatus(200); return res.sendStatus(200);
@ -204,15 +195,14 @@ export default function(app) {
return res.sendStatus(403); return res.sendStatus(403);
} }
return req.user.getChallengeMap$() return req.user.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const completedDate = Date.now(); const completedDate = Date.now();
const { id, solution, timezone } = req.body; const { id, solution, timezone } = req.body;
const { const {
alreadyCompleted, alreadyCompleted,
updateData, updateData
lastUpdated
} = buildUserUpdate( } = buildUserUpdate(
req.user, req.user,
id, id,
@ -230,8 +220,7 @@ export default function(app) {
return res.json({ return res.json({
points, points,
alreadyCompleted, alreadyCompleted,
completedDate, completedDate
lastUpdated
}); });
} }
return res.sendStatus(200); return res.sendStatus(200);
@ -280,12 +269,11 @@ export default function(app) {
} }
return user.getChallengeMap$() return user.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const { const {
alreadyCompleted, alreadyCompleted,
updateData, updateData
lastUpdated
} = buildUserUpdate(user, completedChallenge.id, completedChallenge); } = buildUserUpdate(user, completedChallenge.id, completedChallenge);
return user.update$(updateData) return user.update$(updateData)
@ -295,8 +283,7 @@ export default function(app) {
return res.send({ return res.send({
alreadyCompleted, alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1, points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate, completedDate: completedChallenge.completedDate
lastUpdated
}); });
} }
return res.status(200).send(true); return res.status(200).send(true);
@ -329,12 +316,11 @@ export default function(app) {
completedChallenge.completedDate = Date.now(); completedChallenge.completedDate = Date.now();
return user.getChallengeMap$() return user.getCompletedChallenges$()
.flatMap(() => { .flatMap(() => {
const { const {
alreadyCompleted, alreadyCompleted,
updateData, updateData
lastUpdated
} = buildUserUpdate(user, completedChallenge.id, completedChallenge); } = buildUserUpdate(user, completedChallenge.id, completedChallenge);
return user.update$(updateData) return user.update$(updateData)
@ -344,8 +330,7 @@ export default function(app) {
return res.send({ return res.send({
alreadyCompleted, alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1, points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate, completedDate: completedChallenge.completedDate
lastUpdated
}); });
} }
return res.status(200).send(true); return res.status(200).send(true);

View File

@ -23,11 +23,11 @@ export default function settingsController(app) {
); );
}; };
function refetchChallengeMap(req, res, next) { function refetchCompletedChallenges(req, res, next) {
const { user } = req; const { user } = req;
return user.requestChallengeMap() return user.requestCompletedChallenges()
.subscribe( .subscribe(
challengeMap => res.json({ challengeMap }), completedChallenges => res.json({ completedChallenges }),
next next
); );
} }
@ -136,9 +136,9 @@ export default function settingsController(app) {
} }
api.post( api.post(
'/refetch-user-challenge-map', '/refetch-user-completed-challenges',
ifNoUser401, ifNoUser401,
refetchChallengeMap refetchCompletedChallenges
); );
api.post( api.post(
'/update-flags', '/update-flags',

View File

@ -142,7 +142,7 @@ module.exports = function(app) {
isBackEndCert: false, isBackEndCert: false,
isDataVisCert: false, isDataVisCert: false,
isFullStackCert: false, isFullStackCert: false,
challengeMap: {} completedChallenges: []
}) })
.subscribe( .subscribe(
() => { () => {

View File

@ -8,8 +8,7 @@ const passportOptions = {
}; };
const fields = { const fields = {
progressTimestamps: false, progressTimestamps: false
completedChallenges: false
}; };
function getCompletedCertCount(user) { function getCompletedCertCount(user) {
@ -44,7 +43,6 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
this.userModel.findById(id, { fields }, (err, user) => { this.userModel.findById(id, { fields }, (err, user) => {
if (err || !user) { if (err || !user) {
user.challengeMap = {};
return done(err, user); return done(err, user);
} }
@ -58,11 +56,9 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
user.points = points; user.points = points;
let completedChallengeCount = 0; let completedChallengeCount = 0;
let completedProjectCount = 0; let completedProjectCount = 0;
if ('challengeMap' in user) { if ('completedChallenges' in user) {
completedChallengeCount = Object.keys(user.challengeMap).length; completedChallengeCount = user.completedChallenges.length;
Object.keys(user.challengeMap) user.completedChallenges.forEach(item => {
.map(key => user.challengeMap[key])
.forEach(item => {
if ( if (
'challengeType' in item && 'challengeType' in item &&
(item.challengeType === 3 || item.challengeType === 4) (item.challengeType === 3 || item.challengeType === 4)
@ -74,7 +70,7 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
user.completedChallengeCount = completedChallengeCount; user.completedChallengeCount = completedChallengeCount;
user.completedProjectCount = completedProjectCount; user.completedProjectCount = completedProjectCount;
user.completedCertCount = getCompletedCertCount(user); user.completedCertCount = getCompletedCertCount(user);
user.challengeMap = {}; user.completedChallenges = [];
return done(null, user); return done(null, user);
}); });
}); });

View File

@ -15,7 +15,7 @@
"initial": { "initial": {
"compression": {}, "compression": {},
"morgan": { "morgan": {
"params": ":status :method :response-time ms - :url" "params": ":date[iso] :status :method :response-time ms - :url"
}, },
"cors": { "cors": {
"params": { "params": {
@ -54,7 +54,6 @@
"./middlewares/constant-headers": {}, "./middlewares/constant-headers": {},
"./middlewares/csp": {}, "./middlewares/csp": {},
"./middlewares/jade-helpers": {}, "./middlewares/jade-helpers": {},
"./middlewares/migrate-completed-challenges": {},
"./middlewares/flash-cheaters": {}, "./middlewares/flash-cheaters": {},
"./middlewares/passport-login": {} "./middlewares/passport-login": {}
}, },

View File

@ -1,127 +0,0 @@
import { Observable, Scheduler } from 'rx';
import { ObjectID } from 'mongodb';
import debug from 'debug';
import idMap from '../utils/bad-id-map';
const log = debug('freecc:migrate');
const challengeTypes = {
html: 0,
js: 1,
video: 2,
zipline: 3,
basejump: 4,
bonfire: 5,
hikes: 6,
step: 7,
waypoint: 0
};
const challengeTypeReg = /^(waypoint|hike|zipline|basejump)/i;
const challengeTypeRegWithColon =
/^(bonfire|checkpoint|waypoint|hike|zipline|basejump):\s+/i;
function updateName(challenge) {
if (
challenge.name &&
challenge.challengeType === 5 &&
challengeTypeReg.test(challenge.name)
) {
challenge.name.replace(challengeTypeReg, match => {
// find the correct type
const type = challengeTypes[''.toLowerCase.call(match)];
// if type found, replace current type
//
if (type) {
challenge.challengeType = type;
}
return match;
});
}
if (challenge.name) {
challenge.oldName = challenge.name;
challenge.name = challenge.name.replace(challengeTypeRegWithColon, '');
}
return challenge;
}
function updateId(challenge) {
if (idMap.hasOwnProperty(challenge.id)) {
challenge.id = idMap[challenge.id];
}
return challenge;
}
// buildChallengeMap(
// userId: String,
// completedChallenges: Object[],
// User: User
// ) => Observable
function buildChallengeMap(userId, completedChallenges = [], User) {
return Observable.from(
completedChallenges,
null,
null,
Scheduler.default
)
.map(challenge => {
return challenge && typeof challenge.toJSON === 'function' ?
challenge.toJSON() :
challenge;
})
.map(updateId)
.filter(({ id, _id }) => ObjectID.isValid(id || _id))
.map(updateName)
.reduce((challengeMap, challenge) => {
const id = challenge.id || challenge._id;
challengeMap[id] = challenge;
return challengeMap;
}, {})
.flatMap(challengeMap => {
const updateData = {
$set: {
challengeMap,
isChallengeMapMigrated: true
}
};
return Observable.fromNodeCallback(User.updateAll, User)(
{ id: userId },
updateData,
{ allowExtendedOperators: true }
);
});
}
export default function migrateCompletedChallenges() {
return ({ user, app }, res, next) => {
const User = app.models.User;
if (!user || user.isChallengeMapMigrated) {
return next();
}
const id = user.id.toString();
return User.findOne$({
where: { id },
fields: { completedChallenges: true }
})
.map(({ completedChallenges = [] } = {}) => completedChallenges)
.flatMap(completedChallenges => {
return buildChallengeMap(
id,
completedChallenges,
User
);
})
.subscribe(
count => log('documents update', count),
// errors go here
next,
next
);
};
}

View File

@ -18,10 +18,10 @@ export default function userServices() {
cb) { cb) {
const queryUser = req.user; const queryUser = req.user;
const source = queryUser && Observable.forkJoin( const source = queryUser && Observable.forkJoin(
queryUser.getChallengeMap$(), queryUser.getCompletedChallenges$(),
queryUser.getPoints$(), queryUser.getPoints$(),
(challengeMap, progressTimestamps) => ({ (completedChallenges, progressTimestamps) => ({
challengeMap, completedChallenges,
progress: getProgress(progressTimestamps, queryUser.timezone) progress: getProgress(progressTimestamps, queryUser.timezone)
}) })
); );
@ -29,10 +29,10 @@ export default function userServices() {
() => !queryUser, () => !queryUser,
Observable.of({}), Observable.of({}),
Observable.defer(() => source) Observable.defer(() => source)
.map(({ challengeMap, progress }) => ({ .map(({ completedChallenges, progress }) => ({
...queryUser.toJSON(), ...queryUser.toJSON(),
...progress, ...progress,
challengeMap completedChallenges
})) }))
.map( .map(
user => ({ user => ({
@ -41,7 +41,7 @@ export default function userServices() {
[user.username]: { [user.username]: {
..._.pick(user, userPropsForSession), ..._.pick(user, userPropsForSession),
isEmailVerified: !!user.emailVerified, isEmailVerified: !!user.emailVerified,
isGithub: !!user.githubURL, isGithub: !!user.githubProfile,
isLinkedIn: !!user.linkedIn, isLinkedIn: !!user.linkedIn,
isTwitter: !!user.twitter, isTwitter: !!user.twitter,
isWebsite: !!user.website, isWebsite: !!user.website,

View File

@ -27,17 +27,13 @@ export function createUserUpdatesFromProfile(provider, profile) {
) )
}; };
} }
// using es6 argument destructing
// createProfileAttributes(profile) => profileUpdate // createProfileAttributes(profile) => profileUpdate
function createProfileAttributesFromGithub(profile) { function createProfileAttributesFromGithub(profile) {
const { const {
profileUrl: githubURL, profileUrl: githubProfile,
username, username,
_json: { _json: {
id: githubId,
avatar_url: picture, avatar_url: picture,
email: githubEmail,
created_at: joinedGithubOn,
blog: website, blog: website,
location, location,
bio, bio,
@ -49,14 +45,9 @@ function createProfileAttributesFromGithub(profile) {
username: username.toLowerCase(), username: username.toLowerCase(),
location, location,
bio, bio,
joinedGithubOn,
website, website,
isGithubCool: true,
picture, picture,
githubId, githubProfile
githubURL,
githubEmail,
githubProfile: githubURL
}; };
} }

View File

@ -10,8 +10,8 @@ import {
export const publicUserProps = [ export const publicUserProps = [
'about', 'about',
'calendar', 'calendar',
'challengeMap', 'completedChallenges',
'githubURL', 'githubProfile',
'isApisMicroservicesCert', 'isApisMicroservicesCert',
'isBackEndCert', 'isBackEndCert',
'isCheater', 'isCheater',
@ -20,7 +20,6 @@ export const publicUserProps = [
'isFrontEndCert', 'isFrontEndCert',
'isFullStackCert', 'isFullStackCert',
'isFrontEndLibsCert', 'isFrontEndLibsCert',
'isGithubCool',
'isHonest', 'isHonest',
'isInfosecQaCert', 'isInfosecQaCert',
'isJsAlgoDataStructCert', 'isJsAlgoDataStructCert',
@ -58,25 +57,12 @@ export function normaliseUserFields(user) {
export function getProgress(progressTimestamps, timezone = 'EST') { export function getProgress(progressTimestamps, timezone = 'EST') {
const calendar = progressTimestamps const calendar = progressTimestamps
.map((objOrNum) => { .filter(Boolean)
return typeof objOrNum === 'number' ? .reduce((data, timestamp) => {
objOrNum : data[Math.floor(timestamp / 1000)] = 1;
objOrNum.timestamp;
})
.filter((timestamp) => {
return !!timestamp;
})
.reduce((data, timeStamp) => {
data[Math.floor(timeStamp / 1000)] = 1;
return data; return data;
}, {}); }, {});
const timestamps = progressTimestamps const uniqueHours = prepUniqueDaysByHours(progressTimestamps, timezone);
.map(objOrNum => {
return typeof objOrNum === 'number' ?
objOrNum :
objOrNum.timestamp;
});
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
const streak = { const streak = {
longest: calcLongestStreak(uniqueHours, timezone), longest: calcLongestStreak(uniqueHours, timezone),
current: calcCurrentStreak(uniqueHours, timezone) current: calcCurrentStreak(uniqueHours, timezone)

View File

@ -6,10 +6,6 @@ block content
var challengeName = 'Profile View'; var challengeName = 'Profile View';
if (user && user.username === username) if (user && user.username === username)
.row .row
if (!user.isGithubCool)
a.btn.btn-lg.btn-block.btn-github.btn-link-social(href='/link/github')
i.fa.fa-github
| Link my GitHub to enable my public profile
.col-xs-12 .col-xs-12
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/settings') a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/settings')
| Update my settings | Update my settings