Skip to content
This repository has been archived by the owner on Oct 1, 2019. It is now read-only.

Commit

Permalink
Merge pull request #973 from Lokiedu/feature/comment-notifications-ap…
Browse files Browse the repository at this point in the history
…i-ext

Extend comment email notification API
  • Loading branch information
voidxnull committed Feb 9, 2018
2 parents d947227 + d676b50 commit 229a82f
Show file tree
Hide file tree
Showing 24 changed files with 1,074 additions and 425 deletions.
53 changes: 53 additions & 0 deletions migrations/20180107210900_users.js
@@ -0,0 +1,53 @@
export async function up(knex) {
// Replace `mute_all_posts` with `comment_notifications`
// Initialize`more.last_comment_notification_at` for each user.
const users = await knex('users')
.select('id', 'more');

for (const user of users) {
if (!user.more) {
user.more = {};
}

let comment_notifications = 'on';
if (user.more.mute_all_posts) {
comment_notifications = 'off';
}

const more = {
...users.more,
comment_notifications,
last_comment_notification_at: new Date().toJSON()
};

delete more.mute_all_posts;

await knex('users').where('id', user.id).update({ more });
}
}

export async function down(knex) {
const users = await knex('users')
.select('id', 'more');

for (const user of users) {
if (!user.more) {
user.more = {};
}

let mute_all_posts = false;
if (user.more.comment_notifications === 'off') {
mute_all_posts = false;
}

const more = {
...users.more,
mute_all_posts,
};

delete more.last_comment_notification_at;
delete more.comment_notifications;

await knex('users').where('id', user.id).update({ more });
}
}
5 changes: 3 additions & 2 deletions package.json
Expand Up @@ -45,9 +45,9 @@
"start:_all:dev": "run-p start:server:dev start:tasks:dev",
"start:_all:prod": "run-p start:server:prod start:tasks:prod",
"start:server:prod": "DEV=0 NODE_ENV=production node bin/start-server.js 2>&1 | bunyan",
"start:tasks:prod": "DEV=0 NODE_ENV=production node bin/start-tasks.js",
"start:tasks:prod": "DEV=0 NODE_ENV=production node bin/start-tasks.js 2>&1 | bunyan",
"start:server:dev": "DEV=1 NODE_ENV=development nodemon ./bin/start-server.js --watch ./public/server 2>&1 | bunyan",
"start:tasks:dev": "DEV=1 NODE_ENV=development nodemon ./bin/start-tasks.js --watch ./public/server",
"start:tasks:dev": "DEV=1 NODE_ENV=development nodemon ./bin/start-tasks.js --watch ./public/server 2>&1 | bunyan",
"build:_all:prod": "run-p build:client:prod build:server:prod build:tasks:prod",
"build:_all:dev:watch": "run-p build:client:dev:watch build:server:dev:watch build:tasks:dev:watch",
"build:client:dev": "DEV=1 NODE_ENV=development webpack --config './webpack.config.client.babel.js' --colors --hide-modules --display-error-details",
Expand Down Expand Up @@ -235,6 +235,7 @@
"trace": "~3.0.0",
"transform-loader": "^0.2.3",
"unexpected": "~10.35.0",
"unexpected-date": "^1.1.1",
"unexpected-dom": "~4.0.0",
"unexpected-http": "~6.0.0",
"unexpected-immutable": "~0.2.6",
Expand Down
24 changes: 6 additions & 18 deletions src/api/controllers/comments.js
Expand Up @@ -16,8 +16,10 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import uuid from 'uuid';
import Joi from 'joi';

import { addToSearchIndex } from '../utils/search';
import { CommentValidator } from '../validators';

export async function getPostComments(ctx) {
const Comment = ctx.bookshelf.model('Comment');
Expand All @@ -39,20 +41,13 @@ export async function postComment(ctx) {

const post = await Post.where({ id: ctx.params.id }).fetch({ require: true });

// TODO: Replace with joi validation.
if (!('text' in ctx.request.body) || !ctx.request.body.text) {
ctx.status = 400;
ctx.body = { error: 'Comment text cannot be empty' };
return;
}

const text = ctx.request.body.text.trim();
const attributes = Joi.attempt(ctx.request.body, CommentValidator);

const comment = new Comment({
id: uuid.v4(),
post_id: ctx.params.id,
user_id: ctx.state.user,
text
text: attributes.text
});

post.attributes.updated_at = new Date().toJSON();
Expand Down Expand Up @@ -82,16 +77,9 @@ export async function editComment(ctx) {
ctx.status = 403;
}

// TODO: Replace with joi validation.
if (!('text' in ctx.request.body) || ctx.request.body.text.trim().length === 0) {
ctx.status = 400;
ctx.body = { error: 'Comment text cannot be empty' };
return;
}

const text = ctx.request.body.text.trim();
const attributes = Joi.attempt(ctx.request.body, CommentValidator);

comment.set('text', text);
comment.set('text', attributes.text);
comment.set('updated_at', new Date().toJSON());
post.attributes.updated_at = new Date().toJSON();

Expand Down
66 changes: 66 additions & 0 deletions src/api/controllers/dev/emailTemplates.js
@@ -0,0 +1,66 @@
import path from 'path';
import faker from 'faker';
import moment from 'moment';
import md5 from 'md5';
import { renderFile } from 'ejs';
import { promisify } from 'bluebird';
import { API_HOST } from '../../../config';

const renderFileAsync = promisify(renderFile);

const commonTemplateParams = {
host: API_HOST
};

function post({ numComments } = { numComments: 1 }) {
return {
id: faker.random.uuid(),
title: faker.name.title(),
url: `https://www.libertysoil.com/post/${faker.random.uuid()}`,
author: user(),
comments: Array.apply(null, Array(numComments)).map(() => comment()),
};
}

function user({ avatarSize } = { avatarSize: 36 }) {
return {
name: faker.name.findName(),
url: `https://www.libertysoil.com/u/${faker.random.uuid()}`,
avatarUrl: `http://www.gravatar.com/avatar/${md5(faker.internet.email().toLowerCase())}?s=${avatarSize}&r=g&d=retro`,
};
}

function comment() {
return {
text: faker.lorem.paragraph(),
date: moment(faker.date.recent()).format('Do [of] MMMM YYYY'),
author: user({ avatarSize: 17 }),
};
}

const templateParams = {
'new-comments.ejs': {
posts: [
post({ numComments: 3 }),
post({ numComments: 1 }),
post({ numComments: 4 }),
]
},
'new-comment.ejs': {
post: post(),
}
};

export async function renderEmailTemplate(ctx) {
const filePath = path.join('src/email-templates', ctx.params.name);

try {
ctx.body = await renderFileAsync(filePath, {
...commonTemplateParams,
...templateParams[ctx.params.name],
});
} catch (e) {
console.error(e); // eslint-disable-line no-console
ctx.body = e.stack;
}
}
2 changes: 0 additions & 2 deletions src/api/controllers/misc.js
Expand Up @@ -99,8 +99,6 @@ export async function getRecentlyUsedTags(ctx) {
const yesterday = new Date();
yesterday.setHours(yesterday.getHours() - 24);

// Count fails prob because of the tag models that are used for couting posts (or not)

return {
hashtags: {
entries: await Hashtag.getRecentlyUsed({ limit: 5 }).fetch(),
Expand Down
2 changes: 1 addition & 1 deletion src/api/db/index.js
Expand Up @@ -110,7 +110,7 @@ export function initBookshelfFromKnex(knex) {
if (!email) {
return '';
}
return md5(email);
return md5(email.toLowerCase());
},
fullName() {
const more = this.get('more');
Expand Down
4 changes: 4 additions & 0 deletions src/api/routing.js
Expand Up @@ -189,5 +189,9 @@ export function initApi(bookshelf) {

api.get('/locale/:lang_code', misc.getLocale);

if (process.env.NODE_ENV !== 'production') {
api.get('/email-template/:name', require('./controllers/dev/emailTemplates').renderEmailTemplate);
}

return api.routes();
}
15 changes: 9 additions & 6 deletions src/api/validators.js
Expand Up @@ -46,15 +46,14 @@ export const UserRegistrationValidator = Joi.object({

export const UserSettingsValidator = Joi.object({
more: Joi.object({
summary: Joi.string(),
bio: Joi.string(),
summary: Joi.string().trim().empty(''),
bio: Joi.string().trim().empty(''),
roles: Joi.array(), // TODO: validate role variants
//first_login: Joi.bool(), // private
avatar: PictureAttachment,
head_pic: PictureAttachment,
mute_all_posts: Joi.bool(),
firstName: Joi.string(),
lastName: Joi.string(),
comment_notifications: Joi.string().only(['on', 'off', 'weekly', 'daily']),
firstName: Joi.string().trim().empty(''),
lastName: Joi.string().trim().empty(''),
lang: Joi.string().only(SUPPORTED_LOCALES),
sidebar: Joi.object({
collapsed: Joi.bool()
Expand Down Expand Up @@ -160,3 +159,7 @@ export const PostValidator = Joi.object({
schools: Joi.array().items(Joi.string()),
geotags: Joi.array().items(Joi.string())
}).options({ abortEarly: false, stripUnknown: true });

export const CommentValidator = Joi.object({
text: Joi.string().trim().min(1),
});
2 changes: 1 addition & 1 deletion src/definitions/users.js
Expand Up @@ -84,7 +84,7 @@ export type UserMore = {
firstName?: string,
head_pic?: Attachment,
lastName?: string,
mute_all_posts?: boolean,
comment_notifications?: string,
roles?: Array<UserRole>,
social?: UserSocial,
summary?: string
Expand Down

0 comments on commit 229a82f

Please sign in to comment.