Merge pull request #8119 from FreeCodeCamp/staging

Release staging
This commit is contained in:
Berkeley Martinez
2016-04-14 17:52:40 -07:00
16 changed files with 224 additions and 160 deletions

View File

@ -124,6 +124,30 @@ main = (function(main, global) {
Mousetrap.bind('g c', toggleMainChat); Mousetrap.bind('g c', toggleMainChat);
}); });
function localStorageIO(item = '', input = null) {
if (input) {
try {
input = typeof input === 'string' ? input : JSON.stringify(input);
} catch (e) {
// Do Nothing
}
localStorage.setItem(item, input);
return input;
} else {
let data = typeof localStorage.getItem(item)
!== 'undefined' && localStorage.getItem(item)
!== null ? localStorage.getItem(item) : '';
try {
data = JSON.parse(data);
} catch (e) {
// Do Nothing
}
return data;
}
}
main.localStorageIO = localStorageIO;
return main; return main;
}(main, window)); }(main, window));
@ -612,4 +636,49 @@ $(document).ready(function() {
// Repo // Repo
window.location = 'https://github.com/freecodecamp/freecodecamp/'; window.location = 'https://github.com/freecodecamp/freecodecamp/';
}); });
function getCurrentBillBoard(cb) {
$.ajax({
url: '/api/flyers/findOne?'
+ 'filter=%7B%22order%22%3A%20%20%22id%20DESC%22%7D',
method: 'GET',
dataType: 'JSON',
data: {'order': 'id DESC'}
}).done((resp) => {
cb(resp);
});
$('#dismissBill').on('click', (e) => {
const elemData
= e.target.parentNode.parentNode.children;
const res
= elemData[Object.keys(elemData).filter((key)=> {
return elemData[key].id === 'billContent';
})[0]].innerHTML;
main.localStorageIO('lastBillBoardSeen', res);
});
}
function handleNewBillBoard(resp) {
const data = typeof main.localStorageIO('lastBillBoardSeen')
!== 'undefined' && main.localStorageIO('lastBillBoardSeen')
!== null ? main.localStorageIO('lastBillBoardSeen') : '';
if (
data.replace(/\s*/gi, '')
.replace(/\&\w*\;/gi, '')
.replace(/(\<|\/|\>)/gi, '')
!== resp.message
.replace(/\s*/gi, '')
.replace(/\&\w*\;/gi, '')
.replace(/(\<|\/|\>)/gi, '')
&& resp.active
) {
$('#billContent').html(resp.message);
$('#billBoard').fadeIn();
}
}
getCurrentBillBoard(handleNewBillBoard);
}); });

34
common/models/flyer.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "flyer",
"base": "PersistedModel",
"idInjection": true,
"trackChanges": false,
"properties": {
"message": {
"type": "string"
},
"active": {
"type": "boolean",
"default": false
}
},
"validations": [],
"relations": {
},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
],
"methods": []
}

View File

@ -1,26 +1,26 @@
import { Disposable, Observable, Scheduler } from 'rx'; import { Observable } from 'rx';
import uuid from 'node-uuid'; import uuid from 'node-uuid';
import moment from 'moment'; import moment from 'moment';
import dedent from 'dedent'; import dedent from 'dedent';
import debug from 'debug'; import debugFactory from 'debug';
import { saveUser, observeMethod } from '../../server/utils/rx';
import { blacklistedUsernames } from '../../server/utils/constants'; import { blacklistedUsernames } from '../../server/utils/constants';
const log = debug('fcc:user:remote'); const debug = debugFactory('fcc:user:remote');
const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; const BROWNIEPOINTS_TIMEOUT = [1, 'hour'];
const aboutFilter = {
username: true,
bio: true
};
function getAboutProfile({ function getAboutProfile({
username, username,
bio, githubProfile: github,
points progressTimestamps = [],
bio
}) { }) {
return { return {
username, username,
bio, github,
browniePoints: points browniePoints: progressTimestamps.length,
bio
}; };
} }
@ -54,31 +54,8 @@ module.exports = function(User) {
User.on('dataSourceAttached', () => { User.on('dataSourceAttached', () => {
User.findOne$ = Observable.fromNodeCallback(User.findOne, User); User.findOne$ = Observable.fromNodeCallback(User.findOne, User);
User.findById$ = Observable.fromNodeCallback(User.findById, User);
User.update$ = Observable.fromNodeCallback(User.updateAll, User); User.update$ = Observable.fromNodeCallback(User.updateAll, User);
User.count$ = Observable.fromNodeCallback(User.count, User); User.count$ = Observable.fromNodeCallback(User.count, User);
// getPointsById$(_id: String|ObjectId) => Observable[Number]
User.getPointsById$ = function getPointsById$(id) {
return Observable.create(observer => {
let isDisposed = false;
// safe ObjectID creation
// MongoID(id: ObjectID|String) => ObjectID
// MongoDB requires id's to be of type ObjectID
const _id = this.app.dataSources.db.connector.getDefaultIdType()(id);
this.app.dataSources.db.connector
.collection('user')
.aggregate([
{ $match: { _id } },
{ $project: { points: { $size: '$progressTimestamps' } } }
], (err, [ { points = 1 } = {}]) => {
if (isDisposed) { return null; }
if (err) { return observer.onError(err); }
observer.onNext(points);
return observer.onCompleted();
});
return Disposable.create(() => { isDisposed = true; });
});
};
}); });
User.observe('before save', function({ instance: user }, next) { User.observe('before save', function({ instance: user }, next) {
@ -99,7 +76,7 @@ module.exports = function(User) {
next(); next();
}); });
log('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', {
msg: [ msg: [
@ -151,9 +128,9 @@ module.exports = function(User) {
} }
// the email of the requested user // the email of the requested user
log(info.email); debug(info.email);
// the temp access token to allow password reset // the temp access token to allow password reset
log(info.accessToken.id); debug(info.accessToken.id);
// requires AccessToken.belongsTo(User) // requires AccessToken.belongsTo(User)
var mailOptions = { var mailOptions = {
to: info.email, to: info.email,
@ -173,7 +150,7 @@ module.exports = function(User) {
User.app.models.Email.send(mailOptions, function(err) { User.app.models.Email.send(mailOptions, function(err) {
if (err) { console.error(err); } if (err) { console.error(err); }
log('email reset sent'); debug('email reset sent');
}); });
}); });
@ -196,7 +173,7 @@ module.exports = function(User) {
}; };
if (accessToken && accessToken.id) { if (accessToken && accessToken.id) {
log('setting cookies'); debug('setting cookies');
res.cookie('access_token', accessToken.id, config); res.cookie('access_token', accessToken.id, config);
res.cookie('userId', accessToken.userId, config); res.cookie('userId', accessToken.userId, config);
} }
@ -204,7 +181,7 @@ module.exports = function(User) {
return req.logIn({ id: accessToken.userId.toString() }, function(err) { return req.logIn({ id: accessToken.userId.toString() }, function(err) {
if (err) { return next(err); } if (err) { return next(err); }
log('user logged in'); debug('user logged in');
if (req.session && req.session.returnTo) { if (req.session && req.session.returnTo) {
var redirectTo = req.session.returnTo; var redirectTo = req.session.returnTo;
@ -240,7 +217,7 @@ module.exports = function(User) {
if (!username && !email) { if (!username && !email) {
return Promise.resolve(false); return Promise.resolve(false);
} }
log('checking existence'); debug('checking existence');
// check to see if username is on blacklist // check to see if username is on blacklist
if (username && blacklistedUsernames.indexOf(username) !== -1) { if (username && blacklistedUsernames.indexOf(username) !== -1) {
@ -253,7 +230,7 @@ module.exports = function(User) {
} else { } else {
where.email = email ? email.toLowerCase() : email; where.email = email ? email.toLowerCase() : email;
} }
log('where', where); debug('where', where);
return User.count(where) return User.count(where)
.then(count => count > 0); .then(count => count > 0);
}; };
@ -294,23 +271,16 @@ module.exports = function(User) {
)); ));
}); });
} }
username = username.toLowerCase(); return User.findOne({ where: { username } }, (err, user) => {
const filter = { if (err) {
where: { username }, return cb(err);
fields: { id: true, ...aboutFilter } }
}; if (!user || user.username !== username) {
return User.findOne$(filter) return cb(new Error(`no user found for ${ username }`));
.doOnNext(user => { }
if (!user || user.username !== username) { const aboutUser = getAboutProfile(user);
throw new Error(`no user found for ${ username }`); return cb(null, aboutUser);
} });
})
.flatMap(user => user.getPoints$().map(user))
.map(user => getAboutProfile(user))
.subscribe(
aboutUser => cb(null, aboutUser),
cb
);
}; };
User.remoteMethod( User.remoteMethod(
@ -338,8 +308,7 @@ module.exports = function(User) {
User.giveBrowniePoints = User.giveBrowniePoints =
function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) {
receiver = receiver.toLowerCase(); const findUser = observeMethod(User, 'findOne');
giver = giver.toLowerCase();
if (!receiver) { if (!receiver) {
return nextTick(() => { return nextTick(() => {
cb( cb(
@ -352,84 +321,63 @@ module.exports = function(User) {
cb(new TypeError(`giver should be a string but got ${ giver }`)); cb(new TypeError(`giver should be a string but got ${ giver }`));
}); });
} }
if (giver === receiver) {
return nextTick(() => {
cb(new Error('giver and receiver must be different users'));
});
}
let temp = moment(); let temp = moment();
const browniePoints = temp const browniePoints = temp
.subtract.apply(temp, BROWNIEPOINTS_TIMEOUT) .subtract.apply(temp, BROWNIEPOINTS_TIMEOUT)
.valueOf(); .valueOf();
const user$ = User.findOne$({ const user$ = findUser({ where: { username: receiver }});
where: { username: receiver },
fields: { return user$
...aboutFilter, .tapOnNext((user) => {
progressTimestamps: true
}
});
const giver$ = User.count$({ username: giver });
return Observable.combineLatest(
user$,
giver$,
(user, giver) => ({ doesGiverExist: !!giver, user })
)
.tapOnNext(({ user, doesGiverExist }) => {
if (!user) { if (!user) {
throw new Error(`could not find receiver for ${ receiver }`); throw new Error(`could not find receiver for ${ receiver }`);
} }
if (!doesGiverExist) {
throw new Error(`no user found for giver '${giver}'`);
}
}) })
.flatMap(({ progressTimestamps = [] }) => { .flatMap(({ progressTimestamps = [] }) => {
return Observable.from( return Observable.from(progressTimestamps);
progressTimestamps,
null,
null,
Scheduler.default
);
}) })
// filter out non objects // filter out non objects
.filter((timestamp) => !!timestamp || typeof timestamp === 'object') .filter((timestamp) => !!timestamp || typeof timestamp === 'object')
// filterout timestamps older then an hour // filterout timestamps older then an hour
.filter(({ timestamp = 0 }) => timestamp >= browniePoints) .filter(({ timestamp = 0 }) => {
return timestamp >= browniePoints;
})
// filter out brownie points given by giver // filter out brownie points given by giver
.filter(browniePoint => browniePoint.giver === giver) .filter((browniePoint) => {
return browniePoint.giver === giver;
})
// no results means this is the first brownie point given by giver // no results means this is the first brownie point given by giver
// so return -1 to indicate receiver should receive point // so return -1 to indicate receiver should receive point
.first({ defaultValue: -1 }) .first({ defaultValue: -1 })
.flatMap(browniePointsFromGiver => { .flatMap((browniePointsFromGiver) => {
if (browniePointsFromGiver === -1) { if (browniePointsFromGiver === -1) {
const updateData = {
$push: { return user$.flatMap((user) => {
progressTimestamps: { user.progressTimestamps.push({
giver, giver,
timestamp: Date.now() timestamp: Date.now(),
} ...data
}
};
return user$
.flatMap(user => user.update$(updateData).map(user))
.doOnNext(user => {
user.points = user.progressTimestamps.length + 1;
}); });
return saveUser(user);
});
} }
return Observable.throw( return Observable.throw(
new Error(`${ giver } already gave ${ receiver } points`) new Error(`${ giver } already gave ${ receiver } points`)
); );
}) })
.subscribe( .subscribe(
user => cb( (user) => {
null, return cb(
getAboutProfile(user), null,
dev ? getAboutProfile(user),
{ giver, receiver, data } : dev ?
null { giver, receiver, data } :
), null
e => cb(e, null, dev ? { giver, receiver, data } : null), );
},
(e) => cb(e, null, dev ? { giver, receiver, data } : null),
() => { () => {
log('brownie points assigned completed'); debug('brownie points assigned completed');
} }
); );
}; };
@ -475,7 +423,7 @@ module.exports = function(User) {
} }
); );
// user.update$(updateData: Object) => Observable[Number] // user.updateTo$(updateData: Object) => Observable[Number]
User.prototype.update$ = function update$(updateData) { User.prototype.update$ = function update$(updateData) {
const id = this.getId(); const id = this.getId();
const updateOptions = { allowExtendedOperators: true }; const updateOptions = { allowExtendedOperators: true };
@ -493,7 +441,7 @@ module.exports = function(User) {
} }
return this.constructor.update$({ id }, updateData, updateOptions); return this.constructor.update$({ id }, updateData, updateOptions);
}; };
User.prototype.getTimestamps = function getTimestamps() { User.prototype.getPoints$ = function getPoints$() {
const id = this.getId(); const id = this.getId();
const filter = { const filter = {
where: { id }, where: { id },
@ -517,11 +465,4 @@ module.exports = function(User) {
return user.challengeMap; return user.challengeMap;
}); });
}; };
// user.getPoints$() => Observable[Number]
User.prototype.getPoints$ = function getPoints$() {
const id = this.getId();
return this.constructor
.getPointsById$(id)
.doOnNext(points => { this.points = points; });
};
}; };

View File

@ -585,7 +585,7 @@
"</div>" "</div>"
], ],
"tests": [ "tests": [
"assert.isTrue((/<em>#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Italicize the text in your <code>target4</code> button by adding HTML tags.');", "assert.isTrue((/<em>#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Emphasize the text in your <code>target4</code> button by adding HTML tags.');",
"assert($(\"#target4\") && $(\"#target4\").text() === '#target4', 'message: Make sure the text is otherwise unchanged.');", "assert($(\"#target4\") && $(\"#target4\").text() === '#target4', 'message: Make sure the text is otherwise unchanged.');",
"assert.isFalse((/<em>/gi).test($(\"h3\").html()), 'message: Do not alter any other text.');", "assert.isFalse((/<em>/gi).test($(\"h3\").html()), 'message: Do not alter any other text.');",
"assert(code.match(/\\.html\\(/g), 'message: Make sure you are using <code>.html()</code> and not <code>.text()</code>.');" "assert(code.match(/\\.html\\(/g), 'message: Make sure you are using <code>.html()</code> and not <code>.text()</code>.');"

View File

@ -1,4 +1,3 @@
import { Observable } from 'rx';
import passport from 'passport'; import passport from 'passport';
import { PassportConfigurator } from 'loopback-component-passport'; import { PassportConfigurator } from 'loopback-component-passport';
import passportProviders from './passport-providers'; import passportProviders from './passport-providers';
@ -71,21 +70,22 @@ PassportConfigurator.prototype.init = function passportInit(noSession) {
}); });
passport.deserializeUser((id, done) => { passport.deserializeUser((id, done) => {
Observable.combineLatest(
this.userModel.findById$(id, { fields }), this.userModel.findById(id, { fields }, (err, user) => {
this.userModel.getPointsById$(id), if (err || !user) {
(user, points) => { return done(err, user);
if (user) { user.points = points; }
return user;
} }
) return this.app.dataSources.db.connector
.doOnNext(user => { .collection('user')
if (!user) { throw new Error('deserialize found no user'); } .aggregate([
}) { $match: { _id: user.id } },
.subscribe( { $project: { points: { $size: '$progressTimestamps' } } }
user => done(null, user), ], function(err, [{ points = 1 } = {}]) {
done if (err) { return done(err); }
); user.points = points;
return done(null, user);
});
});
}); });
}; };

View File

@ -66,5 +66,9 @@
"userIdentity": { "userIdentity": {
"dataSource": "db", "dataSource": "db",
"public": true "public": true
},
"flyer": {
"dataSource": "db",
"public": true
} }
} }

View File

@ -1,5 +1,6 @@
extends ../layout extends ../layout
block content block content
include ../partials/flyer
script(src="/bower_components/cal-heatmap/cal-heatmap.min.js") script(src="/bower_components/cal-heatmap/cal-heatmap.min.js")
script. script.
var challengeName = 'Profile View'; var challengeName = 'Profile View';

View File

@ -4,6 +4,7 @@ block content
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css') link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css') link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
link(rel='stylesheet', href='/css/ubuntu.css') link(rel='stylesheet', href='/css/ubuntu.css')
include ../partials/flyer
.row .row
.col-md-4.col-lg-3 .col-md-4.col-lg-3
.scroll-locker(id = "scroll-locker") .scroll-locker(id = "scroll-locker")

View File

@ -4,6 +4,7 @@ block content
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css') link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css') link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
link(rel='stylesheet', href='/css/ubuntu.css') link(rel='stylesheet', href='/css/ubuntu.css')
include ../partials/flyer
.row .row
.col-md-3.col-lg-3 .col-md-3.col-lg-3
.scroll-locker(id = "scroll-locker") .scroll-locker(id = "scroll-locker")

View File

@ -4,6 +4,7 @@ block content
link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css') link(rel='stylesheet', href='/bower_components/CodeMirror/addon/lint/lint.css')
link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css') link(rel='stylesheet', href='/bower_components/CodeMirror/theme/monokai.css')
link(rel='stylesheet', href='/css/ubuntu.css') link(rel='stylesheet', href='/css/ubuntu.css')
include ../partials/flyer
.row .row
.col-md-4.col-lg-3 .col-md-4.col-lg-3
.scroll-locker(id = "scroll-locker") .scroll-locker(id = "scroll-locker")

View File

@ -1,5 +1,6 @@
extends ../layout-wide extends ../layout-wide
block content block content
include ../partials/flyer
.row .row
.col-md-8.col-md-offset-2 .col-md-8.col-md-offset-2
for step, index in description for step, index in description

View File

@ -1,5 +1,6 @@
extends ../layout-wide extends ../layout-wide
block content block content
include ../partials/flyer
.row .row
.col-xs-12.col-sm-12.col-md-4 .col-xs-12.col-sm-12.col-md-4
h4.text-center.challenge-instructions-title= name h4.text-center.challenge-instructions-title= name

View File

@ -1,5 +1,6 @@
extends ../layout-wide extends ../layout-wide
block content block content
include ../partials/flyer
.row .row
.col-md-4 .col-md-4
h4.text-center.challenge-instructions-title= name h4.text-center.challenge-instructions-title= name

View File

@ -6,7 +6,7 @@ html(lang='en')
body.top-and-bottom-margins body.top-and-bottom-margins
include partials/scripts include partials/scripts
include partials/navbar include partials/navbar
include partials/flash
.container .container
include partials/flash
block content block content
include partials/footer include partials/footer

View File

@ -1,20 +1,21 @@
.row.flashMessage .container
.col-xs-12 .row.flashMessage.negative-30
if (messages.errors || messages.error) .col-xs-12
.alert.alert-danger.fade.in if (messages.errors || messages.error)
button.close(type='button', data-dismiss='alert') .alert.alert-danger.fade.in
span.ion-close-circled button.close(type='button', data-dismiss='alert')
for error in (messages.errors || messages.error) span.ion-close-circled
div!= error.msg || error for error in (messages.errors || messages.error)
if messages.info div!= error.msg || error
.alert.alert-info.fade.in if messages.info
button.close(type='button', data-dismiss='alert') .alert.alert-info.fade.in
span.ion-close-circled button.close(type='button', data-dismiss='alert')
for info in messages.info span.ion-close-circled
div!= info.msg for info in messages.info
if messages.success div!= info.msg
.alert.alert-success.fade.in if messages.success
button.close(type='button', data-dismiss='alert') .alert.alert-success.fade.in
span.ion-close-circled button.close(type='button', data-dismiss='alert')
for success in messages.success span.ion-close-circled
div!= success.msg for success in messages.success
div!= success.msg

View File

@ -0,0 +1,8 @@
if (user && user.points > 5)
.container
.row.flashMessage.negative-30
.col-xs-12
#billBoard.alert.alert-info.fade.in(style="display: none;")
button.close(type='button', data-dismiss='alert')
span.ion-close-circled#dismissBill
#billContent