diff --git a/common/app/Map/redux/fetch-map-ui-epic.js b/common/app/Map/redux/fetch-map-ui-epic.js
index 9933f1121e..f0cf90eec7 100644
--- a/common/app/Map/redux/fetch-map-ui-epic.js
+++ b/common/app/Map/redux/fetch-map-ui-epic.js
@@ -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,
diff --git a/common/app/routes/Profile/components/SocialIcons.jsx b/common/app/routes/Profile/components/SocialIcons.jsx
index d8b1b3bb78..a32ae4d5ae 100644
--- a/common/app/routes/Profile/components/SocialIcons.jsx
+++ b/common/app/routes/Profile/components/SocialIcons.jsx
@@ -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
diff --git a/common/app/routes/Profile/components/Timeline.jsx b/common/app/routes/Profile/components/Timeline.jsx
index a7f4516a60..86e01fb3f1 100644
--- a/common/app/routes/Profile/components/Timeline.jsx
+++ b/common/app/routes/Profile/components/Timeline.jsx
@@ -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({
- id: PropTypes.string,
- completedDate: PropTypes.number,
- lastUpdated: PropTypes.number
- }),
- idToNameMap: PropTypes.objectOf(PropTypes.string),
- username: PropTypes.string
+ completedMap: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ completedDate: PropTypes.number,
+ 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 (
{ blockNameify(idToNameMap[id]) } |
@@ -68,53 +58,12 @@ class Timeline extends PureComponent {
}
-
- {
- lastUpdated ?
- :
- ''
- }
- |
-
- {
- files ?
- :
- ''
- }
- |
);
}
- 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 {
Timeline
{
- Object.keys(completedMap).length === 0 ?
+ completedMap.length === 0 ?
No challenges have been completed yet.
@@ -134,45 +83,21 @@ class Timeline extends PureComponent {
Challenge |
First Completed |
- Last Changed |
- |
{
reverse(
sortBy(
- Object.keys(completedMap)
- .filter(key => key in idToNameMap)
- .map(key => completedMap[key]),
+ completedMap,
[ 'completedDate' ]
- )
+ ).filter(({id}) => id in idToNameMap)
)
.map(this.renderCompletion)
}
}
- {
- id &&
-
-
-
- { `${username}'s Solution to ${blockNameify(idToNameMap[id])}` }
-
-
-
-
-
-
-
-
-
- }
);
}
diff --git a/common/app/routes/Settings/components/Cert-Settings.jsx b/common/app/routes/Settings/components/Cert-Settings.jsx
index 2e3d1c0c2a..c2c3582aa5 100644
--- a/common/app/routes/Settings/components/Cert-Settings.jsx
+++ b/common/app/routes/Settings/components/Cert-Settings.jsx
@@ -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
diff --git a/common/app/routes/Settings/components/Internet-Settings.jsx b/common/app/routes/Settings/components/Internet-Settings.jsx
index ee96f1983e..245c79e998 100644
--- a/common/app/routes/Settings/components/Internet-Settings.jsx
+++ b/common/app/routes/Settings/components/Internet-Settings.jsx
@@ -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,
diff --git a/common/app/routes/Settings/redux/index.js b/common/app/routes/Settings/redux/index.js
index 1128f8d008..9d19cf23d0 100644
--- a/common/app/routes/Settings/redux/index.js
+++ b/common/app/routes/Settings/redux/index.js
@@ -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);
diff --git a/common/app/routes/Settings/redux/update-user-epic.js b/common/app/routes/Settings/redux/update-user-epic.js
index c999aa96f7..e01258b0b5 100644
--- a/common/app/routes/Settings/redux/update-user-epic.js
+++ b/common/app/routes/Settings/redux/update-user-epic.js
@@ -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
);
diff --git a/common/app/routes/Settings/utils/buildUserProjectsMap.js b/common/app/routes/Settings/utils/buildUserProjectsMap.js
index 5629a94939..9aeb3d4fe0 100644
--- a/common/app/routes/Settings/utils/buildUserProjectsMap.js
+++ b/common/app/routes/Settings/utils/buildUserProjectsMap.js
@@ -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 = {};
diff --git a/common/models/user.js b/common/models/user.js
index 4448e70550..e703151c88 100644
--- a/common/models/user.js
+++ b/common/models/user.js
@@ -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;
});
};
diff --git a/common/models/user.json b/common/models/user.json
index bbe76f9d31..1c14c82781 100644
--- a/common/models/user.json
+++ b/common/models/user.json
@@ -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"
},
diff --git a/seed/get-emails.js b/seed/get-emails.js
index 290e37eeae..bc9cfd6525 100644
--- a/seed/get-emails.js
+++ b/seed/get-emails.js
@@ -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) {
diff --git a/seed/loopbackMigration.js b/seed/loopbackMigration.js
deleted file mode 100644
index 7b9329e881..0000000000
--- a/seed/loopbackMigration.js
+++ /dev/null
@@ -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();
diff --git a/server/boot/certificate.js b/server/boot/certificate.js
index f28827af5b..5b1367401d 100644
--- a/server/boot/certificate.js
+++ b/server/boot/certificate.js
@@ -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);
},
diff --git a/server/boot/challenge.js b/server/boot/challenge.js
index 30a887fe86..e1d76294f2 100644
--- a/server/boot/challenge.js
+++ b/server/boot/challenge.js
@@ -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);
diff --git a/server/boot/settings.js b/server/boot/settings.js
index 33e7953094..7322c63393 100644
--- a/server/boot/settings.js
+++ b/server/boot/settings.js
@@ -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',
diff --git a/server/boot/user.js b/server/boot/user.js
index 50f88bfcd6..6788e4e7e4 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -142,7 +142,7 @@ module.exports = function(app) {
isBackEndCert: false,
isDataVisCert: false,
isFullStackCert: false,
- challengeMap: {}
+ completedChallenges: []
})
.subscribe(
() => {
diff --git a/server/component-passport.js b/server/component-passport.js
index d28e9bee5f..04b0f4135a 100644
--- a/server/component-passport.js
+++ b/server/component-passport.js
@@ -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,23 +56,21 @@ 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 (
- 'challengeType' in item &&
- (item.challengeType === 3 || item.challengeType === 4)
- ) {
- completedProjectCount++;
- }
- });
+ if ('completedChallenges' in user) {
+ completedChallengeCount = user.completedChallenges.length;
+ user.completedChallenges.forEach(item => {
+ if (
+ 'challengeType' in item &&
+ (item.challengeType === 3 || item.challengeType === 4)
+ ) {
+ completedProjectCount++;
+ }
+ });
}
user.completedChallengeCount = completedChallengeCount;
user.completedProjectCount = completedProjectCount;
user.completedCertCount = getCompletedCertCount(user);
- user.challengeMap = {};
+ user.completedChallenges = [];
return done(null, user);
});
});
diff --git a/server/middleware.json b/server/middleware.json
index 6afd88a9c4..a3a63e13fc 100644
--- a/server/middleware.json
+++ b/server/middleware.json
@@ -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": {}
},
diff --git a/server/middlewares/migrate-completed-challenges.js b/server/middlewares/migrate-completed-challenges.js
deleted file mode 100644
index fa2952ddd1..0000000000
--- a/server/middlewares/migrate-completed-challenges.js
+++ /dev/null
@@ -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
- );
- };
-}
diff --git a/server/services/user.js b/server/services/user.js
index cd1fc61098..5b9fc17d7f 100644
--- a/server/services/user.js
+++ b/server/services/user.js
@@ -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,
diff --git a/server/utils/auth.js b/server/utils/auth.js
index 22c8cbb302..7c44a27029 100644
--- a/server/utils/auth.js
+++ b/server/utils/auth.js
@@ -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
};
}
diff --git a/server/utils/publicUserProps.js b/server/utils/publicUserProps.js
index 5016d960a0..4ff76b404c 100644
--- a/server/utils/publicUserProps.js
+++ b/server/utils/publicUserProps.js
@@ -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)
diff --git a/server/views/account/show.jade b/server/views/account/show.jade
index 5511e0fce5..32187ff4b6 100644
--- a/server/views/account/show.jade
+++ b/server/views/account/show.jade
@@ -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