From 2b32fb3633896a881e6f17a41c653a5b5cde7958 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 3 Aug 2016 15:26:05 -0700 Subject: [PATCH] Feature(challenges): save users current challenge to db This allows us to automatically load their current challenge --- common/app/redux/actions.js | 4 ++ common/app/redux/entities-reducer.js | 13 ++++++ .../app/redux/load-current-challenge-saga.js | 45 ++++++++++++++++++- common/app/redux/types.js | 1 + server/boot/settings.js | 21 +++++++++ server/services/user.js | 2 +- 6 files changed, 83 insertions(+), 3 deletions(-) diff --git a/common/app/redux/actions.js b/common/app/redux/actions.js index 239a7af2d7..37abad4b33 100644 --- a/common/app/redux/actions.js +++ b/common/app/redux/actions.js @@ -61,6 +61,10 @@ export const addUser = createAction( export const updateThisUser = createAction(types.updateThisUser); export const showSignIn = createAction(types.showSignIn); export const loadCurrentChallenge = createAction(types.loadCurrentChallenge); +export const updateMyCurrentChallenge = createAction( + types.updateMyCurrentChallenge, + (username, currentChallengeId) => ({ username, currentChallengeId }) +); // updateUserPoints(username: String, points: Number) => Action export const updateUserPoints = createAction( diff --git a/common/app/redux/entities-reducer.js b/common/app/redux/entities-reducer.js index ad271471c6..18170b461f 100644 --- a/common/app/redux/entities-reducer.js +++ b/common/app/redux/entities-reducer.js @@ -85,5 +85,18 @@ export default function entities(state = initialState, action) { } }; } + + if (action.type === types.updateMyCurrentChallenge) { + return { + ...state, + user: { + ...state.user, + [username]: { + ...state.user[username], + currentChallengeId: action.payload.currentChallengeId + } + } + }; + } return state; } diff --git a/common/app/redux/load-current-challenge-saga.js b/common/app/redux/load-current-challenge-saga.js index 2a1575276b..00e1a3a7dc 100644 --- a/common/app/redux/load-current-challenge-saga.js +++ b/common/app/redux/load-current-challenge-saga.js @@ -1,15 +1,51 @@ import { Observable } from 'rx'; +import debug from 'debug'; import { push } from 'react-router-redux'; import types from './types'; +import { + updateMyCurrentChallenge, + createErrorObservable +} from './actions'; import { userSelector, firstChallengeSelector } from './selectors'; -import getActionsOfType from '../../utils/get-actions-of-type'; import { updateCurrentChallenge } from '../routes/challenges/redux/actions'; +import getActionsOfType from '../../utils/get-actions-of-type'; +import combineSagas from '../utils/combine-sagas'; +import { postJSON$ } from '../../utils/ajax-stream'; -export default function loadCurrentChallengeSaga(actions, getState) { +const log = debug('fcc:app/redux/load-current-challenge-saga'); +export function updateMyCurrentChallengeSaga(actions, getState) { + const updateChallenge$ = getActionsOfType( + actions, + updateCurrentChallenge.toString() + ) + .map(({ payload: { id } }) => id) + .filter(() => { + const { app: { user: username } } = getState(); + return !!username; + }); + const optimistic = updateChallenge$.map(id => { + const { app: { user: username } } = getState(); + return updateMyCurrentChallenge(username, id); + }); + const ajaxUpdate = updateChallenge$ + .debounce(250) + .flatMapLatest(currentChallengeId => { + const { app: { csrfToken: _csrf } } = getState(); + return postJSON$( + '/update-my-current-challenge', + { _csrf, currentChallengeId } + ) + .map(({ message }) => log(message)) + .catch(createErrorObservable); + }); + return Observable.merge(optimistic, ajaxUpdate); +} + +export function loadCurrentChallengeSaga(actions, getState) { return getActionsOfType(actions, types.loadCurrentChallenge) .flatMap(() => { let finalChallenge; @@ -40,3 +76,8 @@ export default function loadCurrentChallengeSaga(actions, getState) { ); }); } + +export default combineSagas( + updateMyCurrentChallengeSaga, + loadCurrentChallengeSaga +); diff --git a/common/app/redux/types.js b/common/app/redux/types.js index f72e9c4625..d0ec7220cb 100644 --- a/common/app/redux/types.js +++ b/common/app/redux/types.js @@ -15,6 +15,7 @@ export default createTypes([ 'updateCompletedChallenges', 'showSignIn', 'loadCurrentChallenge', + 'updateMyCurrentChallenge', 'handleError', 'toggleNightMode', diff --git a/server/boot/settings.js b/server/boot/settings.js index bddad74bd1..496a2df249 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -1,4 +1,5 @@ import { ifNoUser401 } from '../utils/middleware'; +import { isMongoId } from 'validator'; import supportedLanguages from '../../common/utils/supported-languages.js'; export default function settingsController(app) { @@ -49,6 +50,20 @@ export default function settingsController(app) { ); } + function updateMyCurrentChallenge(req, res, next) { + const { user, body: { currentChallengeId } } = req; + if (!isMongoId('' + currentChallengeId)) { + return next(new Error(`${currentChallengeId} is not a valid ObjectId`)); + } + return user.update$({ currentChallengeId }).subscribe( + () => res.json({ + message: + `your current challenge has been updated to ${currentChallengeId}` + }), + next + ); + } + api.post( '/toggle-lockdown', toggleUserFlag('isLocked') @@ -78,5 +93,11 @@ export default function settingsController(app) { ifNoUser401, updateMyLang ); + + api.post( + '/update-my-current-challenge', + ifNoUser401, + updateMyCurrentChallenge + ); app.use(api); } diff --git a/server/services/user.js b/server/services/user.js index a094517ecb..ba8838ba69 100644 --- a/server/services/user.js +++ b/server/services/user.js @@ -26,7 +26,7 @@ const publicUserProps = [ 'sendNotificationEmail', 'sendQuincyEmail', - 'currentChallenge', + 'currentChallengeId', 'challengeMap' ]; const log = debug('fcc:services:user');