diff --git a/common/models/popularity.json b/common/models/popularity.json index 88a0c59a6a7..0032599de48 100644 --- a/common/models/popularity.json +++ b/common/models/popularity.json @@ -11,7 +11,8 @@ "type": [ "object" ], - "required": true + "required": true, + "default": [] } }, "validations": [], diff --git a/news/routes/Show/Show.js b/news/routes/Show/Show.js index ac5cb059080..f9e44c5224c 100644 --- a/news/routes/Show/Show.js +++ b/news/routes/Show/Show.js @@ -7,7 +7,7 @@ import { Image } from 'react-bootstrap'; import Author from './components/Author'; import { Loader } from '../../../common/app/helperComponents'; -import { getArticleById } from '../../utils/ajax'; +import { getArticleById, postPopularityEvent } from '../../utils/ajax'; const propTypes = { history: PropTypes.shape({ @@ -83,6 +83,11 @@ class ShowArticle extends Component { } if (article) { const [, shortId] = slug.split('--'); + postPopularityEvent({ + event: 'view', + timestamp: Date.now(), + shortId + }); /* eslint-disable react/no-did-mount-set-state */ return this.setState( { diff --git a/news/utils/ajax.js b/news/utils/ajax.js index 124304f88cf..05cca79ee9d 100644 --- a/news/utils/ajax.js +++ b/news/utils/ajax.js @@ -21,3 +21,7 @@ export function getFeaturedList(skip = 0) { })}` ); } + +export function postPopularityEvent(event) { + return axios.post('/p', event); +} diff --git a/server/boot/news.js b/server/boot/news.js index a1c59d24e22..42d1cd41c14 100644 --- a/server/boot/news.js +++ b/server/boot/news.js @@ -1,9 +1,30 @@ import React from 'react'; import { renderToString } from 'react-dom/server'; import { StaticRouter } from 'react-router-dom'; +import { has } from 'lodash'; +import debug from 'debug'; import NewsApp from '../../news/NewsApp'; +const routerLog = debug('fcc:boot:news:router'); +const apiLog = debug('fcc:boot:news:api'); + +export default function newsBoot(app) { + const router = app.loopback.Router(); + const api = app.loopback.Router(); + + router.get('/n', (req, res) => res.redirect('/news')); + router.get('/n/:shortId', createReferralHandler(app)); + + router.get('/news', serveNewsApp); + router.get('/news/*', serveNewsApp); + + api.post('/p', createPopularityHandler(app)); + + app.use(api); + app.use(router); +} + function serveNewsApp(req, res) { const context = {}; const markup = renderToString( @@ -12,20 +33,23 @@ function serveNewsApp(req, res) { ); if (context.url) { + routerLog('redirect found in `renderToString`'); // 'client-side' routing hit on a redirect return res.redirect(context.url); } + routerLog('news markup sending'); return res.render('layout-news', { title: 'News | freeCodeCamp', markup }); } function createReferralHandler(app) { + const { Article } = app.models; + return function referralHandler(req, res, next) { - const { Article } = app.models; const { shortId } = req.params; if (!shortId) { return res.redirect('/news'); } - console.log(shortId); + routerLog('shortId', shortId); return Article.findOne( { where: { @@ -51,14 +75,77 @@ function createReferralHandler(app) { }; } -export default function newsBoot(app) { - const router = app.loopback.Router(); +function createPopularityHandler(app) { + const { Article, Popularity } = app.models; - router.get('/n', (req, res) => res.redirect('/news')); - router.get('/n/:shortId', createReferralHandler(app)); - - router.get('/news', serveNewsApp); - router.get('/news/*', serveNewsApp); - - app.use(router); + return function handlePopularityStats(req, res, next) { + const { body, user } = req; + if ( + !has(body, 'event') || + !has(body, 'timestamp') || + !has(body, 'shortId') + ) { + console.warn('Popularity event recieved from client is malformed'); + console.log(JSON.stringify(body, null, 2)); + // sending 200 because the client shouldn't care for this + return res.sendStatus(200); + } + res.sendStatus(200); + const { shortId } = body; + apiLog('shortId', shortId); + const populartiyUpdate = { + ...body, + byAuthenticatedUser: !!user + }; + Popularity.findOne({ where: { articleId: shortId } }, (err, popularity) => { + if (err) { + apiLog(err); + return next(err); + } + if (popularity) { + return popularity.updateAttribute( + 'events', + [populartiyUpdate, ...popularity.events], + err => { + if (err) { + apiLog(err); + return next(err); + } + return apiLog('poplarity updated'); + } + ); + } + return Popularity.create( + { + events: [populartiyUpdate], + articleId: shortId + }, + err => { + if (err) { + apiLog(err); + return next(err); + } + return apiLog('poulartiy created'); + } + ); + }); + return body.event === 'view' + ? Article.findOne({ where: { shortId } }, (err, article) => { + if (err) { + apiLog(err); + next(err); + } + return article.updateAttributes( + { viewCount: article.viewCount + 1 }, + err => { + if (err) { + apiLog(err); + return next(err); + } + return apiLog('article views updated'); + } + ); + }) + : null; + }; } diff --git a/server/middlewares/csurf.js b/server/middlewares/csurf.js index 50b2821ceb1..bf49d0fb668 100644 --- a/server/middlewares/csurf.js +++ b/server/middlewares/csurf.js @@ -11,7 +11,7 @@ export default function() { return function csrf(req, res, next) { const path = req.path.split('/')[1]; - if (/(api|external)/.test(path)) { + if (/(api|external|^p$)/.test(path)) { return next(); } return protection(req, res, next);