Merge branch 'staging' of github.com:FreeCodeCamp/freecodecamp into staging

This commit is contained in:
Quincy Larson
2015-08-06 01:44:37 -07:00
18 changed files with 343 additions and 273 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
public/js/lib/codemirror/* linguist-vendored
*.jsx linguist-language=JavaScript

View File

@ -1,3 +1,4 @@
import assign from 'object.assign';
import debugFactory from 'debug'; import debugFactory from 'debug';
const debug = debugFactory('freecc:models:userIdent'); const debug = debugFactory('freecc:models:userIdent');
@ -10,6 +11,39 @@ function getFirstImageFromProfile(profile) {
null; null;
} }
// using es6 argument destructing
function setProfileFromGithub(
user,
{
profileUrl: githubURL,
username
},
{
location,
email: githubEmail,
id: githubId,
'created_at': joinedGithubOn,
blog: website,
name
}
) {
return assign(
user,
{ isGithubCool: true, isMigrationGrandfathered: false },
{
name,
username: username.toLowerCase(),
location,
joinedGithubOn,
website,
githubId,
githubURL,
githubEmail,
githubProfile: githubURL
}
);
}
export default function(UserIdent) { export default function(UserIdent) {
UserIdent.observe('before save', function(ctx, next) { UserIdent.observe('before save', function(ctx, next) {
var userIdent = ctx.currentInstance || ctx.instance; var userIdent = ctx.currentInstance || ctx.instance;
@ -25,7 +59,8 @@ export default function(UserIdent) {
return next(); return next();
} }
const picture = getFirstImageFromProfile(userIdent.profile); const { profile } = userIdent;
const picture = getFirstImageFromProfile(profile);
debug('picture', picture, user.picture); debug('picture', picture, user.picture);
// check if picture was found // check if picture was found
@ -41,19 +76,10 @@ export default function(UserIdent) {
userChanged = true; userChanged = true;
} }
// if user signed in with github // if user signed in with github refresh their info
// and user is not github cool if (userIdent.provider === 'github-login') {
// or username is different from github username
// then make them github cool
// and set their username from their github profile.
if (
userIdent.provider === 'github-login' &&
(!user.isGithubCool ||
user.username !== userIdent.provider.username.toLowerCase())
) {
debug("user isn't github cool or username from github is different"); debug("user isn't github cool or username from github is different");
user.isGithubCool = true; setProfileFromGithub(user, profile, profile._json);
user.username = userIdent.profile.username.toLowerCase();
userChanged = true; userChanged = true;
} }

View File

@ -6,7 +6,12 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"unique": true "index": {
"mongodb": {
"unique": true,
"background": true
}
}
}, },
"title": { "title": {
"type": "string" "type": "string"

View File

@ -6,7 +6,12 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"unique": true "index": {
"mongodb": {
"unique": true,
"background": true
}
}
}, },
"whatDoesNonprofitDo": { "whatDoesNonprofitDo": {
"type": "string" "type": "string"

View File

@ -6,28 +6,29 @@
"properties": { "properties": {
"name": { "name": {
"type": "string", "type": "string",
"unique": true "index": {
"mongodb": {
"unique": true,
"background": true
}
}
}, },
"headline": { "headline": {
"type": "string", "type": "string"
"unique": false
}, },
"timePosted": { "timePosted": {
"type": "number", "type": "number",
"default": 0 "default": 0
}, },
"link": { "link": {
"type": "string", "type": "string"
"unique": false
}, },
"metaDescription": { "metaDescription": {
"type": "string", "type": "string",
"default": "", "default": ""
"unique": false
}, },
"description": { "description": {
"type": "string", "type": "string"
"unique": false
}, },
"originalStoryAuthorEmail": { "originalStoryAuthorEmail": {
"type": "string", "type": "string",

View File

@ -1,4 +1,5 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import uuid from 'node-uuid';
import moment from 'moment'; import moment from 'moment';
import debugFactory from 'debug'; import debugFactory from 'debug';
@ -44,6 +45,16 @@ module.exports = function(User) {
// username should be unique // username should be unique
User.validatesUniquenessOf('username'); User.validatesUniquenessOf('username');
User.observe('before save', function({ instance: user }, next) {
if (user) {
user.username = user.username.trim().toLowerCase();
user.email = typeof user.email === 'string' ?
user.email.trim().toLowerCase() :
user.email;
}
next();
});
debug('setting up user hooks'); debug('setting up user hooks');
User.afterRemote('confirm', function(ctx) { User.afterRemote('confirm', function(ctx) {
ctx.req.flash('success', { ctx.req.flash('success', {
@ -54,6 +65,11 @@ module.exports = function(User) {
ctx.res.redirect('/email-signin'); ctx.res.redirect('/email-signin');
}); });
User.beforeRemote('create', function({ req }, notUsed, next) {
req.body.username = 'fcc' + uuid.v4().slice(0, 8);
next();
});
User.afterRemote('login', function(ctx, user, next) { User.afterRemote('login', function(ctx, user, next) {
var res = ctx.res; var res = ctx.res;
var req = ctx.req; var req = ctx.req;

View File

@ -10,11 +10,10 @@
"index": { "index": {
"mongodb": { "mongodb": {
"unique": true, "unique": true,
"background": true,
"sparse": true "sparse": true
} }
}, }
"lowercase": true,
"trim": true
}, },
"password": { "password": {
"type": "string" "type": "string"
@ -27,11 +26,31 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"githubId": {
"type": "string"
},
"githubURL": {
"type": "string"
},
"githubEmail": {
"type": "string"
},
"joinedGithubOn": {
"type": "date"
},
"isMigrationGrandfathered": {
"type": "boolean",
"default": false
},
"username": { "username": {
"type": "string", "type": "string",
"lowercase": true, "require": true,
"trim": true, "index": {
"require": true "mongodb": {
"unique": true,
"background": true
}
}
}, },
"bio": { "bio": {
"type": "string", "type": "string",

View File

@ -423,7 +423,7 @@
"description": [ "description": [
"", "",
"In JavaScript we can store lists or collections of data in what are called arrays", "In JavaScript we can store lists or collections of data in what are called arrays",
"Arrays are distinguished by the <code> [ </code> and <code> ] </code> around the data. Each piece of data is separated be a <code> , </code>", "Arrays are distinguished by the <code> [ </code> and <code> ] </code> around the data. Each piece of data is separated by a <code> , </code>",
"Now let's create a new array called <code> myArray </code> with a <code> string </code> and a <code> number </code> with a <code> , </code> separating each one", "Now let's create a new array called <code> myArray </code> with a <code> string </code> and a <code> number </code> with a <code> , </code> separating each one",
"Refer to the example if you get stuck", "Refer to the example if you get stuck",
"" ""
@ -503,7 +503,7 @@
"difficulty":"9.98171", "difficulty":"9.98171",
"description":[ "description":[
"", "",
"We are able to modify the data store in an array be using indexes", "We are able to modify the data stored in an array by using indexes",
"Example:", "Example:",
"<code>", "<code>",
"var ourArray = [1,2,3];", "var ourArray = [1,2,3];",
@ -536,10 +536,10 @@
"difficulty": "9.9818", "difficulty": "9.9818",
"description": [ "description": [
"", "",
"When and array has been defined we still have the ability to make changes to it afterwards", "When an array has been defined we still have the ability to make changes to it afterwards",
"One common way in which we can manipulate the data in an array is through <code> .pop() </code>", "One common way in which we can manipulate the data in an array is through <code> .pop() </code>",
"<code> .pop() </code> is used to \"pop\" a value from the end of an array. We can retrieve this value by preforming the pop in a variable declaration.", "<code> .pop() </code> is used to \"pop\" a value from the end of an array. We can retrieve this value by performing the pop in a variable declaration.",
"any type of variable can be \"popped\" from and array", "Any type of variable can be \"popped\" from an array",
"Let's try <code> .pop() </code> now" "Let's try <code> .pop() </code> now"
], ],
"tests": [ "tests": [
@ -639,13 +639,13 @@
"description":[ "description":[
"", "",
"In JavaScript we can divide up our code into separate and reusable parts called functions", "In JavaScript we can divide up our code into separate and reusable parts called functions",
"here's and example of a function", "Here's an example of a function",
"<code>", "<code>",
"function functionName (a, b){", "function functionName (a, b){",
" return(a + b);", " return(a + b);",
"}", "}",
"</code>", "</code>",
"our function can be called like this", "Our function can be called like this",
"<code>functionName();</code>", "<code>functionName();</code>",
"Let's try creating and calling a function now called <code>myFunction</code>" "Let's try creating and calling a function now called <code>myFunction</code>"
], ],
@ -677,7 +677,7 @@
"description":[ "description":[
"", "",
"A very important data type in javascript is the <code> Object </code>", "A very important data type in javascript is the <code> Object </code>",
"<code> Objects </code> a similar to <code> arrays </code> except that instead of using indexes to access and modify their data, Objects have what are called <code> properties </code>", "<code> Objects </code> are similar to <code> arrays </code> except that instead of using indexes to access and modify their data, Objects have what are called <code> properties </code>",
"Here's a sample Object", "Here's a sample Object",
"<code>", "<code>",
"var cat = {", "var cat = {",
@ -688,7 +688,7 @@
"};", "};",
"</code>", "</code>",
"Objects are useful for storing data in a structured way or in a way that represents a real world object like a cat.", "Objects are useful for storing data in a structured way or in a way that represents a real world object like a cat.",
"Let's try to make a Object that represents a dog called myDog!" "Let's try to make an Object that represents a dog called myDog!"
], ],
"tests":[ "tests":[

View File

@ -1883,7 +1883,7 @@
"Above your right-well, inside its \"col-xs-6\" <code>div</code> element, add a <code>h4</code> element with the text \"#right-well\"." "Above your right-well, inside its \"col-xs-6\" <code>div</code> element, add a <code>h4</code> element with the text \"#right-well\"."
], ],
"tests": [ "tests": [
"assert($('.col-xs-12').children('h4') && $('.col-xs-12').children('h4').length > 1, 'Add an <code>h4</code> element to each of your <code>&#60;div class=\\'col-xs-6\\'&#62;</code> elements.');", "assert($('.col-xs-6').children('h4') && $('.col-xs-6').children('h4').length > 1, 'Add an <code>h4</code> element to each of your <code>&#60;div class=\\'col-xs-6\\'&#62;</code> elements.');",
"assert(new RegExp('#left-well','gi').test($('h4').text()), 'One <code>h4</code> element should have the text \"#left-well\".');", "assert(new RegExp('#left-well','gi').test($('h4').text()), 'One <code>h4</code> element should have the text \"#left-well\".');",
"assert(new RegExp('#right-well','gi').test($('h4').text()), 'One <code>h4</code> element should have the text \"#right-well\".');" "assert(new RegExp('#right-well','gi').test($('h4').text()), 'One <code>h4</code> element should have the text \"#right-well\".');"
], ],
@ -2036,9 +2036,9 @@
"Add a comment at the top of your HTML that says <code>You shouldn't need to modify code below this line</code>." "Add a comment at the top of your HTML that says <code>You shouldn't need to modify code below this line</code>."
], ],
"tests": [ "tests": [
"assert(editor.match(/<!--/g).length > 1, 'Start a comment with <code>&#60;!--</code>.')", "assert(editor.match(/<!--/g) && editor.match(/<!--/g).length > 0, 'Start a comment with <code>&#60;!--</code>.')",
"assert(editor.match(/this line/g).length > 1, 'Your comment should have the text <code>You shouldn't need to modify code below this line</code>')", "assert(editor.match(/this line/g) && editor.match(/this line/g).length > 0, \"Your comment should have the text <code>You shouldn't need to modify code below this line</code>\")",
"assert(editor.match(/-->/g).length > 1, 'Be sure to close your comment with <code>--&#62;</code>.')" "assert(editor.match(/-->/g) && editor.match(/-->/g).length > 0, 'Be sure to close your comment with <code>--&#62;</code>.')"
], ],
"challengeSeed": [ "challengeSeed": [
"<div class='container-fluid'>", "<div class='container-fluid'>",

View File

@ -103,7 +103,7 @@
"difficulty": "2.02", "difficulty": "2.02",
"description": [ "description": [
"Convert the given number into a roman numeral.", "Convert the given number into a roman numeral.",
"All <a href=\"http://www.mathsisfun.com/roman-numerals.html\">roman numerals</a> answers should be provided in upper-case.", "All <a href=\"http://www.mathsisfun.com/roman-numerals.html\" target="_blank">roman numerals</a> answers should be provided in upper-case.",
"Remember to use <a href='//github.com/FreeCodeCamp/freecodecamp/wiki/How-to-get-help-when-you-get-stuck' target='_blank'>RSAP</a> if you get stuck. Try to pair program. Write your own code." "Remember to use <a href='//github.com/FreeCodeCamp/freecodecamp/wiki/How-to-get-help-when-you-get-stuck' target='_blank'>RSAP</a> if you get stuck. Try to pair program. Write your own code."
], ],
"challengeSeed": [ "challengeSeed": [
@ -356,7 +356,7 @@
" return arr1;", " return arr1;",
"}", "}",
"", "",
"unite([1, 2, 3], [5, 2, 1, 4], [2, 1]);" "unite([1, 3, 2], [5, 2, 1, 4], [2, 1]);"
], ],
"tests": [ "tests": [
"assert.deepEqual(unite([1, 3, 2], [5, 2, 1, 4], [2, 1]), [1, 3, 2, 5, 4], 'should return the union of the given arrays');", "assert.deepEqual(unite([1, 3, 2], [5, 2, 1, 4], [2, 1]), [1, 3, 2, 5, 4], 'should return the union of the given arrays');",
@ -675,7 +675,8 @@
"tests": [ "tests": [
"assert.deepEqual(steamroller([[['a']], [['b']]]), ['a', 'b'], 'should flatten nested arrays');", "assert.deepEqual(steamroller([[['a']], [['b']]]), ['a', 'b'], 'should flatten nested arrays');",
"assert.deepEqual(steamroller([1, [2], [3, [[4]]]]), [1, 2, 3, 4], 'should flatten nested arrays');", "assert.deepEqual(steamroller([1, [2], [3, [[4]]]]), [1, 2, 3, 4], 'should flatten nested arrays');",
"assert.deepEqual(steamroller([1, [], [3, [[4]]]]), [1, 3, 4], 'should work with empty arrays');" "assert.deepEqual(steamroller([1, [], [3, [[4]]]]), [1, 3, 4], 'should work with empty arrays');",
"assert.deepEqual(steamroller([1, {}, [3, [[4]]]]), [1, {}, 3, 4], 'should work with actual objects');"
], ],
"MDNlinks": [ "MDNlinks": [
"Array.isArray()" "Array.isArray()"

View File

@ -257,9 +257,9 @@
"dashedName": "waypoint-target-the-same-element-with-multiple-jQuery-Selectors", "dashedName": "waypoint-target-the-same-element-with-multiple-jQuery-Selectors",
"difficulty": 3.06, "difficulty": 3.06,
"description": [ "description": [
"Now you know three ways of targeting elements: by type (<code>$('button')</code>), by class (<code>($('.btn')</code>), and by id (<code>($'#target1')</code>).", "Now you know three ways of targeting elements: by type <code>$('button')</code>, by class <code>$('.btn')</code>), and by id <code>$('#target1')</code>).",
"Use each of these jQuery selectors to target your <code>button</code> element with the class \"btn\" and the id \"target1\".", "Use each of these jQuery selectors to target your <code>button</code> element with the class \"btn\" and the id \"target1\".",
"Use the <code>addClass()</code> jQuery function to give the element one new class for each selector: \"animated\", \"shake\", and \"button-primary\"." "Use the <code>addClass()</code> jQuery function to give the element one new class for each selector: \"animated\", \"shake\", and \"btn-primary\"."
], ],
"tests": [ "tests": [
"assert(editor.match(/\\$\\(\\'button\\'\\)/g), 'Use the <code>$\\(\\'button\\'\\)</code> selector.')", "assert(editor.match(/\\$\\(\\'button\\'\\)/g), 'Use the <code>$\\(\\'button\\'\\)</code> selector.')",

View File

@ -92,6 +92,8 @@ var users = dbObservable
user.username = 'fcc' + uuid.v4().slice(0, 8); user.username = 'fcc' + uuid.v4().slice(0, 8);
if (user.github) { if (user.github) {
user.isGithubCool = true; user.isGithubCool = true;
} else {
user.isMigrationGrandfathered = true;
} }
return user; return user;
}) })

View File

@ -1,35 +1,3 @@
/**
* Created by nathanleniz on 5/15/15.
* Copyright (c) 2015, Free Code Camp
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var R = require('ramda'), var R = require('ramda'),
Rx = require('rx'), Rx = require('rx'),
assign = require('object.assign'), assign = require('object.assign'),

View File

@ -122,113 +122,123 @@ module.exports = function(app) {
*/ */
function returnUser(req, res, next) { function returnUser(req, res, next) {
const username = req.params.username.toLowerCase();
const { path } = req;
User.findOne( User.findOne(
{ where: { 'username': req.params.username.toLowerCase() } }, { where: { username } },
function(err, user) { function(err, user) {
if (err) { if (err) {
return next(err); return next(err);
} }
if (user) { if (!user) {
user.progressTimestamps =
user.progressTimestamps.sort(function(a, b) {
return a - b;
});
var timeObject = Object.create(null);
R.forEach(function(time) {
timeObject[moment(time).format('YYYY-MM-DD')] = time;
}, user.progressTimestamps);
var tmpLongest = 1;
var timeKeys = R.keys(timeObject);
user.longestStreak = 0;
for (var i = 1; i <= timeKeys.length; i++) {
if (moment(timeKeys[i - 1]).add(1, 'd').toString()
=== moment(timeKeys[i]).toString()) {
tmpLongest++;
if (tmpLongest > user.longestStreak) {
user.longestStreak = tmpLongest;
}
} else {
tmpLongest = 1;
}
}
timeKeys = timeKeys.reverse();
tmpLongest = 1;
user.currentStreak = 1;
var today = moment(Date.now()).format('YYYY-MM-DD');
const yesterday = moment(today).subtract(1, 'd').toString();
const yesteryesterday = moment(today).subtract(2, 'd').toString();
if (
moment(today).toString() === moment(timeKeys[0]).toString() ||
yesterday === moment(timeKeys[0]).toString() ||
yesteryesterday === moment(timeKeys[0]).toString()
) {
for (var _i = 1; _i <= timeKeys.length; _i++) {
if (
moment(timeKeys[_i - 1]).subtract(1, 'd').toString() ===
moment(timeKeys[_i]).toString()
) {
tmpLongest++;
if (tmpLongest > user.currentStreak) {
user.currentStreak = tmpLongest;
}
} else {
break;
}
}
} else {
user.currentStreak = 1;
}
var data = {};
var progressTimestamps = user.progressTimestamps;
progressTimestamps.forEach(function(timeStamp) {
data[(timeStamp / 1000)] = 1;
});
user.currentStreak = user.currentStreak || 1;
user.longestStreak = user.longestStreak || 1;
var challenges = user.completedChallenges.filter(function( obj ) {
return obj.challengeType === 3 || obj.challengeType === 4;
});
res.render('account/show', {
title: 'Camper ' + user.username + '\'s portfolio',
username: user.username,
name: user.name,
location: user.location,
githubProfile: user.githubProfile,
linkedinProfile: user.linkedinProfile,
codepenProfile: user.codepenProfile,
facebookProfile: user.facebookProfile,
twitterHandle: user.twitterHandle,
bio: user.bio,
picture: user.picture,
progressTimestamps: user.progressTimestamps,
challenges: challenges,
calender: data,
moment: moment,
longestStreak: user.longestStreak +
(user.longestStreak === 1 ? ' day' : ' days'),
currentStreak: user.currentStreak +
(user.currentStreak === 1 ? ' day' : ' days')
});
} else {
req.flash('errors', { req.flash('errors', {
msg: "404: We couldn't find a page with that url. " + msg: `404: We couldn't find path ${ path }`
'Please double check the link.'
}); });
return res.redirect('/'); return res.redirect('/');
} }
if (!user.isGithubCool && !user.isMigrationGrandfathered) {
req.flash('errors', {
msg: `
user ${ username } has not completed account signup
`
});
return res.redirect('/');
}
user.progressTimestamps =
user.progressTimestamps.sort(function(a, b) {
return a - b;
});
var timeObject = Object.create(null);
R.forEach(function(time) {
timeObject[moment(time).format('YYYY-MM-DD')] = time;
}, user.progressTimestamps);
var tmpLongest = 1;
var timeKeys = R.keys(timeObject);
user.longestStreak = 0;
for (var i = 1; i <= timeKeys.length; i++) {
if (moment(timeKeys[i - 1]).add(1, 'd').toString()
=== moment(timeKeys[i]).toString()) {
tmpLongest++;
if (tmpLongest > user.longestStreak) {
user.longestStreak = tmpLongest;
}
} else {
tmpLongest = 1;
}
}
timeKeys = timeKeys.reverse();
tmpLongest = 1;
user.currentStreak = 1;
var today = moment(Date.now()).format('YYYY-MM-DD');
const yesterday = moment(today).subtract(1, 'd').toString();
const yesteryesterday = moment(today).subtract(2, 'd').toString();
if (
moment(today).toString() === moment(timeKeys[0]).toString() ||
yesterday === moment(timeKeys[0]).toString() ||
yesteryesterday === moment(timeKeys[0]).toString()
) {
for (var _i = 1; _i <= timeKeys.length; _i++) {
if (
moment(timeKeys[_i - 1]).subtract(1, 'd').toString() ===
moment(timeKeys[_i]).toString()
) {
tmpLongest++;
if (tmpLongest > user.currentStreak) {
user.currentStreak = tmpLongest;
}
} else {
break;
}
}
} else {
user.currentStreak = 1;
}
var data = {};
var progressTimestamps = user.progressTimestamps;
progressTimestamps.forEach(function(timeStamp) {
data[(timeStamp / 1000)] = 1;
});
var challenges = user.completedChallenges.filter(function( obj ) {
return obj.challengeType === 3 || obj.challengeType === 4;
});
user.currentStreak = user.currentStreak || 1;
user.longestStreak = user.longestStreak || 1;
res.render('account/show', {
title: 'Camper ' + user.username + '\'s portfolio',
username: user.username,
name: user.name,
isMigrationGrandfathered: user.isMigrationGrandfathered,
isGithubCool: user.isGithubCool,
location: user.location,
githubProfile: user.githubProfile,
linkedinProfile: user.linkedinProfile,
codepenProfile: user.codepenProfile,
facebookProfile: user.facebookProfile,
twitterHandle: user.twitterHandle,
bio: user.bio,
picture: user.picture,
progressTimestamps: user.progressTimestamps,
calender: data,
challenges: challenges,
moment: moment,
longestStreak: user.longestStreak +
(user.longestStreak === 1 ? ' day' : ' days'),
currentStreak: user.currentStreak +
(user.currentStreak === 1 ? ' day' : ' days')
});
} }
); );
} }

View File

@ -2,12 +2,13 @@ require('dotenv').load();
var pmx = require('pmx'); var pmx = require('pmx');
pmx.init(); pmx.init();
var assign = require('lodash').assign, var uuid = require('node-uuid'),
loopback = require('loopback'), assign = require('lodash').assign,
boot = require('loopback-boot'), loopback = require('loopback'),
expressState = require('express-state'), boot = require('loopback-boot'),
path = require('path'), expressState = require('express-state'),
passportProviders = require('./passport-providers'); path = require('path'),
passportProviders = require('./passport-providers');
var generateKey = var generateKey =
require('loopback-component-passport/lib/models/utils').generateKey; require('loopback-component-passport/lib/models/utils').generateKey;
@ -43,6 +44,39 @@ passportConfigurator.setupModels({
userCredentialModel: app.models.userCredential userCredentialModel: app.models.userCredential
}); });
// using es6 argument destructing
function setProfileFromGithub(
user,
{
profileUrl: githubURL,
username
},
{
location,
email: githubEmail,
id: githubId,
'created_at': joinedGithubOn,
blog: website,
name
}
) {
return assign(
user,
{ isGithubCool: true, isMigrationGrandfathered: false },
{
name,
username: username.toLowerCase(),
location,
joinedGithubOn,
website,
githubId,
githubURL,
githubEmail,
githubProfile: githubURL
}
);
}
var passportOptions = { var passportOptions = {
emailOptional: true, emailOptional: true,
profileToUser: function(provider, profile) { profileToUser: function(provider, profile) {
@ -53,9 +87,9 @@ var passportOptions = {
emails[0].value : emails[0].value :
null; null;
var username = (profile.username || profile.id); // create random username
username = typeof username === 'string' ? username.toLowerCase() : // username will be assigned when camper signups for Github
username; var username = 'fcc' + uuid.v4().slice(0, 8);
var password = generateKey('password'); var password = generateKey('password');
var userObj = { var userObj = {
username: username, username: username,
@ -65,8 +99,9 @@ var passportOptions = {
if (email) { if (email) {
userObj.email = email; userObj.email = email;
} }
if (provider === 'github-login') { if (provider === 'github-login') {
userObj.isGithubCool = true; setProfileFromGithub(userObj, profile, profile._json);
} }
return userObj; return userObj;
} }

View File

@ -5,67 +5,74 @@ block content
.panel.panel-info(ng-controller="profileValidationController") .panel.panel-info(ng-controller="profileValidationController")
.panel-heading.text-center Update your portfolio here: .panel-heading.text-center Update your portfolio here:
.panel-body .panel-body
if (!user.github) if (!user.isGithubCool)
.row
.col-xs-12 .col-xs-12
a.btn.btn-lg.btn-block.btn-github.btn-link-social(href='/auth/github') .text-left.btn-info.btn.btn-block.btn-link-social(href='http://www.freecodecamp.com/challenges/waypoint-join-our-chat-room')
i.fa.fa-github span Link your account to GitHub to update your portfolio page. Click here if you dont have a GitHub account yet.
| Link GitHub with my account .row
.col-xs-12 .col-xs-12
form.form-horizontal(action='/account/profile', method='POST', novalidate='novalidate', name='profileForm' ng-show="asyncComplete") a.btn.btn-lg.btn-block.btn-github.btn-link-social(href='/auth/github')
input(type='hidden', name='_csrf', value=_csrf) i.fa.fa-github
.form-group | Link GitHub with my account
label.col-sm-3.col-sm-offset-1.control-label(for='bio') Bio (140 characters)
.col-sm-4 .row
input.form-control(type='text', name='bio', autocomplete="off", ng-model='user.bio', ng-maxlength='140', id='bio') .col-xs-12
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.bio.$error.maxlength && !profileForm.bio.$pristine') form.form-horizontal(action='/account/profile', method='POST', novalidate='novalidate', name='profileForm' ng-show="asyncComplete")
alert(type='danger') input(type='hidden', name='_csrf', value=_csrf)
span.ion-close-circled .form-group
| Your bio must be fewer than 140 characters. label.col-sm-3.col-sm-offset-1.control-label(for='bio') Bio (140 characters)
.form-group .col-sm-4
label.col-sm-3.col-sm-offset-1.control-label(for='email') Twitter input.form-control(type='text', name='bio', autocomplete="off", ng-model='user.bio', ng-maxlength='140', id='bio')
.col-sm-4 .col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.bio.$error.maxlength && !profileForm.bio.$pristine')
.input-group.twitter-input alert(type='danger')
span.input-group-addon @
input.form-control(type='text', name='twitterHandle', autocomplete="off", id='twitterHandle', ng-model='user.twitterHandle', ng-maxlength='15', ng-pattern="/^[A-z0-9_]+$/")
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.twitterHandle.$error.pattern")
alert(type='danger')
span.ion-close-circled span.ion-close-circled
| Your Twitter handle should only contain letters, numbers and underscores (az10_). | Your bio must be fewer than 140 characters.
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.twitterHandle.$error.maxlength && !profileForm.twitterHandle.$pristine') .form-group
alert(type='danger') label.col-sm-3.col-sm-offset-1.control-label(for='email') Twitter
.col-sm-4
.input-group.twitter-input
span.input-group-addon @
input.form-control(type='text', name='twitterHandle', autocomplete="off", id='twitterHandle', ng-model='user.twitterHandle', ng-maxlength='15', ng-pattern="/^[A-z0-9_]+$/")
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.twitterHandle.$error.pattern")
alert(type='danger')
span.ion-close-circled
| Your Twitter handle should only contain letters, numbers and underscores (az10_).
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show='profileForm.twitterHandle.$error.maxlength && !profileForm.twitterHandle.$pristine')
alert(type='danger')
span.ion-close-circled
| Your name must be fewer than 15 characters.
.form-group
label.col-sm-3.col-sm-offset-1.control-label(for='email') CodePen
.col-sm-4
input.form-control(type='url', name='codepenProfile', id='codepenProfile', autocomplete="off", ng-model='user.codepenProfile', placeholder='http://')
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.codepenProfile.$error.url && !profileForm.codepenProfile.$pristine")
alert(type='danger')
span.ion-close-circled span.ion-close-circled
| Your name must be fewer than 15 characters. | Please enter a valid URL format (http://www.example.com).
.form-group .form-group
label.col-sm-3.col-sm-offset-1.control-label(for='email') CodePen label.col-sm-3.col-sm-offset-1.control-label(for='email') LinkedIn
.col-sm-4 .col-sm-4
input.form-control(type='url', name='codepenProfile', id='codepenProfile', autocomplete="off", ng-model='user.codepenProfile', placeholder='http://') input.form-control(type='url', name='linkedinProfile', id='linkedinProfile', autocomplete="off", ng-model='user.linkedinProfile', placeholder='http://')
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.codepenProfile.$error.url && !profileForm.codepenProfile.$pristine") .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.linkedinProfile.$error.url && !profileForm.linkedinProfile.$pristine")
alert(type='danger') alert(type='danger')
span.ion-close-circled span.ion-close-circled
| Please enter a valid URL format (http://www.example.com). | Please enter a valid URL format (http://www.example.com).
.form-group .form-group
label.col-sm-3.col-sm-offset-1.control-label(for='email') LinkedIn label.col-sm-3.col-sm-offset-1.control-label(for='email') Facebook
.col-sm-4 .col-sm-4
input.form-control(type='url', name='linkedinProfile', id='linkedinProfile', autocomplete="off", ng-model='user.linkedinProfile', placeholder='http://') input.form-control(type='url', name='facebookProfile', id='facebookProfile', autocomplete="off", ng-model='user.facebookProfile', placeholder='http://')
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.linkedinProfile.$error.url && !profileForm.linkedinProfile.$pristine") .col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.facebookProfile.$error.url && !profileForm.facebookProfile.$pristine")
alert(type='danger') alert(type='danger')
span.ion-close-circled span.ion-close-circled
| Please enter a valid URL format (http://www.example.com). | Please enter a valid URL format (http://www.example.com).
.form-group button.btn.btn-lg.btn-block.btn-primary.btn-link-social(type='submit', ng-disabled='!user.isGithubCool || profileForm.$invalid')
label.col-sm-3.col-sm-offset-1.control-label(for='email') Facebook span.ion-edit
.col-sm-4 | Update my info
input.form-control(type='url', name='facebookProfile', id='facebookProfile', autocomplete="off", ng-model='user.facebookProfile', placeholder='http://')
.col-sm-4.col-sm-offset-5(ng-cloak, ng-show="profileForm.facebookProfile.$error.url && !profileForm.facebookProfile.$pristine")
alert(type='danger')
span.ion-close-circled
| Please enter a valid URL format (http://www.example.com).
button.btn.btn-lg.btn-block.btn-primary.btn-link-social(type='submit', ng-disabled='profileForm.$invalid')
span.ion-edit
| Update my info
.panel.panel-info .panel.panel-info
.panel-heading.text-center Manage your account here: .panel-heading.text-center Manage your account here:

View File

@ -19,25 +19,6 @@ block content
alert(type='danger') alert(type='danger')
span.ion-close-circled span.ion-close-circled
| That email address is already in use. | That email address is already in use.
.form-group
.col-sm-6.col-sm-offset-3
input.form-control(type='text', name='username', ng-keypress='', autocomplete="off", id='username', placeholder='username', ng-model='username', unique-username='', required, ng-minlength=5, ng-maxlength=20, ng-pattern="/^[A-z0-9_]+$/")
.col-sm-6.col-sm-offset-3(ng-cloak, ng-show="profileForm.username.$error.pattern && !signupForm.username.$pristine")
alert(type='danger')
span.ion-close-circled
| Your username should only contain letters, numbers and underscores (az10_).
.col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.unique && !signupForm.username.$pristine")
alert(type='danger')
span.ion-close-circled
| This username is taken.
.col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.minlength && !signupForm.username.$pristine")
alert(type='danger')
span.ion-close-circled
| Your username must be at least 5 characters long.
.col-sm-6.col-sm-offset-3(ng-cloak, ng-show="signupForm.username.$error.maxlength && !signupForm.username.$pristine")
alert(type='danger')
span.ion-close-circled
| Your usernames must be 20 characters or fewer.
.form-group .form-group
.col-sm-6.col-sm-offset-3 .col-sm-6.col-sm-offset-3
input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=8) input.form-control(type='password', ng-model='password', name='password', id='password', placeholder='password', required, ng-minlength=8)

View File

@ -21,15 +21,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height
a.btn.signup-btn.signup-btn-nav(href='/login') Sign in a.btn.signup-btn.signup-btn-nav(href='/login') Sign in
else else
li li
if (user.username) a(href='/account') [&thinsp;#{user.progressTimestamps.length}&thinsp;]
a(href='/' + user.username) [&nbsp;#{user.progressTimestamps.length}&nbsp;]
else
a(href='/account') [&thinsp;#{user.progressTimestamps.length}&thinsp;]
.hidden-xs.hidden-sm .hidden-xs.hidden-sm
if (user.username) a(href='/account')
a(href='/' + user.username) img.profile-picture.float-right(src='#{user.picture}')
img.profile-picture.float-right(src='#{user.picture}')
else
a(href='/account')
img.profile-picture.float-right(src='#{user.picture}')