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:
committed by
mrugesh mohapatra
parent
156ea1af76
commit
f916204ba5
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 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.
|
No challenges have been completed yet.
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
|
@ -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 = {};
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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) {
|
||||||
|
@ -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();
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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);
|
||||||
|
@ -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',
|
||||||
|
@ -142,7 +142,7 @@ module.exports = function(app) {
|
|||||||
isBackEndCert: false,
|
isBackEndCert: false,
|
||||||
isDataVisCert: false,
|
isDataVisCert: false,
|
||||||
isFullStackCert: false,
|
isFullStackCert: false,
|
||||||
challengeMap: {}
|
completedChallenges: []
|
||||||
})
|
})
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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": {}
|
||||||
},
|
},
|
||||||
|
@ -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
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
@ -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,
|
||||||
|
@ -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
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user