diff --git a/client/main.js b/client/main.js index b7ed7c9bd0..7e50247a10 100644 --- a/client/main.js +++ b/client/main.js @@ -124,6 +124,30 @@ main = (function(main, global) { 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; }(main, window)); @@ -612,4 +636,49 @@ $(document).ready(function() { // Repo 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); }); diff --git a/common/models/flyer.json b/common/models/flyer.json new file mode 100644 index 0000000000..251bbb0d15 --- /dev/null +++ b/common/models/flyer.json @@ -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": [] +} diff --git a/common/models/user.js b/common/models/user.js index e81cb34c7a..4c48f5e3ab 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -1,26 +1,26 @@ -import { Disposable, Observable, Scheduler } from 'rx'; +import { Observable } from 'rx'; import uuid from 'node-uuid'; import moment from 'moment'; 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'; -const log = debug('fcc:user:remote'); +const debug = debugFactory('fcc:user:remote'); const BROWNIEPOINTS_TIMEOUT = [1, 'hour']; -const aboutFilter = { - username: true, - bio: true -}; function getAboutProfile({ username, - bio, - points + githubProfile: github, + progressTimestamps = [], + bio }) { return { username, - bio, - browniePoints: points + github, + browniePoints: progressTimestamps.length, + bio }; } @@ -54,31 +54,8 @@ module.exports = function(User) { User.on('dataSourceAttached', () => { User.findOne$ = Observable.fromNodeCallback(User.findOne, User); - User.findById$ = Observable.fromNodeCallback(User.findById, User); User.update$ = Observable.fromNodeCallback(User.updateAll, 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) { @@ -99,7 +76,7 @@ module.exports = function(User) { next(); }); - log('setting up user hooks'); + debug('setting up user hooks'); User.afterRemote('confirm', function(ctx) { ctx.req.flash('success', { msg: [ @@ -151,9 +128,9 @@ module.exports = function(User) { } // the email of the requested user - log(info.email); + debug(info.email); // the temp access token to allow password reset - log(info.accessToken.id); + debug(info.accessToken.id); // requires AccessToken.belongsTo(User) var mailOptions = { to: info.email, @@ -173,7 +150,7 @@ module.exports = function(User) { User.app.models.Email.send(mailOptions, function(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) { - log('setting cookies'); + debug('setting cookies'); res.cookie('access_token', accessToken.id, config); res.cookie('userId', accessToken.userId, config); } @@ -204,7 +181,7 @@ module.exports = function(User) { return req.logIn({ id: accessToken.userId.toString() }, function(err) { if (err) { return next(err); } - log('user logged in'); + debug('user logged in'); if (req.session && req.session.returnTo) { var redirectTo = req.session.returnTo; @@ -240,7 +217,7 @@ module.exports = function(User) { if (!username && !email) { return Promise.resolve(false); } - log('checking existence'); + debug('checking existence'); // check to see if username is on blacklist if (username && blacklistedUsernames.indexOf(username) !== -1) { @@ -253,7 +230,7 @@ module.exports = function(User) { } else { where.email = email ? email.toLowerCase() : email; } - log('where', where); + debug('where', where); return User.count(where) .then(count => count > 0); }; @@ -294,23 +271,16 @@ module.exports = function(User) { )); }); } - username = username.toLowerCase(); - const filter = { - where: { username }, - fields: { id: true, ...aboutFilter } - }; - return User.findOne$(filter) - .doOnNext(user => { - if (!user || user.username !== username) { - throw new Error(`no user found for ${ username }`); - } - }) - .flatMap(user => user.getPoints$().map(user)) - .map(user => getAboutProfile(user)) - .subscribe( - aboutUser => cb(null, aboutUser), - cb - ); + return User.findOne({ where: { username } }, (err, user) => { + if (err) { + return cb(err); + } + if (!user || user.username !== username) { + return cb(new Error(`no user found for ${ username }`)); + } + const aboutUser = getAboutProfile(user); + return cb(null, aboutUser); + }); }; User.remoteMethod( @@ -338,8 +308,7 @@ module.exports = function(User) { User.giveBrowniePoints = function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { - receiver = receiver.toLowerCase(); - giver = giver.toLowerCase(); + const findUser = observeMethod(User, 'findOne'); if (!receiver) { return nextTick(() => { cb( @@ -352,84 +321,63 @@ module.exports = function(User) { 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(); const browniePoints = temp .subtract.apply(temp, BROWNIEPOINTS_TIMEOUT) .valueOf(); - const user$ = User.findOne$({ - where: { username: receiver }, - fields: { - ...aboutFilter, - progressTimestamps: true - } - }); - const giver$ = User.count$({ username: giver }); - return Observable.combineLatest( - user$, - giver$, - (user, giver) => ({ doesGiverExist: !!giver, user }) - ) - .tapOnNext(({ user, doesGiverExist }) => { + const user$ = findUser({ where: { username: receiver }}); + + return user$ + .tapOnNext((user) => { if (!user) { throw new Error(`could not find receiver for ${ receiver }`); } - if (!doesGiverExist) { - throw new Error(`no user found for giver '${giver}'`); - } }) .flatMap(({ progressTimestamps = [] }) => { - return Observable.from( - progressTimestamps, - null, - null, - Scheduler.default - ); + return Observable.from(progressTimestamps); }) // filter out non objects .filter((timestamp) => !!timestamp || typeof timestamp === 'object') // 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(browniePoint => browniePoint.giver === giver) + .filter((browniePoint) => { + return browniePoint.giver === giver; + }) // no results means this is the first brownie point given by giver // so return -1 to indicate receiver should receive point .first({ defaultValue: -1 }) - .flatMap(browniePointsFromGiver => { + .flatMap((browniePointsFromGiver) => { if (browniePointsFromGiver === -1) { - const updateData = { - $push: { - progressTimestamps: { - giver, - timestamp: Date.now() - } - } - }; - return user$ - .flatMap(user => user.update$(updateData).map(user)) - .doOnNext(user => { - user.points = user.progressTimestamps.length + 1; + + return user$.flatMap((user) => { + user.progressTimestamps.push({ + giver, + timestamp: Date.now(), + ...data }); + return saveUser(user); + }); } return Observable.throw( new Error(`${ giver } already gave ${ receiver } points`) ); }) .subscribe( - user => cb( - null, - getAboutProfile(user), - dev ? - { giver, receiver, data } : - null - ), - e => cb(e, null, dev ? { giver, receiver, data } : null), + (user) => { + return cb( + null, + getAboutProfile(user), + 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) { const id = this.getId(); const updateOptions = { allowExtendedOperators: true }; @@ -493,7 +441,7 @@ module.exports = function(User) { } return this.constructor.update$({ id }, updateData, updateOptions); }; - User.prototype.getTimestamps = function getTimestamps() { + User.prototype.getPoints$ = function getPoints$() { const id = this.getId(); const filter = { where: { id }, @@ -517,11 +465,4 @@ module.exports = function(User) { 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; }); - }; }; diff --git a/seed/challenges/01-front-end-development-certification/jquery.json b/seed/challenges/01-front-end-development-certification/jquery.json index ed8430f735..bf152ccf76 100644 --- a/seed/challenges/01-front-end-development-certification/jquery.json +++ b/seed/challenges/01-front-end-development-certification/jquery.json @@ -585,7 +585,7 @@ "" ], "tests": [ - "assert.isTrue((/#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Italicize the text in your target4 button by adding HTML tags.');", + "assert.isTrue((/#target4<\\/em>/gi).test($(\"#target4\").html()), 'message: Emphasize the text in your target4 button by adding HTML tags.');", "assert($(\"#target4\") && $(\"#target4\").text() === '#target4', 'message: Make sure the text is otherwise unchanged.');", "assert.isFalse((//gi).test($(\"h3\").html()), 'message: Do not alter any other text.');", "assert(code.match(/\\.html\\(/g), 'message: Make sure you are using .html() and not .text().');" diff --git a/server/component-passport.js b/server/component-passport.js index 004d1c8545..db96edcb86 100644 --- a/server/component-passport.js +++ b/server/component-passport.js @@ -1,4 +1,3 @@ -import { Observable } from 'rx'; import passport from 'passport'; import { PassportConfigurator } from 'loopback-component-passport'; import passportProviders from './passport-providers'; @@ -71,21 +70,22 @@ PassportConfigurator.prototype.init = function passportInit(noSession) { }); passport.deserializeUser((id, done) => { - Observable.combineLatest( - this.userModel.findById$(id, { fields }), - this.userModel.getPointsById$(id), - (user, points) => { - if (user) { user.points = points; } - return user; + + this.userModel.findById(id, { fields }, (err, user) => { + if (err || !user) { + return done(err, user); } - ) - .doOnNext(user => { - if (!user) { throw new Error('deserialize found no user'); } - }) - .subscribe( - user => done(null, user), - done - ); + return this.app.dataSources.db.connector + .collection('user') + .aggregate([ + { $match: { _id: user.id } }, + { $project: { points: { $size: '$progressTimestamps' } } } + ], function(err, [{ points = 1 } = {}]) { + if (err) { return done(err); } + user.points = points; + return done(null, user); + }); + }); }); }; diff --git a/server/model-config.json b/server/model-config.json index 1b9d552ab9..60800cf81b 100644 --- a/server/model-config.json +++ b/server/model-config.json @@ -66,5 +66,9 @@ "userIdentity": { "dataSource": "db", "public": true + }, + "flyer": { + "dataSource": "db", + "public": true } } diff --git a/server/views/account/show.jade b/server/views/account/show.jade index df88282365..568fd5ea4d 100644 --- a/server/views/account/show.jade +++ b/server/views/account/show.jade @@ -1,5 +1,6 @@ extends ../layout block content + include ../partials/flyer script(src="/bower_components/cal-heatmap/cal-heatmap.min.js") script. var challengeName = 'Profile View'; @@ -170,4 +171,4 @@ block content if (challenge.solution) a(href='/challenges/' + removeOldTerms(challenge.name) + '?solution=' + encodeURIComponent(encodeFcc(challenge.solution)), target='_blank')= removeOldTerms(challenge.name) else - a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name) \ No newline at end of file + a(href='/challenges/' + removeOldTerms(challenge.name))= removeOldTerms(challenge.name) diff --git a/server/views/challenges/showBonfire.jade b/server/views/challenges/showBonfire.jade index 8ab0684c4a..6bb918d579 100644 --- a/server/views/challenges/showBonfire.jade +++ b/server/views/challenges/showBonfire.jade @@ -4,6 +4,7 @@ block content 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='/css/ubuntu.css') + include ../partials/flyer .row .col-md-4.col-lg-3 .scroll-locker(id = "scroll-locker") diff --git a/server/views/challenges/showHTML.jade b/server/views/challenges/showHTML.jade index 0d08346825..7835148327 100644 --- a/server/views/challenges/showHTML.jade +++ b/server/views/challenges/showHTML.jade @@ -4,6 +4,7 @@ block content 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='/css/ubuntu.css') + include ../partials/flyer .row .col-md-3.col-lg-3 .scroll-locker(id = "scroll-locker") diff --git a/server/views/challenges/showJS.jade b/server/views/challenges/showJS.jade index eae63ab02a..3939b29bed 100644 --- a/server/views/challenges/showJS.jade +++ b/server/views/challenges/showJS.jade @@ -4,6 +4,7 @@ block content 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='/css/ubuntu.css') + include ../partials/flyer .row .col-md-4.col-lg-3 .scroll-locker(id = "scroll-locker") diff --git a/server/views/challenges/showStep.jade b/server/views/challenges/showStep.jade index 1249b842a2..d276c836c8 100644 --- a/server/views/challenges/showStep.jade +++ b/server/views/challenges/showStep.jade @@ -1,5 +1,6 @@ extends ../layout-wide block content + include ../partials/flyer .row .col-md-8.col-md-offset-2 for step, index in description diff --git a/server/views/challenges/showVideo.jade b/server/views/challenges/showVideo.jade index bf3d45636b..f29911013e 100644 --- a/server/views/challenges/showVideo.jade +++ b/server/views/challenges/showVideo.jade @@ -1,5 +1,6 @@ extends ../layout-wide block content + include ../partials/flyer .row .col-xs-12.col-sm-12.col-md-4 h4.text-center.challenge-instructions-title= name diff --git a/server/views/challenges/showZiplineOrBasejump.jade b/server/views/challenges/showZiplineOrBasejump.jade index 19197c1795..5c3639a8d6 100644 --- a/server/views/challenges/showZiplineOrBasejump.jade +++ b/server/views/challenges/showZiplineOrBasejump.jade @@ -1,5 +1,6 @@ extends ../layout-wide block content + include ../partials/flyer .row .col-md-4 h4.text-center.challenge-instructions-title= name diff --git a/server/views/layout.jade b/server/views/layout.jade index f64024fdf9..eefd4090e0 100644 --- a/server/views/layout.jade +++ b/server/views/layout.jade @@ -6,7 +6,7 @@ html(lang='en') body.top-and-bottom-margins include partials/scripts include partials/navbar + include partials/flash .container - include partials/flash block content include partials/footer diff --git a/server/views/partials/flash.jade b/server/views/partials/flash.jade index e76ab6edf4..8668a4174d 100644 --- a/server/views/partials/flash.jade +++ b/server/views/partials/flash.jade @@ -1,20 +1,21 @@ -.row.flashMessage - .col-xs-12 - if (messages.errors || messages.error) - .alert.alert-danger.fade.in - button.close(type='button', data-dismiss='alert') - span.ion-close-circled - for error in (messages.errors || messages.error) - div!= error.msg || error - if messages.info - .alert.alert-info.fade.in - button.close(type='button', data-dismiss='alert') - span.ion-close-circled - for info in messages.info - div!= info.msg - if messages.success - .alert.alert-success.fade.in - button.close(type='button', data-dismiss='alert') - span.ion-close-circled - for success in messages.success - div!= success.msg +.container + .row.flashMessage.negative-30 + .col-xs-12 + if (messages.errors || messages.error) + .alert.alert-danger.fade.in + button.close(type='button', data-dismiss='alert') + span.ion-close-circled + for error in (messages.errors || messages.error) + div!= error.msg || error + if messages.info + .alert.alert-info.fade.in + button.close(type='button', data-dismiss='alert') + span.ion-close-circled + for info in messages.info + div!= info.msg + if messages.success + .alert.alert-success.fade.in + button.close(type='button', data-dismiss='alert') + span.ion-close-circled + for success in messages.success + div!= success.msg diff --git a/server/views/partials/flyer.jade b/server/views/partials/flyer.jade new file mode 100644 index 0000000000..92d99e5c21 --- /dev/null +++ b/server/views/partials/flyer.jade @@ -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