chore(server): Move api-server in to it's own DIR
This commit is contained in:
committed by
mrugesh mohapatra
parent
9fba6bce4c
commit
46a217d0a5
95
api-server/server/utils/about.js
Normal file
95
api-server/server/utils/about.js
Normal file
@ -0,0 +1,95 @@
|
||||
import _ from 'lodash';
|
||||
import debug from 'debug';
|
||||
import dedent from 'dedent';
|
||||
import fs from 'fs';
|
||||
import goog from 'googleapis';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { timeCache, observeMethod } from './rx';
|
||||
|
||||
// one million!
|
||||
const upperBound = 1000 * 1000;
|
||||
const scope = 'https://www.googleapis.com/auth/analytics.readonly';
|
||||
const pathToCred = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
|
||||
const log = debug('fcc:server:utils:about');
|
||||
const analytics = goog.analytics('v3');
|
||||
const makeRequest = observeMethod(analytics.data.realtime, 'get');
|
||||
export const toBoundInt = _.flow(
|
||||
// first convert string to integer
|
||||
_.toInteger,
|
||||
// then we bound the integer to prevent weird things like Infinity
|
||||
// and negative numbers
|
||||
// can't wait to the day we need to update this!
|
||||
_.partialRight(_.clamp, 0, upperBound)
|
||||
);
|
||||
|
||||
export function createActiveUsers() {
|
||||
const zero = Observable.of(0);
|
||||
let credentials;
|
||||
if (!pathToCred) {
|
||||
// if no path to credentials set to zero;
|
||||
log(dedent`
|
||||
no google applications credentials environmental variable found
|
||||
'GOOGLE_APPLICATION_CREDENTIALS'
|
||||
'activeUser' api will always return 0
|
||||
this can safely be ignored during development
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
try {
|
||||
credentials = require(fs.realpathSync(pathToCred));
|
||||
} catch (err) {
|
||||
log('google applications credentials file failed to require');
|
||||
console.error(err);
|
||||
// if we can't require credentials set to zero;
|
||||
return zero;
|
||||
}
|
||||
if (
|
||||
!credentials.private_key ||
|
||||
!credentials.client_email ||
|
||||
!credentials.viewId
|
||||
) {
|
||||
log(dedent`
|
||||
google applications credentials json should have a
|
||||
* private_key
|
||||
* client_email
|
||||
* viewId
|
||||
but none were found
|
||||
`);
|
||||
return zero;
|
||||
}
|
||||
|
||||
const client = new goog.auth.JWT(
|
||||
credentials['client_email'],
|
||||
null,
|
||||
credentials['private_key'],
|
||||
[scope],
|
||||
);
|
||||
const authorize = observeMethod(client, 'authorize');
|
||||
const options = {
|
||||
ids: `ga:${credentials.viewId}`,
|
||||
auth: client,
|
||||
metrics: 'rt:activeUsers'
|
||||
};
|
||||
return Observable.defer(
|
||||
// we wait for authorize to complete before attempting to make request
|
||||
// this ensures our token is initialized and valid
|
||||
// we defer here to make sure the actual request is done per subscription
|
||||
// instead of once at startup
|
||||
() => authorize().flatMap(() => makeRequest(options))
|
||||
)
|
||||
// data: Array[body|Object, request: Request]
|
||||
.map(data => data[0])
|
||||
.map(
|
||||
({ totalsForAllResults } = {}) => totalsForAllResults['rt:activeUsers']
|
||||
)
|
||||
.map(toBoundInt)
|
||||
// print errors to error log for logging, duh
|
||||
.do(null, err => console.error(err))
|
||||
// always send a number down
|
||||
.catch(() => Observable.of(0))
|
||||
// cache for 2 seconds to prevent hitting our daily request limit
|
||||
::timeCache(2, 'seconds');
|
||||
}
|
||||
|
56
api-server/server/utils/auth.js
Normal file
56
api-server/server/utils/auth.js
Normal file
@ -0,0 +1,56 @@
|
||||
const githubRegex = (/github/i);
|
||||
const providerHash = {
|
||||
facebook: ({ id }) => id,
|
||||
github: ({ username }) => username,
|
||||
twitter: ({ username }) => username,
|
||||
linkedin({ _json }) {
|
||||
return _json && _json.publicProfileUrl || null;
|
||||
},
|
||||
google: ({ id }) => id
|
||||
};
|
||||
|
||||
export function getUsernameFromProvider(provider, profile) {
|
||||
return typeof providerHash[provider] === 'function' ?
|
||||
providerHash[provider](profile) :
|
||||
null;
|
||||
}
|
||||
|
||||
// createProfileAttributes(provider: String, profile: {}) => Object
|
||||
export function createUserUpdatesFromProfile(provider, profile) {
|
||||
if (githubRegex.test(provider)) {
|
||||
return createProfileAttributesFromGithub(profile);
|
||||
}
|
||||
return {
|
||||
[getSocialProvider(provider)]: getUsernameFromProvider(
|
||||
getSocialProvider(provider),
|
||||
profile
|
||||
)
|
||||
};
|
||||
}
|
||||
// createProfileAttributes(profile) => profileUpdate
|
||||
function createProfileAttributesFromGithub(profile) {
|
||||
const {
|
||||
profileUrl: githubProfile,
|
||||
username,
|
||||
_json: {
|
||||
avatar_url: picture,
|
||||
blog: website,
|
||||
location,
|
||||
bio,
|
||||
name
|
||||
} = {}
|
||||
} = profile;
|
||||
return {
|
||||
name,
|
||||
username: username.toLowerCase(),
|
||||
location,
|
||||
bio,
|
||||
website,
|
||||
picture,
|
||||
githubProfile
|
||||
};
|
||||
}
|
||||
|
||||
export function getSocialProvider(provider) {
|
||||
return provider.split('-')[0];
|
||||
}
|
19
api-server/server/utils/bad-id-map.js
Normal file
19
api-server/server/utils/bad-id-map.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default {
|
||||
bg9997c9c79feddfaeb9bdef: '56bbb991ad1ed5201cd392ca',
|
||||
bg9995c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cb',
|
||||
bg9994c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cc',
|
||||
bg9996c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392cd',
|
||||
bg9997c9c69feddfaeb9bdef: '56bbb991ad1ed5201cd392ce',
|
||||
bg9997c9c89feddfaeb9bdef: '56bbb991ad1ed5201cd392cf',
|
||||
bg9998c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d0',
|
||||
bg9999c9c99feddfaeb9bdef: '56bbb991ad1ed5201cd392d1',
|
||||
bg9999c9c99feedfaeb9bdef: '56bbb991ad1ed5201cd392d2',
|
||||
bg9999c9c99fdddfaeb9bdef: '56bbb991ad1ed5201cd392d3',
|
||||
bb000000000000000000001: '56bbb991ad1ed5201cd392d4',
|
||||
bc000000000000000000001: '56bbb991ad1ed5201cd392d5',
|
||||
bb000000000000000000002: '56bbb991ad1ed5201cd392d6',
|
||||
bb000000000000000000003: '56bbb991ad1ed5201cd392d7',
|
||||
bb000000000000000000004: '56bbb991ad1ed5201cd392d8',
|
||||
bb000000000000000000005: '56bbb991ad1ed5201cd392d9',
|
||||
bb000000000000000000006: '56bbb991ad1ed5201cd392da'
|
||||
};
|
12
api-server/server/utils/certTypes.json
Normal file
12
api-server/server/utils/certTypes.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"frontEnd": "isFrontEndCert",
|
||||
"backEnd": "isBackEndCert",
|
||||
"dataVis": "isDataVisCert",
|
||||
"respWebDesign": "isRespWebDesignCert",
|
||||
"frontEndLibs": "isFrontEndLibsCert",
|
||||
"dataVis2018": "is2018DataVisCert",
|
||||
"jsAlgoDataStruct": "isJsAlgoDataStructCert",
|
||||
"apisMicroservices": "isApisMicroservicesCert",
|
||||
"infosecQa": "isInfosecQaCert",
|
||||
"fullStack": "isFullStackCert"
|
||||
}
|
11
api-server/server/utils/commit-goals.json
Normal file
11
api-server/server/utils/commit-goals.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"frontEndCert": "Front End Development Certification",
|
||||
"backEndCert": "Back End Development Certification",
|
||||
"fullStackCert": "Full Stack Development Certification",
|
||||
"respWebDesign": "Responsive Web Design Certification",
|
||||
"frontEndLibs": "Front End Libraries Certification",
|
||||
"jsAlgoDataStruct": "JavaScript Algorithms and Data Structures Certification",
|
||||
"dataVis": "Data Visualisation Certification",
|
||||
"apisMicroservices": "APIs and Microservices Certification",
|
||||
"infosecQa": "Information Security and Quality Assurance Certification"
|
||||
}
|
55
api-server/server/utils/commit.js
Normal file
55
api-server/server/utils/commit.js
Normal file
@ -0,0 +1,55 @@
|
||||
import dedent from 'dedent';
|
||||
import debugFactory from 'debug';
|
||||
import { Observable } from 'rx';
|
||||
import commitGoals from './commit-goals.json';
|
||||
|
||||
const debug = debugFactory('fcc:utils/commit');
|
||||
|
||||
export { commitGoals };
|
||||
|
||||
export function completeCommitment$(user) {
|
||||
const {
|
||||
isFrontEndCert,
|
||||
isBackEndCert,
|
||||
isFullStackCert,
|
||||
isRespWebDesignCert,
|
||||
isFrontEndLibsCert,
|
||||
isJsAlgoDataStructCert,
|
||||
isDataVisCert,
|
||||
isApisMicroservicesCert,
|
||||
isInfosecQaCert
|
||||
} = user;
|
||||
|
||||
return Observable.fromNodeCallback(user.pledge, user)()
|
||||
.flatMap(pledge => {
|
||||
if (!pledge) {
|
||||
return Observable.just('No pledge found');
|
||||
}
|
||||
|
||||
const { goal } = pledge;
|
||||
|
||||
if (
|
||||
(isFrontEndCert && goal === commitGoals.frontEndCert) ||
|
||||
(isBackEndCert && goal === commitGoals.backEndCert) ||
|
||||
(isFullStackCert && goal === commitGoals.fullStackCert) ||
|
||||
(isRespWebDesignCert && goal === commitGoals.respWebDesignCert) ||
|
||||
(isFrontEndLibsCert && goal === commitGoals.frontEndLibsCert) ||
|
||||
(isJsAlgoDataStructCert && goal === commitGoals.jsAlgoDataStructCert) ||
|
||||
(isDataVisCert && goal === commitGoals.dataVisCert) ||
|
||||
(isApisMicroservicesCert &&
|
||||
goal === commitGoals.apisMicroservicesCert) ||
|
||||
(isInfosecQaCert && goal === commitGoals.infosecQaCert)
|
||||
) {
|
||||
debug('marking goal complete');
|
||||
pledge.isCompleted = true;
|
||||
pledge.dateEnded = new Date();
|
||||
pledge.formerUserId = pledge.userId;
|
||||
pledge.userId = null;
|
||||
return Observable.fromNodeCallback(pledge.save, pledge)();
|
||||
}
|
||||
return Observable.just(dedent`
|
||||
You have not yet reached your goal of completing the ${goal}
|
||||
Please retry when you have met the requirements.
|
||||
`);
|
||||
});
|
||||
}
|
50
api-server/server/utils/commit.json
Normal file
50
api-server/server/utils/commit.json
Normal file
@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"name": "girl develop it",
|
||||
"displayName": "Girl Develop It",
|
||||
"donateUrl": "https://www.girldevelopit.com/donate",
|
||||
"description": "Girl Develop It provides in-person classes for women to learn to code.",
|
||||
"imgAlt": "Girl Develop It participants coding at tables.",
|
||||
"imgUrl": "https://i.imgur.com/U1CyEuA.jpg"
|
||||
},
|
||||
{
|
||||
"name": "black girls code",
|
||||
"displayName": "Black Girls CODE",
|
||||
"donateUrl": "http://www.blackgirlscode.com/",
|
||||
"description": "Black Girls CODE is devoted to showing the world that black girls can code, and do so much more.",
|
||||
"imgAlt": "Girls developing code with instructor",
|
||||
"imgUrl": "https://i.imgur.com/HBVrdaj.jpg"
|
||||
},
|
||||
{
|
||||
"name": "coderdojo",
|
||||
"displayName": "CoderDojo",
|
||||
"donateUrl": "https://www.globalgiving.org/projects/coderdojo-start-a-dojo-support/",
|
||||
"description": "CoderDojo is the global network of free computer programming clubs for young people.",
|
||||
"imgAlt": "Two adults help several kids program on their laptops.",
|
||||
"imgUrl": "https://i.imgur.com/701RLfV.jpg"
|
||||
},
|
||||
{
|
||||
"name": "women who code",
|
||||
"displayName": "Women Who Code",
|
||||
"donateUrl": "https://www.womenwhocode.com/donate",
|
||||
"description": "Women Who Code (WWCode) is a global leader in propelling women in the tech industry.",
|
||||
"imgAlt": "Four women sitting in a classroom together learning to code.",
|
||||
"imgUrl": "https://i.imgur.com/tKUi6Rf.jpg"
|
||||
},
|
||||
{
|
||||
"name": "girls who code",
|
||||
"displayName": "Girls Who Code",
|
||||
"donateUrl": "http://girlswhocode.com/",
|
||||
"description": "Girls Who Code programs work to inspire, educate, and equip girls with the computing skills to pursue 21st century opportunities.",
|
||||
"imgAlt": "Three women smiling while they code on a computer together.",
|
||||
"imgUrl": "https://i.imgur.com/op8BVph.jpg"
|
||||
},
|
||||
{
|
||||
"name": "hack club",
|
||||
"displayName": "Hack Club",
|
||||
"donateUrl": "https://hackclub.com/donate",
|
||||
"description": "Hack Club helps high schoolers start after-school coding clubs.",
|
||||
"imgAlt": "A bunch of high school students posing for a photo in their Hack Club.",
|
||||
"imgUrl": "https://i.imgur.com/G2YvPHf.jpg"
|
||||
}
|
||||
]
|
15
api-server/server/utils/constantStrings.json
Normal file
15
api-server/server/utils/constantStrings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"gitHubUserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1521.3 Safari/537.36",
|
||||
|
||||
"legacyFrontEndChallengeId": "561add10cb82ac38a17513be",
|
||||
"legacyBackEndChallengeId": "660add10cb82ac38a17513be",
|
||||
"legacyDataVisId": "561add10cb82ac39a17513bc",
|
||||
|
||||
"respWebDesignId": "561add10cb82ac38a17513bc",
|
||||
"frontEndLibsId": "561acd10cb82ac38a17513bc",
|
||||
"dataVis2018Id": "5a553ca864b52e1d8bceea14",
|
||||
"jsAlgoDataStructId": "561abd10cb81ac38a17513bc",
|
||||
"apisMicroservicesId": "561add10cb82ac38a17523bc",
|
||||
"infosecQaId": "561add10cb82ac38a17213bc",
|
||||
"fullStackId": "561add10cb82ac38a17213bd"
|
||||
}
|
75
api-server/server/utils/constants.js
Normal file
75
api-server/server/utils/constants.js
Normal file
@ -0,0 +1,75 @@
|
||||
let alphabet = '';
|
||||
|
||||
for (let i = 0; i < 26; i++) {
|
||||
alphabet = alphabet.concat(String.fromCharCode(97 + i));
|
||||
}
|
||||
|
||||
export const blacklistedUsernames = [
|
||||
...alphabet.split(''),
|
||||
'about',
|
||||
'academic-honesty',
|
||||
'account',
|
||||
'agile',
|
||||
'all-stories',
|
||||
'api',
|
||||
'backend-challenge-completed',
|
||||
'bonfire',
|
||||
'cats.json',
|
||||
'challenge',
|
||||
'challenge-completed',
|
||||
'challenges',
|
||||
'chat',
|
||||
'coding-bootcamp-cost-calculator',
|
||||
'completed-bonfire',
|
||||
'completed-challenge',
|
||||
'completed-field-guide',
|
||||
'completed-zipline-or-basejump',
|
||||
'donate',
|
||||
'events',
|
||||
'explorer',
|
||||
'external',
|
||||
'field-guide',
|
||||
'forgot',
|
||||
'forum',
|
||||
'get-help',
|
||||
'get-help',
|
||||
'get-pai',
|
||||
'guide',
|
||||
'how-nonprofit-projects-work',
|
||||
'internal',
|
||||
'jobs',
|
||||
'jobs-form',
|
||||
'learn-to-code',
|
||||
'login',
|
||||
'logout',
|
||||
'map',
|
||||
'modern-challenge-completed',
|
||||
'news',
|
||||
'nonprofits',
|
||||
'nonproifts-form',
|
||||
'open-api',
|
||||
'pmi-acp-agile-project-managers',
|
||||
'pmi-acp-agile-project-managers-form',
|
||||
'privacy',
|
||||
'privacy',
|
||||
'project-completed',
|
||||
'reset',
|
||||
'services',
|
||||
'signin',
|
||||
'signout',
|
||||
'sitemap.xml',
|
||||
'software-resources-for-nonprofits',
|
||||
'stories',
|
||||
'terms',
|
||||
'the-fastest-web-page-on-the-internet',
|
||||
'twitch',
|
||||
'unsubscribe',
|
||||
'unsubscribed',
|
||||
'update-my-portfolio',
|
||||
'update-my-profile-ui',
|
||||
'update-my-projects',
|
||||
'update-my-theme',
|
||||
'update-my-username',
|
||||
'user',
|
||||
'wiki'
|
||||
];
|
32
api-server/server/utils/create-handled-error.js
Normal file
32
api-server/server/utils/create-handled-error.js
Normal file
@ -0,0 +1,32 @@
|
||||
const _handledError = Symbol('handledError');
|
||||
|
||||
export function isHandledError(err) {
|
||||
return !!err[_handledError];
|
||||
}
|
||||
|
||||
export function unwrapHandledError(err) {
|
||||
return err[_handledError] || {};
|
||||
}
|
||||
|
||||
export function wrapHandledError(err, {
|
||||
type,
|
||||
message,
|
||||
redirectTo,
|
||||
status = 200
|
||||
}) {
|
||||
err[_handledError] = { type, message, redirectTo, status };
|
||||
return err;
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorFormatter = (type, redirectTo) =>
|
||||
({ msg }) => wrapHandledError(
|
||||
new Error(msg),
|
||||
{
|
||||
type,
|
||||
message: msg,
|
||||
redirectTo,
|
||||
// we default to 400 as these are malformed requests
|
||||
status: 400
|
||||
}
|
||||
);
|
11
api-server/server/utils/date-utils.js
Normal file
11
api-server/server/utils/date-utils.js
Normal file
@ -0,0 +1,11 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
// day count between two epochs (inclusive)
|
||||
export function dayCount([head, tail], timezone = 'UTC') {
|
||||
return Math.ceil(
|
||||
moment(moment(head).tz(timezone).endOf('day')).diff(
|
||||
moment(tail).tz(timezone).startOf('day'),
|
||||
'days',
|
||||
true)
|
||||
);
|
||||
}
|
75
api-server/server/utils/date-utils.test.js
Normal file
75
api-server/server/utils/date-utils.test.js
Normal file
@ -0,0 +1,75 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { dayCount } from './date-utils';
|
||||
import test from 'tape';
|
||||
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
test('Day count between two epochs (inclusive) calculation', function(t) {
|
||||
t.plan(7);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
1,
|
||||
'should return 1 day given epochs of the same day'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
1,
|
||||
'should return 1 day given same epochs'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when there is a 24 hours difference'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when the diff is less than 24h but different in UTC'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 23:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST),
|
||||
1,
|
||||
'should return 1 day when the diff is less than 24h ' +
|
||||
'and days are different in UTC, but given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('10/27/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('5/12/1982 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
12222,
|
||||
'should return correct count when there is very big period'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
dayCount([
|
||||
moment.utc('8/4/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
2,
|
||||
'should return 2 days when there is a 24 hours difference ' +
|
||||
'between dates given `undefined` timezone'
|
||||
);
|
||||
});
|
52
api-server/server/utils/getDynamicPropsForUser.js
Normal file
52
api-server/server/utils/getDynamicPropsForUser.js
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
function getCompletedCertCount(user) {
|
||||
return [
|
||||
'isApisMicroservicesCert',
|
||||
'is2018DataVisCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
function getLegacyCertCount(user) {
|
||||
return [
|
||||
'isFrontEndCert',
|
||||
'isBackEndCert',
|
||||
'isDataVisCert'
|
||||
].reduce((sum, key) => user[key] ? sum + 1 : sum, 0);
|
||||
}
|
||||
|
||||
export default function populateUser(db, user) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let populatedUser = {...user};
|
||||
db.collection('user')
|
||||
.aggregate([
|
||||
{ $match: { _id: user.id } },
|
||||
{ $project: { points: { $size: '$progressTimestamps' } } }
|
||||
], function(err, [{ points = 1 } = {}]) {
|
||||
if (err) { return reject(err); }
|
||||
user.points = points;
|
||||
let completedChallengeCount = 0;
|
||||
let completedProjectCount = 0;
|
||||
if ('completedChallenges' in user) {
|
||||
completedChallengeCount = user.completedChallenges.length;
|
||||
user.completedChallenges.forEach(item => {
|
||||
if (
|
||||
'challengeType' in item &&
|
||||
(item.challengeType === 3 || item.challengeType === 4)
|
||||
) {
|
||||
completedProjectCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
populatedUser.completedChallengeCount = completedChallengeCount;
|
||||
populatedUser.completedProjectCount = completedProjectCount;
|
||||
populatedUser.completedCertCount = getCompletedCertCount(user);
|
||||
populatedUser.completedLegacyCertCount = getLegacyCertCount(user);
|
||||
populatedUser.completedChallenges = [];
|
||||
return resolve(populatedUser);
|
||||
});
|
||||
});
|
||||
}
|
26
api-server/server/utils/index.js
Normal file
26
api-server/server/utils/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
export function dasherize(name) {
|
||||
return ('' + name)
|
||||
.toLowerCase()
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/[^a-z0-9\-\.]/gi, '')
|
||||
.replace(/\:/g, '');
|
||||
}
|
||||
|
||||
export function nameify(str) {
|
||||
return ('' + str)
|
||||
.replace(/[^a-zA-Z0-9\s]/g, '')
|
||||
.replace(/\:/g, '');
|
||||
}
|
||||
|
||||
export function unDasherize(name) {
|
||||
return ('' + name)
|
||||
// replace dash with space
|
||||
.replace(/\-/g, ' ')
|
||||
// strip nonalphanumarics chars except whitespace
|
||||
.replace(/[^a-zA-Z\d\s]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function addPlaceholderImage(name) {
|
||||
return `https://identicon.org?t=${name}&s=256`;
|
||||
}
|
8
api-server/server/utils/lang-passthrough-urls.js
Normal file
8
api-server/server/utils/lang-passthrough-urls.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default [
|
||||
'auth',
|
||||
'services',
|
||||
'link'
|
||||
].reduce((throughs, route) => {
|
||||
throughs[route] = true;
|
||||
return throughs;
|
||||
}, {});
|
241
api-server/server/utils/map.js
Normal file
241
api-server/server/utils/map.js
Normal file
@ -0,0 +1,241 @@
|
||||
import _ from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
|
||||
import { unDasherize, nameify } from '../utils';
|
||||
import {
|
||||
addNameIdMap as _addNameIdToMap,
|
||||
checkMapData,
|
||||
getFirstChallenge as _getFirstChallenge
|
||||
} from '../../common/utils/map.js';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const isBeta = !!process.env.BETA;
|
||||
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
|
||||
const addNameIdMap = _.once(_addNameIdToMap);
|
||||
const getFirstChallenge = _.once(_getFirstChallenge);
|
||||
/*
|
||||
* interface ChallengeMap {
|
||||
* result: {
|
||||
* superBlocks: [ ...superBlockDashedName: String ]
|
||||
* },
|
||||
* entities: {
|
||||
* superBlock: {
|
||||
* [ ...superBlockDashedName ]: SuperBlock
|
||||
* },
|
||||
* block: {
|
||||
* [ ...blockDashedNameg ]: Block,
|
||||
* challenge: {
|
||||
* [ ...challengeDashedNameg ]: Challenge
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function _cachedMap({ Block, Challenge }) {
|
||||
const challenges = Challenge.find$({
|
||||
order: [ 'order ASC', 'suborder ASC' ],
|
||||
where: { isPrivate: false }
|
||||
});
|
||||
const challengeMap = challenges
|
||||
.map(
|
||||
challenges => challenges
|
||||
.map(challenge => challenge.toJSON())
|
||||
.reduce((hash, challenge) => {
|
||||
hash[challenge.dashedName] = challenge;
|
||||
return hash;
|
||||
}, {})
|
||||
);
|
||||
const blocks = Block.find$({
|
||||
order: [ 'superOrder ASC', 'order ASC' ],
|
||||
where: { isPrivate: false }
|
||||
});
|
||||
const blockMap = Observable.combineLatest(
|
||||
blocks.map(
|
||||
blocks => blocks
|
||||
.map(block => block.toJSON())
|
||||
.reduce((hash, block) => {
|
||||
hash[block.dashedName] = block;
|
||||
return hash;
|
||||
}, {})
|
||||
),
|
||||
challenges
|
||||
)
|
||||
.map(([ blocksMap, challenges ]) => {
|
||||
return challenges.reduce((blocksMap, challenge) => {
|
||||
if (blocksMap[challenge.block].challenges) {
|
||||
blocksMap[challenge.block].challenges.push(challenge.dashedName);
|
||||
} else {
|
||||
blocksMap[challenge.block] = {
|
||||
...blocksMap[challenge.block],
|
||||
challenges: [ challenge.dashedName ]
|
||||
};
|
||||
}
|
||||
return blocksMap;
|
||||
}, blocksMap);
|
||||
});
|
||||
const superBlockMap = blocks.map(blocks => blocks.reduce((map, block) => {
|
||||
if (
|
||||
map[block.superBlock] &&
|
||||
map[block.superBlock].blocks
|
||||
) {
|
||||
map[block.superBlock].blocks.push(block.dashedName);
|
||||
} else {
|
||||
map[block.superBlock] = {
|
||||
title: _.startCase(block.superBlock),
|
||||
order: block.superOrder,
|
||||
name: nameify(_.startCase(block.superBlock)),
|
||||
dashedName: block.superBlock,
|
||||
blocks: [block.dashedName],
|
||||
message: block.superBlockMessage
|
||||
};
|
||||
}
|
||||
return map;
|
||||
}, {}));
|
||||
const superBlocks = superBlockMap.map(superBlockMap => {
|
||||
return Object.keys(superBlockMap)
|
||||
.map(key => superBlockMap[key])
|
||||
.map(({ dashedName }) => dashedName);
|
||||
});
|
||||
return Observable.combineLatest(
|
||||
superBlockMap,
|
||||
blockMap,
|
||||
challengeMap,
|
||||
superBlocks,
|
||||
(superBlock, block, challenge, superBlocks) => ({
|
||||
entities: {
|
||||
superBlock,
|
||||
block,
|
||||
challenge
|
||||
},
|
||||
result: {
|
||||
superBlocks
|
||||
}
|
||||
})
|
||||
)
|
||||
.do(checkMapData)
|
||||
.shareReplay();
|
||||
}
|
||||
|
||||
export const cachedMap = _.once(_cachedMap);
|
||||
|
||||
// type ObjectId: String;
|
||||
// getChallengeById(
|
||||
// map: Observable[map],
|
||||
// id: ObjectId
|
||||
// ) => Observable[Challenge] | Void;
|
||||
export function getChallengeById(map, id) {
|
||||
return Observable.if(
|
||||
() => !id,
|
||||
map.map(getFirstChallenge),
|
||||
map.map(addNameIdMap)
|
||||
.map(map => {
|
||||
const {
|
||||
entities: { challenge: challengeMap, challengeIdToName }
|
||||
} = map;
|
||||
let finalChallenge;
|
||||
const dashedName = challengeIdToName[id];
|
||||
finalChallenge = challengeMap[dashedName];
|
||||
if (!finalChallenge) {
|
||||
finalChallenge = getFirstChallenge(map);
|
||||
}
|
||||
return finalChallenge;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function getChallengeInfo(map) {
|
||||
return map.map(addNameIdMap)
|
||||
.map(({
|
||||
entities: {
|
||||
challenge: challengeMap,
|
||||
challengeIdToName
|
||||
}
|
||||
}) => ({
|
||||
challengeMap,
|
||||
challengeIdToName
|
||||
}));
|
||||
}
|
||||
|
||||
// if challenge is not isComingSoon or isBeta => load
|
||||
// if challenge is ComingSoon we are in beta||dev => load
|
||||
// if challenge is beta and we are in beta||dev => load
|
||||
// else hide
|
||||
function loadComingSoonOrBetaChallenge({
|
||||
isComingSoon,
|
||||
isBeta: challengeIsBeta
|
||||
}) {
|
||||
return !(isComingSoon || challengeIsBeta) || isDev || isBeta;
|
||||
}
|
||||
|
||||
// this is a hard search
|
||||
// falls back to soft search
|
||||
export function getChallenge(
|
||||
challengeDashedName,
|
||||
blockDashedName,
|
||||
map) {
|
||||
return map
|
||||
.flatMap(({ entities, result: { superBlocks } }) => {
|
||||
const superBlock = entities.superBlock;
|
||||
const block = entities.block[blockDashedName];
|
||||
const challenge = entities.challenge[challengeDashedName];
|
||||
return Observable.if(
|
||||
() => (
|
||||
!blockDashedName ||
|
||||
!block ||
|
||||
!challenge ||
|
||||
!loadComingSoonOrBetaChallenge(challenge)
|
||||
),
|
||||
getChallengeByDashedName(challengeDashedName, map),
|
||||
Observable.just({ block, challenge })
|
||||
)
|
||||
.map(({ challenge, block }) => ({
|
||||
redirect: challenge.block !== blockDashedName ?
|
||||
`/challenges/${block.dashedName}/${challenge.dashedName}` :
|
||||
false,
|
||||
entities: {
|
||||
superBlock,
|
||||
challenge: {
|
||||
[challenge.dashedName]: challenge
|
||||
}
|
||||
},
|
||||
result: {
|
||||
block: block.dashedName,
|
||||
challenge: challenge.dashedName,
|
||||
superBlocks
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
export function getBlockForChallenge(map, challenge) {
|
||||
return map.map(({ entities: { block } }) => block[challenge.block]);
|
||||
}
|
||||
|
||||
export function getChallengeByDashedName(dashedName, map) {
|
||||
const challengeName = unDasherize(dashedName)
|
||||
.replace(challengesRegex, '');
|
||||
const testChallengeName = new RegExp(challengeName, 'i');
|
||||
|
||||
return map
|
||||
.map(({ entities }) => entities.challenge)
|
||||
.flatMap(challengeMap => {
|
||||
return Observable.from(Object.keys(challengeMap))
|
||||
.map(key => challengeMap[key]);
|
||||
})
|
||||
.filter(challenge => {
|
||||
return loadComingSoonOrBetaChallenge(challenge) &&
|
||||
testChallengeName.test(challenge.name);
|
||||
})
|
||||
.last({ defaultValue: null })
|
||||
.flatMap(challengeOrNull => {
|
||||
return Observable.if(
|
||||
() => !!challengeOrNull,
|
||||
Observable.just(challengeOrNull),
|
||||
map.map(getFirstChallenge)
|
||||
);
|
||||
})
|
||||
.flatMap(challenge => {
|
||||
return getBlockForChallenge(map, challenge)
|
||||
.map(block => ({ challenge, block }));
|
||||
});
|
||||
}
|
||||
|
74
api-server/server/utils/middleware.js
Normal file
74
api-server/server/utils/middleware.js
Normal file
@ -0,0 +1,74 @@
|
||||
import dedent from 'dedent';
|
||||
import { validationResult } from 'express-validator/check';
|
||||
|
||||
import { createValidatorErrorFormatter } from './create-handled-error.js';
|
||||
|
||||
export function ifNoUserRedirectTo(url, message, type = 'errors') {
|
||||
return function(req, res, next) {
|
||||
const { path } = req;
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
req.flash(type, message || `You must be signed in to access ${path}`);
|
||||
|
||||
return res.redirect(url);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUserSend(sendThis) {
|
||||
return function(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(200).send(sendThis);
|
||||
};
|
||||
}
|
||||
|
||||
export function ifNoUser401(req, res, next) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
export function ifNotVerifiedRedirectToUpdateEmail(req, res, next) {
|
||||
const { user } = req;
|
||||
if (!user) {
|
||||
return next();
|
||||
}
|
||||
if (!user.emailVerified) {
|
||||
req.flash(
|
||||
'danger',
|
||||
dedent`
|
||||
We do not have your verified email address on record,
|
||||
please add it in the settings to continue with your request.
|
||||
`
|
||||
);
|
||||
return res.redirect('/settings');
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
export function ifUserRedirectTo(path = '/', status) {
|
||||
status = status === 302 ? 302 : 301;
|
||||
return (req, res, next) => {
|
||||
if (req.user) {
|
||||
return res.status(status).redirect(path);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
// for use with express-validator error formatter
|
||||
export const createValidatorErrorHandler = (...args) => (req, res, next) => {
|
||||
const validation = validationResult(req)
|
||||
.formatWith(createValidatorErrorFormatter(...args));
|
||||
|
||||
if (!validation.isEmpty()) {
|
||||
const errors = validation.array();
|
||||
return next(errors.pop());
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
79
api-server/server/utils/publicUserProps.js
Normal file
79
api-server/server/utils/publicUserProps.js
Normal file
@ -0,0 +1,79 @@
|
||||
import { isURL } from 'validator';
|
||||
|
||||
import { addPlaceholderImage } from './';
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from '../utils/user-stats';
|
||||
|
||||
export const publicUserProps = [
|
||||
'about',
|
||||
'calendar',
|
||||
'completedChallenges',
|
||||
'githubProfile',
|
||||
'isApisMicroservicesCert',
|
||||
'isBackEndCert',
|
||||
'isCheater',
|
||||
'isDonating',
|
||||
'is2018DataVisCert',
|
||||
'isDataVisCert',
|
||||
'isFrontEndCert',
|
||||
'isFullStackCert',
|
||||
'isFrontEndLibsCert',
|
||||
'isHonest',
|
||||
'isInfosecQaCert',
|
||||
'isJsAlgoDataStructCert',
|
||||
'isRespWebDesignCert',
|
||||
'linkedin',
|
||||
'location',
|
||||
'name',
|
||||
'points',
|
||||
'portfolio',
|
||||
'profileUI',
|
||||
'projects',
|
||||
'streak',
|
||||
'twitter',
|
||||
'username',
|
||||
'website',
|
||||
'yearsTopContributor'
|
||||
];
|
||||
|
||||
export const userPropsForSession = [
|
||||
...publicUserProps,
|
||||
'currentChallengeId',
|
||||
'email',
|
||||
'emailVerified',
|
||||
'id',
|
||||
'sendQuincyEmail',
|
||||
'theme',
|
||||
'completedChallengeCount',
|
||||
'completedProjectCount',
|
||||
'completedCertCount',
|
||||
'completedLegacyCertCount',
|
||||
'acceptedPrivacyTerms'
|
||||
];
|
||||
|
||||
export function normaliseUserFields(user) {
|
||||
const about = user.bio && !user.about ? user.bio : user.about;
|
||||
const picture = user.picture || addPlaceholderImage(user.username);
|
||||
const twitter = user.twitter && isURL(user.twitter) ?
|
||||
user.twitter :
|
||||
user.twitter && `https://www.twitter.com/${user.twitter.replace(/^@/, '')}`;
|
||||
return { about, picture, twitter };
|
||||
}
|
||||
|
||||
export function getProgress(progressTimestamps, timezone = 'EST') {
|
||||
const calendar = progressTimestamps
|
||||
.filter(Boolean)
|
||||
.reduce((data, timestamp) => {
|
||||
data[Math.floor(timestamp / 1000)] = 1;
|
||||
return data;
|
||||
}, {});
|
||||
const uniqueHours = prepUniqueDaysByHours(progressTimestamps, timezone);
|
||||
const streak = {
|
||||
longest: calcLongestStreak(uniqueHours, timezone),
|
||||
current: calcCurrentStreak(uniqueHours, timezone)
|
||||
};
|
||||
return { calendar, streak };
|
||||
}
|
6
api-server/server/utils/react.js
vendored
Normal file
6
api-server/server/utils/react.js
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export const errorThrowerMiddleware = () => next => action => {
|
||||
if (action.error) {
|
||||
throw action.payload;
|
||||
}
|
||||
return next(action);
|
||||
};
|
125
api-server/server/utils/resources.json
Normal file
125
api-server/server/utils/resources.json
Normal file
@ -0,0 +1,125 @@
|
||||
{
|
||||
"verbs": [
|
||||
"aced",
|
||||
"nailed",
|
||||
"rocked",
|
||||
"destroyed",
|
||||
"owned",
|
||||
"crushed",
|
||||
"conquered",
|
||||
"shredded",
|
||||
"demolished",
|
||||
"devoured",
|
||||
"banished",
|
||||
"wrangled"
|
||||
],
|
||||
"compliments": [
|
||||
"Over the top!",
|
||||
"Down the rabbit hole we go!",
|
||||
"Bring that rain!",
|
||||
"Target acquired!",
|
||||
"Feel that need for speed!",
|
||||
"You've got guts!",
|
||||
"We have liftoff!",
|
||||
"To infinity and beyond!",
|
||||
"Encore!",
|
||||
"Onward, ho!",
|
||||
"Challenge destroyed!",
|
||||
"It's on like Donkey Kong!",
|
||||
"Power level? It's over 9000!",
|
||||
"Coding spree!",
|
||||
"Code long and prosper.",
|
||||
"The crowd goes wild!",
|
||||
"One for the guinness book!",
|
||||
"Flawless victory!",
|
||||
"Most efficient!",
|
||||
"Party on, Wayne!",
|
||||
"You've got the touch!",
|
||||
"You're on fire!",
|
||||
"Don't hurt 'em, Hammer!",
|
||||
"The town is now red!",
|
||||
"To the nines!",
|
||||
"The world rejoices!",
|
||||
"That's the way it's done!",
|
||||
"You rock!",
|
||||
"Woo-hoo!",
|
||||
"We knew you could do it!",
|
||||
"Hyper Combo Finish!",
|
||||
"Nothing but net!",
|
||||
"Boom-shakalaka!",
|
||||
"You're a shooting star!",
|
||||
"You're unstoppable!",
|
||||
"Way cool!",
|
||||
"You're king of the world!",
|
||||
"Walk on that sunshine!",
|
||||
"Keep on trucking!",
|
||||
"Off the charts!",
|
||||
"There is no spoon!",
|
||||
"Cranked it up to 11!",
|
||||
"Escape velocity reached!",
|
||||
"You make this look easy!",
|
||||
"Passed with flying colors!",
|
||||
"You've got this!",
|
||||
"Happy, happy, joy, joy!",
|
||||
"Tomorrow, the world!",
|
||||
"Your powers combined!",
|
||||
"A winner is you!",
|
||||
"It's alive. It's alive!",
|
||||
"Sonic Boom!",
|
||||
"Here's looking at you, Code!",
|
||||
"Ride like the wind!",
|
||||
"Legen - wait for it - dary!",
|
||||
"Ludicrous Speed! Go!",
|
||||
"Yes we can!",
|
||||
"Most triumphant!",
|
||||
"One loop to rule them all!",
|
||||
"By the power of Grayskull!",
|
||||
"You did it!",
|
||||
"Storm that castle!",
|
||||
"Face-melting guitar solo!",
|
||||
"Checkmate!",
|
||||
"Bodacious!",
|
||||
"Tubular!",
|
||||
"You're outta sight!",
|
||||
"Keep calm and code on!",
|
||||
"Even sad panda smiles!",
|
||||
"Even grumpy cat approves!",
|
||||
"Kool-Aid Man says oh yeah!",
|
||||
"Bullseye!",
|
||||
"Far out!",
|
||||
"You're heating up!",
|
||||
"Hasta la vista, challenge!",
|
||||
"Terminated.",
|
||||
"Off the hook!",
|
||||
"Thundercats, Hooo!",
|
||||
"Shiver me timbers!",
|
||||
"Raise the roof!",
|
||||
"I also live dangerously.",
|
||||
"Get to the choppa!",
|
||||
"Bingo!",
|
||||
"And you're all out of gum.",
|
||||
"Even honeybadger cares!",
|
||||
"Helm, Warp Nine. Engage!",
|
||||
"Gotta code 'em all!",
|
||||
"Spool up the FTL drive!",
|
||||
"Cool beans!",
|
||||
"They're in another castle.",
|
||||
"Power UP!",
|
||||
"Pikachu chooses you!",
|
||||
"We're gonna pump you up!",
|
||||
"I gotta have more cow bell."
|
||||
],
|
||||
"phrases": [
|
||||
"Shout it from on top of a mountain",
|
||||
"Tell everyone and their dogs",
|
||||
"Show them. Show them all!",
|
||||
"Inspire your friends",
|
||||
"Tell the world of your greatness",
|
||||
"Look accomplished on social media",
|
||||
"Share news of your grand endeavor",
|
||||
"Establish your alibi for the past two hours",
|
||||
"Prove to mom that computers aren't just for games",
|
||||
"With coding power comes sharing responsibility",
|
||||
"Have you told your friends of your coding powers?"
|
||||
]
|
||||
}
|
58
api-server/server/utils/rx.js
Normal file
58
api-server/server/utils/rx.js
Normal file
@ -0,0 +1,58 @@
|
||||
import Rx, { AsyncSubject, Observable } from 'rx';
|
||||
import moment from 'moment';
|
||||
import debugFactory from 'debug';
|
||||
|
||||
const debug = debugFactory('fcc:rxUtils');
|
||||
|
||||
export function saveInstance(instance) {
|
||||
return new Rx.Observable.create(function(observer) {
|
||||
if (!instance || typeof instance.save !== 'function') {
|
||||
debug('no instance or save method');
|
||||
observer.onNext();
|
||||
return observer.onCompleted();
|
||||
}
|
||||
return instance.save(function(err, savedInstance) {
|
||||
if (err) {
|
||||
return observer.onError(err);
|
||||
}
|
||||
debug('instance saved');
|
||||
observer.onNext(savedInstance);
|
||||
return observer.onCompleted();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// alias saveInstance
|
||||
export const saveUser = saveInstance;
|
||||
|
||||
// observeQuery(Model: Object, methodName: String, query: Any) => Observable
|
||||
export function observeQuery(Model, methodName, query) {
|
||||
return Rx.Observable.fromNodeCallback(Model[methodName], Model)(query);
|
||||
}
|
||||
|
||||
// observeMethod(
|
||||
// context: Object, methodName: String
|
||||
// ) => (query: Any) => Observable
|
||||
export function observeMethod(context, methodName) {
|
||||
return Rx.Observable.fromNodeCallback(context[methodName], context);
|
||||
}
|
||||
|
||||
// must be bound to an observable instance
|
||||
// timeCache(amount: Number, unit: String) => Observable
|
||||
export function timeCache(time, unit) {
|
||||
const source = this;
|
||||
let cache;
|
||||
let expireCacheAt;
|
||||
return Observable.create(observable => {
|
||||
// if there is no expire time set
|
||||
// or if expireCacheAt is smaller than now,
|
||||
// set new expire time in MS and create new subscription to source
|
||||
if (!expireCacheAt || expireCacheAt < Date.now()) {
|
||||
// set expire in ms;
|
||||
expireCacheAt = moment().add(time, unit).valueOf();
|
||||
cache = new AsyncSubject();
|
||||
source.subscribe(cache);
|
||||
}
|
||||
return cache.subscribe(observable);
|
||||
});
|
||||
}
|
19
api-server/server/utils/superBlockCertTypeMap.js
Normal file
19
api-server/server/utils/superBlockCertTypeMap.js
Normal file
@ -0,0 +1,19 @@
|
||||
import certTypes from './certTypes.json';
|
||||
|
||||
const superBlockCertTypeMap = {
|
||||
// legacy
|
||||
'legacy-front-end': certTypes.frontEnd,
|
||||
'legacy-back-end': certTypes.backEnd,
|
||||
'legacy-data-visualization': certTypes.dataVis,
|
||||
|
||||
// modern
|
||||
'responsive-web-design': certTypes.respWebDesign,
|
||||
'javascript-algorithms-and-data-structures': certTypes.jsAlgoDataStruct,
|
||||
'front-end-libraries': certTypes.frontEndLibs,
|
||||
'data-visualization': certTypes.dataVis2018,
|
||||
'apis-and-microservices': certTypes.apisMicroservices,
|
||||
'information-security-and-quality-assurance': certTypes.infosecQa,
|
||||
'full-stack': certTypes.fullStack
|
||||
};
|
||||
|
||||
export default superBlockCertTypeMap;
|
37
api-server/server/utils/url-utils.js
Normal file
37
api-server/server/utils/url-utils.js
Normal file
@ -0,0 +1,37 @@
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const isBeta = !!process.env.BETA;
|
||||
|
||||
export function getEmailSender() {
|
||||
return process.env.SES_MAIL_FROM || 'team@freecodecamp.org';
|
||||
}
|
||||
|
||||
export function getPort() {
|
||||
if (!isDev) {
|
||||
return '443';
|
||||
}
|
||||
return process.env.SYNC_PORT || '3000';
|
||||
}
|
||||
|
||||
export function getProtocol() {
|
||||
return isDev ? 'http' : 'https';
|
||||
}
|
||||
|
||||
export function getHost() {
|
||||
if (isDev) {
|
||||
return process.env.HOST || 'localhost';
|
||||
}
|
||||
return isBeta ? 'beta.freecodecamp.org' : 'www.freecodecamp.org';
|
||||
}
|
||||
|
||||
export function getServerFullURL() {
|
||||
if (!isDev) {
|
||||
return getProtocol()
|
||||
+ '://'
|
||||
+ getHost();
|
||||
}
|
||||
return getProtocol()
|
||||
+ '://'
|
||||
+ getHost()
|
||||
+ ':'
|
||||
+ getPort();
|
||||
}
|
91
api-server/server/utils/user-stats.js
Normal file
91
api-server/server/utils/user-stats.js
Normal file
@ -0,0 +1,91 @@
|
||||
import compose from 'lodash/fp/compose';
|
||||
import map from 'lodash/fp/map';
|
||||
import sortBy from 'lodash/fp/sortBy';
|
||||
import trans from 'lodash/fp/transform';
|
||||
import last from 'lodash/fp/last';
|
||||
import forEachRight from 'lodash/fp/forEachRight';
|
||||
import moment from 'moment-timezone';
|
||||
import { dayCount } from '../utils/date-utils';
|
||||
|
||||
const transform = trans.convert({ cap: false });
|
||||
|
||||
const hoursBetween = 24;
|
||||
const hoursDay = 24;
|
||||
|
||||
export function prepUniqueDaysByHours(cals, tz = 'UTC') {
|
||||
|
||||
let prev = null;
|
||||
|
||||
// compose goes bottom to top (map > sortBy > transform)
|
||||
return compose(
|
||||
transform((data, cur, i) => {
|
||||
if (i < 1) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
} else if (
|
||||
moment(cur)
|
||||
.tz(tz)
|
||||
.diff(moment(prev).tz(tz).startOf('day'), 'hours')
|
||||
>= hoursDay
|
||||
) {
|
||||
data.push(cur);
|
||||
prev = cur;
|
||||
}
|
||||
}, []),
|
||||
sortBy(e => e),
|
||||
map(ts => moment(ts).tz(tz).startOf('hours').valueOf())
|
||||
)(cals);
|
||||
}
|
||||
|
||||
export function calcCurrentStreak(cals, tz = 'UTC') {
|
||||
|
||||
let prev = last(cals);
|
||||
if (
|
||||
moment()
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(prev).tz(tz), 'hours')
|
||||
> hoursBetween
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
let currentStreak = 0;
|
||||
let streakContinues = true;
|
||||
forEachRight(cur => {
|
||||
if (
|
||||
moment(prev)
|
||||
.tz(tz)
|
||||
.startOf('day')
|
||||
.diff(moment(cur).tz(tz), 'hours')
|
||||
<= hoursBetween
|
||||
) {
|
||||
prev = cur;
|
||||
currentStreak++;
|
||||
} else {
|
||||
// current streak found
|
||||
streakContinues = false;
|
||||
}
|
||||
return streakContinues;
|
||||
})(cals);
|
||||
|
||||
return currentStreak;
|
||||
}
|
||||
|
||||
export function calcLongestStreak(cals, tz = 'UTC') {
|
||||
|
||||
let tail = cals[0];
|
||||
const longest = cals.reduce((longest, head, index) => {
|
||||
const last = cals[index === 0 ? 0 : index - 1];
|
||||
// is streak broken
|
||||
if (moment(head).tz(tz).startOf('day').diff(moment(last).tz(tz), 'hours')
|
||||
> hoursBetween) {
|
||||
tail = head;
|
||||
}
|
||||
if (dayCount(longest, tz) < dayCount([head, tail], tz)) {
|
||||
return [head, tail];
|
||||
}
|
||||
return longest;
|
||||
}, [cals[0], cals[0]]);
|
||||
|
||||
return dayCount(longest, tz);
|
||||
}
|
429
api-server/server/utils/user-stats.test.js
Normal file
429
api-server/server/utils/user-stats.test.js
Normal file
@ -0,0 +1,429 @@
|
||||
import test from 'tape';
|
||||
import moment from 'moment-timezone';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
prepUniqueDaysByHours,
|
||||
calcCurrentStreak,
|
||||
calcLongestStreak
|
||||
} from './user-stats';
|
||||
|
||||
// setting now to 2016-02-03T11:00:00 (PST)
|
||||
const clock = sinon.useFakeTimers(1454526000000);
|
||||
const PST = 'America/Los_Angeles';
|
||||
|
||||
test('Prepare calendar items', function(t) {
|
||||
|
||||
t.plan(5);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1438567200000],
|
||||
'should return correct epoch when all entries fall into one day in UTC'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1438567200000],
|
||||
'should return correct epoch when given two identical dates'
|
||||
);
|
||||
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
// 8/2/2015 in America/Los_Angeles
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('8/3/2015 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST),
|
||||
[1438567200000, 1438610400000],
|
||||
'should return 2 epochs when dates fall into two days in PST'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/1/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('3/3/2015 14:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/30/2014 20:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]),
|
||||
[1412107200000, 1425391200000, 1438394400000],
|
||||
'should return 3 epochs when dates fall into three days'
|
||||
);
|
||||
|
||||
t.deepEqual(
|
||||
prepUniqueDaysByHours([
|
||||
1438387200000, 1425340800000, 1412035200000
|
||||
]),
|
||||
[1412035200000, 1425340800000, 1438387200000],
|
||||
'should return same but sorted array if all input dates are start of day'
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
test('Current streak calculation', function(t) {
|
||||
|
||||
t.plan(11);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 day when today one challenge was completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 day when today more than one challenge was completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
0,
|
||||
'should return 0 day when today 0 challenges were completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 days when today and yesterday challenges were completed'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
3,
|
||||
'should return 3 when today and for two days before user was activity'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(47, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(11, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is 1.5 days long break and ' +
|
||||
'dates fall into two days separated by third'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(40, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when the break is more than 1.5 days ' +
|
||||
'but dates fall into two consecutive days'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return correct count in default timezone UTC ' +
|
||||
'given `undefined` timezone'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
2,
|
||||
'should return 2 days when today and yesterday ' +
|
||||
'challenges were completed given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.tz(PST).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
17,
|
||||
'should return 17 when there is no break in given timezone ' +
|
||||
'(but would be the break if in UTC)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcCurrentStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a break in UTC ' +
|
||||
'(but would be no break in PST)'
|
||||
);
|
||||
});
|
||||
|
||||
test('Longest streak calculation', function(t) {
|
||||
t.plan(14);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/12/2015 4:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is the only one one-day-long streak available'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is the only one ' +
|
||||
'more-than-one-days-long streak available'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
1,
|
||||
'should return 1 when there is only one one-day-long streak and it is today'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when yesterday and today makes longest streak'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/4/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/5/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/6/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/7/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('11/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a month long break'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 15:30', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc('9/12/2015 15:30', 'M/D/YYYY H:mm')
|
||||
.add(37, 'hours')).valueOf(),
|
||||
|
||||
moment.utc('9/14/2015 22:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/15/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/3/2015 2:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return 2 when there is a more than 1.5 days ' +
|
||||
'long break of (36 hours)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc(moment.utc().subtract(2, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when the longest streak consist of ' +
|
||||
'several same day timestamps'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('8/3/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 5:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/12/2015 1:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/13/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('10/14/2015 5:00', 'M/D/YYYY H:mm').valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there are several longest streaks (same length)'
|
||||
);
|
||||
|
||||
let cals = [];
|
||||
const n = 100;
|
||||
for (var i = 0; i < n; i++) {
|
||||
cals.push(moment.utc(moment.utc().subtract(i, 'days')).valueOf());
|
||||
}
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(prepUniqueDaysByHours(cals)),
|
||||
n,
|
||||
'should return correct longest streak when there is a very long period'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc(moment.utc().subtract(1, 'days')).valueOf(),
|
||||
moment.utc(moment.utc().subtract(1, 'hours')).valueOf()
|
||||
])
|
||||
),
|
||||
2,
|
||||
'should return correct longest streak in default timezone ' +
|
||||
'UTC given `undefined` timezone'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 4:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 3:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 1:00', 'M/D/YYYY H:mm').valueOf()
|
||||
]), PST
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is the only one more-than-one-days-long ' +
|
||||
'streak available given PST'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
moment.utc('9/11/2015 23:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/12/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/13/2015 2:00', 'M/D/YYYY H:mm').valueOf(),
|
||||
moment.utc('9/14/2015 6:00', 'M/D/YYYY H:mm').valueOf()
|
||||
], PST), PST
|
||||
),
|
||||
3,
|
||||
'should return 3 when longest streak is 3 in PST ' +
|
||||
'(but would be different in default timezone UTC)'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.tz(PST).valueOf()
|
||||
], PST),
|
||||
PST
|
||||
),
|
||||
17,
|
||||
'should return 17 when there is no break in PST ' +
|
||||
'(but would be break in UTC) and it is current'
|
||||
);
|
||||
|
||||
t.equal(
|
||||
calcLongestStreak(
|
||||
prepUniqueDaysByHours([
|
||||
1453174506164, 1453175436725, 1453252466853, 1453294968225,
|
||||
1453383782844, 1453431903117, 1453471373080, 1453594733026,
|
||||
1453645014058, 1453746762747, 1453747659197, 1453748029416,
|
||||
1453818029213, 1453951796007, 1453988570615, 1454069704441,
|
||||
1454203673979, 1454294055498, 1454333545125, 1454415163903,
|
||||
1454519128123, moment.utc().valueOf()
|
||||
])
|
||||
),
|
||||
4,
|
||||
'should return 4 when there is a break in UTC (but no break in PST)'
|
||||
);
|
||||
});
|
||||
|
||||
test.onFinish(() => {
|
||||
clock.restore();
|
||||
});
|
Reference in New Issue
Block a user