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 }
|
||||
) {
|
||||
return actions.do(console.log)::ofType(
|
||||
return actions::ofType(
|
||||
appTypes.appMounted,
|
||||
types.fetchMapUi.start
|
||||
)
|
||||
@ -25,7 +25,6 @@ export default function fetchMapUiEpic(
|
||||
};
|
||||
return services.readService$(options)
|
||||
.retry(3)
|
||||
.do(console.info)
|
||||
.map(({ entities, ...res }) => ({
|
||||
entities: shapeChallenges(
|
||||
entities,
|
||||
|
@ -12,7 +12,7 @@ import { userByNameSelector } from '../../../redux';
|
||||
|
||||
const propTypes = {
|
||||
email: PropTypes.string,
|
||||
githubURL: PropTypes.string,
|
||||
githubProfile: PropTypes.string,
|
||||
isGithub: PropTypes.bool,
|
||||
isLinkedIn: PropTypes.bool,
|
||||
isTwitter: PropTypes.bool,
|
||||
@ -26,7 +26,7 @@ const propTypes = {
|
||||
const mapStateToProps = createSelector(
|
||||
userByNameSelector,
|
||||
({
|
||||
githubURL,
|
||||
githubProfile,
|
||||
isLinkedIn,
|
||||
isGithub,
|
||||
isTwitter,
|
||||
@ -35,7 +35,7 @@ const mapStateToProps = createSelector(
|
||||
twitter,
|
||||
website
|
||||
}) => ({
|
||||
githubURL,
|
||||
githubProfile,
|
||||
isLinkedIn,
|
||||
isGithub,
|
||||
isTwitter,
|
||||
@ -97,7 +97,7 @@ function TwitterIcon(handle) {
|
||||
|
||||
function SocialIcons(props) {
|
||||
const {
|
||||
githubURL,
|
||||
githubProfile,
|
||||
isLinkedIn,
|
||||
isGithub,
|
||||
isTwitter,
|
||||
@ -121,7 +121,7 @@ function SocialIcons(props) {
|
||||
isLinkedIn ? LinkedInIcon(linkedIn) : null
|
||||
}
|
||||
{
|
||||
isGithub ? githubIcon(githubURL) : null
|
||||
isGithub ? githubIcon(githubProfile) : null
|
||||
}
|
||||
{
|
||||
isWebsite ? WebsiteIcon(website) : null
|
||||
|
@ -5,8 +5,6 @@ import { connect } from 'react-redux';
|
||||
import format from 'date-fns/format';
|
||||
import { reverse, sortBy } from 'lodash';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Table
|
||||
} from 'react-bootstrap';
|
||||
|
||||
@ -16,48 +14,40 @@ import { homeURL } from '../../../../utils/constantStrings.json';
|
||||
import blockNameify from '../../../utils/blockNameify';
|
||||
import { FullWidthRow } from '../../../helperComponents';
|
||||
import { Link } from '../../../Router';
|
||||
import SolutionViewer from '../../Settings/components/SolutionViewer.jsx';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeIdToNameMapSelector,
|
||||
userByNameSelector,
|
||||
(
|
||||
idToNameMap,
|
||||
{ challengeMap: completedMap = {}, username }
|
||||
{ completedChallenges: completedMap = [] }
|
||||
) => ({
|
||||
completedMap,
|
||||
idToNameMap,
|
||||
username
|
||||
idToNameMap
|
||||
})
|
||||
);
|
||||
|
||||
const propTypes = {
|
||||
completedMap: PropTypes.shape({
|
||||
completedMap: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
completedDate: PropTypes.number,
|
||||
lastUpdated: PropTypes.number
|
||||
}),
|
||||
idToNameMap: PropTypes.objectOf(PropTypes.string),
|
||||
username: PropTypes.string
|
||||
challengeType: PropTypes.number
|
||||
})
|
||||
),
|
||||
idToNameMap: PropTypes.objectOf(PropTypes.string)
|
||||
};
|
||||
|
||||
class Timeline extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
solutionToView: null,
|
||||
solutionOpen: false
|
||||
};
|
||||
|
||||
this.closeSolution = this.closeSolution.bind(this);
|
||||
this.renderCompletion = this.renderCompletion.bind(this);
|
||||
this.viewSolution = this.viewSolution.bind(this);
|
||||
}
|
||||
|
||||
renderCompletion(completed) {
|
||||
const { idToNameMap } = this.props;
|
||||
const { id, completedDate, lastUpdated, files } = completed;
|
||||
const { id, completedDate } = completed;
|
||||
return (
|
||||
<tr key={ id }>
|
||||
<td>{ blockNameify(idToNameMap[id]) }</td>
|
||||
@ -68,53 +58,12 @@ class Timeline extends PureComponent {
|
||||
}
|
||||
</time>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
viewSolution(id) {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
solutionToView: id,
|
||||
solutionOpen: true
|
||||
}));
|
||||
}
|
||||
|
||||
closeSolution() {
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
solutionToView: null,
|
||||
solutionOpen: false
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { completedMap, idToNameMap, username } = this.props;
|
||||
const { solutionToView: id, solutionOpen } = this.state;
|
||||
const { completedMap, idToNameMap } = this.props;
|
||||
if (!Object.keys(idToNameMap).length) {
|
||||
return null;
|
||||
}
|
||||
@ -122,7 +71,7 @@ class Timeline extends PureComponent {
|
||||
<FullWidthRow>
|
||||
<h2 className='text-center'>Timeline</h2>
|
||||
{
|
||||
Object.keys(completedMap).length === 0 ?
|
||||
completedMap.length === 0 ?
|
||||
<p className='text-center'>
|
||||
No challenges have been completed yet.
|
||||
<Link to={ homeURL }>
|
||||
@ -134,45 +83,21 @@ class Timeline extends PureComponent {
|
||||
<tr>
|
||||
<th>Challenge</th>
|
||||
<th>First Completed</th>
|
||||
<th>Last Changed</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
reverse(
|
||||
sortBy(
|
||||
Object.keys(completedMap)
|
||||
.filter(key => key in idToNameMap)
|
||||
.map(key => completedMap[key]),
|
||||
completedMap,
|
||||
[ 'completedDate' ]
|
||||
)
|
||||
).filter(({id}) => id in idToNameMap)
|
||||
)
|
||||
.map(this.renderCompletion)
|
||||
}
|
||||
</tbody>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ const mapStateToProps = createSelector(
|
||||
projectsSelector,
|
||||
(
|
||||
{
|
||||
challengeMap,
|
||||
completedChallenges,
|
||||
isRespWebDesignCert,
|
||||
is2018DataVisCert,
|
||||
isFrontEndLibsCert,
|
||||
@ -45,7 +45,7 @@ const mapStateToProps = createSelector(
|
||||
legacyProjects: projects.filter(p => p.superBlock.includes('legacy')),
|
||||
modernProjects: projects.filter(p => !p.superBlock.includes('legacy')),
|
||||
userProjects: projects
|
||||
.map(block => buildUserProjectsMap(block, challengeMap))
|
||||
.map(block => buildUserProjectsMap(block, completedChallenges))
|
||||
.reduce((projects, current) => ({
|
||||
...projects,
|
||||
...current
|
||||
|
@ -13,13 +13,13 @@ import { updateUserBackend } from '../redux';
|
||||
const mapStateToProps = createSelector(
|
||||
userSelector,
|
||||
({
|
||||
githubURL = '',
|
||||
githubProfile = '',
|
||||
linkedin = '',
|
||||
twitter = '',
|
||||
website = ''
|
||||
}) => ({
|
||||
initialValues: {
|
||||
githubURL,
|
||||
githubProfile,
|
||||
linkedin,
|
||||
twitter,
|
||||
website
|
||||
@ -27,7 +27,7 @@ const mapStateToProps = createSelector(
|
||||
})
|
||||
);
|
||||
|
||||
const formFields = [ 'githubURL', 'linkedin', 'twitter', 'website' ];
|
||||
const formFields = [ 'githubProfile', 'linkedin', 'twitter', 'website' ];
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({
|
||||
@ -37,7 +37,7 @@ function mapDispatchToProps(dispatch) {
|
||||
|
||||
const propTypes = {
|
||||
fields: PropTypes.object,
|
||||
githubURL: PropTypes.string,
|
||||
githubProfile: PropTypes.string,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
linkedin: PropTypes.string,
|
||||
twitter: PropTypes.string,
|
||||
|
@ -36,7 +36,7 @@ export const types = createTypes([
|
||||
createAsyncTypes('updateMyPortfolio'),
|
||||
'updateNewUsernameValidity',
|
||||
createAsyncTypes('validateUsername'),
|
||||
createAsyncTypes('refetchChallengeMap'),
|
||||
createAsyncTypes('refetchCompletedChallenges'),
|
||||
createAsyncTypes('deleteAccount'),
|
||||
createAsyncTypes('resetProgress'),
|
||||
|
||||
@ -103,8 +103,8 @@ export const updateNewUsernameValidity = createAction(
|
||||
types.updateNewUsernameValidity
|
||||
);
|
||||
|
||||
export const refetchChallengeMap = createAction(
|
||||
types.refetchChallengeMap.start
|
||||
export const refetchCompletedChallenges = createAction(
|
||||
types.refetchCompletedChallenges.start
|
||||
);
|
||||
|
||||
export const validateUsername = createAction(types.validateUsername.start);
|
||||
|
@ -3,7 +3,7 @@ import { combineEpics, ofType } from 'redux-epic';
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
types,
|
||||
refetchChallengeMap,
|
||||
refetchCompletedChallenges,
|
||||
updateUserBackendComplete,
|
||||
updateMyPortfolioComplete
|
||||
} from './';
|
||||
@ -90,7 +90,7 @@ function backendUserUpdateEpic(actions$, { getState }) {
|
||||
const complete = actions$::ofType(types.updateUserBackend.complete)
|
||||
.flatMap(({ payload: { message } }) => Observable.if(
|
||||
() => message.includes('project'),
|
||||
Observable.of(refetchChallengeMap(), makeToast({ message })),
|
||||
Observable.of(refetchCompletedChallenges(), makeToast({ message })),
|
||||
Observable.of(makeToast({ message }))
|
||||
)
|
||||
);
|
||||
@ -98,16 +98,16 @@ function backendUserUpdateEpic(actions$, { getState }) {
|
||||
return Observable.merge(server, optimistic, complete);
|
||||
}
|
||||
|
||||
function refetchChallengeMapEpic(actions$, { getState }) {
|
||||
return actions$::ofType(types.refetchChallengeMap.start)
|
||||
function refetchCompletedChallengesEpic(actions$, { getState }) {
|
||||
return actions$::ofType(types.refetchCompletedChallenges.start)
|
||||
.flatMap(() => {
|
||||
const {
|
||||
app: { csrfToken: _csrf }
|
||||
} = getState();
|
||||
const username = usernameSelector(getState());
|
||||
return postJSON$('/refetch-user-challenge-map', { _csrf })
|
||||
.map(({ challengeMap }) =>
|
||||
updateMultipleUserFlags({ username, flags: { challengeMap } })
|
||||
return postJSON$('/refetch-user-completed-challenges', { _csrf })
|
||||
.map(({ completedChallenges }) =>
|
||||
updateMultipleUserFlags({ username, flags: { completedChallenges } })
|
||||
)
|
||||
.catch(createErrorObservable);
|
||||
});
|
||||
@ -190,7 +190,7 @@ function updateUserEmailEpic(actions, { getState }) {
|
||||
|
||||
export default combineEpics(
|
||||
backendUserUpdateEpic,
|
||||
refetchChallengeMapEpic,
|
||||
refetchCompletedChallengesEpic,
|
||||
updateMyPortfolioEpic,
|
||||
updateUserEmailEpic
|
||||
);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { find } from 'lodash';
|
||||
|
||||
export const jsProjectSuperBlock = 'javascript-algorithms-and-data-structures';
|
||||
|
||||
export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||
export function buildUserProjectsMap(projectBlock, completedChallenges) {
|
||||
const {
|
||||
challenges,
|
||||
superBlock
|
||||
@ -8,7 +10,10 @@ export function buildUserProjectsMap(projectBlock, challengeMap) {
|
||||
return {
|
||||
[superBlock]: challenges.reduce((solutions, current) => {
|
||||
const { id } = current;
|
||||
const completed = challengeMap[id];
|
||||
const completed = find(
|
||||
completedChallenges,
|
||||
({ id: completedId }) => completedId === id
|
||||
);
|
||||
let solution = '';
|
||||
if (superBlock === jsProjectSuperBlock) {
|
||||
solution = {};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Observable } from 'rx';
|
||||
import uuid from 'uuid';
|
||||
import uuid from 'uuid/v4';
|
||||
import moment from 'moment';
|
||||
import dedent from 'dedent';
|
||||
import debugFactory from 'debug';
|
||||
@ -7,6 +7,7 @@ import { isEmail } from 'validator';
|
||||
import path from 'path';
|
||||
import loopback from 'loopback';
|
||||
import _ from 'lodash';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
||||
import { themes } from '../utils/themes';
|
||||
import { saveUser, observeMethod } from '../../server/utils/rx.js';
|
||||
@ -41,51 +42,51 @@ function destroyAll(id, Model) {
|
||||
)({ userId: id });
|
||||
}
|
||||
|
||||
function buildChallengeMapUpdate(challengeMap, project) {
|
||||
function buildCompletedChallengesUpdate(completedChallenges, project) {
|
||||
const key = Object.keys(project)[0];
|
||||
const solutions = project[key];
|
||||
const currentChallengeMap = { ...challengeMap };
|
||||
const currentCompletedProjects = _.pick(
|
||||
currentChallengeMap,
|
||||
Object.keys(solutions)
|
||||
);
|
||||
const currentCompletedChallenges = [ ...completedChallenges ];
|
||||
const currentCompletedProjects = currentCompletedChallenges
|
||||
.filter(({id}) => Object.keys(solutions).includes(id));
|
||||
const now = Date.now();
|
||||
const update = Object.keys(solutions).reduce((update, currentId) => {
|
||||
const indexOfCurrentId = _.findIndex(
|
||||
currentCompletedProjects,
|
||||
({id}) => id === currentId
|
||||
);
|
||||
const isCurrentlyCompleted = indexOfCurrentId !== -1;
|
||||
if (
|
||||
currentId in currentCompletedProjects &&
|
||||
currentCompletedProjects[currentId].solution !== solutions[currentId]
|
||||
isCurrentlyCompleted &&
|
||||
currentCompletedProjects[
|
||||
indexOfCurrentId
|
||||
].solution !== solutions[currentId]
|
||||
) {
|
||||
return {
|
||||
...update,
|
||||
[currentId]: {
|
||||
...currentCompletedProjects[currentId],
|
||||
solution: solutions[currentId],
|
||||
numOfAttempts: currentCompletedProjects[currentId].numOfAttempts + 1
|
||||
}
|
||||
update[indexOfCurrentId] = {
|
||||
...update[indexOfCurrentId],
|
||||
solution: solutions[currentId]
|
||||
};
|
||||
}
|
||||
if (!(currentId in currentCompletedProjects)) {
|
||||
return {
|
||||
if (!isCurrentlyCompleted) {
|
||||
return [
|
||||
...update,
|
||||
[currentId]: {
|
||||
{
|
||||
id: currentId,
|
||||
solution: solutions[currentId],
|
||||
challengeType: 3,
|
||||
completedDate: now,
|
||||
numOfAttempts: 1
|
||||
completedDate: now
|
||||
}
|
||||
};
|
||||
];
|
||||
}
|
||||
return update;
|
||||
}, {});
|
||||
const updatedExisting = {
|
||||
...currentCompletedProjects,
|
||||
...update
|
||||
};
|
||||
return {
|
||||
...currentChallengeMap,
|
||||
...updatedExisting
|
||||
};
|
||||
}, currentCompletedProjects);
|
||||
const updatedExisting = _.uniqBy(
|
||||
[
|
||||
...update,
|
||||
...currentCompletedChallenges
|
||||
],
|
||||
'id'
|
||||
);
|
||||
return updatedExisting;
|
||||
}
|
||||
|
||||
function isTheSame(val1, val2) {
|
||||
@ -219,7 +220,14 @@ module.exports = function(User) {
|
||||
// assign random username to new users
|
||||
// actual usernames will come from github
|
||||
// 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) {
|
||||
user.progressTimestamps = [];
|
||||
@ -269,7 +277,15 @@ module.exports = function(User) {
|
||||
}
|
||||
|
||||
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();
|
||||
@ -645,9 +661,11 @@ module.exports = function(User) {
|
||||
});
|
||||
};
|
||||
|
||||
User.prototype.requestChallengeMap = function requestChallengeMap() {
|
||||
return this.getChallengeMap$();
|
||||
};
|
||||
function requestCompletedChallenges() {
|
||||
return this.getCompletedChallenges$();
|
||||
}
|
||||
|
||||
User.prototype.requestCompletedChallenges = requestCompletedChallenges;
|
||||
|
||||
User.prototype.requestUpdateFlags = function requestUpdateFlags(values) {
|
||||
const flagsToCheck = Object.keys(values);
|
||||
@ -715,10 +733,10 @@ module.exports = function(User) {
|
||||
|
||||
User.prototype.updateMyProjects = function updateMyProjects(project) {
|
||||
const updateData = {};
|
||||
return this.getChallengeMap$()
|
||||
.flatMap(challengeMap => {
|
||||
updateData.challengeMap = buildChallengeMapUpdate(
|
||||
challengeMap,
|
||||
return this.getCompletedChallenges$()
|
||||
.flatMap(completedChallenges => {
|
||||
updateData.completedChallenges = buildCompletedChallengesUpdate(
|
||||
completedChallenges,
|
||||
project
|
||||
);
|
||||
return this.update$(updateData);
|
||||
@ -767,18 +785,18 @@ module.exports = function(User) {
|
||||
if (!user) {
|
||||
return Observable.of({});
|
||||
}
|
||||
const { challengeMap, progressTimestamps, timezone } = user;
|
||||
const { completedChallenges, progressTimestamps, timezone } = user;
|
||||
return Observable.of({
|
||||
entities: {
|
||||
user: {
|
||||
[user.username]: {
|
||||
..._.pick(user, publicUserProps),
|
||||
isGithub: !!user.githubURL,
|
||||
isGithub: !!user.githubProfile,
|
||||
isLinkedIn: !!user.linkedIn,
|
||||
isTwitter: !!user.twitter,
|
||||
isWebsite: !!user.website,
|
||||
points: progressTimestamps.length,
|
||||
challengeMap,
|
||||
completedChallenges,
|
||||
...getProgress(progressTimestamps, timezone),
|
||||
...normaliseUserFields(user)
|
||||
}
|
||||
@ -1000,16 +1018,16 @@ module.exports = function(User) {
|
||||
return user.progressTimestamps;
|
||||
});
|
||||
};
|
||||
User.prototype.getChallengeMap$ = function getChallengeMap$() {
|
||||
User.prototype.getCompletedChallenges$ = function getCompletedChallenges$() {
|
||||
const id = this.getId();
|
||||
const filter = {
|
||||
where: { id },
|
||||
fields: { challengeMap: true }
|
||||
fields: { completedChallenges: true }
|
||||
};
|
||||
return this.constructor.findOne$(filter)
|
||||
.map(user => {
|
||||
this.challengeMap = user.challengeMap;
|
||||
return user.challengeMap;
|
||||
this.completedChallenges = user.completedChallenges;
|
||||
return user.completedChallenges;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -29,8 +29,17 @@
|
||||
"emailAuthLinkTTL": {
|
||||
"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": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "No longer used for new accounts"
|
||||
},
|
||||
"progressTimestamps": {
|
||||
"type": "array",
|
||||
@ -46,35 +55,15 @@
|
||||
"description": "Users who are confirmed to have broken academic honesty policy are marked as cheaters",
|
||||
"default": false
|
||||
},
|
||||
"isGithubCool": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"githubId": {
|
||||
"githubProfile": {
|
||||
"type": "string"
|
||||
},
|
||||
"githubURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"githubEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"joinedGithubOn": {
|
||||
"type": "date"
|
||||
},
|
||||
"website": {
|
||||
"type": "string"
|
||||
},
|
||||
"githubProfile": {
|
||||
"type": "string"
|
||||
},
|
||||
"_csrf": {
|
||||
"type": "string"
|
||||
},
|
||||
"isMigrationGrandfathered": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
"index": {
|
||||
@ -114,22 +103,6 @@
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
"currentStreak": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"longestStreak": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
},
|
||||
"sendMonthlyEmail": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"sendNotificationEmail": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"sendQuincyEmail": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
@ -144,15 +117,6 @@
|
||||
"description": "The challenge last visited by the user",
|
||||
"default": ""
|
||||
},
|
||||
"currentChallenge": {
|
||||
"type": {},
|
||||
"description": "deprecated"
|
||||
},
|
||||
"isUniqMigrated": {
|
||||
"type": "boolean",
|
||||
"description": "Campers completedChallenges array is free of duplicates",
|
||||
"default": false
|
||||
},
|
||||
"isHonest": {
|
||||
"type": "boolean",
|
||||
"description": "Camper has signed academic honesty policy",
|
||||
@ -208,35 +172,25 @@
|
||||
"description": "Camper is information security and quality assurance certified",
|
||||
"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": {
|
||||
"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": {
|
||||
"type": [
|
||||
{
|
||||
"completedDate": "number",
|
||||
"lastUpdated": "number",
|
||||
"numOfAttempts": "number",
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"completedWith": "string",
|
||||
"solution": "string",
|
||||
"githubLink": "string",
|
||||
"verified": "boolean",
|
||||
"challengeType": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
}
|
||||
"challengeType": "number"
|
||||
}
|
||||
],
|
||||
"default": []
|
||||
@ -249,9 +203,6 @@
|
||||
"type": "number",
|
||||
"index": true
|
||||
},
|
||||
"tshirtVote": {
|
||||
"type": "number"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ MongoClient.connect(secrets.db, function(err, database) {
|
||||
{$match: { 'email': { $exists: true } } },
|
||||
{$match: { 'email': { $ne: '' } } },
|
||||
{$match: { 'email': { $ne: null } } },
|
||||
{$match: { 'sendMonthlyEmail': true } },
|
||||
{$match: { 'sendQuincyEmail': true } },
|
||||
{$match: { 'email': { $not: /(test|fake)/i } } },
|
||||
{$group: { '_id': 1, 'emails': {$addToSet: '$email' } } }
|
||||
], 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'
|
||||
));
|
||||
|
||||
function isCertified(ids, challengeMap = {}) {
|
||||
return _.every(ids, ({ id }) => _.has(challengeMap, id));
|
||||
function isCertified(ids, completedChallenges = []) {
|
||||
return _.every(
|
||||
ids,
|
||||
({ id }) => _.find(
|
||||
completedChallenges,
|
||||
({ id: completedId }) => completedId === id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const certIds = {
|
||||
@ -73,17 +79,17 @@ const certViews = {
|
||||
};
|
||||
|
||||
const certText = {
|
||||
[certTypes.frontEnd]: 'Legacy Front End certified',
|
||||
[certTypes.backEnd]: 'Legacy Back End Certified',
|
||||
[certTypes.dataVis]: 'Legacy Data Visualization Certified',
|
||||
[certTypes.fullStack]: 'Legacy Full Stack Certified',
|
||||
[certTypes.respWebDesign]: 'Responsive Web Design Certified',
|
||||
[certTypes.frontEndLibs]: 'Front End Libraries Certified',
|
||||
[certTypes.frontEnd]: 'Legacy Front End',
|
||||
[certTypes.backEnd]: 'Legacy Back End',
|
||||
[certTypes.dataVis]: 'Legacy Data Visualization',
|
||||
[certTypes.fullStack]: 'Legacy Full Stack',
|
||||
[certTypes.respWebDesign]: 'Responsive Web Design',
|
||||
[certTypes.frontEndLibs]: 'Front End Libraries',
|
||||
[certTypes.jsAlgoDataStruct]:
|
||||
'JavaScript Algorithms and Data Structures Certified',
|
||||
[certTypes.dataVis2018]: 'Data Visualization Certified',
|
||||
[certTypes.apisMicroservices]: 'APIs and Microservices Certified',
|
||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance Certified'
|
||||
'JavaScript Algorithms and Data Structures',
|
||||
[certTypes.dataVis2018]: 'Data Visualization',
|
||||
[certTypes.apisMicroservices]: 'APIs and Microservices',
|
||||
[certTypes.infosecQa]: 'Information Security and Quality Assurance'
|
||||
};
|
||||
|
||||
function getIdsForCert$(id, Challenge) {
|
||||
@ -101,19 +107,6 @@ function getIdsForCert$(id, Challenge) {
|
||||
.shareReplay();
|
||||
}
|
||||
|
||||
// sendCertifiedEmail(
|
||||
// {
|
||||
// email: String,
|
||||
// username: String,
|
||||
// isRespWebDesignCert: Boolean,
|
||||
// isFrontEndLibsCert: Boolean,
|
||||
// isJsAlgoDataStructCert: Boolean,
|
||||
// isDataVisCert: Boolean,
|
||||
// isApisMicroservicesCert: Boolean,
|
||||
// isInfosecQaCert: Boolean
|
||||
// },
|
||||
// send$: Observable
|
||||
// ) => Observable
|
||||
function sendCertifiedEmail(
|
||||
{
|
||||
email,
|
||||
@ -230,46 +223,49 @@ export default function certificate(app) {
|
||||
log(superBlock);
|
||||
let certType = superBlockCertTypeMap[superBlock];
|
||||
log(certType);
|
||||
return user.getChallengeMap$()
|
||||
return user.getCompletedChallenges$()
|
||||
.flatMap(() => certTypeIds[certType])
|
||||
.flatMap(challenge => {
|
||||
const {
|
||||
id,
|
||||
tests,
|
||||
name,
|
||||
challengeType
|
||||
} = challenge;
|
||||
const certName = certText[certType];
|
||||
if (user[certType]) {
|
||||
return Observable.just(alreadyClaimedMessage(name));
|
||||
return Observable.just(alreadyClaimedMessage(certName));
|
||||
}
|
||||
if (!user[certType] && !isCertified(tests, user.challengeMap)) {
|
||||
return Observable.just(notCertifiedMessage(name));
|
||||
if (!user[certType] && !isCertified(tests, user.completedChallenges)) {
|
||||
return Observable.just(notCertifiedMessage(certName));
|
||||
}
|
||||
if (!user.name) {
|
||||
return Observable.just(noNameMessage);
|
||||
}
|
||||
const updateData = {
|
||||
$set: {
|
||||
[`challengeMap.${id}`]: {
|
||||
$push: {
|
||||
completedChallenges: {
|
||||
id,
|
||||
name,
|
||||
completedDate: new Date(),
|
||||
challengeType
|
||||
}
|
||||
},
|
||||
$set: {
|
||||
[certType]: true
|
||||
}
|
||||
};
|
||||
// set here so sendCertifiedEmail works properly
|
||||
// not used otherwise
|
||||
user[certType] = true;
|
||||
user.challengeMap[id] = { completedDate: new Date() };
|
||||
user.completedChallenges[
|
||||
user.completedChallenges.length - 1
|
||||
] = { id, completedDate: new Date() };
|
||||
return Observable.combineLatest(
|
||||
// update user data
|
||||
user.update$(updateData),
|
||||
// If user has committed to nonprofit,
|
||||
// this will complete their pledge
|
||||
completeCommitment$(user),
|
||||
// sends notification email is user has all three certs
|
||||
// sends notification email is user has all 6 certs
|
||||
// if not it noop
|
||||
sendCertifiedEmail(user, Email.send$),
|
||||
({ count }, pledgeOrMessage) => ({ count, pledgeOrMessage })
|
||||
@ -280,7 +276,7 @@ export default function certificate(app) {
|
||||
log(pledgeOrMessage);
|
||||
}
|
||||
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,
|
||||
username: true,
|
||||
name: true,
|
||||
challengeMap: true
|
||||
completedChallenges: true
|
||||
}
|
||||
)
|
||||
.subscribe(
|
||||
user => {
|
||||
const profile = `/${user.username}`;
|
||||
const profile = `/portfolio/${user.username}`;
|
||||
if (!user) {
|
||||
req.flash(
|
||||
'danger',
|
||||
@ -352,7 +348,7 @@ export default function certificate(app) {
|
||||
}
|
||||
|
||||
if (user.isCheater) {
|
||||
return res.redirect(`/${user.username}`);
|
||||
return res.redirect(profile);
|
||||
}
|
||||
|
||||
if (user.isLocked) {
|
||||
@ -378,8 +374,10 @@ export default function certificate(app) {
|
||||
}
|
||||
|
||||
if (user[certType]) {
|
||||
const { challengeMap = {} } = user;
|
||||
const { completedDate = new Date() } = challengeMap[certId] || {};
|
||||
const { completedChallenges = {} } = user;
|
||||
const { completedDate = new Date() } = _.find(
|
||||
completedChallenges, ({ id }) => certId === id
|
||||
) || {};
|
||||
|
||||
return res.render(
|
||||
certViews[certType],
|
||||
@ -392,7 +390,7 @@ export default function certificate(app) {
|
||||
}
|
||||
req.flash(
|
||||
'danger',
|
||||
`Looks like user ${username} is not ${certText[certType]}`
|
||||
`Looks like user ${username} is not ${certText[certType]} certified`
|
||||
);
|
||||
return res.redirect(profile);
|
||||
},
|
||||
|
@ -20,39 +20,33 @@ function buildUserUpdate(
|
||||
timezone
|
||||
) {
|
||||
let finalChallenge;
|
||||
let numOfAttempts = 1;
|
||||
const updateData = { $set: {} };
|
||||
const { timezone: userTimezone, challengeMap = {} } = user;
|
||||
const updateData = { $set: {}, $push: {} };
|
||||
const { timezone: userTimezone, completedChallenges = [] } = user;
|
||||
|
||||
const oldChallenge = challengeMap[challengeId];
|
||||
const oldChallenge = _.find(
|
||||
completedChallenges,
|
||||
({ id }) => challengeId === id
|
||||
);
|
||||
const alreadyCompleted = !!oldChallenge;
|
||||
|
||||
if (alreadyCompleted) {
|
||||
// add data from old challenge
|
||||
if (oldChallenge.numOfAttempts) {
|
||||
numOfAttempts = oldChallenge.numOfAttempts + 1;
|
||||
}
|
||||
finalChallenge = {
|
||||
...completedChallenge,
|
||||
completedDate: oldChallenge.completedDate,
|
||||
lastUpdated: completedChallenge.completedDate,
|
||||
numOfAttempts
|
||||
completedDate: oldChallenge.completedDate
|
||||
};
|
||||
} else {
|
||||
updateData.$push = {
|
||||
progressTimestamps: {
|
||||
timestamp: Date.now(),
|
||||
completedChallenge: challengeId
|
||||
}
|
||||
...updateData.$push,
|
||||
progressTimestamps: Date.now()
|
||||
};
|
||||
finalChallenge = {
|
||||
...completedChallenge,
|
||||
numOfAttempts
|
||||
...completedChallenge
|
||||
};
|
||||
}
|
||||
|
||||
updateData.$set = {
|
||||
[`challengeMap.${challengeId}`]: finalChallenge
|
||||
updateData.$push = {
|
||||
...updateData.$push,
|
||||
completedChallenges: finalChallenge
|
||||
};
|
||||
|
||||
if (
|
||||
@ -71,8 +65,7 @@ function buildUserUpdate(
|
||||
return {
|
||||
alreadyCompleted,
|
||||
updateData,
|
||||
completedDate: finalChallenge.completedDate,
|
||||
lastUpdated: finalChallenge.lastUpdated
|
||||
completedDate: finalChallenge.completedDate
|
||||
};
|
||||
}
|
||||
|
||||
@ -153,7 +146,7 @@ export default function(app) {
|
||||
}
|
||||
|
||||
const user = req.user;
|
||||
return user.getChallengeMap$()
|
||||
return user.getCompletedChallenges$()
|
||||
.flatMap(() => {
|
||||
const completedDate = Date.now();
|
||||
const {
|
||||
@ -163,8 +156,7 @@ export default function(app) {
|
||||
|
||||
const {
|
||||
alreadyCompleted,
|
||||
updateData,
|
||||
lastUpdated
|
||||
updateData
|
||||
} = buildUserUpdate(
|
||||
user,
|
||||
id,
|
||||
@ -180,8 +172,7 @@ export default function(app) {
|
||||
return res.json({
|
||||
points,
|
||||
alreadyCompleted,
|
||||
completedDate,
|
||||
lastUpdated
|
||||
completedDate
|
||||
});
|
||||
}
|
||||
return res.sendStatus(200);
|
||||
@ -204,15 +195,14 @@ export default function(app) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
return req.user.getChallengeMap$()
|
||||
return req.user.getCompletedChallenges$()
|
||||
.flatMap(() => {
|
||||
const completedDate = Date.now();
|
||||
const { id, solution, timezone } = req.body;
|
||||
|
||||
const {
|
||||
alreadyCompleted,
|
||||
updateData,
|
||||
lastUpdated
|
||||
updateData
|
||||
} = buildUserUpdate(
|
||||
req.user,
|
||||
id,
|
||||
@ -230,8 +220,7 @@ export default function(app) {
|
||||
return res.json({
|
||||
points,
|
||||
alreadyCompleted,
|
||||
completedDate,
|
||||
lastUpdated
|
||||
completedDate
|
||||
});
|
||||
}
|
||||
return res.sendStatus(200);
|
||||
@ -280,12 +269,11 @@ export default function(app) {
|
||||
}
|
||||
|
||||
|
||||
return user.getChallengeMap$()
|
||||
return user.getCompletedChallenges$()
|
||||
.flatMap(() => {
|
||||
const {
|
||||
alreadyCompleted,
|
||||
updateData,
|
||||
lastUpdated
|
||||
updateData
|
||||
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
|
||||
|
||||
return user.update$(updateData)
|
||||
@ -295,8 +283,7 @@ export default function(app) {
|
||||
return res.send({
|
||||
alreadyCompleted,
|
||||
points: alreadyCompleted ? user.points : user.points + 1,
|
||||
completedDate: completedChallenge.completedDate,
|
||||
lastUpdated
|
||||
completedDate: completedChallenge.completedDate
|
||||
});
|
||||
}
|
||||
return res.status(200).send(true);
|
||||
@ -329,12 +316,11 @@ export default function(app) {
|
||||
completedChallenge.completedDate = Date.now();
|
||||
|
||||
|
||||
return user.getChallengeMap$()
|
||||
return user.getCompletedChallenges$()
|
||||
.flatMap(() => {
|
||||
const {
|
||||
alreadyCompleted,
|
||||
updateData,
|
||||
lastUpdated
|
||||
updateData
|
||||
} = buildUserUpdate(user, completedChallenge.id, completedChallenge);
|
||||
|
||||
return user.update$(updateData)
|
||||
@ -344,8 +330,7 @@ export default function(app) {
|
||||
return res.send({
|
||||
alreadyCompleted,
|
||||
points: alreadyCompleted ? user.points : user.points + 1,
|
||||
completedDate: completedChallenge.completedDate,
|
||||
lastUpdated
|
||||
completedDate: completedChallenge.completedDate
|
||||
});
|
||||
}
|
||||
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;
|
||||
return user.requestChallengeMap()
|
||||
return user.requestCompletedChallenges()
|
||||
.subscribe(
|
||||
challengeMap => res.json({ challengeMap }),
|
||||
completedChallenges => res.json({ completedChallenges }),
|
||||
next
|
||||
);
|
||||
}
|
||||
@ -136,9 +136,9 @@ export default function settingsController(app) {
|
||||
}
|
||||
|
||||
api.post(
|
||||
'/refetch-user-challenge-map',
|
||||
'/refetch-user-completed-challenges',
|
||||
ifNoUser401,
|
||||
refetchChallengeMap
|
||||
refetchCompletedChallenges
|
||||
);
|
||||
api.post(
|
||||
'/update-flags',
|
||||
|
@ -142,7 +142,7 @@ module.exports = function(app) {
|
||||
isBackEndCert: false,
|
||||
isDataVisCert: false,
|
||||
isFullStackCert: false,
|
||||
challengeMap: {}
|
||||
completedChallenges: []
|
||||
})
|
||||
.subscribe(
|
||||
() => {
|
||||
|
@ -8,8 +8,7 @@ const passportOptions = {
|
||||
};
|
||||
|
||||
const fields = {
|
||||
progressTimestamps: false,
|
||||
completedChallenges: false
|
||||
progressTimestamps: false
|
||||
};
|
||||
|
||||
function getCompletedCertCount(user) {
|
||||
@ -44,7 +43,6 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||
|
||||
this.userModel.findById(id, { fields }, (err, user) => {
|
||||
if (err || !user) {
|
||||
user.challengeMap = {};
|
||||
return done(err, user);
|
||||
}
|
||||
|
||||
@ -58,11 +56,9 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||
user.points = points;
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('challengeMap' in user) {
|
||||
completedChallengeCount = Object.keys(user.challengeMap).length;
|
||||
Object.keys(user.challengeMap)
|
||||
.map(key => user.challengeMap[key])
|
||||
.forEach(item => {
|
||||
if ('completedChallenges' in user) {
|
||||
completedChallengeCount = user.completedChallenges.length;
|
||||
user.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
@ -74,7 +70,7 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
|
||||
user.completedChallengeCount = completedChallengeCount;
|
||||
user.completedProjectCount = completedProjectCount;
|
||||
user.completedCertCount = getCompletedCertCount(user);
|
||||
user.challengeMap = {};
|
||||
user.completedChallenges = [];
|
||||
return done(null, user);
|
||||
});
|
||||
});
|
||||
|
@ -15,7 +15,7 @@
|
||||
"initial": {
|
||||
"compression": {},
|
||||
"morgan": {
|
||||
"params": ":status :method :response-time ms - :url"
|
||||
"params": ":date[iso] :status :method :response-time ms - :url"
|
||||
},
|
||||
"cors": {
|
||||
"params": {
|
||||
@ -54,7 +54,6 @@
|
||||
"./middlewares/constant-headers": {},
|
||||
"./middlewares/csp": {},
|
||||
"./middlewares/jade-helpers": {},
|
||||
"./middlewares/migrate-completed-challenges": {},
|
||||
"./middlewares/flash-cheaters": {},
|
||||
"./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) {
|
||||
const queryUser = req.user;
|
||||
const source = queryUser && Observable.forkJoin(
|
||||
queryUser.getChallengeMap$(),
|
||||
queryUser.getCompletedChallenges$(),
|
||||
queryUser.getPoints$(),
|
||||
(challengeMap, progressTimestamps) => ({
|
||||
challengeMap,
|
||||
(completedChallenges, progressTimestamps) => ({
|
||||
completedChallenges,
|
||||
progress: getProgress(progressTimestamps, queryUser.timezone)
|
||||
})
|
||||
);
|
||||
@ -29,10 +29,10 @@ export default function userServices() {
|
||||
() => !queryUser,
|
||||
Observable.of({}),
|
||||
Observable.defer(() => source)
|
||||
.map(({ challengeMap, progress }) => ({
|
||||
.map(({ completedChallenges, progress }) => ({
|
||||
...queryUser.toJSON(),
|
||||
...progress,
|
||||
challengeMap
|
||||
completedChallenges
|
||||
}))
|
||||
.map(
|
||||
user => ({
|
||||
@ -41,7 +41,7 @@ export default function userServices() {
|
||||
[user.username]: {
|
||||
..._.pick(user, userPropsForSession),
|
||||
isEmailVerified: !!user.emailVerified,
|
||||
isGithub: !!user.githubURL,
|
||||
isGithub: !!user.githubProfile,
|
||||
isLinkedIn: !!user.linkedIn,
|
||||
isTwitter: !!user.twitter,
|
||||
isWebsite: !!user.website,
|
||||
|
@ -27,17 +27,13 @@ export function createUserUpdatesFromProfile(provider, profile) {
|
||||
)
|
||||
};
|
||||
}
|
||||
// using es6 argument destructing
|
||||
// createProfileAttributes(profile) => profileUpdate
|
||||
function createProfileAttributesFromGithub(profile) {
|
||||
const {
|
||||
profileUrl: githubURL,
|
||||
profileUrl: githubProfile,
|
||||
username,
|
||||
_json: {
|
||||
id: githubId,
|
||||
avatar_url: picture,
|
||||
email: githubEmail,
|
||||
created_at: joinedGithubOn,
|
||||
blog: website,
|
||||
location,
|
||||
bio,
|
||||
@ -49,14 +45,9 @@ function createProfileAttributesFromGithub(profile) {
|
||||
username: username.toLowerCase(),
|
||||
location,
|
||||
bio,
|
||||
joinedGithubOn,
|
||||
website,
|
||||
isGithubCool: true,
|
||||
picture,
|
||||
githubId,
|
||||
githubURL,
|
||||
githubEmail,
|
||||
githubProfile: githubURL
|
||||
githubProfile
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@ import {
|
||||
export const publicUserProps = [
|
||||
'about',
|
||||
'calendar',
|
||||
'challengeMap',
|
||||
'githubURL',
|
||||
'completedChallenges',
|
||||
'githubProfile',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
@ -20,7 +20,6 @@ export const publicUserProps = [
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isGithubCool',
|
||||
'isHonest',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
@ -58,25 +57,12 @@ export function normaliseUserFields(user) {
|
||||
|
||||
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||
const calendar = progressTimestamps
|
||||
.map((objOrNum) => {
|
||||
return typeof objOrNum === 'number' ?
|
||||
objOrNum :
|
||||
objOrNum.timestamp;
|
||||
})
|
||||
.filter((timestamp) => {
|
||||
return !!timestamp;
|
||||
})
|
||||
.reduce((data, timeStamp) => {
|
||||
data[Math.floor(timeStamp / 1000)] = 1;
|
||||
.filter(Boolean)
|
||||
.reduce((data, timestamp) => {
|
||||
data[Math.floor(timestamp / 1000)] = 1;
|
||||
return data;
|
||||
}, {});
|
||||
const timestamps = progressTimestamps
|
||||
.map(objOrNum => {
|
||||
return typeof objOrNum === 'number' ?
|
||||
objOrNum :
|
||||
objOrNum.timestamp;
|
||||
});
|
||||
const uniqueHours = prepUniqueDaysByHours(timestamps, timezone);
|
||||
const uniqueHours = prepUniqueDaysByHours(progressTimestamps, timezone);
|
||||
const streak = {
|
||||
longest: calcLongestStreak(uniqueHours, timezone),
|
||||
current: calcCurrentStreak(uniqueHours, timezone)
|
||||
|
@ -6,10 +6,6 @@ block content
|
||||
var challengeName = 'Profile View';
|
||||
if (user && user.username === username)
|
||||
.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
|
||||
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/settings')
|
||||
| Update my settings
|
||||
|
Reference in New Issue
Block a user