Added notifications ctrl to v2

refs #9866
This commit is contained in:
kirrg001 2018-10-12 19:44:03 +02:00 committed by Katharina Irrgang
parent b899a6fec8
commit 9f2d68a027
7 changed files with 492 additions and 8 deletions

View File

@ -31,6 +31,10 @@ module.exports = {
return shared.pipeline(require('./posts'), localUtils);
},
get notifications() {
return shared.pipeline(require('./notifications'), localUtils);
},
get settings() {
return shared.pipeline(require('./settings'), localUtils);
}

View File

@ -0,0 +1,196 @@
const moment = require('moment-timezone');
const semver = require('semver');
const Promise = require('bluebird');
const _ = require('lodash');
const settingsCache = require('../../services/settings/cache');
const ghostVersion = require('../../lib/ghost-version');
const common = require('../../lib/common');
const ObjectId = require('bson-objectid');
const api = require('./index');
const internalContext = {context: {internal: true}};
const _private = {};
_private.fetchAllNotifications = () => {
let allNotifications = settingsCache.get('notifications');
allNotifications.forEach((notification) => {
notification.addedAt = moment(notification.addedAt).toDate();
});
return allNotifications;
};
module.exports = {
docName: 'notifications',
browse: {
permissions: true,
query() {
let allNotifications = _private.fetchAllNotifications();
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
allNotifications = allNotifications.filter((notification) => {
// CASE: do not return old release notification
if (!notification.custom && notification.message) {
const notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/),
blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
if (notificationVersion && blogVersion && semver.gt(notificationVersion[0], blogVersion[0])) {
return true;
} else {
return false;
}
}
return notification.seen !== true;
});
return allNotifications;
}
},
add: {
statusCode(result) {
if (result.notifications.length) {
return 201;
} else {
return 200;
}
},
permissions: true,
query(frame) {
const defaults = {
dismissible: true,
location: 'bottom',
status: 'alert',
id: ObjectId.generate()
};
const overrides = {
seen: false,
addedAt: moment().toDate()
};
let notificationsToCheck = frame.data.notifications;
let addedNotifications = [];
const allNotifications = _private.fetchAllNotifications();
notificationsToCheck.forEach((notification) => {
const isDuplicate = allNotifications.find((n) => {
return n.id === notification.id;
});
if (!isDuplicate) {
addedNotifications.push(Object.assign({}, defaults, notification, overrides));
}
});
const hasReleaseNotification = notificationsToCheck.find((notification) => {
return !notification.custom;
});
// CASE: remove any existing release notifications if a new release notification comes in
if (hasReleaseNotification) {
_.remove(allNotifications, (el) => {
return !el.custom;
});
}
// CASE: nothing to add, skip
if (!addedNotifications.length) {
return Promise.resolve();
}
const addedReleaseNotifications = addedNotifications.filter((notification) => {
return !notification.custom;
});
// CASE: only latest release notification
if (addedReleaseNotifications.length > 1) {
addedNotifications = addedNotifications.filter((notification) => {
return notification.custom;
});
addedNotifications.push(_.orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
}
return api.settings.edit({
settings: [{
key: 'notifications',
value: allNotifications.concat(addedNotifications)
}]
}, internalContext).then(() => {
return addedNotifications;
});
}
},
destroy: {
statusCode: 204,
options: ['notification_id'],
validation: {
options: {
notification_id: {
required: true
}
}
},
permissions: true,
query(frame) {
const allNotifications = _private.fetchAllNotifications();
const notificationToMarkAsSeen = allNotifications.find((notification) => {
return notification.id === frame.options.notification_id;
}),
notificationToMarkAsSeenIndex = allNotifications.findIndex((notification) => {
return notification.id === frame.options.notification_id;
});
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
}));
}
if (notificationToMarkAsSeenIndex < 0) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
}));
}
if (notificationToMarkAsSeen.seen) {
return Promise.resolve();
}
allNotifications[notificationToMarkAsSeenIndex].seen = true;
return api.settings.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext).return();
}
},
destroyAll: {
statusCode: 204,
permissions: {
method: 'destroy'
},
query() {
const allNotifications = _private.fetchAllNotifications();
allNotifications.forEach((notification) => {
notification.seen = true;
});
return api.settings.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext).return();
}
}
};

View File

@ -21,5 +21,9 @@ module.exports = {
get settings() {
return require('./settings');
},
get notifications() {
return require('./notifications');
}
};

View File

@ -0,0 +1,28 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:notifications');
module.exports = {
all(response, apiConfig, frame) {
if (!response) {
return;
}
if (!response || !response.length) {
frame.response = {
notifications: []
};
return;
}
response.forEach((notification) => {
delete notification.seen;
delete notification.addedAt;
});
frame.response = {
notifications: response
};
debug(frame.response);
}
};

View File

@ -124,9 +124,9 @@ module.exports = function apiRoutes() {
);
// ## Notifications
router.get('/notifications', mw.authAdminAPI, api.http(api.notifications.browse));
router.post('/notifications', mw.authAdminAPI, api.http(api.notifications.add));
router.del('/notifications/:id', mw.authAdminAPI, api.http(api.notifications.destroy));
router.get('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.browse));
router.post('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.add));
router.del('/notifications/:notification_id', mw.authAdminAPI, apiv2.http(apiv2.notifications.destroy));
// ## DB
router.get('/db', mw.authAdminAPI, api.http(api.db.exportContent));

View File

@ -0,0 +1,253 @@
const should = require('should');
const supertest = require('supertest');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../../core/server/config');
const ghost = testUtils.startGhost;
let request;
describe('Notifications API V2', function () {
let ghostServer;
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request);
});
});
describe('Add', function () {
it('creates a new notification and sets default fields', function (done) {
const newNotification = {
type: 'info',
message: 'test notification',
custom: true,
id: 'customId'
};
request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [newNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.notifications);
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
jsonResponse.notifications[0].type.should.equal(newNotification.type);
jsonResponse.notifications[0].message.should.equal(newNotification.message);
jsonResponse.notifications[0].status.should.equal('alert');
jsonResponse.notifications[0].dismissible.should.be.true();
should.exist(jsonResponse.notifications[0].location);
jsonResponse.notifications[0].location.should.equal('bottom');
jsonResponse.notifications[0].id.should.be.a.String();
done();
});
});
it('creates duplicate', function (done) {
const newNotification = {
type: 'info',
message: 'add twice',
custom: true,
id: 'customId-2'
};
request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [newNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.exist(jsonResponse.notifications);
jsonResponse.notifications.should.be.an.Array().with.lengthOf(1);
jsonResponse.notifications[0].message.should.equal(newNotification.message);
request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [newNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.exist(jsonResponse.notifications);
jsonResponse.notifications.should.be.an.Array().with.lengthOf(0);
done();
});
});
});
it('should have correct order', function () {
const firstNotification = {
status: 'alert',
type: 'info',
custom: true,
id: 'firstId',
dismissible: true,
message: '1'
};
const secondNotification = {
status: 'alert',
type: 'info',
custom: true,
id: 'secondId',
dismissible: true,
message: '2'
};
return request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [firstNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then(() => {
return request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [secondNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201);
})
.then(() => {
return request.get(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then(res => {
const jsonResponse = res.body;
jsonResponse.notifications.should.be.an.Array().with.lengthOf(4);
jsonResponse.notifications[0].id.should.equal(secondNotification.id);
jsonResponse.notifications[1].id.should.equal(firstNotification.id);
jsonResponse.notifications[2].id.should.equal('customId-2');
jsonResponse.notifications[3].id.should.equal('customId');
});
});
});
});
describe('Delete', function () {
var newNotification = {
type: 'info',
message: 'test notification',
status: 'alert',
custom: true
};
it('deletes a notification', function (done) {
// create the notification that is to be deleted
request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [newNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, res) {
if (err) {
return done(err);
}
const jsonResponse = res.body;
should.exist(jsonResponse.notifications);
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
jsonResponse.notifications.length.should.eql(1);
jsonResponse.notifications[0].type.should.equal(newNotification.type);
jsonResponse.notifications[0].message.should.equal(newNotification.message);
jsonResponse.notifications[0].status.should.equal(newNotification.status);
// begin delete test
request.del(localUtils.API.getApiQuery(`notifications/${jsonResponse.notifications[0].id}/`))
.set('Origin', config.get('url'))
.expect(204)
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.should.be.empty();
done();
});
});
});
it('returns 404 when removing notification with unknown id', function () {
return request.del(localUtils.API.getApiQuery('notifications/unknown'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404)
.then(res => {
res.body.errors[0].message.should.equal('Notification does not exist.');
});
});
});
describe('As Editor', function () {
before(function () {
return ghost()
.then(function (_ghostServer) {
ghostServer = _ghostServer;
request = supertest.agent(config.get('url'));
})
.then(function () {
return testUtils.createUser({
user: testUtils.DataGenerator.forKnex.createUser({
email: 'test+1@ghost.org'
}),
role: testUtils.DataGenerator.Content.roles[2].name
});
})
.then((user) => {
request.user = user;
return localUtils.doAuth(request);
});
});
it('Add notification', function () {
const newNotification = {
type: 'info',
message: 'test notification',
custom: true,
id: 'customId'
};
return request.post(localUtils.API.getApiQuery('notifications/'))
.set('Origin', config.get('url'))
.send({notifications: [newNotification]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(403);
});
});
});

View File

@ -934,11 +934,10 @@ startGhost = function startGhost(options) {
})
.then((clients) => {
module.exports.existingData.clients = clients.toJSON();
return models.User.findAll({columns: ['id']});
return models.User.findAll({columns: ['id', 'email']});
})
.then((users) => {
module.exports.existingData.users = users.toJSON();
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
return models.Tag.findAll({columns: ['id']});
})
@ -1009,10 +1008,10 @@ startGhost = function startGhost(options) {
.then((clients) => {
module.exports.existingData.clients = clients.toJSON();
return models.User.findAll({columns: ['id']});
return models.User.findAll({columns: ['id', 'email']});
})
.then((users) => {
module.exports.existingData.users = users.toJSON();
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
return models.Tag.findAll({columns: ['id']});
})