mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-10 23:36:14 -05:00
Subscribers: Model, API & CSV import/export
- subscriber model - subscriber app updates - subscriber end points - import/export CSV - added headers to export file - added dynamic email field detection for import - returns stats object after CSV import - mask error message from DB
This commit is contained in:
parent
4ca0c67f9c
commit
01ae7ae49f
17 changed files with 690 additions and 18 deletions
|
@ -5,6 +5,7 @@
|
|||
// from a theme, an app, or from an external app, you'll use the Ghost JSON API to do so.
|
||||
|
||||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
config = require('../config'),
|
||||
// Include Endpoints
|
||||
configuration = require('./configuration'),
|
||||
|
@ -19,6 +20,7 @@ var _ = require('lodash'),
|
|||
themes = require('./themes'),
|
||||
users = require('./users'),
|
||||
slugs = require('./slugs'),
|
||||
subscribers = require('./subscribers'),
|
||||
authentication = require('./authentication'),
|
||||
uploads = require('./upload'),
|
||||
exporter = require('../data/export'),
|
||||
|
@ -28,7 +30,8 @@ var _ = require('lodash'),
|
|||
addHeaders,
|
||||
cacheInvalidationHeader,
|
||||
locationHeader,
|
||||
contentDispositionHeader,
|
||||
contentDispositionHeaderExport,
|
||||
contentDispositionHeaderSubscribers,
|
||||
init;
|
||||
|
||||
/**
|
||||
|
@ -138,12 +141,18 @@ locationHeader = function locationHeader(req, result) {
|
|||
* @see http://tools.ietf.org/html/rfc598
|
||||
* @return {string}
|
||||
*/
|
||||
contentDispositionHeader = function contentDispositionHeader() {
|
||||
|
||||
contentDispositionHeaderExport = function contentDispositionHeaderExport() {
|
||||
return exporter.fileName().then(function then(filename) {
|
||||
return 'Attachment; filename="' + filename + '"';
|
||||
});
|
||||
};
|
||||
|
||||
contentDispositionHeaderSubscribers = function contentDispositionHeaderSubscribers() {
|
||||
var datetime = (new Date()).toJSON().substring(0, 10);
|
||||
return Promise.resolve('Attachment; filename="subscribers.' + datetime + '.csv"');
|
||||
};
|
||||
|
||||
addHeaders = function addHeaders(apiMethod, req, res, result) {
|
||||
var cacheInvalidation,
|
||||
location,
|
||||
|
@ -164,15 +173,24 @@ addHeaders = function addHeaders(apiMethod, req, res, result) {
|
|||
}
|
||||
}
|
||||
|
||||
// Add Export Content-Disposition Header
|
||||
if (apiMethod === db.exportContent) {
|
||||
contentDisposition = contentDispositionHeader()
|
||||
.then(function addContentDispositionHeader(header) {
|
||||
// Add Content-Disposition Header
|
||||
if (apiMethod === db.exportContent) {
|
||||
res.set({
|
||||
'Content-Disposition': header
|
||||
});
|
||||
}
|
||||
contentDisposition = contentDispositionHeaderExport()
|
||||
.then(function addContentDispositionHeaderExport(header) {
|
||||
res.set({
|
||||
'Content-Disposition': header
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add Subscribers Content-Disposition Header
|
||||
if (apiMethod === subscribers.exportCSV) {
|
||||
contentDisposition = contentDispositionHeaderSubscribers()
|
||||
.then(function addContentDispositionHeaderSubscribers(header) {
|
||||
res.set({
|
||||
'Content-Disposition': header,
|
||||
'Content-Type': 'text/csv'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -195,7 +213,7 @@ http = function http(apiMethod) {
|
|||
var object = req.body,
|
||||
options = _.extend({}, req.file, req.query, req.params, {
|
||||
context: {
|
||||
user: (req.user && req.user.id) ? req.user.id : null
|
||||
user: ((req.user && req.user.id) || (req.user && req.user.id === 0)) ? req.user.id : null
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -213,7 +231,10 @@ http = function http(apiMethod) {
|
|||
if (req.method === 'DELETE') {
|
||||
return res.status(204).end();
|
||||
}
|
||||
|
||||
// Keep CSV header and formatting
|
||||
if (res.get('Content-Type') && res.get('Content-Type').indexOf('text/csv') === 0) {
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
// Send a properly formatting HTTP response containing the data with correct headers
|
||||
res.json(response || {});
|
||||
}).catch(function onAPIError(error) {
|
||||
|
@ -243,6 +264,7 @@ module.exports = {
|
|||
themes: themes,
|
||||
users: users,
|
||||
slugs: slugs,
|
||||
subscribers: subscribers,
|
||||
authentication: authentication,
|
||||
uploads: uploads,
|
||||
slack: slack
|
||||
|
|
367
core/server/api/subscribers.js
Normal file
367
core/server/api/subscribers.js
Normal file
|
@ -0,0 +1,367 @@
|
|||
// # Tag API
|
||||
// RESTful API for the Tag resource
|
||||
var Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
fs = require('fs'),
|
||||
pUnlink = Promise.promisify(fs.unlink),
|
||||
readline = require('readline'),
|
||||
dataProvider = require('../models'),
|
||||
errors = require('../errors'),
|
||||
utils = require('./utils'),
|
||||
pipeline = require('../utils/pipeline'),
|
||||
i18n = require('../i18n'),
|
||||
|
||||
docName = 'subscribers',
|
||||
subscribers;
|
||||
|
||||
/**
|
||||
* ### Subscribers API Methods
|
||||
*
|
||||
* **See:** [API Methods](index.js.html#api%20methods)
|
||||
*/
|
||||
subscribers = {
|
||||
/**
|
||||
* ## Browse
|
||||
* @param {{context}} options
|
||||
* @returns {Promise<Subscriber>} Subscriber Collection
|
||||
*/
|
||||
browse: function browse(options) {
|
||||
var tasks;
|
||||
|
||||
/**
|
||||
* ### Model Query
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
* @returns {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.findPage(options);
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName, {opts: utils.browseDefaultOptions}),
|
||||
// TODO: handlePermissions
|
||||
doQuery
|
||||
];
|
||||
|
||||
// Pipeline calls each task passing the result of one to be the arguments for the next
|
||||
return pipeline(tasks, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Read
|
||||
* @param {{id}} options
|
||||
* @return {Promise<Subscriber>} Subscriber
|
||||
*/
|
||||
read: function read(options) {
|
||||
var attrs = ['id'],
|
||||
tasks;
|
||||
|
||||
/**
|
||||
* ### Model Query
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
* @returns {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.findOne(options.data, _.omit(options, ['data']));
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName, {attrs: attrs}),
|
||||
// TODO: handlePermissions
|
||||
doQuery
|
||||
];
|
||||
|
||||
// Pipeline calls each task passing the result of one to be the arguments for the next
|
||||
return pipeline(tasks, options).then(function formatResponse(result) {
|
||||
if (result) {
|
||||
return {subscribers: [result.toJSON(options)]};
|
||||
}
|
||||
|
||||
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscriber.subscriberNotFound')));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Add
|
||||
* @param {Subscriber} object the subscriber to create
|
||||
* @returns {Promise(Subscriber)} Newly created Subscriber
|
||||
*/
|
||||
add: function add(object, options) {
|
||||
var tasks;
|
||||
|
||||
function cleanError(error) {
|
||||
if (error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return new errors.DataImportError('Email already exists.');
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Model Query
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
* @returns {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.add(options.data.subscribers[0], _.omit(options, ['data'])).catch(function (error) {
|
||||
if (error.errno) {
|
||||
// DB error
|
||||
return Promise.reject(cleanError(error));
|
||||
}
|
||||
return Promise.reject(error[0]);
|
||||
});
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName),
|
||||
// TODO: handlePermissions
|
||||
doQuery
|
||||
];
|
||||
|
||||
// Pipeline calls each task passing the result of one to be the arguments for the next
|
||||
return pipeline(tasks, object, options).then(function formatResponse(result) {
|
||||
var subscriber = result.toJSON(options);
|
||||
|
||||
return {subscribers: [subscriber]};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Edit
|
||||
*
|
||||
* @public
|
||||
* @param {Subscriber} object Subscriber or specific properties to update
|
||||
* @param {{id, context, include}} options
|
||||
* @return {Promise<Subscriber>} Edited Subscriber
|
||||
*/
|
||||
edit: function edit(object, options) {
|
||||
var tasks;
|
||||
|
||||
/**
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
* @returns {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.edit(options.data.subscribers[0], _.omit(options, ['data']));
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName, {opts: utils.idDefaultOptions}),
|
||||
// TODO: handlePermissions
|
||||
doQuery
|
||||
];
|
||||
|
||||
// Pipeline calls each task passing the result of one to be the arguments for the next
|
||||
return pipeline(tasks, object, options).then(function formatResponse(result) {
|
||||
if (result) {
|
||||
var subscriber = result.toJSON(options);
|
||||
|
||||
return {subscribers: [subscriber]};
|
||||
}
|
||||
|
||||
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscriber.subscriberNotFound')));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* ## Destroy
|
||||
*
|
||||
* @public
|
||||
* @param {{id, context}} options
|
||||
* @return {Promise}
|
||||
*/
|
||||
destroy: function destroy(options) {
|
||||
var tasks;
|
||||
|
||||
/**
|
||||
* ### Delete Subscriber
|
||||
* Make the call to the Model layer
|
||||
* @param {Object} options
|
||||
*/
|
||||
function doQuery(options) {
|
||||
return dataProvider.Subscriber.destroy(options).return(null);
|
||||
}
|
||||
|
||||
// Push all of our tasks into a `tasks` array in the correct order
|
||||
tasks = [
|
||||
utils.validate(docName, {opts: utils.idDefaultOptions}),
|
||||
// TODO: handlePermissions
|
||||
doQuery
|
||||
];
|
||||
|
||||
// Pipeline calls each task passing the result of one to be the arguments for the next
|
||||
return pipeline(tasks, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Export Subscribers
|
||||
* Generate the CSV to export
|
||||
*
|
||||
* @public
|
||||
* @param {{context}} options
|
||||
* @returns {Promise} Ghost Export CSV format
|
||||
*/
|
||||
exportCSV: function exportCSV(options) {
|
||||
var tasks = [];
|
||||
|
||||
options = options || {};
|
||||
|
||||
function formatCSV(data) {
|
||||
var fields = ['id', 'email', 'created_at', 'deleted_at'],
|
||||
csv = fields.join(',') + '\r\n',
|
||||
subscriber,
|
||||
field,
|
||||
j,
|
||||
i;
|
||||
|
||||
for (j = 0; j < data.length; j = j + 1) {
|
||||
subscriber = data[j];
|
||||
|
||||
for (i = 0; i < fields.length; i = i + 1) {
|
||||
field = fields[i];
|
||||
csv += subscriber[field] !== null ? subscriber[field] : '';
|
||||
if (i !== fields.length - 1) {
|
||||
csv += ',';
|
||||
}
|
||||
}
|
||||
csv += '\r\n';
|
||||
}
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Export data, otherwise send error 500
|
||||
function exportSubscribers() {
|
||||
return dataProvider.Subscriber.findPage(options).then(function (data) {
|
||||
return formatCSV(data.subscribers);
|
||||
}).catch(function (error) {
|
||||
return Promise.reject(new errors.InternalServerError(error.message || error));
|
||||
});
|
||||
}
|
||||
|
||||
tasks = [
|
||||
// TODO: handlePermissions
|
||||
exportSubscribers
|
||||
];
|
||||
|
||||
return pipeline(tasks, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* ### Import CSV
|
||||
* Import subscribers from a CSV file
|
||||
*
|
||||
* @public
|
||||
* @param {{context}} options
|
||||
* @returns {Promise} Success
|
||||
*/
|
||||
importCSV: function (options) {
|
||||
var tasks = [];
|
||||
|
||||
options = options || {};
|
||||
|
||||
function validate(options) {
|
||||
options.name = options.originalname;
|
||||
options.type = options.mimetype;
|
||||
|
||||
// Check if a file was provided
|
||||
if (!utils.checkFileExists(options)) {
|
||||
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.db.selectFileToImport')));
|
||||
}
|
||||
|
||||
// TODO: check for valid entries
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function importCSV(options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var filePath = options.path,
|
||||
importTasks = [],
|
||||
emailIdx = -1,
|
||||
firstLine = true,
|
||||
rl;
|
||||
|
||||
rl = readline.createInterface({
|
||||
input: fs.createReadStream(filePath),
|
||||
terminal: false
|
||||
});
|
||||
|
||||
rl.on('line', function (line) {
|
||||
var dataToImport = line.split(',');
|
||||
|
||||
if (firstLine) {
|
||||
emailIdx = _.findIndex(dataToImport, function (columnName) {
|
||||
if (columnName.match(/email/g)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (emailIdx === -1) {
|
||||
return reject(new errors.ValidationError('Email column not found'));
|
||||
}
|
||||
firstLine = false;
|
||||
} else if (emailIdx > -1) {
|
||||
importTasks.push(function () {
|
||||
return subscribers.add({
|
||||
subscribers: [{
|
||||
email: dataToImport[emailIdx]
|
||||
}
|
||||
]}, {context: options.context});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rl.on('close', function () {
|
||||
var fulfilled = 0,
|
||||
duplicates = 0,
|
||||
invalid = 0;
|
||||
|
||||
Promise.all(importTasks.map(function (promise) {
|
||||
return promise().reflect();
|
||||
})).each(function (inspection) {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason().errorType === 'ValidationError') {
|
||||
invalid = invalid + 1;
|
||||
} else if (inspection.reason().errorType === 'DataImportError') {
|
||||
duplicates = duplicates + 1;
|
||||
}
|
||||
}
|
||||
}).then(function () {
|
||||
return resolve({
|
||||
stats: [{
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}]
|
||||
});
|
||||
}).catch(function (err) {
|
||||
return reject(err);
|
||||
}).finally(function () {
|
||||
// Remove uploaded file from tmp location
|
||||
return pUnlink(filePath);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tasks = [
|
||||
validate,
|
||||
// TODO: handlePermissions
|
||||
importCSV
|
||||
];
|
||||
|
||||
return pipeline(tasks, options);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = subscribers;
|
|
@ -11,7 +11,7 @@ module.exports = {
|
|||
// Correct way to register a helper from an app
|
||||
ghost.helpers.register('form_subscribe', function formSubscribeHelper(options) {
|
||||
var data = _.merge({}, options.hash, {
|
||||
action: path.join(config.paths.subdir, config.routeKeywords.subscribe) + '/'
|
||||
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/')
|
||||
});
|
||||
return template.execute('form_subscribe', data, options);
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ var path = require('path'),
|
|||
express = require('express'),
|
||||
templates = require('../../../controllers/frontend/templates'),
|
||||
setResponseContext = require('../../../controllers/frontend/context'),
|
||||
api = require('../../../api'),
|
||||
subscribeRouter = express.Router();
|
||||
|
||||
function controller(req, res) {
|
||||
|
@ -21,12 +22,20 @@ function controller(req, res) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function storeSubscriber(req, res, next) {
|
||||
return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}}).then(function (result) {
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// subscribe frontend route
|
||||
subscribeRouter.route('/')
|
||||
.get(
|
||||
controller
|
||||
)
|
||||
.post(
|
||||
storeSubscriber,
|
||||
controller
|
||||
);
|
||||
|
||||
|
|
|
@ -194,7 +194,7 @@ ConfigManager.prototype.set = function (config) {
|
|||
private: 'private',
|
||||
subscribe: 'subscribe'
|
||||
},
|
||||
internalApps: ['private-blogging'],
|
||||
internalApps: ['private-blogging', 'subscribers'],
|
||||
slugs: {
|
||||
// Used by generateSlug to generate slugs for posts, tags, users, ..
|
||||
// reserved slugs are reserved but can be extended/removed by apps
|
||||
|
|
|
@ -99,6 +99,7 @@ auth = {
|
|||
} else if (isBearerAutorizationHeader(req)) {
|
||||
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
|
||||
} else if (req.client) {
|
||||
req.user = {id: 0};
|
||||
return next();
|
||||
}
|
||||
|
||||
|
@ -110,7 +111,7 @@ auth = {
|
|||
// Workaround for missing permissions
|
||||
// TODO: rework when https://github.com/TryGhost/Ghost/issues/3911 is done
|
||||
requiresAuthorizedUser: function requiresAuthorizedUser(req, res, next) {
|
||||
if (req.user) {
|
||||
if (req.user && req.user.id) {
|
||||
return next();
|
||||
} else {
|
||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
|
@ -122,7 +123,7 @@ auth = {
|
|||
if (labs.isSet('publicAPI') === true) {
|
||||
return next();
|
||||
} else {
|
||||
if (req.user) {
|
||||
if (req.user && req.user.id) {
|
||||
return next();
|
||||
} else {
|
||||
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
|
||||
|
|
|
@ -26,6 +26,7 @@ var bodyParser = require('body-parser'),
|
|||
uncapitalise = require('./uncapitalise'),
|
||||
cors = require('./cors'),
|
||||
netjet = require('netjet'),
|
||||
labs = require('./labs'),
|
||||
|
||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||
|
@ -44,7 +45,8 @@ middleware = {
|
|||
requiresAuthorizedUser: auth.requiresAuthorizedUser,
|
||||
requiresAuthorizedUserPublicAPI: auth.requiresAuthorizedUserPublicAPI,
|
||||
errorHandler: errors.handleAPIError,
|
||||
cors: cors
|
||||
cors: cors,
|
||||
labs: labs
|
||||
}
|
||||
};
|
||||
|
||||
|
|
15
core/server/middleware/labs.js
Normal file
15
core/server/middleware/labs.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
var errors = require('../errors'),
|
||||
labsUtil = require('../utils/labs'),
|
||||
labs;
|
||||
|
||||
labs = {
|
||||
subscribers: function subscribers(req, res, next) {
|
||||
if (labsUtil.isSet('subscribers') === true) {
|
||||
return next();
|
||||
} else {
|
||||
return errors.handleAPIError(new errors.NotFoundError(), req, res, next);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = labs;
|
|
@ -134,11 +134,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
|||
// Get the user from the options object
|
||||
contextUser: function contextUser(options) {
|
||||
// Default to context user
|
||||
if (options.context && options.context.user) {
|
||||
if ((options.context && options.context.user) || (options.context && options.context.user === 0)) {
|
||||
return options.context.user;
|
||||
// Other wise use the internal override
|
||||
} else if (options.context && options.context.internal) {
|
||||
return 1;
|
||||
} else if (options.context && options.context.external) {
|
||||
return 0;
|
||||
} else {
|
||||
errors.logAndThrowError(new Error(i18n.t('errors.models.base.index.missingContext')));
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ models = [
|
|||
'refreshtoken',
|
||||
'role',
|
||||
'settings',
|
||||
'subscriber',
|
||||
'tag',
|
||||
'user'
|
||||
];
|
||||
|
|
46
core/server/models/subscriber.js
Normal file
46
core/server/models/subscriber.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
var ghostBookshelf = require('./base'),
|
||||
|
||||
Subscriber,
|
||||
Subscribers;
|
||||
|
||||
Subscriber = ghostBookshelf.Model.extend({
|
||||
tableName: 'subscribers'
|
||||
}, {
|
||||
|
||||
orderDefaultOptions: function orderDefaultOptions() {
|
||||
return {};
|
||||
},
|
||||
/**
|
||||
* @deprecated in favour of filter
|
||||
*/
|
||||
processOptions: function processOptions(options) {
|
||||
return options;
|
||||
},
|
||||
|
||||
permittedOptions: function permittedOptions(methodName) {
|
||||
var options = ghostBookshelf.Model.permittedOptions(),
|
||||
|
||||
// whitelists for the `options` hash argument on methods, by method name.
|
||||
// these are the only options that can be passed to Bookshelf / Knex.
|
||||
validOptions = {
|
||||
findPage: ['page', 'limit', 'columns', 'filter', 'order'],
|
||||
findAll: ['columns']
|
||||
};
|
||||
|
||||
if (validOptions[methodName]) {
|
||||
options = options.concat(validOptions[methodName]);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Subscribers = ghostBookshelf.Collection.extend({
|
||||
model: Subscriber
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Subscriber: ghostBookshelf.model('Subscriber', Subscriber),
|
||||
Subscribers: ghostBookshelf.collection('Subscriber', Subscribers)
|
||||
};
|
|
@ -65,6 +65,20 @@ apiRoutes = function apiRoutes(middleware) {
|
|||
router.put('/tags/:id', authenticatePrivate, api.http(api.tags.edit));
|
||||
router.del('/tags/:id', authenticatePrivate, api.http(api.tags.destroy));
|
||||
|
||||
// ## Subscribers
|
||||
router.get('/subscribers', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.browse));
|
||||
router.get('/subscribers/csv', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.exportCSV));
|
||||
router.post('/subscribers/csv',
|
||||
middleware.api.labs.subscribers,
|
||||
authenticatePrivate,
|
||||
middleware.upload.single('subscribersfile'),
|
||||
api.http(api.subscribers.importCSV)
|
||||
);
|
||||
router.get('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.read));
|
||||
router.post('/subscribers', middleware.api.labs.subscribers, authenticatePublic, api.http(api.subscribers.add));
|
||||
router.put('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.edit));
|
||||
router.del('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.destroy));
|
||||
|
||||
// ## Roles
|
||||
router.get('/roles/', authenticatePrivate, api.http(api.roles.browse));
|
||||
|
||||
|
|
|
@ -333,6 +333,9 @@
|
|||
"tags": {
|
||||
"tagNotFound": "Tag not found."
|
||||
},
|
||||
"subscriber": {
|
||||
"subscriberNotFound": "Subscriber not found."
|
||||
},
|
||||
"themes": {
|
||||
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
|
||||
"noPermissionToEditThemes": "You do not have permission to edit themes.",
|
||||
|
|
176
core/test/integration/api/api_subscription_spec.js
Normal file
176
core/test/integration/api/api_subscription_spec.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*globals describe, before, beforeEach, afterEach, it */
|
||||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
_ = require('lodash'),
|
||||
// Stuff we are testing
|
||||
context = testUtils.context,
|
||||
|
||||
SubscribersAPI = require('../../../server/api/subscribers');
|
||||
|
||||
describe('Subscribers API', function () {
|
||||
// Keep the DB clean
|
||||
before(testUtils.teardown);
|
||||
afterEach(testUtils.teardown);
|
||||
beforeEach(testUtils.setup('users:roles', 'permission', 'perms:init', 'subscriber'));
|
||||
|
||||
should.exist(SubscribersAPI);
|
||||
|
||||
describe('Add', function () {
|
||||
var newSubscriber;
|
||||
|
||||
beforeEach(function () {
|
||||
newSubscriber = _.clone(testUtils.DataGenerator.forKnex.createSubscriber(testUtils.DataGenerator.Content.subscribers[1]));
|
||||
Promise.resolve(newSubscriber);
|
||||
});
|
||||
|
||||
it('can add a subscriber (admin)', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.admin)
|
||||
.then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can add a subscriber (external)', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
|
||||
.then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('CANNOT add subscriber without context', function (done) {
|
||||
SubscribersAPI.add({subscribers: [newSubscriber]}).then(function () {
|
||||
done(new Error('Add subscriber is not denied without authentication.'));
|
||||
}, function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
var newSubscriberEmail = 'subscriber@updated.com',
|
||||
firstSubscriber = 1;
|
||||
|
||||
it('can edit a subscriber (admin)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.admin, {id: firstSubscriber}))
|
||||
.then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can edit a subscriber (editor)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.editor, {id: firstSubscriber}))
|
||||
.then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
// needs permissions to work properly
|
||||
it.skip('CANNOT edit subscriber (external)', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.external, {id: firstSubscriber}))
|
||||
.then(function () {
|
||||
done(new Error('Edit subscriber is not denied with external context.'));
|
||||
}, function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('CANNOT edit subscriber that doesn\'t exit', function (done) {
|
||||
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.internal, {id: 999}))
|
||||
.then(function () {
|
||||
done(new Error('Edit non-existent subscriber is possible.'));
|
||||
}, function (err) {
|
||||
should.exist(err);
|
||||
err.message.should.eql('Subscriber not found.');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destroy', function () {
|
||||
var firstSubscriber = 1;
|
||||
it('can destroy subscriber', function (done) {
|
||||
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstSubscriber}))
|
||||
.then(function (results) {
|
||||
should.not.exist(results);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Browse', function () {
|
||||
it('can browse (internal)', function (done) {
|
||||
SubscribersAPI.browse(testUtils.context.internal).then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.should.have.lengthOf(1);
|
||||
testUtils.API.checkResponse(results.subscribers[0], 'subscriber');
|
||||
results.subscribers[0].created_at.should.be.an.instanceof(Date);
|
||||
|
||||
results.meta.pagination.should.have.property('page', 1);
|
||||
results.meta.pagination.should.have.property('limit', 15);
|
||||
results.meta.pagination.should.have.property('pages', 1);
|
||||
results.meta.pagination.should.have.property('total', 1);
|
||||
results.meta.pagination.should.have.property('next', null);
|
||||
results.meta.pagination.should.have.property('prev', null);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
// needs permissions to work properly
|
||||
it.skip('CANNOT browse subscriber (external)', function (done) {
|
||||
SubscribersAPI.browse(testUtils.context.external).then(function () {
|
||||
done(new Error('Browse subscriber is not denied with external context.'));
|
||||
}, function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Read', function () {
|
||||
function extractFirstSubscriber(subscribers) {
|
||||
return _.filter(subscribers, {id: 1})[0];
|
||||
}
|
||||
|
||||
it('with id', function (done) {
|
||||
SubscribersAPI.browse({context: {user: 1}}).then(function (results) {
|
||||
should.exist(results);
|
||||
should.exist(results.subscribers);
|
||||
results.subscribers.length.should.be.above(0);
|
||||
|
||||
var firstSubscriber = extractFirstSubscriber(results.subscribers);
|
||||
|
||||
return SubscribersAPI.read({context: {user: 1}, id: firstSubscriber.id});
|
||||
}).then(function (found) {
|
||||
should.exist(found);
|
||||
testUtils.API.checkResponse(found.subscribers[0], 'subscriber');
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('cannot fetch a subscriber which doesn\'t exist', function (done) {
|
||||
SubscribersAPI.read({context: {user: 1}, id: 999}).then(function () {
|
||||
done(new Error('Should not return a result'));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
err.message.should.eql('Subscriber not found.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ var _ = require('lodash'),
|
|||
tags: ['tags', 'meta'],
|
||||
users: ['users', 'meta'],
|
||||
settings: ['settings', 'meta'],
|
||||
subscribers: ['subscribers', 'meta'],
|
||||
roles: ['roles'],
|
||||
pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'],
|
||||
slugs: ['slugs'],
|
||||
|
@ -25,6 +26,7 @@ var _ = require('lodash'),
|
|||
// Tag API swaps parent_id to parent
|
||||
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
|
||||
setting: _.keys(schema.settings),
|
||||
subscriber: _.keys(schema.subscribers),
|
||||
accesstoken: _.keys(schema.accesstokens),
|
||||
role: _.keys(schema.roles),
|
||||
permission: _.keys(schema.permissions),
|
||||
|
|
|
@ -236,6 +236,15 @@ DataGenerator.Content = {
|
|||
key: 'setting',
|
||||
value: 'value'
|
||||
}
|
||||
],
|
||||
|
||||
subscribers: [
|
||||
{
|
||||
email: 'subscriber1@test.com'
|
||||
},
|
||||
{
|
||||
email: 'subscriber2@test.com'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -440,6 +449,7 @@ DataGenerator.forKnex = (function () {
|
|||
createAppField: createAppField,
|
||||
createAppSetting: createAppSetting,
|
||||
createToken: createToken,
|
||||
createSubscriber: createBasic,
|
||||
|
||||
posts: posts,
|
||||
tags: tags,
|
||||
|
|
|
@ -386,6 +386,7 @@ toDoList = {
|
|||
role: function insertRole() { return fixtures.insertOne('roles', 'createRole'); },
|
||||
roles: function insertRoles() { return fixtures.insertRoles(); },
|
||||
tag: function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
|
||||
subscriber: function insertSubscriber() { return fixtures.insertOne('subscribers', 'createSubscriber'); },
|
||||
|
||||
posts: function insertPosts() { return fixtures.insertPosts(); },
|
||||
'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); },
|
||||
|
@ -581,6 +582,7 @@ module.exports = {
|
|||
// Helpers to make it easier to write tests which are easy to read
|
||||
context: {
|
||||
internal: {context: {internal: true}},
|
||||
external: {context: {external: true}},
|
||||
owner: {context: {user: 1}},
|
||||
admin: {context: {user: 2}},
|
||||
editor: {context: {user: 3}},
|
||||
|
|
Loading…
Add table
Reference in a new issue